macos-notarization-workflow
Automate Apple notarization workflow using xcrun notarytool for macOS applications. This skill handles the complete notarization process including submission, status checking, and stapling.
Capabilities
- Submit apps for notarization via notarytool
- Monitor notarization status
- Staple notarization ticket to app
- Handle notarization errors
- Generate CI/CD notarization scripts
- Configure App Store Connect API keys
- Validate apps before submission
- Generate notarization reports
Input Schema
{
"type": "object",
"properties": {
"projectPath": {
"type": "string",
"description": "Path to the project"
},
"appPath": {
"type": "string",
"description": "Path to the signed app bundle or DMG"
},
"authMethod": {
"enum": ["app-store-connect-api", "apple-id", "keychain"],
"default": "app-store-connect-api"
},
"credentials": {
"type": "object",
"properties": {
"keyId": { "type": "string" },
"issuerId": { "type": "string" },
"keyPath": { "type": "string" },
"appleId": { "type": "string" },
"teamId": { "type": "string" }
}
},
"waitForCompletion": {
"type": "boolean",
"default": true
},
"staple": {
"type": "boolean",
"default": true
}
},
"required": ["projectPath", "appPath"]
}
Output Schema
{
"type": "object",
"properties": {
"success": { "type": "boolean" },
"submissionId": { "type": "string" },
"status": { "enum": ["Accepted", "Invalid", "In Progress", "Rejected"] },
"logUrl": { "type": "string" },
"errors": { "type": "array" },
"stapled": { "type": "boolean" }
},
"required": ["success"]
}
Notarization Workflow
1. Prerequisites
# Ensure Xcode command line tools are installed
xcode-select --install
# Verify code signing
codesign --verify --deep --strict MyApp.app
codesign -vvv --deep --strict MyApp.app
# Check hardened runtime
codesign -dvvv MyApp.app | grep runtime
# Should show: flags=0x10000(runtime)
2. Store Credentials (Recommended)
# Store App Store Connect API key in keychain
xcrun notarytool store-credentials "MyProfile" \
--key ~/private_keys/AuthKey_XXXXXXXXXX.p8 \
--key-id XXXXXXXXXX \
--issuer xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Or store Apple ID credentials
xcrun notarytool store-credentials "MyAppleIDProfile" \
--apple-id your.email@example.com \
--team-id XXXXXXXXXX \
--password @keychain:AC_PASSWORD
3. Submit for Notarization
# Using stored credentials
xcrun notarytool submit MyApp.app \
--keychain-profile "MyProfile" \
--wait
# Using API key directly
xcrun notarytool submit MyApp.app \
--key ~/private_keys/AuthKey_XXXXXXXXXX.p8 \
--key-id XXXXXXXXXX \
--issuer xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
--wait
# Using Apple ID
xcrun notarytool submit MyApp.app \
--apple-id your.email@example.com \
--team-id XXXXXXXXXX \
--password @keychain:AC_PASSWORD \
--wait
4. Check Status
# Check specific submission
xcrun notarytool info <submission-id> \
--keychain-profile "MyProfile"
# Get submission log
xcrun notarytool log <submission-id> \
--keychain-profile "MyProfile" \
developer_log.json
# List recent submissions
xcrun notarytool history \
--keychain-profile "MyProfile"
5. Staple Ticket
# Staple to app bundle
xcrun stapler staple MyApp.app
# Staple to DMG
xcrun stapler staple MyApp.dmg
# Staple to pkg
xcrun stapler staple MyApp.pkg
# Validate stapling
xcrun stapler validate MyApp.app
Complete Script
#!/bin/bash
# notarize.sh
set -e
APP_PATH="${1}"
KEYCHAIN_PROFILE="${2:-MyProfile}"
echo "=== Validating app bundle ==="
codesign --verify --deep --strict "$APP_PATH"
echo "=== Submitting for notarization ==="
SUBMISSION_OUTPUT=$(xcrun notarytool submit "$APP_PATH" \
--keychain-profile "$KEYCHAIN_PROFILE" \
--wait \
--output-format json)
SUBMISSION_ID=$(echo "$SUBMISSION_OUTPUT" | jq -r '.id')
STATUS=$(echo "$SUBMISSION_OUTPUT" | jq -r '.status')
echo "Submission ID: $SUBMISSION_ID"
echo "Status: $STATUS"
if [ "$STATUS" != "Accepted" ]; then
echo "=== Notarization failed, fetching log ==="
xcrun notarytool log "$SUBMISSION_ID" \
--keychain-profile "$KEYCHAIN_PROFILE" \
notarization_log.json
cat notarization_log.json
exit 1
fi
echo "=== Stapling ticket ==="
xcrun stapler staple "$APP_PATH"
echo "=== Validating staple ==="
xcrun stapler validate "$APP_PATH"
echo "=== Notarization complete ==="
GitHub Actions Integration
name: Build and Notarize
on:
push:
tags: ['v*']
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Import signing certificate
env:
CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE }}
CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
echo -n "$CERTIFICATE_BASE64" | base64 --decode > $CERTIFICATE_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 $CERTIFICATE_PATH -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Build app
run: |
xcodebuild -project MyApp.xcodeproj \
-scheme MyApp \
-configuration Release \
-archivePath build/MyApp.xcarchive \
archive
xcodebuild -exportArchive \
-archivePath build/MyApp.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath build/
- name: Store notarization credentials
env:
API_KEY: ${{ secrets.NOTARIZATION_API_KEY }}
API_KEY_ID: ${{ secrets.NOTARIZATION_API_KEY_ID }}
API_ISSUER: ${{ secrets.NOTARIZATION_API_ISSUER }}
run: |
mkdir -p ~/private_keys
echo -n "$API_KEY" > ~/private_keys/AuthKey.p8
xcrun notarytool store-credentials "CI_PROFILE" \
--key ~/private_keys/AuthKey.p8 \
--key-id "$API_KEY_ID" \
--issuer "$API_ISSUER"
- name: Notarize app
run: |
xcrun notarytool submit build/MyApp.app \
--keychain-profile "CI_PROFILE" \
--wait
xcrun stapler staple build/MyApp.app
- name: Create DMG
run: |
create-dmg build/MyApp.app build/
xcrun notarytool submit build/*.dmg \
--keychain-profile "CI_PROFILE" \
--wait
xcrun stapler staple build/*.dmg
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: MyApp
path: build/*.dmg
Common Issues
Issue: Hardened runtime not enabled
Error: The signature does not include a secure timestamp.
Fix: Sign with hardened runtime and timestamp:
codesign --force --options runtime --timestamp --sign "Developer ID" MyApp.app
Issue: Missing entitlements
Error: The executable does not have the hardened runtime enabled.
Fix: Include entitlements in signing:
codesign --force --options runtime --timestamp \
--entitlements MyApp.entitlements \
--sign "Developer ID Application: Company" MyApp.app
Issue: Unsigned nested code
Error: The signature of the binary is invalid.
Fix: Sign all nested components:
find MyApp.app -name "*.dylib" -o -name "*.framework" | \
xargs -I {} codesign --force --options runtime --timestamp --sign "Developer ID" {}
Best Practices
- Use App Store Connect API: More reliable than Apple ID
- Store credentials securely: Use keychain profiles
- Validate before submitting: codesign --verify
- Always staple: Makes offline verification possible
- Archive submission logs: For debugging
- Test on fresh Mac: Verify Gatekeeper acceptance
Related Skills
macos-entitlements-generator- Entitlements configurationmacos-codesign-workflow- Code signingcode-signing-setupprocess - Full signing workflow
Related Agents
swiftui-macos-expert- macOS developmentcode-signing-specialist- Signing expertise