Automate this: /mobile-release:ship-beta for the TestFlight leg, /mobile-release:ship-release
for the production leg — the latter stops for explicit approval before submitting and before any
rollout. Which steps need which credential: skills/release-automation/references/automation-matrix.md.
Take a signed .ipa that already exists and get it to testers and then to the store. This skill starts at the .ipa and ends at a version in review.
Producing the .ipa is not this skill. Certificates, provisioning profiles, xcodebuild archive / -exportArchive, ExportOptions.plist, match and gym belong to ios-build-sign. Do not re-derive signing here — if the artifact is unsigned, ad-hoc signed, or signed with a development identity, stop and hand back.
Confidence marks used throughout this skill and its references: [verified] — read off a primary vendor source on 2026-08-05. [snippet] — secondary source only, plausible but unconfirmed. [unverified] — not confirmed anywhere; run the check given before relying on it.
Publish process:
- Clear the prerequisites below. A missing app record or an unregistered bundle id fails late and with a misleading error.
- Pick one upload path and stay on it — 1. Upload paths, detail in
references/upload-paths.md. - Validate before uploading (
--validate-app). It costs a minute and catches most ITMS errors before they burn a build number. - Upload, then wait for Apple to finish processing. The build is not addressable by any other tool until processing completes.
- Distribute on TestFlight — 2. TestFlight, detail in
references/testflight.md. - Submit the version for review — 3. Submitting for review, endpoints in
references/review-submission-api.md. - On failure, diagnose with 4. Troubleshooting before re-uploading. A re-upload at the same build number cannot succeed, so a blind retry wastes a build number as well as the time.
Run the app-store-review skill before step 6. Upload succeeds long before a submission is compliant.
Load only the reference you need.
Contents
- Prerequisites
- 1. Upload paths
- 2. TestFlight
- 3. Submitting for review
- 4. Troubleshooting
- Core Instructions
- Output Format
- References
Prerequisites
- App Store Connect API key, team-scoped. Users and Access → Integrations → App Store Connect API → +. The
.p8downloads once and Apple keeps no copy[verified]. Record the Key ID and the Issuer ID (one issuer per account, at the top of the Keys tab). Team key, not individual — individual keys cannot use the Provisioning endpoints, Sales and Finance, ornotarytool[verified]. - Put the key where the tools look:
~/private_keys/,~/.private_keys/, or~/.appstoreconnect/private_keys/, namedAuthKey_<KEYID>.p8[verified].altoollocates the file from the key id alone. - App record exists in App Store Connect with this bundle id on it — an upload is matched to an app by bundle id plus version
[verified]. The bundle id is registered in the Developer Portal, explicit (non-wildcard) if the app has IAP or push. - Uploader role: Account Holder, Admin, App Manager, or Developer
[verified]. Never print the.p8contents,MATCH_PASSWORD, or an app-specific password into the transcript or a CI log.
Key creation, the team-vs-individual split, how each tool consumes the key, and every fastlane env var name: references/asc-api-key.md.
1. Upload paths
| Path | Use when | Tradeoff |
|---|---|---|
xcrun altool --upload-app |
Anything scriptable on a Mac with Xcode. The default. | No metadata, no TestFlight group assignment, no submission — binary only. Ships with Xcode, nothing to install. |
xcodebuild -exportArchive with destination = upload |
You are already exporting and want one fewer step | Skips the local .ipa, so you cannot validate or archive the artifact you shipped. ios-build-sign deliberately pins destination to export for exactly that reason — do not turn this on without agreeing to lose the verified artifact. |
fastlane pilot (upload_to_testflight) |
TestFlight, with changelog, groups, and tester notification in one call | Ruby toolchain. Waits on processing by default, which is usually what you want. |
fastlane deliver (upload_to_app_store) |
A full release: binary plus metadata, screenshots, and the review submission | Largest blast radius — it will overwrite store metadata from local files. |
Transporter (Mac App Store id 1450874784) |
A human needs to watch a large upload, or altool is failing opaquely |
GUI, macOS only, not scriptable. Shows delivery progress, warnings, errors, and logs [verified]. |
xcrun notarytool |
Never, for iOS. | notarytool is macOS-distribution notarization. It is not an App Store upload path. See below. |
altool is not deprecated for App Store uploads. Apple deprecated it for notarization only: "Remember that notarytool only replaces the notarization aspects of altool. If you use altool for other tasks, like submitting an app to the App Store, continue to use it for that." [verified: TN3147] Do not "modernise" an altool --upload-app call into notarytool.
# Validate first — same flags, no build number consumed on failure.
xcrun altool --validate-app \
-f build/export/MyApp.ipa \
-t ios \
--apiKey ABC1234567 \
--apiIssuer 6053b7fe-68a8-4acb-89be-165aa6465141 \
--output-format xml
xcrun altool --upload-app \
-f build/export/MyApp.ipa \
-t ios \
--apiKey ABC1234567 \
--apiIssuer 6053b7fe-68a8-4acb-89be-165aa6465141
--validate-app/--upload-app with -f/-t/--output-format are [verified]; --apiKey/--apiIssuer are [verified] as altool credentials, but Apple documents only the -u <username> form on the upload page, so the combination is [snippet]. -t values ios / macos / appletvos / visionos are [unverified] — run xcrun altool --help.
--apiKey takes the key ID, not a path. altool searches the private-keys directories for AuthKey_<KEYID>.p8 [verified].
fastlane, using the JSON key file (see references/asc-api-key.md for its shape):
fastlane pilot distribute --api_key_path fastlane/asc_key.json # [verified: this exact CLI form]
fastlane pilot upload --api_key_path fastlane/asc_key.json --ipa build/MyApp.ipa
fastlane deliver --api_key_path fastlane/asc_key.json --ipa build/MyApp.ipa --force
Only pilot distribute --api_key_path is [verified] as a CLI spelling. Other CLI flags mirror the action option keys, which are verified — but flag spelling is [unverified]; confirm with fastlane pilot --help / fastlane deliver --help.
macOS only, for completeness — notarytool submit, which has nothing to do with TestFlight:
xcrun notarytool submit MyMacApp.zip \
--key ~/private_keys/AuthKey_ABC1234567.p8 \
--key-id ABC1234567 \
--issuer 6053b7fe-68a8-4acb-89be-165aa6465141
submit, --key, --key-id, --issuer are [verified: TN3147]; --wait, store-credentials, --keychain-profile are [unverified].
Complete Fastfile with a beta lane (sign, bump, build, upload to TestFlight, notify testers) and a release lane (build, upload, submit for review, phased release): references/Fastfile, annotated in references/fastlane-lanes.md.
2. TestFlight
| Fact | Value |
|---|---|
| Build test window | 90 days, then the build becomes unavailable to testers [verified] |
| Internal testers | up to 100 App Store Connect users with access to your content [verified] |
| External testers | up to 10,000 people per app [verified] |
| Public link cap | any number between 1 and 10,000 [verified] |
| Beta App Review | "When you add the first build of your app to a group, the build gets sent to App Review" [verified] |
| Internal testing | no Beta App Review [unverified — inferred from the above, not stated] |
| Devices per tester | 30 [unverified] |
Internal means App Store Connect users on your team: instant, no review, and the only destination for a build uploaded as TestFlight Internal Only [verified]. External means anyone with an email invite or a public link, and reaching them requires Beta App Review on the first build of a group. Create an internal group before you can start external testing [verified].
Set the tester-facing text at upload time. In pilot: changelog (PILOT_CHANGELOG) is the "What to Test" note attached to the build; beta_app_description, beta_app_feedback_email, beta_app_review_info, demo_account_required carry the app-level Test Information and the Beta App Review answers [verified: option names]. Pass uses_non_exempt_encryption: false to skip the export-compliance prompt when the app uses only standard HTTPS.
Group semantics, expiring builds, public-link filtering, managed-account exclusions, and the guideline 2.2 rule that you may not compensate testers: references/testflight.md.
3. Submitting for review
Version-level decisions, all set before or at submission:
- Release method — manual (you press Release), automatic on approval, or automatic on a date you pick.
- Phased release — a staged rollout to existing users over roughly a week, with a pause control. The mechanism exists; its exact percentage schedule, its interaction with manual release, and fastlane's option spelling for it are all
[unverified]here. Confirm in App Store Connect before promising a rollout curve. - What's New — required for every version update
[verified], and generic text on a feature release is a 2.3.12 rejection[verified]. - Submission assembly — a version and its in-app purchases go into one review submission. A first-ever IAP must ride in the same submission as the version.
Doing it without fastlane, over the App Store Connect API (JWT signed with the .p8):
| Step | Endpoint | Confidence |
|---|---|---|
| Upload a build | POST /v1/buildUploads |
[verified] |
| Create the review submission | POST /v1/reviewSubmissions |
[unverified] |
| Attach the version to it | POST /v1/reviewSubmissionItems |
[unverified] |
| Submit it | PATCH /v1/reviewSubmissions/{id} with submitted: true |
[unverified] |
| Release an approved version | POST /v1/appStoreVersionReleaseRequests |
[unverified] |
| Drive phased release | PATCH /v1/appStoreVersionPhasedReleases/{id} |
[unverified] |
Only POST /v1/buildUploads was confirmed against an Apple page. Every other row is a plausible path name from training, not a read endpoint. Resolve each one before calling it — the method is in references/review-submission-api.md, and it is the same JSON-endpoint trick the app-store-review skill uses to refresh guideline text.
4. Troubleshooting
Match on the message text, not the code — Apple renumbers, and only the first three codes below appear in this plugin's source material.
| Symptom | Code | Cause | Fix |
|---|---|---|---|
| Invalid App Store Icon | ITMS-90717 [snippet] |
The 1024×1024 icon has an alpha channel or transparency | Flatten to opaque RGB. sips -g hasAlpha icon.png; re-export without alpha |
| Missing / incomplete privacy manifest, or required-reason API undeclared | ITMS-91053, ITMS-91061 [snippet] |
No PrivacyInfo.xcprivacy, an undeclared required-reason API, or a listed third-party SDK shipped without its own manifest and signature |
Add the manifest with an approved reason code; bump the SDK. Codes and the SDK list live in the app-store-review skill |
| Missing purpose string for a permission | ITMS-90683 [unverified] |
A framework that gates on a permission is linked, with no matching NS…UsageDescription in Info.plist |
Add the exact key the framework requires. Every permission the binary can request needs one |
| "The bundle version must be higher than the previously uploaded version" | — | CFBundleVersion collides with or trails an existing upload for this CFBundleShortVersionString |
Bump the build number. Build numbers are never reusable [unverified]. Derive it from app_store_build_number rather than a local counter |
| Unsupported architecture / invalid executable | ITMS-90240 [unverified] |
A simulator slice, an unstripped architecture, or a framework built for the wrong platform is embedded | lipo -info every binary in the payload; rebuild for generic/platform=iOS |
| Missing required icon asset | ITMS-90022 [unverified] |
The asset catalog lacks a required size, or the App Store icon is absent from the archive | Fill the App Icon set; verify the archive, not the source |
| Invalid binary, no explanation, status flips in App Store Connect | — | Processing rejected the build after upload; the reason arrives by email, not in the UI | Read the email, fix, bump the build number, re-upload. The failed build number is spent |
| Build uploaded, never appears | — | Still processing, or the export-compliance answer is pending | Wait for Apple's processed-email; check the build's Manage/Compliance prompt. Set uses_non_exempt_encryption at upload to avoid the prompt |
altool 401 / "Unable to authenticate" |
— | Individual key instead of a team key, wrong issuer id, or the .p8 is not in a searched directory |
Confirm the key is team-scoped; place AuthKey_<KEYID>.p8 in ~/.appstoreconnect/private_keys/ |
Full error strings, diagnostic order, and the validate-first workflow: references/troubleshooting.md.
Core Instructions
- Validate before every upload, and never re-run a failed upload without changing the build number.
- Derive the build number from
app_store_build_number(api_key:), not a git counter — a local counter drifts from what App Store Connect has actually seen. - Prefer the API key over Apple ID and password everywhere: no 2FA, better performance, documented API
[verified]. - Report the version, build number, upload path, and resulting TestFlight/review state after every run.
- Adjacent third-party skills, if installed:
asc-testflight-orchestration,asc-release-flow, andasc-build-lifecyclein theascpack cover this ground through the unofficialascCLI rather than fastlane.
Output Format
Report the upload as a status block, then the distribution state, then the single next action. Do not narrate every HTTP call or paste raw altool XML.
Example output:
App Store upload — com.example.notes
| Field | Value |
|---|---|
| Path | xcrun altool --upload-app (API key ABC1234567) |
| Artifact | build/export/Notes.ipa (42.8 MB) |
| Version | 2.7.0 (CFBundleShortVersionString) |
| Build | 143 (CFBundleVersion) — from app_store_build_number + 1 |
| Validation | passed, 0 errors, 1 warning |
| Upload | accepted, processed after 6 min |
| TestFlight | internal group Core team (9 testers) |
| Review | not submitted |
Warning surfaced by validation: the archive embeds FirebaseCrashlytics 10.4, which predates that SDK's privacy manifest. It uploaded, but a version submission will be held. Bump to a release that ships PrivacyInfo.xcprivacy and a signature before submitting.
Next: run the app-store-review skill, then distribute externally:
fastlane pilot distribute \
--api_key_path fastlane/asc_key.json \
--app_identifier com.example.notes \
--build_number 143 \
--groups "Public beta" \
--notify_external_testers true
Adding build 143 to Public beta sends it to Beta App Review — it is the first build in that group. Budget a day before external testers see it.
End of example.
References
references/asc-api-key.md— creating the key, team vs individual scope, search paths, howxcodebuild/altool/notarytool/fastlane each consume it, everyAPP_STORE_CONNECT_API_KEY_*env var, and the JSON key-file shape.references/upload-paths.md— altool, Transporter, Xcode, the API, and Xcode Cloud side by side; the notarytool correction in full; validation output and what its warnings mean.references/Fastfile— complete, copy-pasteablebetaandreleaselanes.references/fastlane-lanes.md— the same Fastfile annotated line by line, plus the Appfile, the verified pilot and gym option tables, and which options are guesses.references/testflight.md— internal vs external groups, the 90-day expiry, tester and public-link limits, what triggers Beta App Review, Test Information fields, export compliance, and the guideline 2.2 rules.references/review-submission-api.md— JWT construction, the review-submission endpoints with their confidence, release methods, phased release, and how to resolve an endpoint before calling it.references/troubleshooting.md— full ITMS symptom table, the validate-first workflow, and what to do with a spent build number.