Release — Archive, Notarize & Staple #54
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Release — Archive, Notarize & Staple | |
| # | |
| # Runs when a GitHub Release is published (or manually via workflow_dispatch). | |
| # Archives the Agent! scheme with Xcode, exports a Developer ID build, | |
| # notarizes it with Apple, staples the ticket, and uploads a stapled | |
| # .zip and .dmg to the release. | |
| # | |
| # REQUIRED REPOSITORY SECRETS (Settings → Secrets and variables → Actions): | |
| # MACOS_CERTIFICATE_P12 Base64 of the "Developer ID Application" cert + private key (.p12) | |
| # export from Keychain Access, then: base64 -i cert.p12 | pbcopy | |
| # MACOS_CERTIFICATE_PASSWORD Password used when exporting the .p12 | |
| # MACOS_PROVISIONING_PROFILE Base64 of the "Mac Team Direct" (Developer ID) provisioning | |
| # profile for Agent.app.toddbruss (needed for keychain-access-groups) | |
| # MACOS_DEV_CERTIFICATE_P12 Base64 of the "Apple Development" cert + key (.p12) — automatic | |
| # archive signing, mirrors the local Xcode/ArchiveXcode flow | |
| # MACOS_DEV_CERTIFICATE_PASSWORD Password for the dev .p12 | |
| # MACOS_DEV_PROVISIONING_PROFILE Base64 of "Mac Team Provisioning Profile: Agent.app.toddbruss" | |
| # APPLE_ID Apple ID email used for notarization | |
| # APPLE_APP_SPECIFIC_PASSWORD App-specific password (appleid.apple.com → Sign-In and Security) | |
| # | |
| # Team ID (469UCUB275) matches ExportOptions.plist and is not secret. | |
| name: Release — Archive, Notarize & Staple | |
| on: | |
| release: | |
| types: [published] | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: "Existing release tag to build and attach artifacts to (e.g. v1.0.93)" | |
| required: true | |
| permissions: | |
| contents: write # needed to upload assets to the release | |
| env: | |
| SCHEME: "Agent!" | |
| APP_NAME: "Agent!" | |
| TEAM_ID: "469UCUB275" | |
| jobs: | |
| release: | |
| name: Archive → Notarize → Staple → Upload | |
| runs-on: xcode-27 # macOS 27 + Xcode 27 (Swift 6.4) — TypeSafeKit needs swift-tools 6.4 | |
| timeout-minutes: 90 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.release.tag_name || github.event.inputs.tag }} | |
| - name: Select latest Xcode | |
| run: | | |
| sudo xcode-select -s "$(ls -d /Applications/Xcode*.app | sort -V | tail -1)/Contents/Developer" | |
| xcodebuild -version | |
| - name: Import signing certificates into temporary keychain | |
| env: | |
| MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }} | |
| MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} | |
| MACOS_DEV_CERTIFICATE_P12: ${{ secrets.MACOS_DEV_CERTIFICATE_P12 }} | |
| MACOS_DEV_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_DEV_CERTIFICATE_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| KEYCHAIN_PASSWORD="$(uuidgen)" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| # Developer ID Application (export/notarization) | |
| echo "$MACOS_CERTIFICATE_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12" | |
| security import "$RUNNER_TEMP/cert.p12" -k "$KEYCHAIN_PATH" \ | |
| -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security | |
| # Apple Development (automatic archive signing, same as local Xcode) | |
| echo "$MACOS_DEV_CERTIFICATE_P12" | base64 --decode > "$RUNNER_TEMP/devcert.p12" | |
| security import "$RUNNER_TEMP/devcert.p12" -k "$KEYCHAIN_PATH" \ | |
| -P "$MACOS_DEV_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db | |
| rm -f "$RUNNER_TEMP/cert.p12" "$RUNNER_TEMP/devcert.p12" | |
| security find-identity -v -p codesigning "$KEYCHAIN_PATH" | |
| - name: Install provisioning profiles | |
| env: | |
| MACOS_PROVISIONING_PROFILE: ${{ secrets.MACOS_PROVISIONING_PROFILE }} | |
| MACOS_DEV_PROVISIONING_PROFILE: ${{ secrets.MACOS_DEV_PROVISIONING_PROFILE }} | |
| run: | | |
| set -euo pipefail | |
| PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles" | |
| XCODE_PROFILE_DIR="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles" | |
| mkdir -p "$PROFILE_DIR" "$XCODE_PROFILE_DIR" | |
| for VAR in MACOS_PROVISIONING_PROFILE MACOS_DEV_PROVISIONING_PROFILE; do | |
| printenv "$VAR" | base64 --decode > "$RUNNER_TEMP/p.provisionprofile" | |
| UUID="$(security cms -D -i "$RUNNER_TEMP/p.provisionprofile" | plutil -extract UUID raw -o - -)" | |
| NAME="$(security cms -D -i "$RUNNER_TEMP/p.provisionprofile" | plutil -extract Name raw -o - -)" | |
| cp "$RUNNER_TEMP/p.provisionprofile" "$PROFILE_DIR/$UUID.provisionprofile" | |
| cp "$RUNNER_TEMP/p.provisionprofile" "$XCODE_PROFILE_DIR/$UUID.provisionprofile" | |
| echo "Installed profile: $NAME ($UUID)" | |
| done | |
| rm -f "$RUNNER_TEMP/p.provisionprofile" | |
| - name: Archive (automatic signing, same as local ArchiveXcode flow) | |
| run: | | |
| set -euo pipefail | |
| xcodebuild \ | |
| -project Agent.xcodeproj \ | |
| -scheme "$SCHEME" \ | |
| -configuration Release \ | |
| -destination 'generic/platform=macOS' \ | |
| -archivePath "$RUNNER_TEMP/Agent.xcarchive" \ | |
| archive | |
| - name: Export with Developer ID (automatic, same plist ArchiveXcode generates) | |
| run: | | |
| set -euo pipefail | |
| # ExportOptions.plist is gitignored (generated locally by ArchiveXcode) — recreate it | |
| cat > "$RUNNER_TEMP/ExportOptions.plist" <<'PLIST' | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>method</key> | |
| <string>developer-id</string> | |
| <key>teamID</key> | |
| <string>469UCUB275</string> | |
| <key>signingStyle</key> | |
| <string>automatic</string> | |
| </dict> | |
| </plist> | |
| PLIST | |
| xcodebuild -exportArchive \ | |
| -archivePath "$RUNNER_TEMP/Agent.xcarchive" \ | |
| -exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \ | |
| -exportPath "$RUNNER_TEMP/export" | |
| ls -la "$RUNNER_TEMP/export" | |
| - name: Notarize app | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| APP="$RUNNER_TEMP/export/$APP_NAME.app" | |
| ditto -c -k --keepParent "$APP" "$RUNNER_TEMP/notarize.zip" | |
| SUBMIT_JSON="$(xcrun notarytool submit "$RUNNER_TEMP/notarize.zip" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --team-id "$TEAM_ID" \ | |
| --wait --timeout 45m \ | |
| --output-format json)" | |
| echo "$SUBMIT_JSON" | |
| STATUS="$(echo "$SUBMIT_JSON" | /usr/bin/plutil -extract status raw -o - - 2>/dev/null || echo unknown)" | |
| SUBMISSION_ID="$(echo "$SUBMIT_JSON" | /usr/bin/plutil -extract id raw -o - - 2>/dev/null || true)" | |
| if [ "$STATUS" != "Accepted" ]; then | |
| echo "Notarization failed (status: $STATUS) — fetching log:" | |
| [ -n "$SUBMISSION_ID" ] && xcrun notarytool log "$SUBMISSION_ID" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --team-id "$TEAM_ID" || true | |
| exit 1 | |
| fi | |
| - name: Staple app + verify | |
| run: | | |
| set -euo pipefail | |
| APP="$RUNNER_TEMP/export/$APP_NAME.app" | |
| # Apple's CloudKit ticket service sometimes returns 503 — retry instead of failing the release | |
| for i in 1 2 3 4 5 6; do | |
| xcrun stapler staple "$APP" && xcrun stapler validate "$APP" && break | |
| [ "$i" = 6 ] && exit 1 | |
| echo "stapler attempt $i failed — retrying in 60s"; sleep 60 | |
| done | |
| spctl -a -vvv --type execute "$APP" | |
| - name: Package stapled artifacts (.zip + .dmg) | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ github.event.release.tag_name || github.event.inputs.tag }}" | |
| APP="$RUNNER_TEMP/export/$APP_NAME.app" | |
| OUT="$RUNNER_TEMP/artifacts" | |
| mkdir -p "$OUT" | |
| # Stapled zip | |
| ditto -c -k --keepParent "$APP" "$OUT/Agent-$TAG-macOS.zip" | |
| # DMG containing the stapled app, then notarize + staple the DMG itself | |
| STAGE="$RUNNER_TEMP/dmg-stage" | |
| mkdir -p "$STAGE" | |
| cp -R "$APP" "$STAGE/" | |
| ln -s /Applications "$STAGE/Applications" | |
| hdiutil create -volname "$APP_NAME" -srcfolder "$STAGE" -ov -format UDZO \ | |
| "$OUT/Agent-$TAG-macOS.dmg" | |
| xcrun notarytool submit "$OUT/Agent-$TAG-macOS.dmg" \ | |
| --apple-id "$APPLE_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --team-id "$TEAM_ID" \ | |
| --wait --timeout 45m | |
| for i in 1 2 3 4 5 6; do | |
| xcrun stapler staple "$OUT/Agent-$TAG-macOS.dmg" && xcrun stapler validate "$OUT/Agent-$TAG-macOS.dmg" && break | |
| [ "$i" = 6 ] && exit 1 | |
| echo "stapler attempt $i failed — retrying in 60s"; sleep 60 | |
| done | |
| - name: Upload artifacts to the release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ github.event.release.tag_name || github.event.inputs.tag }}" | |
| gh release upload "$TAG" \ | |
| "$RUNNER_TEMP/artifacts/Agent-$TAG-macOS.zip" \ | |
| "$RUNNER_TEMP/artifacts/Agent-$TAG-macOS.dmg" \ | |
| --clobber | |
| - name: Clean up keychain | |
| if: always() | |
| run: | | |
| security delete-keychain "$RUNNER_TEMP/build.keychain-db" 2>/dev/null || true |