Going Live
Build, Upload & TestFlight with asc
The manual ship-it checklist ends at the Xcode Organizer: archive, validate, distribute, then click through TestFlight in a browser. This page is the automation layer over that same step 8. Every Organizer button has an asc equivalent that runs unattended — which is what you want once the first manual release proves the flow works, and what you need in CI. The pipeline produces deterministic .xcarchive and .ipa paths that feed straight into upload, so each stage is a clean, scriptable handoff.
This page assumes asc is authenticated and a default app id is set — see Authenticating the asc CLI first. Metadata, submission, and the review traps live in Metadata & Submit with asc. Here we cover only the binary: version → archive → export → validate → upload → TestFlight.
What asc xcode wraps
asc xcode is a thin, deterministic wrapper over xcodebuild and agvtool, so it is macOS only and needs your normal signing setup (team, certificates, provisioning) already working in Xcode. It does not replace the production prerequisites — your Config.xcconfig secrets, production Supabase, RevenueCat products, and the AI proxy must already be in place per the manual checklist. It replaces the clicking, not the configuring.
The Pipeline at a Glance
asc xcode version edit → set marketing + build number
asc xcode archive → ShipThatApp.xcarchive (deterministic path)
asc xcode export → ShipThatApp.ipa (deterministic path)
asc xcode validate → Apple server-side validation (pre-flight)
asc publish appstore → upload (no --submit) + wait for processing
asc builds list / info → confirm VALID, grab the build id
asc testflight … → groups, testers, What to Test
Each arrow is one command. The two artifact paths (--archive-path, --ipa-path) are the contract between stages: choose them once and reuse them everywhere. The examples below pin everything under .asc/artifacts/ so a run is reproducible.
1. Version & Build Numbers
asc xcode version reads and writes the project's marketing version (CFBundleShortVersionString) and build number (CFBundleVersion) via agvtool. It requires Apple Generic Versioning to be enabled in the project.
Read the current state first:
asc xcode version view --output table
App Store Connect rejects any upload whose build number isn't strictly greater than every prior upload for that version. Two ways to advance it:
# Set both explicitly — the deterministic choice for a release.
asc xcode version edit --version "1.0.0" --build-number "1"
# Or increment just the build number for a re-upload of the same version.
asc xcode version bump --type build
bump --type also accepts major, minor, and patch for the marketing version (1.2.3 → 2.0.0 / 1.3.0 / 1.2.4). If the repo holds more than one .xcodeproj, point at ShipThatApp's explicitly with --project ./ShipThatApp.xcodeproj; otherwise --project-dir . (the default) is fine.
Commit the version bump
asc xcode version edit and bump modify the project file in place. Run them on a clean tree and commit the change so the build number that shipped is recorded in git — the same discipline the manual checklist's "increasing build number" step asks for, just scripted.
2. Archive
asc xcode archive runs xcodebuild archive into an exact path you choose, so downstream commands never have to guess where Xcode put the archive. Provide exactly one of --workspace or --project, plus --scheme and --archive-path.
ShipThatApp is a plain Xcode project (no workspace), and the scheme is ShipThatApp:
asc xcode archive \
--project ShipThatApp.xcodeproj \
--scheme ShipThatApp \
--configuration Release \
--clean \
--archive-path .asc/artifacts/ShipThatApp.xcarchive \
--output table
--configuration Release— archive the Release config, not Debug. This is also where the manual checklist's reminder to dropPurchases.logLevelto.warnmatters: a Release archive is what reviewers run.--clean— runcleanbefore archiving for a from-scratch build (slower, but reproducible in CI).--overwrite— replace an existing.xcarchiveat the path instead of failing. Add it when re-running.--xcodebuild-flag— pass a raw argument straight through toxcodebuild, repeatable. Useful for pinning the destination, e.g.--xcodebuild-flag=-destination --xcodebuild-flag=generic/platform=iOS.
The generic/platform=iOS destination is the CLI equivalent of selecting Any iOS Device (arm64) in Xcode — you cannot archive against a simulator.
3. Export to a Signed IPA
asc xcode export runs xcodebuild -exportArchive and moves the produced .ipa to your --ipa-path. It needs the archive from step 2, an ExportOptions.plist, and the destination path:
asc xcode export \
--archive-path .asc/artifacts/ShipThatApp.xcarchive \
--export-options ExportOptions.plist \
--ipa-path .asc/artifacts/ShipThatApp.ipa \
--overwrite \
--output table
The ExportOptions.plist controls signing and method (App Store distribution, your team id, manual vs. automatic signing). Generate one the first time by exporting once through the Xcode Organizer and copying the ExportOptions.plist it writes, then reuse it.
Two export modes
If ExportOptions.plist produces a local IPA (the App Store Connect distribution method), asc writes it to --ipa-path and you upload it in a later step. If the plist sets destination=upload, xcodebuild uploads directly and no local IPA is written — add --wait to poll until the build appears and finishes processing. The split pipeline below uses the local-IPA mode so each stage stays inspectable; --ipa-path is still required either way.
Other useful flags: --timeout 10m caps the export, and --xcodebuild-flag=-allowProvisioningUpdates lets xcodebuild fetch/refresh profiles on the fly.
4. Validate Before Uploading
asc xcode validate wraps xcrun altool --validate-app to run Apple's server-side validation on the IPA before you spend an upload on it — the CLI equivalent of the Organizer's Validate App button. It catches missing icons, entitlement mismatches, and bad provisioning early.
asc xcode validate --ipa .asc/artifacts/ShipThatApp.ipa --output table
If asc auth stores your key in the keychain this is all you need; in a bare CI environment you can pass --api-key and --api-issuer explicitly (the same App Store Connect API key from Authenticating the asc CLI).
For ShipThatApp, validation is where an entitlement mistake surfaces cheaply — the placeholder aps-environment, App Group, or associated-domains values flagged in step 7 of the manual checklist will fail here rather than after a slow upload.
5. Upload & Wait for Processing
The canonical upload path is asc publish appstore without --submit — it uploads the IPA, waits for processing, and finds-or-creates the App Store version, but stops short of sending the build to review. (Submission, with --submit --confirm, is covered in Metadata & Submit with asc.)
asc publish appstore \
--app "app.shipthat.demo" \
--ipa .asc/artifacts/ShipThatApp.ipa \
--version "1.0.0" \
--wait \
--output table
--appaccepts the app id, the bundle id (app.shipthat.demo— swap in your own), or the exact app name. Omit it entirely ifASC_APP_IDis set.--versiondefaults to the version baked into the IPA, so it's optional when step 1 already set it; pass it to be explicit.--waitblocks until App Store Connect finishes processing the build.--poll-interval 30s(the default) and--timeout 30mtune the wait.--dry-runpreviews the full plan (upload → version → attach) without mutating anything — run it once to confirm the target version before a real upload.
If you only want the raw upload with no version handling, the lower-level asc builds upload --app "…" --ipa "…" --wait does exactly that. Either way the build lands in App Store Connect and begins processing.
Confirm the build processed
Processing turns an uploaded binary into a VALID build that TestFlight and the App Store can use. Inspect it without leaving the terminal:
# All builds for the version, with processing state.
asc builds list --app "app.shipthat.demo" --version "1.0.0" --output table
# Just the latest, full detail (grab its build id here).
asc builds info --app "app.shipthat.demo" --latest --output table
asc builds list reports each build's processingState (PROCESSING → VALID, or FAILED / INVALID). To block a script until the build is usable instead of polling by hand:
asc builds wait --app "app.shipthat.demo" --latest --timeout 20m
It exits 0 on VALID and non-zero on FAILED — clean to gate a CI step on.
Encryption compliance can stall a build
A build can sit waiting on the export-compliance question (the same one Xcode asks). ShipThatApp ships no non-exempt cryptography of its own — auth, purchases, and the AI proxy all ride standard HTTPS. Clear the prompt for the latest build in one shot:
asc builds update --app "app.shipthat.demo" --latest --uses-non-exempt-encryption=false
Confirm your app's actual usage before asserting this; set it true if you add your own encryption.
6. Distribute to TestFlight
A VALID build is ready for beta testing. The whole point of a TestFlight pass for ShipThatApp is to exercise the App-Store-specific flows on a real device, signed exactly as the store will sign it — the four that get boilerplate apps rejected:
- RevenueCat purchase + Restore Purchases — buy a subscription in the sandbox, then verify Restore re-grants it (the manual checklist flags that only
Paywall3Viewships a restore button). - Camera — open the Dex Scanner so iOS actually triggers the
NSCameraUsageDescriptionprompt; a missing string crashes here, not in review. - Account deletion — run Settings → Delete Account against production Supabase and confirm the user is gone.
Groups and testers
List and create beta groups, then add testers:
# See existing groups (and internal ones).
asc testflight groups list --app "app.shipthat.demo" --output table
# Create an internal group for yourself + teammates.
asc testflight groups create --app "app.shipthat.demo" --name "Internal Testers" --internal
# Add testers to a group by email.
asc testflight groups add-testers --group "GROUP_ID" --email "you@example.com,teammate@example.com"
asc testflight testers manages testers directly — list, add, invite, and export/import for CSV round-trips:
asc testflight testers add --app "app.shipthat.demo" --email "tester@example.com" --group "Internal Testers"
Push a build to a group
asc publish testflight uploads (or reuses) a build and distributes it to one or more groups in a single command. Reuse the already-processed build by id or build number so you don't re-upload:
asc publish testflight \
--app "app.shipthat.demo" \
--build-number "1" \
--group "Internal Testers" \
--wait \
--notify
--build-number(or--build BUILD_ID) distributes an existing build — no second upload.--grouptakes group ids or names, comma-separated.--notifyemails testers that a new build is available.--ipa app.ipainstead would upload a fresh binary;--submit --confirmwould additionally send it for beta app review (required before external testers can install).
"What to Test" notes
Give testers a checklist that mirrors the review traps. asc builds test-notes manages the per-build, per-locale "What to Test" text:
asc builds test-notes create \
--app "app.shipthat.demo" \
--latest \
--locale "en-US" \
--whats-new "Sign in with Apple, buy + restore the subscription (sandbox), scan with the camera, then delete your account."
The same notes can be managed per build with asc build-localizations (list / create / update) when you key off a specific --build BUILD_ID rather than --latest.
Test purchases for free in the sandbox
TestFlight builds run StoreKit in the sandbox, so the full RevenueCat purchase + restore flow costs nothing. Create a Sandbox tester in App Store Connect (Users and Access → Sandbox) and sign into it on the device under Settings → Developer — exactly as in the manual checklist. Your real RevenueCat products (the productIds / subGroupId you set in Config.swift) drive the paywall here.
Where This Lands You
After this pipeline you have a VALID build in App Store Connect, validated by Apple, distributed to TestFlight, and exercised against production Supabase, RevenueCat, and the AI proxy on a real device. The only thing left is metadata and the actual review submission — asc publish appstore … --submit --confirm and the readiness checks — which is the next page.
Related
- Authenticating the asc CLI —
asc auth login,asc doctor, API keys, and theASC_APP_ID/ env-var fallbacks every command here relies on. - Metadata & Submit with asc —
asc metadata,asc validate, and the canonicalasc publish appstore … --submit --confirmthat ships this build. - Shipping to the App Store — the manual/GUI checklist this pipeline automates: production secrets, Supabase, RevenueCat, the AI proxy, entitlements, and the review traps.
- Dex Scanner — the camera feature whose
NSCameraUsageDescriptionprompt you verify on the TestFlight build.