Skip to main content

Automating macOS App Notarization: Migrating from altool to xcrun notarytool

 If your automated build pipelines recently started failing with an altool not found or an unsupported endpoint error, you have encountered Apple's hard deprecation of the legacy notarization service. In Xcode 15 and later, Apple completely removed altool from the toolchain.

Builds that previously sailed through your pipeline will now crash during the submission phase, effectively halting macOS CI/CD deployment. This failure requires an immediate migration to its modern successor: xcrun notarytool.

This guide provides a comprehensive xcrun notarytool migration path. It covers the architectural differences, the authentication changes, and the exact pipeline code required to restore your macOS app notarization process.

The Root Cause: Why Did Apple Deprecate altool?

To understand the altool deprecated fix, you must understand the underlying API shift. Historically, xcrun altool operated against Apple's legacy XML-based App Store Connect endpoints. This system was originally built for iOS App Store submissions and was retrofitted to handle macOS Gatekeeper notarization.

The legacy endpoint required uploading the entire application binary via slow, synchronous REST calls. Because notarization takes time, altool required developers to implement custom polling scripts. You had to request a RequestUUID, sleep the CI runner, and repeatedly ping Apple's servers until a success or failure state was returned.

Apple replaced this infrastructure with the specialized Notary API. The new backend utilizes Amazon S3 for direct, parallelized, chunked uploads. xcrun notarytool is the dedicated CLI built to interface with this modern API. It is significantly faster, inherently resilient to network drops, and eliminates the need for manual polling scripts.

The Fix: Implementing xcrun notarytool in CI/CD

Migrating your macOS CI/CD deployment requires updating both your authentication strategy and your execution scripts. notarytool supports App-Specific Passwords, but transitioning to an App Store Connect API Key (JWT) is the industry standard for automated, headless environments.

Step 1: Provisioning the App Store Connect API Key

Unlike App-Specific Passwords, API keys do not expire and are not tied to an individual developer's Apple ID. This prevents pipelines from breaking when a team member leaves the organization.

  1. Navigate to App Store Connect > Users and Access > Keys.
  2. Generate a new key with the Developer or App Manager role.
  3. Download the .p8 private key file. Note your Issuer ID and the Key ID.

For CI/CD security, store the contents of the .p8 file as a base64-encoded environment variable (e.g., APP_STORE_CONNECT_API_KEY_BASE64) in your secret manager (GitHub Actions Secrets, AWS Secrets Manager, or GitLab CI Variables).

Step 2: The Notarization Script

The following Bash script demonstrates a robust xcrun notarytool migration. It decodes the private key, submits the app, waits for the result, and cleans up the credentials.

#!/bin/bash
set -e

# Environment variables expected from CI/CD:
# APP_STORE_CONNECT_API_KEY_BASE64
# APP_STORE_CONNECT_KEY_ID
# APP_STORE_CONNECT_ISSUER_ID
# APP_TARGET_PATH (e.g., "build/MyApp.zip" or "build/MyApp.dmg")

echo "Starting macOS app notarization..."

# 1. Decode the p8 key from CI secrets into a temporary file
AUTH_KEY_PATH=$(mktemp -d)/AuthKey_${APP_STORE_CONNECT_KEY_ID}.p8
echo -n "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode > "$AUTH_KEY_PATH"

# 2. Submit to Notary API with synchronous waiting
echo "Submitting ${APP_TARGET_PATH} to Notary backend..."

xcrun notarytool submit "$APP_TARGET_PATH" \
  --key "$AUTH_KEY_PATH" \
  --key-id "$APP_STORE_CONNECT_KEY_ID" \
  --issuer "$APP_STORE_CONNECT_ISSUER_ID" \
  --wait

# 3. Securely remove the private key
rm "$AUTH_KEY_PATH"

echo "Notarization successful."

Step 3: Stapling the Ticket

Notarization simply tells Apple's servers that your app is safe. To ensure the app can be launched offline without Gatekeeper contacting Apple's servers, you must "staple" the notarization ticket directly to the binary.

Add the stapler command immediately after a successful notarytool submission:

# 4. Staple the ticket to the artifact
# Note: You can staple .dmg, .pkg, or .app, but NOT .zip files.
if [[ "$APP_TARGET_PATH" != *.zip ]]; then
  echo "Stapling notarization ticket..."
  xcrun stapler staple "$APP_TARGET_PATH"
else
  echo "Skipping stapling for .zip archive."
fi

Deep Dive: Architectural Improvements in notarytool

The script above highlights a major architectural improvement: the --wait flag.

Under the hood, xcrun notarytool submit --wait maintains a persistent, lightweight connection to the Notary API. It automatically handles the polling logic, respecting Apple's rate limits and backoff requirements. If the process is interrupted, notarytool caches the upload state in ~/Library/Caches/com.apple.notarytool, allowing for seamless resumes on persistent CI runners.

Furthermore, notarytool natively parses the JSON response from the Notary backend. If the binary is rejected, notarytool immediately prints the localized error log directly to stdout. With altool, fetching failure logs required executing a secondary command (altool --notarization-info) and manually parsing a remote URL to download the rejection report.

Common Pitfalls and Edge Cases

1. Zipping Application Bundles Incorrectly

When notarizing an .app bundle directly, it must be zipped. Standard zip -r commands frequently break symlinks and strip critical internal file permissions, leading to immediate invalid signature rejections from the Notary API.

Always use /usr/bin/ditto to preserve the exact file structure before running notarytool:

# Correct way to archive an .app bundle for notarization
/usr/bin/ditto -c -k --keepParent "MyApp.app" "MyApp.zip"

2. Persistent Runner Keychain Conflicts

If you manage a fleet of persistent self-hosted Mac mini runners, you might prefer storing credentials locally rather than injecting them via environment variables. notarytool supports a local keychain profile.

You can configure the profile once per runner:

xcrun notarytool store-credentials "CI_PROFILE" \
  --apple-id "ci-bot@company.com" \
  --team-id "TEAMID123" \
  --password "app-specific-password"

You can then run the submission step cleanly:

xcrun notarytool submit "MyApp.dmg" --keychain-profile "CI_PROFILE" --wait

Warning: Do not use this method on ephemeral cloud runners (like default GitHub-hosted runners), as the keychain is destroyed after every job, resulting in authentication failures.

3. Updating Fastlane Configurations

If your macOS CI/CD deployment utilizes Fastlane, relying on the legacy notarize action will trigger the altool not found error. Ensure you have updated Fastlane to version 2.210.0 or higher.

Fastlane has seamlessly transitioned its internal logic. You only need to swap the notarize action for the modern notarize_app action, which invokes notarytool automatically.

Conclusion

Migrating to xcrun notarytool is a mandatory step for modern macOS development. By deprecating altool, Apple forced the adoption of a faster, more reliable infrastructure. Implementing API Key authentication, utilizing the --wait parameter, and properly packaging your artifacts with ditto will completely stabilize your CI/CD delivery pipelines against Gatekeeper restrictions.