Skip to content

Instantly share code, notes, and snippets.

@nevyn
Last active April 25, 2025 23:03
Show Gist options
  • Save nevyn/b76ea3bae8f22fe06dcd8e1b3f2d8491 to your computer and use it in GitHub Desktop.
Save nevyn/b76ea3bae8f22fe06dcd8e1b3f2d8491 to your computer and use it in GitHub Desktop.
Github Action for Mac notarized + appstore build + TestFlight + visionOS

GH Actions workflow for Mac and Vision Pro

This is taken from my app koja.works, as an attempt to get full notarized + appstore builds made in a single CI workflow so that I can keep track of all moving parts, without involving Fastlane (which in my experience breaks way too often due to system rubies etc).

Builds for both Mac and visionOS, and the vision file is probably easily extensible to iOS as well (maybe with matrix builds?)

I'm using a self-hosted Mac mini on my desktop, which is why I have to fiddle with PATH etc. I'm doing this because I require metal support during build, which Xcode Cloud does not support.

In retrospect, I should've just gone with Fastlane... But this works now. It's probably pretty easy to adapt to another app.

These scripts were written April 2025, and are adapted from various sources linked inline. Some calls have been updated to match how Xcode and Appstore Connect APIs work today.

Code signing and secrets

  • Three different kinds of certificates are stored in GitHub Actions Secrets:
    • AppStore, aka "Apple Distribution", for distributing to appstore for all platforms
    • MacInstaller, aka "3rd Party Mac Installer", for submitting the pkg to to Mac App Store
    • Developer ID, for notarization for out-of-appstore Mac deployment
  • They should be base64 encoded and added to secrets.
  • Mac app for app store needs a provisioning profile. It needs to both be set in the Release configuration of the Xcode project, and added as MAC_APPSTORE_PROVISIONING_PROFILE github secret. Its name must be "KojaMacAppstore". A future iteration will likely download the provisioning profile from appstore connect api.
  • visionOS also needs a provisioning profile as VISION_APPSTORE_PROVISIONING_PROFILE
  • Notarization requires the secret NOTARY_PASSWORD, an App Specific Password. It should probably be rewritten to use JWT instead.
  • ... because publishing to TestFlight does require a JWT token. So put these in secrets:
    • APPCONNECT_API_KEY_PRIVATE is the actual private key file, as-is, not base64-encoded
    • APPCONNECT_API_ISSUER is the issuer
    • APPCONNECT_API_KEY_ID is, yeah you guessed it, the Key ID.
  • KEYCHAIN_PASSWORD is just some random string for the temporary keychain.

Room for improvement

  • There's a lot of duplication between Mac and Vision/iOS. They could probably be combined to a single file with a matrix build, and special-case the notarized build for mac-only or something.
  • I'll add iOS as well at some point.
  • Use JWT auth instead of app-specific-password for uploads
  • Maybe making this a proper reusable github actions plugin/published workflow, so using it from your project is like a one-liner? But I do like that it's very clear what's going on with almost everything in the same file.
name: Mac app
on: push
env:
PATH: /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin
jobs:
build_mac_notarized:
runs-on: self-hosted
env:
DIST_DIR: dist-mac-notarized
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
lfs: true
- name: Really fetch large files
run:
git lfs install
git lfs pull
- name: Install the Apple Mac certificate and provisioning profile
env:
DEVELOPER_ID_CER: ${{ secrets.DEVELOPER_ID_CER }}
DEVELOPER_ID_CER_PASSWORD: ${{ secrets.DEVELOPER_ID_CER_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
# https://docs.github.com/en/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development
# https://defn.io/2023/09/22/distributing-mac-apps-with-github-actions/
DEVELOPER_ID_CER_PATH=$RUNNER_TEMP/devid.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
echo -n "$DEVELOPER_ID_CER" | base64 --decode -o $DEVELOPER_ID_CER_PATH
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $DEVELOPER_ID_CER_PATH -P "$DEVELOPER_ID_CER_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
security find-identity -p codesigning -v
- name: Build Koja.Works.app for Mac
run: |
mkdir -p $DIST_DIR
xcodebuild \
archive \
-project Koja.xcodeproj/ \
-scheme Koja \
-configuration Release \
-destination 'generic/platform=macOS' \
-archivePath $DIST_DIR/Koja.xcarchive \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Developer ID Application: Nevyn Bengtsson (M4Q.......)" \
KOJA_APP_PROVISIONING_PROFILE=""
xcodebuild \
-exportArchive \
-archivePath $DIST_DIR/Koja.xcarchive \
-exportOptionsPlist Koja/NotarizedExportOptions.plist \
-exportPath $DIST_DIR/ \
-allowProvisioningUpdates
npx create-dmg $DIST_DIR/Koja.Works.app $DIST_DIR/
mv $DIST_DIR/Koja*.dmg $DIST_DIR/Koja.Works.dmg
- name: Notarize Koja.Works.dmg
env:
NOTARY_USERNAME: YOUR APPLE ID
NOTARY_PASSWORD: ${{ secrets.NOTARY_PASSWORD }}
run: |
xcrun notarytool submit \
--team-id 'M4Q.......' \
--apple-id "$NOTARY_USERNAME" \
--password "$NOTARY_PASSWORD" \
--wait \
$DIST_DIR/Koja.Works.dmg
xcrun stapler staple $DIST_DIR/Koja.Works.dmg
- name: Upload Koja.Works.dmg
uses: actions/upload-artifact@v4
with:
name: Koja.Works.dmg
path: ${{ env.DIST_DIR }}/Koja.Works.dmg
- name: Clean up keychain and provisioning profile
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
build_mac_appstore:
runs-on: self-hosted
if: github.ref == 'refs/heads/main'
env:
DIST_DIR: dist-mac-appstore
outputs:
DELIVERY_UUID: ${{ steps.upload-to-testflight.outputs.DELIVERY_UUID }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
lfs: true
- name: Really fetch large files
run: |
git lfs install
git lfs pull
- name: Install the App Store certificate and provisioning profile
env:
APPSTORE_CER: ${{ secrets.APPSTORE_CER }}
APPSTORE_CER_PASSWORD: ${{ secrets.APPSTORE_CER_PASSWORD }}
MACINSTALLER_CER: ${{ secrets.MACINSTALLER_CER }}
MACINSTALLER_CER_PASSWORD: ${{ secrets.MACINSTALLER_CER_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
MAC_APPSTORE_PROVISIONING_PROFILE: ${{ secrets.MAC_APPSTORE_PROVISIONING_PROFILE }}
run: |
echo "🔑 Installing appstore distribution and installer certificates and private keys..."
# Apple documentation says to use "Apple Distribution" to sign the code.
# https://developer.apple.com/documentation/xcode/creating-distribution-signed-code-for-the-mac
KEYCHAIN_PATH=$RUNNER_TEMP/appstore-signing.keychain-db
APPSTORE_CER_PATH=$RUNNER_TEMP/appstore.p12
echo -n "$APPSTORE_CER" | base64 --decode -o $APPSTORE_CER_PATH
MACINSTALLER_CER_PATH=$RUNNER_TEMP/macinstaller.p12
echo -n "$MACINSTALLER_CER" | base64 --decode -o $MACINSTALLER_CER_PATH
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $APPSTORE_CER_PATH -P "$APPSTORE_CER_PASSWORD" -A -k $KEYCHAIN_PATH
security import $MACINSTALLER_CER_PATH -P "$MACINSTALLER_CER_PASSWORD" -A -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:codesign,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s $KEYCHAIN_PATH
echo "📋 Available code signing identities:"
security find-identity -p codesigning -v
# TODO: download provisioning profile from appstore connect API instead
echo "📃 Installing provisioning profile..."
mkdir -p "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
PROVIS_TMP="$RUNNER_TEMP/temp.provisioningprofile"
echo -n "$MAC_APPSTORE_PROVISIONING_PROFILE" | base64 --decode -o "$PROVIS_TMP"
DECODED_PLIST="$RUNNER_TEMP/KojaMacAppstore.plist"
security cms -D -i "$PROVIS_TMP" -o "$DECODED_PLIST"
PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print :UUID" "$DECODED_PLIST")
echo "PROFILE_UUID=$PROFILE_UUID" >> $GITHUB_ENV
# Note: This path is new for Xcode 16
PROVIS_PATH="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles/${PROFILE_UUID}.provisionprofile"
cp "$PROVIS_TMP" "$PROVIS_PATH"
echo "✅ Installed provisioning profile with UUID: $PROFILE_UUID"
- name: Calculate build version
run: |
BUILD_VERSION="100.${{ github.run_id }}.${{ github.run_attempt }}"
echo "BUILD_VERSION=$BUILD_VERSION" >> $GITHUB_ENV
echo "Build version: $BUILD_VERSION"
- name: Build Koja.Works.app for App Store/TestFlight
run: |
mkdir -p $DIST_DIR
xcodebuild \
archive \
-project Koja.xcodeproj/ \
-scheme Koja \
-configuration Release \
-destination 'generic/platform=macOS' \
-archivePath $DIST_DIR/KojaAppstore.xcarchive \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM=M4Q....... \
CODE_SIGN_IDENTITY="Apple Distribution: Nevyn Bengtsson (M4Q.......)" \
CURRENT_PROJECT_VERSION=$BUILD_VERSION \
KOJA_APP_PROVISIONING_PROFILE="KojaMacAppstore"
xcodebuild \
-exportArchive \
-archivePath $DIST_DIR/KojaAppstore.xcarchive \
-exportOptionsPlist Koja/MacAppStoreExportOptions.plist \
-exportPath $DIST_DIR/ \
-allowProvisioningUpdates
- name: Read short version from the xcarchive
run: |
ARCHIVE_APP="$DIST_DIR/KojaAppstore.xcarchive/Products/Applications/Koja.Works.app"
PLIST="$ARCHIVE_APP/Contents/Info.plist"
SHORT_VERSION=$(/usr/libexec/PlistBuddy \
-c "Print :CFBundleShortVersionString" "$PLIST")
echo "SHORT_VERSION=$SHORT_VERSION" >> $GITHUB_ENV
echo "🔍 This is version $SHORT_VERSION, build $BUILD_VERSION."
- name: Upload to TestFlight
id: upload-to-testflight
env:
APPLE_ID: YOUR APPLE ID
APP_STORE_APP_SPECIFIC_PASSWORD: ${{ secrets.NOTARY_PASSWORD }}
run: |
echo "🕵️‍♀️ Validating…"
xcrun altool --validate-app -f $DIST_DIR/Koja.Works.pkg \
-t macos --username "$APPLE_ID" --password "$APP_STORE_APP_SPECIFIC_PASSWORD" --team-id "M4Q......."
echo "🚀 Uploading build $BUILD_VERSION (ver $SHORT_VERSION) to TestFlight…"
UPLOAD_OUT=$(xcrun altool --upload-package $DIST_DIR/Koja.Works.pkg \
-t macos \
--apple-id 673....... \
--bundle-id works.koja.app \
--bundle-short-version-string $SHORT_VERSION \
--bundle-version $BUILD_VERSION \
--username "$APPLE_ID" --password "$APP_STORE_APP_SPECIFIC_PASSWORD" --team-id "M4Q......."\
2>&1)
echo $UPLOAD_OUT
# todo: use jwt auth for this instead of app specific password
DELIVERY_UUID=$(printf "%s\n" "$UPLOAD_OUT" \
| grep -Eo 'Delivery UUID: [0-9a-fA-F-]+' \
| awk '{print $3}')
echo "DELIVERY_UUID=$DELIVERY_UUID" >> $GITHUB_OUTPUT
echo "✅ Delivery UUID: $DELIVERY_UUID"
- name: Clean up keychain
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/appstore-signing.keychain-db
rm -rf "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
submit_to_mac_testflight:
needs: build_mac_appstore
uses: ./.github/workflows/publish-to-testflight.yml
with:
group_name: "Koja%20External"
delivery_uuid: "${{ needs.build_mac_appstore.outputs.DELIVERY_UUID }}"
secrets:
APPCONNECT_API_ISSUER: ${{ secrets.APPCONNECT_API_ISSUER }}
APPCONNECT_API_KEY_ID: ${{ secrets.APPCONNECT_API_KEY_ID }}
APPCONNECT_API_KEY_PRIVATE: ${{ secrets.APPCONNECT_API_KEY_PRIVATE }}
# adapted from https://gist.github.com/Preecington/c1c2b30582a0ecf858da0a100ba621b4
# todo: use this auth for `xcrun altool --upload-package` too
name: Publish to TestFlight
on:
workflow_call:
inputs:
delivery_uuid:
description: "UUID as captured from altool"
required: true
type: string
group_name:
required: true
type: string
secrets:
APPCONNECT_API_ISSUER:
required: true
APPCONNECT_API_KEY_ID:
required: true
APPCONNECT_API_KEY_PRIVATE:
required: true
jobs:
publish:
runs-on: self-hosted
env:
DELIVERY_UUID: ${{ inputs.delivery_uuid }}
GROUP_NAME: ${{ inputs.group_name }}
steps:
- name: Generate App Store Connect API Token
id: app-store-api-token
env:
ISSUER_ID: ${{ secrets.APPCONNECT_API_ISSUER }}
KEY_ID: ${{ secrets.APPCONNECT_API_KEY_ID }}
AUTH_KEY: ${{ secrets.APPCONNECT_API_KEY_PRIVATE }}
run: |
export NOW=$(date +%s)
export EXP=$(($NOW + 1200))
export JWT=$(ruby -rjson -ropenssl -rjwt -e '
key = OpenSSL::PKey::EC.new(ENV["AUTH_KEY"])
header = { alg: "ES256", kid: ENV["KEY_ID"] }
payload = {
iss: ENV["ISSUER_ID"],
exp: ENV["EXP"].to_i,
aud: "appstoreconnect-v1"
}
puts JWT.encode(payload, key, "ES256", header)
')
echo "Saving JWT to env"
echo "JWT=$JWT" >> $GITHUB_ENV
- name: Prime App Store Connect API
run: |
curl --header "Authorization: Bearer $JWT" -s -o /dev/null -w "%{http_code}" --fail https://api.appstoreconnect.apple.com/v1/bundleIds
- name: Extract release notes
uses: ffurrer2/extract-release-notes@v2
with:
prerelease: true
- name: Publish Testflight Build
env:
API_BASE: https://api.appstoreconnect.apple.com/v1
REL_NOTES: ${{ steps.extract-release-notes.outputs.release_notes }}
run: |
set -o pipefail
output=""
# callWithRetry: Make an API call, parse it with JQ to extract data and deduce access, and retry on failure.
callWithRetry() { # url, jq_query, http_method, post_data, num_attempts, delay, print_per_try
attempt_counter=0
output=""
max_attempts=${5:-30}
while true ; do
response=$(curl --globoff --header "Authorization: Bearer $JWT" -s -w "\n%{http_code}" -X ${3:-GET} -H "Content-Type: application/json" -d "${4:-}" "$1")
http_code="${response##*$'\n'}"
body="${response%$'\n'*}"
output=$(echo "$body" | jq -r -e "$2") && jq_status=0 || jq_status=$?
# If curl+jq both succeeded, break
# XXX: 422 means _can_ mean ANOTHER_BUILD_IN_REVIEW, so treat it as success
if [[ $http_code == 422 ]]; then
echo "⚠️ Another build likely in review, treating as success as to not fail build. But this won't go to TestFlight.\n(body: $body)"
break
fi
# 204 means "no content", and thus jq will fail so just treat it as success.
if [[ $http_code -ge 200 && $http_code -lt 300 && ($http_code == 204 || $jq_status -eq 0) ]]; then
echo "☑️ OK"
break
fi
if [[ $http_code -ge 300 ]]; then
echo "⚠️ HTTP status $http_code, body:\n$response"
fi
if [ ${attempt_counter} -eq ${max_attempts} ];then
echo "❌ Max attempts reached"
exit 1
fi
printf "${7:-.}\n"
attempt_counter=$(($attempt_counter+1))
sleep ${6:-1}
done
}
echo "1️⃣ Waiting for App Store Connect to process build '$DELIVERY_UUID'..."
api_url="$API_BASE/builds?filter[id]=$DELIVERY_UUID&include=buildBetaDetail,betaBuildLocalizations&fields[buildBetaDetails]=externalBuildState&fields[betaBuildLocalizations]=whatsNew"
query='. as $parent | .included[] | select(.type == "buildBetaDetails") | select(.attributes.externalBuildState != "PROCESSING") | $parent'
callWithRetry "$api_url" "$query" "GET" "" 100 10 "Waiting for build processing..."
build_id=$(echo $output | jq '.data[].id' -r)
build_localization_id=$(echo $output | jq '.included[] | select(.type == "betaBuildLocalizations") | .id' -r)
# Set compliance -- not needed, it's set in info.plist. And trying to set it again will fail.
#echo "Updating export compliance"
#data='{"data":{"attributes":{"usesNonExemptEncryption":false},"id":"'$build_id'","type":"builds"}}'
#callWithRetry "$API_BASE/builds/$build_id" "." "PATCH" "$data"
echo "2️⃣ Fetch group ID..."
callWithRetry "$API_BASE/betaGroups?filter[name]=$GROUP_NAME" ".data[].id"
group_id=$output
echo "Group $GROUP_NAME = $group_id"
echo "3️⃣ Assigning external testers"
data='{"data":[{"id":"'$group_id'","type":"betaGroups"}]}'
callWithRetry "$API_BASE/builds/$build_id/relationships/betaGroups" "." "POST" "$data"
echo "4️⃣ Assigning release notes: $REL_NOTES"
data='{"data":{"id":"'$build_localization_id'","attributes":{"whatsNew":"'$REL_NOTES'"},"type":"betaBuildLocalizations"}}'
callWithRetry "$API_BASE/betaBuildLocalizations/$build_localization_id" "." "PATCH" "$data"
echo "5️⃣ Submitting for external review"
data='{"data":{"relationships":{"build":{"data":{"id":"'$build_id'","type":"builds"}}},"type":"betaAppReviewSubmissions"}}'
callWithRetry "$API_BASE/betaAppReviewSubmissions" "." "POST" "$data"
echo "✅ Done!"
name: visionOS app
on: push
env:
PATH: /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin
jobs:
build_vision_appstore:
runs-on: self-hosted
if: github.ref == 'refs/heads/main'
env:
DIST_DIR: dist-vision-appstore
outputs:
DELIVERY_UUID: ${{ steps.upload-to-testflight.outputs.DELIVERY_UUID }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
lfs: true
- name: Really fetch large files
run: |
git lfs install
git lfs pull
- name: Install the App Store certificate and provisioning profile
env:
APPSTORE_CER: ${{ secrets.APPSTORE_CER }}
APPSTORE_CER_PASSWORD: ${{ secrets.APPSTORE_CER_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
VISION_APPSTORE_PROVISIONING_PROFILE: ${{ secrets.VISION_APPSTORE_PROVISIONING_PROFILE }}
run: |
echo "🔑 Installing appstore distribution and installer certificates and private keys..."
# Apple documentation says to use "Apple Distribution" to sign the code.
# https://developer.apple.com/documentation/xcode/creating-distribution-signed-code-for-the-mac
KEYCHAIN_PATH=$RUNNER_TEMP/appstore-signing.keychain-db
APPSTORE_CER_PATH=$RUNNER_TEMP/appstore.p12
echo -n "$APPSTORE_CER" | base64 --decode -o $APPSTORE_CER_PATH
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $APPSTORE_CER_PATH -P "$APPSTORE_CER_PASSWORD" -A -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:codesign,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychain -d user -s $KEYCHAIN_PATH
echo "📋 Available code signing identities:"
security find-identity -p codesigning -v
# TODO: download provisioning profile from appstore connect API instead
echo "📃 Installing provisioning profile..."
mkdir -p "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
PROVIS_TMP="$RUNNER_TEMP/temp.provisioningprofile"
echo -n "$VISION_APPSTORE_PROVISIONING_PROFILE" | base64 --decode -o "$PROVIS_TMP"
DECODED_PLIST="$RUNNER_TEMP/KojaVisionAppstore.plist"
security cms -D -i "$PROVIS_TMP" -o "$DECODED_PLIST"
PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print :UUID" "$DECODED_PLIST")
echo "PROFILE_UUID=$PROFILE_UUID" >> $GITHUB_ENV
# Note: This path is new for Xcode 16
PROVIS_PATH="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles/${PROFILE_UUID}.mobileprovision"
cp "$PROVIS_TMP" "$PROVIS_PATH"
echo "✅ Installed provisioning profile with UUID: $PROFILE_UUID"
- name: Calculate build version
run: |
BUILD_VERSION="100.${{ github.run_id }}.${{ github.run_attempt }}"
echo "BUILD_VERSION=$BUILD_VERSION" >> $GITHUB_ENV
echo "Build version: $BUILD_VERSION"
- name: Build Koja.Works.app for App Store/TestFlight
run: |
mkdir -p $DIST_DIR
xcodebuild \
archive \
-project Koja.xcodeproj/ \
-scheme Koja \
-configuration Release \
-destination 'generic/platform=visionOS' \
-archivePath $DIST_DIR/KojaAppstore.xcarchive \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM=M4Q....... \
CODE_SIGN_IDENTITY="Apple Distribution: Nevyn Bengtsson (M4Q.......)" \
CURRENT_PROJECT_VERSION=$BUILD_VERSION \
KOJA_APP_PROVISIONING_PROFILE="KojaVisionAppstore"
xcodebuild \
-exportArchive \
-archivePath $DIST_DIR/KojaAppstore.xcarchive \
-exportOptionsPlist Koja/VisionExportOptions.plist \
-exportPath $DIST_DIR/ \
-allowProvisioningUpdates
- name: Read short version from the xcarchive
run: |
ARCHIVE_APP="$DIST_DIR/KojaAppstore.xcarchive/Products/Applications/Koja.Works.app"
PLIST="$ARCHIVE_APP/Info.plist"
SHORT_VERSION=$(/usr/libexec/PlistBuddy \
-c "Print :CFBundleShortVersionString" "$PLIST")
echo "SHORT_VERSION=$SHORT_VERSION" >> $GITHUB_ENV
echo "🔍 This is version $SHORT_VERSION, build $BUILD_VERSION."
- name: Save build artifacts
uses: actions/upload-artifact@v4
with:
name: app
path: |
${{ env.DIST_DIR }}/Koja.Works.ipa
${{ env.DIST_DIR }}/KojaAppstore.xcarchive/dSYMs
${{ env.DIST_DIR }}/DistributionSummary.plist
${{ env.DIST_DIR }}/Packaging.log
- name: Upload to TestFlight
id: upload-to-testflight
env:
APPLE_ID: YOUR APPLE ID
APP_STORE_APP_SPECIFIC_PASSWORD: ${{ secrets.NOTARY_PASSWORD }}
run: |
echo "🕵️‍♀️ Validating…"
xcrun altool --validate-app -f $DIST_DIR/Koja.Works.ipa \
-t visionos --username "$APPLE_ID" --password "$APP_STORE_APP_SPECIFIC_PASSWORD" --team-id "M4Q......."
echo "🚀 Uploading build $BUILD_VERSION (ver $SHORT_VERSION) to TestFlight…"
UPLOAD_OUT=$(xcrun altool --upload-package $DIST_DIR/Koja.Works.ipa \
-t visionos \
--apple-id 6739489297 \
--bundle-id works.koja.app \
--bundle-short-version-string $SHORT_VERSION \
--bundle-version $BUILD_VERSION \
--username "$APPLE_ID" --password "$APP_STORE_APP_SPECIFIC_PASSWORD" --team-id "M4Q......."\
2>&1)
echo $UPLOAD_OUT
# todo: use jwt auth for this instead of app specific password
DELIVERY_UUID=$(printf "%s\n" "$UPLOAD_OUT" \
| grep -Eo 'Delivery UUID: [0-9a-fA-F-]+' \
| awk '{print $3}')
echo "DELIVERY_UUID=$DELIVERY_UUID" >> $GITHUB_OUTPUT
echo "✅ Delivery UUID: $DELIVERY_UUID"
- name: Clean up keychain
if: ${{ always() }}
run: |
security delete-keychain $RUNNER_TEMP/appstore-signing.keychain-db
rm -rf "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
submit_to_vision_testflight:
needs: build_vision_appstore
uses: ./.github/workflows/publish-to-testflight.yml
with:
group_name: "Koja%20External"
delivery_uuid: "${{ needs.build_vision_appstore.outputs.DELIVERY_UUID }}"
secrets:
APPCONNECT_API_ISSUER: ${{ secrets.APPCONNECT_API_ISSUER }}
APPCONNECT_API_KEY_ID: ${{ secrets.APPCONNECT_API_KEY_ID }}
APPCONNECT_API_KEY_PRIVATE: ${{ secrets.APPCONNECT_API_KEY_PRIVATE }}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment