SuperAPI AI Directory

app-store-publish

Deployment & operations

View repository

Updated 10-16 18:29

About this skill

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:

  1. Clear the prerequisites below. A missing app record or an unregistered bundle id fails late and with a misleading error.
  2. Pick one upload path and stay on it — 1. Upload paths, detail in references/upload-paths.md.
  3. Validate before uploading (--validate-app). It costs a minute and catches most ITMS errors before they burn a build number.
  4. Upload, then wait for Apple to finish processing. The build is not addressable by any other tool until processing completes.
  5. Distribute on TestFlight — 2. TestFlight, detail in references/testflight.md.
  6. Submit the version for review — 3. Submitting for review, endpoints in references/review-submission-api.md.
  7. 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 .p8 downloads 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, or notarytool [verified].
  • Put the key where the tools look: ~/private_keys/, ~/.private_keys/, or ~/.appstoreconnect/private_keys/, named AuthKey_<KEYID>.p8 [verified]. altool locates 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 .p8 contents, 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, and asc-build-lifecycle in the asc pack cover this ground through the unofficial asc CLI 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, how xcodebuild/altool/notarytool/fastlane each consume it, every APP_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-pasteable beta and release lanes.
  • 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.