Skip to content

Release — Archive, Notarize & Staple #54

Release — Archive, Notarize & Staple

Release — Archive, Notarize & Staple #54

Workflow file for this run

# 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