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:
- Apple Distribution certificate (for App Store distribution) plus its private key, exported as
.p12and imported into the node's keychain. - App Store Connect API Key: generate it on the Users and Access page in App Store Connect, download the
.p8private 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. - Provisioning profile: matched to your Bundle ID and certificate. We recommend managing it centrally with
fastlane matchinstead 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=4layer 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