Automating notarization and TestFlight delivery on a cloud Mac mini

DevOps & CI/CD ·~6 min read

Automating notarization and TestFlight delivery on a cloud Mac mini

Automating notarization and TestFlight delivery on a cloud Mac mini

It's 2 a.m. Someone on the growth team pings the group chat asking "when's the new build hitting TestFlight," while you're staring at the terminal watching codesign fail for the third time. If even one link in the notarization chain is misconfigured, the whole pipeline quietly stalls right at submission — no error, no pass, just a vague status code. This post documents how we wired signing, notarization, and TestFlight upload into a fully unattended pipeline on a cloud Mac mini node.

Why notarization is a hidden bottleneck for global teams

Since Xcode 13, Apple has deprecated altool's notarization interface in favor of notarytool. Plenty of teams still run CI scripts copied from old tutorials, and if the certificate type, timestamp flag, or entitlements config is off in any way, the notarization request gets silently rejected — with the actual error buried in a JSON log you have to pull separately. Worse, notarization depends on Apple's server-side queue, so you can't locally reproduce "why did this one take 40 minutes longer." The only real lever you have is reducing variables through a standardized process.

Running this pipeline on a cloud Mac mini has a clear upside: certificates, API keys, and keychain state all live on one node that stays online long-term, so you're not re-importing certs every time someone swaps laptops or leaves the team. That's exactly why we lease dedicated physical machines on a recurring basis instead of spinning up a fresh temporary environment each time.

Prerequisites: certificates and API keys

Make sure you have all three of these ready:

  1. Apple Distribution certificate (for App Store distribution) plus its private key, exported as .p12 and imported into the node's keychain.
  2. App Store Connect API Key: generate it on the Users and Access page in App Store Connect, download the .p8 private key file, and note the Key ID and Issuer ID. This is what lets you skip logging into any Apple account entirely — both notarization and upload authenticate via the API key.
  3. Provisioning profile: matched to your Bundle ID and certificate. We recommend managing it centrally with fastlane match instead of generating one on each machine separately.

Import the certificate into the node's keychain:

security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security import DistributionCert.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain
security list-keychains -d user -s build.keychain login.keychain

Place the .p8 file at a fixed path on the node, e.g. ~/.appstoreconnect/private_keys/AuthKey_ABCDE12345.p8, set permissions to 600, and never commit it to any repository.

Going from signing to notarization with notarytool

Signing must include a timestamp and hardened runtime, otherwise notarization will fail outright for lack of a secure timestamp:

codesign --sign "Apple Distribution: Your Company" \
  --timestamp --options runtime \
  --entitlements App.entitlements \
  build/App.app

After packaging as a .pkg or exporting an .ipa via xcodebuild -exportArchive, submit it for notarization:

xcrun notarytool submit build/App.ipa \
  --key ~/.appstoreconnect/private_keys/AuthKey_ABCDE12345.p8 \
  --key-id ABCDE12345 \
  --issuer 69a6de7x-xxxx-47e3-e053-5b8c7c11a4d1 \
  --wait

--wait blocks until Apple's server returns a result — usually 3 to 8 minutes, occasionally 20+ minutes during peak queue times. Once you get the submissionId, if the status comes back Invalid, always pull the detailed log before re-signing on a guess:

xcrun notarytool log <submissionId> \
  --key ~/.appstoreconnect/private_keys/AuthKey_ABCDE12345.p8 \
  --key-id ABCDE12345 --issuer 69a6de7x-xxxx-47e3-e053-5b8c7c11a4d1

One trap we fell into: App Sandbox was enabled in the entitlements, but a third-party dynamic library inside the bundle wasn't signed correctly. The notarytool log only says "The binary is not signed with a valid Developer ID certificate," which looks like a certificate problem but is actually an unsigned submodule. Running codesign --verify --deep --verbose=4 layer by layer is what actually pinpoints the offending file.

Auto-distributing to TestFlight with fastlane pilot

Once notarization passes, use fastlane pilot to upload to TestFlight — also authenticated via API key, no interactive login required:

lane :beta do
  api_key = app_store_connect_api_key(
    key_id: ENV["ASC_KEY_ID"],
    issuer_id: ENV["ASC_ISSUER_ID"],
    key_filepath: ENV["ASC_KEY_PATH"]
  )

  build_app(scheme: "App", export_method: "app-store")

  pilot(
    api_key: api_key,
    skip_waiting_for_build_processing: true,
    changelog: "Automated release: fixed startup crash and network retry logic"
  )
end

skip_waiting_for_build_processing: true lets the script exit right after upload instead of blocking other work on the node. Track processing progress via polling or Apple's email notification — there's no reason to burn CI time sitting around waiting.

Common errors and a troubleshooting checklist

Error keyword Likely cause Fix
signature does not include a secure timestamp codesign was run without --timestamp Re-sign with --timestamp --options runtime
The binary is not signed with a valid Developer ID An embedded framework/dylib wasn't signed Run codesign --verify --deep layer by layer, then sign it individually
Missing Info.plist key: ITSAppUsesNonExemptEncryption Encryption-related APIs are used but undeclared Add the corresponding key to Info.plist, or declare the exemption if applicable
Invalid Provisioning Profile Profile doesn't match the certificate/Bundle ID Re-sync certificates and profiles with fastlane match
No response for a long time after submission Apple's server-side queue is backed up Don't resubmit — wait or poll the submissionId status

Pin this table to your team's troubleshooting doc; it'll save you a lot of "whose fault is this" back-and-forth.

Scheduling the whole thing on a cloud Mac mini

The last step is making the pipeline run itself instead of relying on someone to trigger it manually. Register a launchd job on the cloud Mac mini that responds to a Git webhook or polls on a schedule, chaining signing, notarization, and upload into one run — and on failure, notify the owner via email or a ticketing system rather than letting the script silently exit. Because the node is a dedicated physical machine that stays online, keychain state and certificates don't get reset on every run the way they would in an ephemeral container, which keeps notarization success rates and queue times noticeably more consistent.

Frequently asked questions

notarytool says the signature does not include a secure timestamp, what now?

Your signing step skipped a timestamp server. Re-sign with codesign --timestamp --options runtime and resubmit; older codesign flags won't add it automatically.

Is it normal for TestFlight to stay in Processing for a long time?

Apple usually processes builds in 15-60 minutes. Past 2 hours, check Activity in App Store Connect for a rejection email — a missing ITSAppUsesNonExemptEncryption key is a frequent cause of manual review delays.

Do I need to sign in with an Apple account on the cloud Mac mini?

No. Import your Developer ID/Apple Distribution certificate into the node's keychain and place the App Store Connect API key .p8 file plus env vars on the box — the whole pipeline runs headless.

Need a dedicated Mac mini for your builds?

Physical nodes in Singapore, Tokyo, Seoul, Hong Kong, and Silicon Valley, billed by the day — get your VNC/SSH credentials in under 10 minutes.

Rent a Mac mini now