local-archive-export-upload
Use when a build must reach TestFlight without Xcode Cloud (quota, outage, CI not wired yet), or when a local `xcodebuild archive` / `-exportArchive -exportOptionsPlist` / `xcrun altool --upload-package` command fails — `ExportOptions.plist` keys (`method`, `destination`, `teamID
Install
npx skills add https://github.com/wei18/apple-dev-skills/tree/main/apple-dev-skills/skills/local-archive-export-upload
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install wei18-apple-dev-skills@llmmart
git clone https://github.com/wei18/apple-dev-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole wei18/apple-dev-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Local Archive, Export, Upload
The manual escape hatch for getting a build to TestFlight when Xcode Cloud's
Main CI workflow (→ xcode-cloud-single-track-ci) can't run — quota
exhausted, an outage, or CI not wired up yet. Same three Apple CLIs Xcode
Cloud uses under the hood — xcodebuild archive, -exportArchive, upload —
driven by hand instead of Apple's managed runner.
When to invoke
- Xcode Cloud can't run but a build must reach internal TestFlight today.
- Writing or debugging a local archive/export/upload script or one-off command.
- Diagnosing an
ExportOptions.plist/ signing / build-number failure. - Asked "how do I ship a build without Xcode Cloud" or "what does
-exportOptionsPlistneed".
Scope
Owns: the local three-step pipeline (archive → export → upload) and its signing/export-key semantics. Does NOT own:
- ASC operations once the build exists (TestFlight groups, what's-new,
submission) →
asc-api-automation. - What has to be true for the build to pass review →
app-store-review-rejections. .p8/ API-key storage →build-time-secret-injection+apple-public-repo-security.- Restoring Xcode Cloud once quota returns — this is a temporary substitute, not a parallel permanent CI track.
Pipeline
| Step | Command | Notes |
|---|---|---|
| 1. Archive | xcodebuild archive -scheme <Scheme> -destination 'generic/platform=iOS' -archivePath build/App.xcarchive |
-destination picks the platform; a generic/platform=... destination (not a specific simulator/device) is what produces an archivable, distributable build. |
| 2. Export | xcodebuild -exportArchive -archivePath build/App.xcarchive -exportPath build/export -exportOptionsPlist ExportOptions.plist |
Requires -archivePath + -exportOptionsPlist; -exportPath only needed when the plist's destination is export (see below). |
| 3. Upload | xcrun altool --upload-package build/export/App.ipa --api-key <keyID> --api-issuer <issuerID> --wait |
--api-key / --api-issuer are the spelling altool --help's own example uses; add --wait to block until Apple finishes processing instead of polling separately. Or fold into step 2 — see "One-step vs two-step" below. |
Signing: two non-interactive paths
xcodebuild -help documents two credential sources for -allowProvisioningUpdates:
an account added in Xcode's Accounts settings, or an App Store Connect authentication
key passed via the -authenticationKey* trio. Both paths below therefore pass the flag
(needed on a script/cron path, not just CI):
| Path | Flags | Use when |
|---|---|---|
| Automatic signing via your Xcode account | -allowProvisioningUpdates (+ -allowProvisioningDeviceRegistration if a new device needs registering) |
Run on your own dev Mac with an account added in Xcode's Accounts settings; xcodebuild creates/updates profiles and certs as needed. |
| API-key signing, no Apple ID session | -allowProvisioningUpdates -authenticationKeyPath <p8> -authenticationKeyID <id> -authenticationKeyIssuerID <issuer> |
Unattended/scripted runs — reuse the same ASC API key from asc-api-automation / build-time-secret-injection, no Xcode account needed. |
ExportOptions.plist: the common shape
The full key set is printed by xcodebuild -help under "Available keys for
-exportOptionsPlist" (18 keys on Xcode 26.5, covering thinning, manifests, on-demand
resources). For a plain "ship to TestFlight" export, six keys are load-bearing
(cross-checked against the key list in xcodebuild -help):
<key>destination</key> <string>export</string> <!-- or "upload" -->
<key>method</key> <string>app-store-connect</string>
<key>manageAppVersionAndBuildNumber</key> <false/>
<key>signingStyle</key> <string>automatic</string>
<key>teamID</key> <string>ABCDE12345</string>
<key>uploadSymbols</key> <true/>
method: app-store-connect— the current name;app-storestill works but Xcode 26.5'sxcodebuild -helpalready marks it "deprecated: use app-store-connect" (the exact Xcode version the rename landed in is unconfirmed).manageAppVersionAndBuildNumber: false— defaults toYES(Xcode bumps the build number for you on export); setfalsewhen your own tooling controlsCFBundleVersion— see build-number coordination below.teamID— omit to inherit the archive's signing team; set explicitly when a machine/CI identity could resolve ambiguously.
One-step vs two-step upload — pick two-step on purpose
destination |
Artifact | Gate before upload | Use when |
|---|---|---|---|
export |
.ipa/.pkg written to -exportPath |
Your own --dry-run/--i-am-sure-style flag on a separate upload command |
Default |
upload |
None — xcodebuild uploads directly to Apple, no local artifact, no separate altool call |
None — the network push happens the instant -exportArchive runs |
Deliberate one-shot only |
Prefer destination: export + a separate, deliberately-gated upload command
so archive/export stay safe to run freely and only upload needs a human's
explicit go-ahead.
Upload tool
xcrun altool --help (Xcode 26.5) lists --upload-package <file> first among
App-Upload commands and uses it in its own canonical example; the older
--upload-app -f <file> still works but isn't the tool's own example anymore.
altool searches fixed directories for AuthKey_<keyID>.p8 (./private_keys,
~/private_keys, ~/.private_keys, ~/.appstoreconnect/private_keys, or
$API_PRIVATE_KEYS_DIR) — stage a per-run symlink into one of these if your
key lives in a gitignored secrets/ dir, rather than moving the real file.
notarytool is not this path — it handles Developer-ID notarization
(outside-the-App-Store distribution), unrelated to TestFlight/App Store
uploads. The ASC API's Build Uploads path also works (see
references/official-docs.md), but this skill still defaults to altool.
For the other upload paths (Xcode Cloud, Xcode Organizer, Transporter), read
references/official-docs.md.
Build-number coordination with Xcode Cloud
Per xcode-cloud-single-track-ci, Xcode Cloud assigns its own sequential
CI_BUILD_NUMBER per build, independent of whatever's in the repo. A local
build needs a CFBundleVersion that will never collide with — or fall below —
that counter:
- Use a high-resolution timestamp (
YYYYMMDDHHmm) rather than a small hand-incremented integer — it can't collide with Xcode Cloud's small sequential counter, and TestFlight rejects a duplicateCFBundleVersionfor the sameCFBundleShortVersionString. - macOS additionally requires build numbers to strictly increase across
versions — if a local timestamp-based number ends up higher than Xcode
Cloud's next assigned number, fix it once on the ASC side (Xcode Cloud →
Settings → Build Number → Edit), the same fix
xcode-cloud-single-track-cidocuments for its "existing Mac app" exception. - Set
manageAppVersionAndBuildNumber: false(above) so Xcode's export-time bump doesn't fight your chosen number.
Rationale
Every flag and key here is verifiable straight from Apple's own CLI (man xcodebuild, xcodebuild -help, xcrun altool --help) — no fastlane, no
third-party packaging tool, consistent with this catalog's no-Homebrew /
Apple-native-first baseline (asc-api-automation makes the same call for ASC
REST automation). Provenance for each claim (doc-verified vs practice-observed)
is in references/evidence.md.
Deviation considerations
- Xcode Cloud is back / never was the bottleneck — retire the local path;
don't run it as a second permanent track (
xcode-cloud-single-track-ci's single-track rule). - Frequent local ships — the "temporary substitute" framing breaks down; invest in restoring/expanding Xcode Cloud capacity instead of hardening this manual path further.
Common Mistakes
- Pre-
touchingExportOptions.plistbefore writing it withPlistBuddy. - Using
destination: uploadas the default — loses the archive-only dry run. - Leaving
method: app-store(deprecated form) in an old plist. - Hand-incrementing a small
CFBundleVersionlocally — collides with Xcode Cloud's own counter. - Reaching for
xcrun notarytoolfor a TestFlight upload — it's for Developer-ID notarization, a different distribution path entirely. - Forgetting
ITSAppUsesNonExemptEncryptionon a new app — the build is marked Missing Compliance until the export-compliance questions are answered in ASC or viaPATCH /v1/builds/{id}(usesNonExemptEncryption).
Review Checklist
- Archive uses a
generic/platform=...destination, not a specific simulator/device. - Export uses
method: app-store-connect(not the deprecatedapp-store). - Signing passes
-allowProvisioningUpdateson every path, with a deliberate credential source: the Xcode Accounts login (your dev Mac) or the-authenticationKeyPathtrio (unattended). -
destination: export+ a separate gated upload step, unless a one-stepuploadis a deliberate choice. -
CFBundleVersionsource can't collide with Xcode Cloud'sCI_BUILD_NUMBER. -
ITSAppUsesNonExemptEncryptionset in Info.plist (or export-compliance is otherwise answered). -
.p8staged only via a per-run symlink into an altool-searched directory, never committed or left behind after the run.
Related skills
xcode-cloud-single-track-ci— the primary CI path this substitutes for; restore it once quota/outage clears.asc-api-automation— TestFlight/App Store operations once the build lands in ASC.app-store-review-rejections— its export-compliance row (an ASC upload step, not a Guideline number) and what has to be true for review to pass.apple-skills:guide-macos-spm-packaging(aggregated external) — Developer-ID signing /notarytoolnotarization for outside-the-App-Store distribution.build-time-secret-injection/apple-public-repo-security— where the.p8and its issuer/key IDs live and how leaks are prevented.storekit2-iap-defaults— this pipeline is how a build carrying that skill's IAP code reaches TestFlight.- Official sources: when verifying or updating a factual or version-sensitive claim, read
references/official-docs.md.
Files (apple-dev-skills)
-
references
-
evidence.md 1.7 KB
# Evidence ## War stories (evidence tier in italics) - **A pre-`touch`ed ExportOptions.plist breaks `PlistBuddy`** — it can't parse a 0-byte file ("Cannot parse a NULL or zero-length data"); remove the file first and let `PlistBuddy Add` create it fresh. *Practice observed* — the actual first-run failure on a real project's local-upload script. - **A missing export-compliance answer marks a build "Missing Compliance."** Declaring `ITSAppUsesNonExemptEncryption` (`false` if you use no encryption or only exempt encryption) once means every future upload skips ASC's export compliance questionnaire; without it the build is marked Missing Compliance until the questions are answered in ASC or via `PATCH /v1/builds/{id}` with `usesNonExemptEncryption`. A human upload is needed only when non-exempt encryption requires documentation (see `app-store-review-rejections`'s export-compliance row — an ASC upload step, not a Guideline number). *Doc-verified* (ASC Help "Provide export compliance information for beta builds"; `ITSAppUsesNonExemptEncryption`; `BuildUpdateRequest`). - **`app-store` → `app-store-connect` rename** — the deprecation note ("app-store (deprecated: use app-store-connect)") is confirmed in `xcodebuild -help` on Xcode 26.5. The Xcode version the rename landed in is *unconfirmed*: Apple's Xcode 15.x release notes never mention `app-store-connect`; third-party reports place it around Xcode 15.3/15.4. *Doc-verified* for the deprecation only, Xcode 26.5. - **The six load-bearing exportOptionsPlist keys** — confirmed as the common shape across four real ExportOptions.plist files (2 apps × 2 platforms) and cross-checked against `xcodebuild -help`'s key list. *Doc-verified + cross-project-observed.* -
official-docs.md 2.1 KB
Official pages backing this skill's claims; read when verifying or updating a factual or version-sensitive claim. | Page | URL | Backs | |---|---|---| | Distributing your app for beta testing and releases | https://developer.apple.com/documentation/xcode/distributing-your-app-for-beta-testing-and-releases | Pipeline concept: "You can export the archive or upload it to App Store Connect" (Organizer view; CLI flags follow local `-help` instead) | | Upload builds (ASC Help) | https://developer.apple.com/help/app-store-connect/manage-builds/upload-builds/ | Upload tool options: Xcode / Swift Playground / altool / Transporter; "You can upload your app binary with the App Store Connect API" | | Build uploads | https://developer.apple.com/documentation/appstoreconnectapi/build-uploads | `buildUploads` represents the whole upload; `buildUploadFiles` the actual files; "You can also use Xcode or Transporter" | | Create a Build Upload (App Store Connect API 4.1) | https://developer.apple.com/documentation/appstoreconnectapi/post-v1-builduploads | The concrete Build Upload endpoint and API version | | Automate your development process with the App Store Connect API (WWDC25) | https://developer.apple.com/videos/play/wwdc2025/324/ | Build Upload API walkthrough (secondary: video) | | Setting the next build number for Xcode Cloud builds | https://developer.apple.com/documentation/xcode/setting-the-next-build-number-for-xcode-cloud-builds | Build-number coordination: the Mac build number must keep increasing release over release | | ITSAppUsesNonExemptEncryption | https://developer.apple.com/documentation/bundleresources/information-property-list/itsappusesnonexemptencryption | Common Mistake 6; Review Checklist item 6 | | Complying with Encryption Export Regulations | https://developer.apple.com/documentation/security/complying-with-encryption-export-regulations | Same claim (the Apple doc the ASC Help page points to) | | Provide export compliance information for beta builds | https://developer.apple.com/help/app-store-connect/test-a-beta-version/provide-export-compliance-information-for-beta-builds/ | The "Missing Compliance" status name |
-
-
SKILL.md 10.9 KB
--- name: local-archive-export-upload description: 'Use when a build must reach TestFlight without Xcode Cloud (quota, outage, CI not wired yet), or when a local `xcodebuild archive` / `-exportArchive -exportOptionsPlist` / `xcrun altool --upload-package` command fails — `ExportOptions.plist` keys (`method`, `destination`, `teamID`, `manageAppVersionAndBuildNumber`), `-authenticationKeyPath` vs `-allowProvisioningUpdates`, `CFBundleVersion` colliding with Xcode Cloud''s counter. Temporary fallback for xcode-cloud-single-track-ci; does NOT cover ASC operations after upload → asc-api-automation, nor `notarytool` notarization → apple-skills:guide-macos-spm-packaging.' allowed-tools: Bash(xcodebuild archive *) --- # Local Archive, Export, Upload The manual escape hatch for getting a build to TestFlight when Xcode Cloud's **Main CI** workflow (→ `xcode-cloud-single-track-ci`) can't run — quota exhausted, an outage, or CI not wired up yet. Same three Apple CLIs Xcode Cloud uses under the hood — `xcodebuild archive`, `-exportArchive`, upload — driven by hand instead of Apple's managed runner. ## When to invoke - Xcode Cloud can't run but a build must reach internal TestFlight today. - Writing or debugging a local archive/export/upload script or one-off command. - Diagnosing an `ExportOptions.plist` / signing / build-number failure. - Asked "how do I ship a build without Xcode Cloud" or "what does `-exportOptionsPlist` need". ## Scope Owns: the local three-step pipeline (archive → export → upload) and its signing/export-key semantics. Does NOT own: - ASC operations once the build exists (TestFlight groups, what's-new, submission) → `asc-api-automation`. - What has to be true for the build to *pass* review → `app-store-review-rejections`. - `.p8` / API-key storage → `build-time-secret-injection` + `apple-public-repo-security`. - Restoring Xcode Cloud once quota returns — this is a **temporary substitute**, not a parallel permanent CI track. ## Pipeline | Step | Command | Notes | |---|---|---| | 1. Archive | `xcodebuild archive -scheme <Scheme> -destination 'generic/platform=iOS' -archivePath build/App.xcarchive` | `-destination` picks the platform; a `generic/platform=...` destination (not a specific simulator/device) is what produces an archivable, distributable build. | | 2. Export | `xcodebuild -exportArchive -archivePath build/App.xcarchive -exportPath build/export -exportOptionsPlist ExportOptions.plist` | Requires `-archivePath` + `-exportOptionsPlist`; `-exportPath` only needed when the plist's `destination` is `export` (see below). | | 3. Upload | `xcrun altool --upload-package build/export/App.ipa --api-key <keyID> --api-issuer <issuerID> --wait` | `--api-key` / `--api-issuer` are the spelling `altool --help`'s own example uses; add `--wait` to block until Apple finishes processing instead of polling separately. Or fold into step 2 — see "One-step vs two-step" below. | ## Signing: two non-interactive paths `xcodebuild -help` documents two credential sources for `-allowProvisioningUpdates`: an account added in Xcode's Accounts settings, or an App Store Connect authentication key passed via the `-authenticationKey*` trio. Both paths below therefore pass the flag (needed on a script/cron path, not just CI): | Path | Flags | Use when | |---|---|---| | Automatic signing via your Xcode account | `-allowProvisioningUpdates` (+ `-allowProvisioningDeviceRegistration` if a new device needs registering) | Run on your own dev Mac with an account added in Xcode's Accounts settings; xcodebuild creates/updates profiles and certs as needed. | | API-key signing, no Apple ID session | `-allowProvisioningUpdates -authenticationKeyPath <p8> -authenticationKeyID <id> -authenticationKeyIssuerID <issuer>` | Unattended/scripted runs — reuse the same ASC API key from `asc-api-automation` / `build-time-secret-injection`, no Xcode account needed. | ## ExportOptions.plist: the common shape The full key set is printed by `xcodebuild -help` under "Available keys for -exportOptionsPlist" (18 keys on Xcode 26.5, covering thinning, manifests, on-demand resources). For a plain "ship to TestFlight" export, six keys are load-bearing (cross-checked against the key list in `xcodebuild -help`): ```xml <key>destination</key> <string>export</string> <!-- or "upload" --> <key>method</key> <string>app-store-connect</string> <key>manageAppVersionAndBuildNumber</key> <false/> <key>signingStyle</key> <string>automatic</string> <key>teamID</key> <string>ABCDE12345</string> <key>uploadSymbols</key> <true/> ``` - **`method: app-store-connect`** — the current name; `app-store` still works but Xcode 26.5's `xcodebuild -help` already marks it "deprecated: use app-store-connect" (the exact Xcode version the rename landed in is unconfirmed). - **`manageAppVersionAndBuildNumber: false`** — defaults to `YES` (Xcode bumps the build number for you on export); set `false` when your own tooling controls `CFBundleVersion` — see build-number coordination below. - **`teamID`** — omit to inherit the archive's signing team; set explicitly when a machine/CI identity could resolve ambiguously. ### One-step vs two-step upload — pick two-step on purpose | `destination` | Artifact | Gate before upload | Use when | |---|---|---|---| | `export` | `.ipa`/`.pkg` written to `-exportPath` | Your own `--dry-run`/`--i-am-sure`-style flag on a separate upload command | Default | | `upload` | None — xcodebuild uploads directly to Apple, no local artifact, no separate `altool` call | None — the network push happens the instant `-exportArchive` runs | Deliberate one-shot only | Prefer `destination: export` + a separate, deliberately-gated upload command so archive/export stay safe to run freely and only upload needs a human's explicit go-ahead. ## Upload tool `xcrun altool --help` (Xcode 26.5) lists `--upload-package <file>` first among App-Upload commands and uses it in its own canonical example; the older `--upload-app -f <file>` still works but isn't the tool's own example anymore. altool searches fixed directories for `AuthKey_<keyID>.p8` (`./private_keys`, `~/private_keys`, `~/.private_keys`, `~/.appstoreconnect/private_keys`, or `$API_PRIVATE_KEYS_DIR`) — stage a per-run symlink into one of these if your key lives in a gitignored `secrets/` dir, rather than moving the real file. **`notarytool`** is not this path — it handles Developer-ID notarization (outside-the-App-Store distribution), unrelated to TestFlight/App Store uploads. The ASC API's Build Uploads path also works (see `references/official-docs.md`), but this skill still defaults to `altool`. For the other upload paths (Xcode Cloud, Xcode Organizer, Transporter), read `references/official-docs.md`. ## Build-number coordination with Xcode Cloud Per `xcode-cloud-single-track-ci`, Xcode Cloud assigns its own sequential `CI_BUILD_NUMBER` per build, independent of whatever's in the repo. A local build needs a `CFBundleVersion` that will never collide with — or fall below — that counter: - Use a high-resolution timestamp (`YYYYMMDDHHmm`) rather than a small hand-incremented integer — it can't collide with Xcode Cloud's small sequential counter, and TestFlight rejects a duplicate `CFBundleVersion` for the same `CFBundleShortVersionString`. - macOS additionally requires build numbers to strictly increase *across* versions — if a local timestamp-based number ends up higher than Xcode Cloud's next assigned number, fix it once on the ASC side (Xcode Cloud → Settings → Build Number → Edit), the same fix `xcode-cloud-single-track-ci` documents for its "existing Mac app" exception. - Set `manageAppVersionAndBuildNumber: false` (above) so Xcode's export-time bump doesn't fight your chosen number. ## Rationale Every flag and key here is verifiable straight from Apple's own CLI (`man xcodebuild`, `xcodebuild -help`, `xcrun altool --help`) — no fastlane, no third-party packaging tool, consistent with this catalog's no-Homebrew / Apple-native-first baseline (`asc-api-automation` makes the same call for ASC REST automation). Provenance for each claim (doc-verified vs practice-observed) is in `references/evidence.md`. ## Deviation considerations - **Xcode Cloud is back / never was the bottleneck** — retire the local path; don't run it as a second permanent track (`xcode-cloud-single-track-ci`'s single-track rule). - **Frequent local ships** — the "temporary substitute" framing breaks down; invest in restoring/expanding Xcode Cloud capacity instead of hardening this manual path further. ## Common Mistakes 1. Pre-`touch`ing `ExportOptions.plist` before writing it with `PlistBuddy`. 2. Using `destination: upload` as the default — loses the archive-only dry run. 3. Leaving `method: app-store` (deprecated form) in an old plist. 4. Hand-incrementing a small `CFBundleVersion` locally — collides with Xcode Cloud's own counter. 5. Reaching for `xcrun notarytool` for a TestFlight upload — it's for Developer-ID notarization, a different distribution path entirely. 6. Forgetting `ITSAppUsesNonExemptEncryption` on a new app — the build is marked Missing Compliance until the export-compliance questions are answered in ASC or via `PATCH /v1/builds/{id}` (`usesNonExemptEncryption`). ## Review Checklist - [ ] Archive uses a `generic/platform=...` destination, not a specific simulator/device. - [ ] Export uses `method: app-store-connect` (not the deprecated `app-store`). - [ ] Signing passes `-allowProvisioningUpdates` on every path, with a deliberate credential source: the Xcode Accounts login (your dev Mac) or the `-authenticationKeyPath` trio (unattended). - [ ] `destination: export` + a separate gated upload step, unless a one-step `upload` is a deliberate choice. - [ ] `CFBundleVersion` source can't collide with Xcode Cloud's `CI_BUILD_NUMBER`. - [ ] `ITSAppUsesNonExemptEncryption` set in Info.plist (or export-compliance is otherwise answered). - [ ] `.p8` staged only via a per-run symlink into an altool-searched directory, never committed or left behind after the run. ## Related skills - `xcode-cloud-single-track-ci` — the primary CI path this substitutes for; restore it once quota/outage clears. - `asc-api-automation` — TestFlight/App Store operations once the build lands in ASC. - `app-store-review-rejections` — its export-compliance row (an ASC upload step, not a Guideline number) and what has to be true for review to pass. - `apple-skills:guide-macos-spm-packaging` (aggregated external) — Developer-ID signing / `notarytool` notarization for outside-the-App-Store distribution. - `build-time-secret-injection` / `apple-public-repo-security` — where the `.p8` and its issuer/key IDs live and how leaks are prevented. - `storekit2-iap-defaults` — this pipeline is how a build carrying that skill's IAP code reaches TestFlight. - Official sources: when verifying or updating a factual or version-sensitive claim, read `references/official-docs.md`.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.