Apple App Delivery
This changes production data at Apple. Read and plan first. For stable Xcode, default to
APP_STORE: stage the build in TestFlight first, then keep the same verified build eligible
for a separately approved App Review and App Store release. Treat archive, upload,
metadata, external Beta Review, App Review submission, and public release as separate
approval gates. Select TESTFLIGHT_INTERNAL_ONLY only when the operator explicitly asks
for an internal-only build. That opt-in path may use the tightly bounded automation below;
it never authorizes external testing or App Store delivery.
Session start
Do this before reading anything else. It decides whether this is a first run or a resumed release, and it prevents asking the operator for values that can be derived.
1. Verify prerequisites
node --version
xcodebuild -version
node <skill-dir>/scripts/credential-check.mjs identity
node <skill-dir>/scripts/asc-api.mjs self-test
credential-check.mjs identity reports the resolved Key ID, Issuer ID, and key path
without printing key material. If it fails, setup is incomplete: name the missing piece,
point at the matching section of README.md, and stop. Never ask the operator to paste
key contents, and never create, move, or chmod a key on their behalf.
Once identity resolves, confirm the key file and basic API access.
node <skill-dir>/scripts/credential-check.mjs validate
node <skill-dir>/scripts/asc-api.mjs request GET \
'/v1/apps?limit=1&fields%5Bapps%5D=name%2CbundleId'
2. Determine whether a release is already in progress
Resolve a release manifest path from the conversation, repository release instructions,
or the deterministic private release directory selected for this release before asking
the operator. For a brief TestFlight or App Store delivery request with no manifest, follow
references/brief-testflight-request.md and create a new private manifest only after
checking the derived app/version/build against live Apple state. Ask for a path only when
there are multiple plausible resumable releases. This skill keeps no state of its own:
the manifest and the provenance receipt are the technical state. They never preserve
release authorization or an intended stop point. When resumed work has no current
conversation or repository instruction stating where to stop, ask before advancing beyond
the current phase; never infer permission from an APP_STORE scope.
When a manifest exists, read it and run the validator for each phase in order. Treat the
first failing phase as the current position, then confirm that position against live state
with asc-release.mjs status before proposing anything. Never infer the phase from the
conversation alone.
When build.provenancePath is set, read that receipt before any other action.
node <skill-dir>/scripts/upload-provenance.mjs read \
--file /absolute/path/upload-provenance.json
A receipt with uploadCompleted=false means an earlier upload stopped after reserving. Go
to references/failure-runbook.md and do not upload again.
3. Check that the app exists before planning a release
node <skill-dir>/scripts/asc-release.mjs app-record-guide --bundle-id com.example.app
This is read-only. It reports whether the bundle ID is registered, which capabilities it carries, whether an App Store Connect app record exists, and whether the signing assets a first build needs are in place. When the record is missing it returns a filled-in walkthrough. Hand that walkthrough to the operator as instructions, including the values they must type and the fields they can never change afterwards, then wait and re-run it rather than guessing that the record appeared.
Never present the missing app record as a skill limitation to work around. The App Store Connect API has no endpoint that creates one, so it is a genuine platform boundary. It is one of exactly two manual steps: the app record, which is always manual, and the signing assets for a bundle ID that has never been built on this machine, described under "Provisioning". Everything else in the flow is automated behind a gate, so treat any other "do it in the web interface" instinct as a bug in the plan.
4. On a first run, resolve the production boundary
For the default stable workflow, set the maximum distribution scope to APP_STORE. Ask
only for an unresolved bundle ID and where this release should stop: internal TestFlight,
external TestFlight, App Review, or production release. Do not ask the operator to choose a
distribution scope unless they request an internal-only/TestFlight-only boundary or a
prerelease Xcode route.
Then derive everything in the table below and present a draft manifest for confirmation. Ask for the remaining human-only values when the phase that needs them is reached, not up front.
A brief request to upload or deliver the current app to TestFlight is a special case. When
references/brief-testflight-request.md resolves every required identity uniquely, select
APP_STORE, set testFlightInternalTestingOnly=false, and use internal TestFlight as the
current checkpoint. This keeps the exact build available for a later App Store continuation
without treating the TestFlight request as approval to submit or release it. If the request
explicitly includes App Store delivery, keep APP_STORE and use the requested App Review
or production stop point, while preserving every later approval gate. Only explicit wording
such as "internal TestFlight only" selects TESTFLIGHT_INTERNAL_ONLY.
Derive, do not ask
Never ask the operator for a value in this table. They generally cannot answer it, and a guessed value fails closed later.
| Manifest field | Derive from |
|---|---|
app.appId |
asc-release.mjs status --bundle-id ID |
app.teamId |
xcodebuild -showBuildSettings DEVELOPMENT_TEAM when a source root is given; otherwise confirm with the operator |
toolchain.expectedXcodeProductVersion, toolchain.expectedXcodeBuild |
xcodebuild -version under the chosen DEVELOPER_DIR |
toolchain.expectedSdkVersion |
xcodebuild -version -sdk <platform sdk> SDKVersion. This is the canonical version that DTSDKName encodes; never use ProductVersion, which differs |
toolchain.expectedSdkBuild |
for APP_STORE, xcodebuild -version -sdk <platform sdk> ProductBuildVersion; for either TestFlight-only scope, null |
toolchain.expectedPlatformBuild |
for APP_STORE, the policy entry's storeBuildMetadata[platform].platformBuild; for either TestFlight-only scope, null. xcodebuild does not report it, and an App Store archive's DTPlatformBuild is verified against it after the build |
toolchain.channel, toolchain.policyEntryId |
toolchain-policy.mjs inspect output entry.channel and entry.id |
testFlight.groupIds |
status betaGroups |
testFlight.localizations, appStore.localizations |
status, when a prior version exists |
appStore.copyright, appStore.releaseType, appStore.phasedRelease |
status, from the existing version |
build.appStoreConnectBuildId |
asc-release.mjs wait-build, after upload |
build.provenancePath |
the --provenance-output path chosen for that upload |
asc-release.mjs init-manifest performs the whole derivation in one read-only step and
writes a draft manifest with mode 0600, refusing to overwrite an existing file:
node <skill-dir>/scripts/asc-release.mjs init-manifest \
--bundle-id com.example.app --platform IOS \
--out /absolute/private/release-work/release.json \
--developer-dir /Applications/Xcode.app/Contents/Developer \
--distribution-scope APP_STORE
It fills the derivable fields, leaves every human-only field null, and prints an initial
list of human inputs. The validator for the intended phase remains authoritative. Omitting
--developer-dir leaves toolchain.* null.
Omitting --distribution-scope selects and records APP_STORE; pass a TestFlight-only
scope only after the operator explicitly chooses that narrower boundary.
Derive the toolchain values by measuring the machine, then pass them through
toolchain-policy.mjs inspect and use its result. Never copy the example values in
README.md or the policy file into a manifest without measuring, and stop fail-closed on
any mismatch rather than adjusting the manifest to match the machine.
For a qualified brief request, references/brief-testflight-request.md may resolve the
marketing version, build number, and artifact or source coordinates. Outside that resolver,
ask for those values rather than guessing them. Never infer a TestFlight-only scope. The
following items still require a person when the phase that needs them is reached:
- Local screenshot directories per display type
review.demoAccountRequiredandreview.demoCredentialReference- All seven
complianceflags, each confirmed individually by a person review.contact.emailandreview.contact.phone.statusredacts these as PII, so they cannot be derived even when they already exist at Apple.- Every TestFlight tester address. Never infer one from Git history, a commit author, or the operator's own account. An invitation reaches a real person and cannot be unsent.
New app with no prior version
For an app that has never shipped, status returns no localizations, no editable version,
and possibly no beta groups. That is expected, not an error. There is nothing to derive for
metadata, so collect it from the operator when the App Review phase is reached. If the
bundle ID does not resolve to an app at all, stop: the API cannot create an app record, and
the operator must create it in App Store Connect first.
Start here
After "Session start", before executing anything:
- Read
references/setup.mdandreferences/workflow.mdend to end. - Also read
references/prerelease-xcode.mdfor any Xcode beta or TestFlight-only scope. - Also read
references/metadata.mdfor external TestFlight or App Review. - Also read
references/failure-runbook.mdfor errors and resumed work. - Check Apple's official OpenAPI spec, upload requirements, and App Store Connect release notes live. Never prefer a cached endpoint over the current spec.
- When working in the user's app repository, follow that repository's instructions and signing configuration. Never write secrets or release state into this skill folder.
- If the operator uses a brief request such as "upload this app to TestFlight", read
references/brief-testflight-request.mdend to end and resolve its inputs before any mutation. - For any exact or repository-derived stable build whose operator explicitly selected
TESTFLIGHT_INTERNAL_ONLYas its maximum scope and internal TestFlight as its stop point, readreferences/internal-testflight-automation.mdend to end before deciding whether that request qualifies as up-front authorization.
Safety boundaries
- Keep
.p8contents, JWTs,Authorizationheaders, signed upload URLs, and demo account passwords out of chat, command output, audit logs, and Git. Accept private keys only as local absolute paths, and never inside the app repository or the archive--source-root. - Do not create or infer the initial app record, contract acceptance, tax and banking details, App Privacy disclosures, age ratings, content rights, or export compliance legal determinations on App Store Connect.
- Never identify a target by display name alone. Resolve the app ID from the bundle ID and confirm platform, version, build number, build ID, group ID, and version ID against live responses.
- Always re-fetch the same target immediately before writing. Never treat a
409as success, and never blindly retry a POST. - Default the release type to
MANUAL. UseAFTER_APPROVALandSCHEDULEDonly when explicitly requested, after explaining that no separate release gate may remain after review approval. - Treat
--distribution-scopeas the maximum reach. The default and production value isAPP_STORE. Stable external TestFlight also usesAPP_STOREunder current policy, but App Review and release still require their own later approvals.TESTFLIGHT_INTERNAL_ONLYis internal only, andTESTFLIGHT_INTERNAL_EXTERNALis currently beta internal/external only. Neither may proceed to the App Store. Never choose either TestFlight-only scope merely because the first audience is an internal TestFlight group. - Set
testFlightInternalTestingOnlytotrueonly for the internal-only scope, andfalseotherwise. Thefalserequired for external TestFlight is not App Store permission. - Produce a receipt for every upload with
--provenance-output, stored outside the repository with mode0600. The uploader reserves a receipt withuploadCompleted=falsebefore sending and completes it only after success. Never use a prepared receipt for a later operation. On failure, reconcile the upload at Apple usingreferences/failure-runbook.md. Reject beta or TestFlight-only provenance for every App Store build attach, review, and release operation. Never edit a receipt. - Receipts and source/archive hashes are not cryptographic attestations against the same OS user. When stronger guarantees are required, build from a dedicated build user or an isolated CI without App Store Connect keys, and hand a hash-verified immutable archive/artifact to a separate upload/release identity.
- Never implicitly reply to a review rejection, resubmit, delete, change pricing or territories, or alter phased release state.
- A general wish to "handle it consistently" is not permission to release to production this time.
Explicit stable internal-TestFlight automation
An operator may explicitly authorize one complete stable internal-only TestFlight delivery
up front. This is an opt-in exception to the conversational approval cadence, not the
default interpretation of a TestFlight request and not an exception to validation or the
command-line approval checks. Follow
references/internal-testflight-automation.md exactly.
The request qualifies only when it explicitly selects the internal-only boundary and the brief-request resolver unambiguously derives any omitted release coordinates:
- The app/source, marketing version, and build number
TESTFLIGHT_INTERNAL_ONLYas the maximum distribution scope- Internal TestFlight as the stop point
- An explicit instruction to upload or deliver the current app/build to TestFlight
A generic request such as "Upload this app to TestFlight" does not select this exception;
it follows the default APP_STORE path with per-operation approvals.
For a qualifying request, run every operation's dry run, verify its current target and
planSha256, and then supply that operation's exact phrase and hash to the helper
mechanically. Do not ask the operator to copy either value back into the conversation.
Before the first mutation, report the resolved app, source, environment, version/build,
scope, stop point, Xcode, and internal audience as a non-blocking status update. Continue
unless the operator corrects it or a resolver stop condition applies.
The authorization covers archive creation, upload with provisioning updates, processing
wait, matching manifest-backed TestFlight build localization, and verification that an
existing all-builds internal group has access.
It never covers beta or prerelease Xcode, creating or changing groups/testers, sending invitations, external TestFlight, export-compliance determinations or corrections, App Store metadata, App Review, release, destructive operations, or any scope/version/build/ source change. Stop and request a new decision if any required identity is ambiguous or changes, any fail-closed check fails, a prepared upload receipt requires reconciliation, or Apple requires a mutation outside this allowlist.
Approval gates
This is the default path, and it remains mandatory for every operation outside the
explicitly bounded stable internal-TestFlight automation above. Immediately before each gate, display the
app name, bundle ID, platform, marketing
version, build number, Apple resource ID, target group or version, release type, and the
planSha256 produced by that operation's own dry run. If anything changed, redo the dry
run and obtain approval again. Never substitute a whole-manifest hash for a per-operation
hash.
| Gate | Command | Required explicit approval |
|---|---|---|
| Register a bundle ID | provision-bundle-id |
CREATE_BUNDLE_ID |
| Enable a bundle ID capability | provision-capability |
ENABLE_CAPABILITY |
| Create archive for APP_STORE | xcode-upload.sh archive |
CREATE_ARCHIVE |
| Create stable internal-only TestFlight archive | xcode-upload.sh archive |
CREATE_TESTFLIGHT_ARCHIVE |
| Create beta TestFlight-only archive | xcode-upload.sh archive |
CREATE_TESTFLIGHT_PRERELEASE_ARCHIVE |
| Upload archive for APP_STORE | xcode-upload.sh upload |
UPLOAD_ARCHIVE |
| Upload stable internal-only TestFlight archive | xcode-upload.sh upload |
UPLOAD_TESTFLIGHT_ARCHIVE |
| Upload beta TestFlight-only archive | xcode-upload.sh upload |
UPLOAD_TESTFLIGHT_PRERELEASE_ARCHIVE |
| Update Xcode provisioning | xcode-upload.sh upload |
ALLOW_PROVISIONING_UPDATES |
| Upload IPA/PKG for APP_STORE | altool-upload.sh |
UPLOAD_BUILD |
| Upload beta external TestFlight-only IPA/PKG | altool-upload.sh |
UPLOAD_TESTFLIGHT_PRERELEASE_BUILD |
| Declare export compliance on a build | set-build-encryption |
SET_EXPORT_COMPLIANCE |
| Set TestFlight localizations | create/update-beta-build-localization, create/update-beta-app-localization |
SET_BETA_METADATA |
| Set the external Beta Review contact and notes | update-beta-review-detail |
SET_BETA_REVIEW_DETAILS |
| Change tester notification | set-beta-auto-notify |
SET_TESTER_NOTIFICATION |
| Create a TestFlight group | create-beta-group |
CREATE_BETA_GROUP |
| Invite a TestFlight tester | add-beta-tester |
ADD_BETA_TESTER |
| Add to a TestFlight group | add-beta-group |
ADD_TO_BETA_GROUP |
| Submit for external Beta Review | submit-beta-review |
SUBMIT_BETA_REVIEW |
| Create an App Store version | create-version |
CREATE_APP_STORE_VERSION |
| Set App Store metadata or copyright | create/update-app-store-localization, set-version-copyright |
SET_APP_STORE_METADATA |
| Upload screenshots | asc-screenshots.mjs upload |
UPLOAD_SCREENSHOTS |
| Attach a build to a version | attach-build |
ATTACH_BUILD |
| Set App Review contact, demo access, notes | create/update-app-review-detail |
SET_APP_REVIEW_DETAILS |
| Create a review submission draft | create-review-submission |
CREATE_REVIEW_DRAFT |
| Add a version to a review submission | add-review-item |
ADD_REVIEW_ITEM |
| Submit for App Review | submit-review-submission |
SUBMIT_APP_REVIEW |
| Set the release policy | set-release-policy |
SET_RELEASE_POLICY |
| Configure phased release | create/update-phased-release |
CONFIGURE_PHASED_RELEASE |
| Manual App Store release | release-version |
RELEASE_TO_APP_STORE |
Every one of these is a dry run until the exact phrase and that dry run's
planSha256 are supplied. In the bounded automation mode, the agent supplies those values
to the helper after its own verification; the operator does not echo them.
Outside bounded automation, add --execute --confirm PHRASE --plan-sha256 HASH only after
explicit approval in the conversation. Inside bounded automation, add them only after the
matching dry run passes every check in the automation reference. Never carry an earlier
stage's phrase or hash forward to a later stage. If a hash does not match at execution
time, stop, identify what changed, and do not silently broaden the authorization.
Provisioning
Everything the Developer Portal needs, apart from the app record and the first-build signing assets below, can be created through gated commands. Use them instead of sending a beginner to the portal to guess at capabilities.
node <skill-dir>/scripts/asc-release.mjs list-bundle-ids --bundle-id com.example.app
node <skill-dir>/scripts/asc-release.mjs provision-bundle-id \
--bundle-id com.example.app --name 'Example App' [--bundle-id-platform UNIVERSAL]
node <skill-dir>/scripts/asc-release.mjs provision-capability \
--bundle-id com.example.app --capability APPLE_ID_AUTH [--settings-file /absolute/settings.json]
Sign In with Apple is called APPLE_ID_AUTH in the API, not SIGN_IN_WITH_APPLE. The
command accepts the portal wording as an alias and reports both in the approval summary.
With no --settings-file, APPLE_ID_AUTH is enabled as a primary App ID
(PRIMARY_APP_CONSENT); pass settings to group it under a different primary App ID.
Both commands refuse to duplicate an existing bundle ID or capability, and they re-check live state after approval and before writing. Enabling a capability can invalidate existing provisioning profiles, so say that in the approval summary. Provisioning never resolves an app record, so it works before one exists.
The first build on a new bundle ID
Registering the bundle ID and its capabilities is not enough to sign. A distribution
certificate and an App Store provisioning profile must exist too, and the certificate's
private key must be in this Mac's Keychain. xcode-upload.sh archive deliberately creates
none of it: it runs with the App Store Connect key blocked and without
-allowProvisioningUpdates, so a first archive on a new bundle ID fails to sign until the
assets exist.
app-record-guide reports this under signing. When signingAssetsReady is false, hand
the operator signing.firstBuildGuide and stop, exactly as with a missing app record. They
create the assets once, either through Xcode's automatic signing or by running
xcodebuild -allowProvisioningUpdates themselves, outside this skill. Then re-run the
command instead of assuming the assets appeared. This is a deliberate boundary, not a
missing feature: creating signing assets is the one place where Xcode's own account gets
the private key into the Keychain, which the API cannot do.
TestFlight groups and testers
Groups and testers are created through gated commands, so a new app does not need manual TestFlight setup.
node <skill-dir>/scripts/asc-release.mjs create-beta-group \
--bundle-id com.example.app --app-id APP_ID \
--name 'Internal Testers' --internal true
node <skill-dir>/scripts/asc-release.mjs add-beta-tester \
--bundle-id com.example.app --group-id GROUP_ID --email tester@example.com
--internal is required, because internal and external are different audiences, not a
default. An internal group takes only App Store Connect users on this team, and
add-beta-tester refuses an address that is not one of them, or that holds no role
allowing internal testing, or that cannot see the app. An external group reaches people
outside the team and needs Beta App Review before anyone can install.
Public links are refused by create-beta-group. A link admits anyone holding the URL, and
this skill has no gate for that; it belongs in App Store Connect as a separate decision.
Tester addresses are personal data. The dry run prints only a mask such as
t****r@example.com, while the approval hash still covers the exact address, so
approving a hash still approves exactly one person. Read the mask back to the operator and
confirm it against what they gave you.
--has-access-to-all-builds true makes an internal group receive every future build
automatically, with no per-build approval. It is off by default, and when it is on
add-beta-group refuses that group: Apple rejects an explicit build link there, and both
commands say so instead of letting an approved plan fail at Apple.
Collect inputs
Copy assets/release-manifest.example.json into the operator's workspace, outside this
skill folder and outside the app repository, and write it with mode 0600. Fill it using
"Derive, do not ask" above: measure or fetch everything derivable, and ask only for the
human-only values, when the phase that needs them is reached.
Present the draft manifest before using it. State which values were derived and from where, so the operator can correct a wrong one before it reaches a dry run.
The full field set the manifest carries, by phase:
| Phase | Fields that must be settled |
|---|---|
plan |
app.*, delivery.distributionScope, toolchain.*, build.marketingVersion, build.buildNumber, and either build.artifactPath or build.source |
upload |
the above, plus build.appStoreConnectBuildId and build.provenancePath after the upload completes |
internal-beta |
testFlight.audience, testFlight.groupIds, testFlight.localizations whatsNew |
external-beta |
the above, plus beta app localization, feedback email, and the external Beta Review contact and notes |
app-review |
appStore.* per locale, screenshots per display type, review.*, and all seven compliance flags |
release |
appStore.releaseType, earliestReleaseDate, and phasedRelease |
Never ask for secret contents. The .p8 is referenced only through ASC_KEY_ID and
ASC_PRIVATE_KEY_PATH, and demo account passwords are referenced only through
review.demoCredentialReference, never stored in the manifest.
Plan and preflight
Validate the schema v2 manifest per phase. Choose the current point from plan, upload,
internal-beta, external-beta, app-review, and release. Phases after upload require
build.appStoreConnectBuildId and build.provenancePath, and a TestFlight-only scope
never advances to an App Store phase. That hash is a release record. For every mutating
operation, include the planSha256 from that operation's own dry run in the approval
summary, or record and verify it internally when bounded automation applies.
node <skill-dir>/scripts/validate-manifest.mjs /absolute/path/release.json --phase plan
node <skill-dir>/scripts/asc-api.mjs self-test
node <skill-dir>/scripts/asc-release.mjs status \
--bundle-id com.example.app --platform IOS
Pass credentials through the ASC_KEY_ID, ASC_ISSUER_ID, and ASC_PRIVATE_KEY_PATH
environment variables. Prefer a team key with App Manager privileges.
Preflight must verify all of the following.
- Default to stable Xcode. Require an exact match against the bundled
assets/toolchain-acceptance-2026-08-18.jsonfor the selected Xcode ProductVersion, exact build ID, SDK version and build, scope, and, forAPP_STORE, the per-platform Store tuple. Stop fail-closed on any mismatch. - Check Apple's official release notes live for every beta use. If acceptance changed,
create and review a new dated policy file and stop until the script default is updated.
Never rewrite an existing policy. The 2026-08-18 policy permits Xcode 27 beta 5 build
27A5237l/ SDK27.0for TestFlight only, withvalidUntil=2026-08-25. Re-check the official sources even inside that window. - Validating or reserving a new receipt enforces
createdAtfreshness and the current policy expiry. A completed receipt is verified against its receipt-bound policy by creation date, so it stays readable as evidence after expiry, but App Store Connect distribution and release operations also check the current bundled policy. Continue using an existing receipt only when a newly reviewed dated entry differs from the receipt-bound entry solely inverifiedAt/validUntil, with identical toolchain identity and acceptance conditions. - The 2026-08-18 stable entry is Xcode 26.6 ProductVersion
26.6/ build17F113, with scopesAPP_STOREandTESTFLIGHT_INTERNAL_ONLY, and SDK version26.5. An SDK has three distinct identifiers:SDKVersion(26.5, whatDTSDKNameencodes asiphoneos26.5and what the policy stores),ProductBuildVersion(23F81a, stored assdkBuild), andProductVersion(26.5.1, from the SDK's ownSystemVersion.plist, which no archive records and which this skill never compares). An iOS archive confirmedDTSDKBuildandDTPlatformBuildboth equal23F81a; the other platforms'platformBuildvalues are unconfirmed. Compare the selected Xcode, the archive'sDTXcodeBuild/DTSDKBuild/DTPlatformBuild, and the BuildBundle at Apple against the policy exactly, and stop on any mismatch. - App record, bundle ID, and signing/provisioning entitlements
- Existing builds with the same version/build number
- Current beta groups, editable App Store version, and active review submission
- Manual checks for contracts, privacy, pricing and territories, and export compliance
Archive and upload
When building from Xcode sources, dry run first.
<skill-dir>/scripts/xcode-upload.sh archive \
--source-root /absolute/path/to/repository \
--workspace /absolute/path/to/repository/App.xcworkspace \
--scheme App --archive-path /absolute/path/App.xcarchive \
--bundle-id com.example.app \
--platform IOS --marketing-version 1.2.0 --build-number 42 \
--team-id ABCDE12345 --distribution-scope APP_STORE \
--expected-xcode-build 17F113 --expected-sdk-version 26.5 \
--developer-dir /Applications/Xcode.app/Contents/Developer
After archive-creation approval, or after the archive dry run qualifies under bounded
automation, execute, then dry run xcode-upload.sh upload ... separately using the
archive digest that was printed. The archive hashes the whole
--source-root except .git and builds from a temporary copy that allows only safe
relative symlinks. It rejects *.p8 / *.pem / *.p12 inside the source root and an ASC
key path pointing inside it. The archive xcodebuild runs with ASC_KEY_ID,
ASC_ISSUER_ID, and ASC_PRIVATE_KEY_PATH removed from the environment, and
sandbox-exec explicitly denies file reads of the standard key directory and any custom
key path. This is not a general build sandbox: network, Keychain, other files, and the
rest of the caller environment remain allowed. Archive creation does not pass
-allowProvisioningUpdates, so it makes no intended Developer Portal changes. Package
versions are pinned through Package.resolved, but project tooling may still use the
network. Replace build phases that depend on .git with pre-generated values. Because a
source hash cannot prove that an arbitrary build script did not read outside the root, the
caller environment, or the network, explain that the later archive digest is the boundary
that pins the actual output bytes.
Pass upload the same scope, exact Xcode build, --expected-sdk-version, developer
directory, and a --provenance-output outside the repository. Because Xcode may create or
update provisioning assets, include --allow-provisioning-updates in the plan. By
default, take an explicit --confirm-provisioning-updates ALLOW_PROVISIONING_UPDATES
approval separate from the scoped upload approval. Bounded automation may supply it from
the same up-front authorization only when the dry run still matches the exact archive and
internal-only scope. Only then pin the archive to a temporary snapshot,
re-verify bundle/version/build/team/platform/Xcode build/SDK, the code-signing team, and
the digest, and upload using the app-store-connect method. The receipt is reserved
before upload and completed after success. If it stops while prepared, do not resend;
follow failure-runbook.md.
For an existing signed IPA/PKG, use scripts/altool-upload.sh --file ... --bundle-id ... --marketing-version ... --build-number ... --team-id ... --platform ... --distribution-scope ... --expected-artifact-xcode-build ... --expected-uploader-xcode-build ... --expected-sdk-version ... --developer-dir ... --provenance-output ... and display the
identity inside the package, the installer/app signing team, and the artifact SHA-256
before approval. Do not conflate artifact and uploader build identity. altool rejects
TESTFLIGHT_INTERNAL_ONLY; use the .xcarchive workflow when that constraint is needed.
Follow references/prerelease-xcode.md for per-scope export settings, allowlists, and
approval phrases. Under current policy, stable external TestFlight uses APP_STORE +
UPLOAD_BUILD, and only beta external uses TESTFLIGHT_INTERNAL_EXTERNAL +
UPLOAD_TESTFLIGHT_PRERELEASE_BUILD.
After upload, do not resend the build. Wait for visibility and processingState=VALID
through the API.
node <skill-dir>/scripts/asc-release.mjs wait-build \
--bundle-id com.example.app --platform IOS \
--marketing-version 1.2.0 --build-number 42
Advance TestFlight
- Confirm
buildAudienceType, expiry, export compliance, beta detail, and the upload receipt. ExpectINTERNAL_ONLYfor internal-only andAPP_STORE_ELIGIBLEotherwise, but never treat the latter alone as App Store permission. - Set
What to TestthroughbetaBuildLocalizations. - Add to an internal group and include the notification audience in the approval summary.
Create the group with
create-beta-groupand its testers withadd-beta-testerwhen the app has none yet.wait-buildreportsresolved.buildBetaDetailId, which is the argumentset-beta-auto-notify --build-beta-detail-id ID --enabled falseneeds; that command takes the buildBetaDetail ID, never the build ID. Then pass the same upload receipt toadd-beta-groupwith--provenance-fileand verify scope against the live audience. Skipadd-beta-groupfor a group withhasAccessToAllBuilds, which already has the build. Internal testing does not create a Beta App Review. - For external testing, complete beta app localization, feedback email, review contact, and demo access.
- Dry run
submit-beta-reviewwith the same--provenance-fileand execute after a separate approval. Reject an internal-only receipt before external review. Monitor throughAPPROVED/REJECTED. - On rejection, report the reason and stop. Do not auto-fix or auto-resubmit.
Advance App Review
Before starting, verify that the upload receipt is stable-toolchain with
distributionScope=APP_STORE and eligibility=STORE_ALLOWED. Pass the same receipt's
absolute path with --provenance-file to attach-build, add-review-item,
review-snapshot, and submit-review-submission. Stop on a beta, TestFlight-only,
missing, modified, or mismatched receipt.
- Reuse an existing editable version, or dry run
create-version. - Confirm every item in
references/metadata.md, dry runasc-screenshots.mjswith an explicit bundle ID, version ID, platform, and version for each display type, then upload after a separate approval and confirmCOMPLETE. - Dry run
attach-buildto link the same build ID as the final TestFlight build to the version. - Create a draft with
create-review-submissionand add the version withadd-review-item. - Run
review-snapshot --bundle-id ... --submission-id ... --version-id ... --provenance-file /absolute/path/upload-provenance.jsonand display the snapshot and hash covering every submission item, metadata, review detail, screenshots/previews, build, pricing and territories, phased release, and release type. Combine that with the human privacy/compliance confirmation to obtain explicit approval to submit for App Review. - Dry run
submit-review-submissionwith the version ID andreviewSnapshotSha256, then execute that operation hash after approval. Monitor the review submission andappVersionState.
Do not use the legacy appStoreVersionSubmissions creation API. Use the current
reviewSubmissions + reviewSubmissionItems flow.
Release
For MANUAL, wait for PENDING_DEVELOPER_RELEASE after Apple's approval. Immediately
before release, re-fetch live state with release-snapshot --provenance-file /absolute/path/upload-provenance.json, display territories, pricing, build, version, and
phased release, and obtain a production approval specific to this release. Pass the same
--provenance-file to release-version so it is bound to both that snapshot hash and the
operation hash.
release-version re-verifies PENDING_DEVELOPER_RELEASE and MANUAL, but the API's
release request cannot be cancelled. After execution, monitor through
PROCESSING_FOR_DISTRIBUTION until READY_FOR_DISTRIBUTION.
With AFTER_APPROVAL or SCHEDULED, the App Review submission can become the effective
final release gate. set-release-policy also requires --provenance-file. Do not execute
unless release authority was explicitly approved before submission.
Final report
Report the following concisely, excluding secrets.
- App, bundle ID, platform, version, build number, and resource IDs
- Artifact SHA-256, distribution scope, provenance receipt, the Xcode version/exact build/SDK used, and artifact/uploader identity
- TestFlight group/audience and Beta Review outcome
- App Review submission ID and current state
- Release type, phased release, and release state
- Completed operations, pending Apple processing, and remaining human work
- Failures including the Apple request ID, and the exact next command to resume safely