| name | ios-app-store-release |
| description | Ship iOS apps to App Store Connect end-to-end — Xcode Cloud, ASC API metadata and screenshots, release manifests, TestFlight, and prepare-for-submission without auto-submit. Use when publishing an iOS app, setting up App Store automation, ASC API scripts, store screenshots, Xcode Cloud release workflows, or preparing a version for App Review. |
iOS App Store Release
Agent playbook for repeatable, mostly-automated iOS releases. Assumes Swift/SwiftUI, optional XcodeGen, Python ASC client, and Xcode Cloud for archives.
Out of scope (agent already knows): creating Apple Developer accounts, basic xcodebuild, writing App Store copy from scratch, generic git tagging.
Before starting
Collect per-app constants (store in scripts/asc/constants.py or equivalent):
| Constant | Example | Notes |
|---|
APP_ID | ASC apps resource id | Not bundle id |
BUNDLE_ID | co.example.app | |
DEFAULT_LOCALE | en-US | |
CI_PRODUCT_NAME | App name in Xcode Cloud | May lag in ASC API after first workflow setup |
| Screenshot display types | APP_IPHONE_67, APP_IPAD_PRO_3GEN_129 | Validate against ASC — invalid enums fail upload |
| Composed pixel sizes | iPhone 1290×2796, iPad 2064×2752 | Must match display type |
Credentials: ~/.config/app-store-connect/credentials.json with issuer_id, key_id, private_key_path, optional team_id.
pip install -r scripts/requirements-asc.txt
Target architecture (per app repo)
releases/<version>.yaml # ship intent: version, whats_new, flags
store-assets/
metadata/<locale>.yaml # keywords, URLs, copyright, review contact
metadata/review-notes.txt
scenes.yaml # screenshot scenes → display type + raw path
composed/ # marketing frames (generated)
scripts/
release.py # orchestrator: validate → test → bump → tag → wait → assets → asc-sync → submit
asc/{client,metadata,builds,screenshots,submit}.py
compose_store_assets.py
upload_store_assets.py
capture-screenshots.sh
check_xcode_cloud.py
ci_scripts/
ci_post_clone.sh # brew xcodegen + generate (if spec-driven)
ci_pre_xcodebuild.sh # sanity check generated .xcodeproj exists
Design choices that paid off:
- Tests local, archive in Cloud — saves Cloud compute; only Release workflow on tag.
- YAML manifest per release — agents edit ship intent, not ASC UI.
submit_for_review: false default for first ship — human presses Submit after visual check.
.release-state.json + --resume — idempotent recovery; skip completed steps with strict < ordering (failed step must re-run).
- Legal/support URLs — separate public repo + GitHub Pages; app repo stays private.
Reference templates and API traps: reference.md.
End-to-end ship workflow
Copy and track:
- [ ] ASC app record exists; version created (PREPARE_FOR_SUBMISSION)
- [ ] Legal pages live (privacy + support URLs reachable)
- [ ] Xcode Cloud Release workflow: correct *.xcodeproj path, tag trigger
- [ ] Demo/screenshot data isolated from Release builds (#if DEBUG)
- [ ] release manifest written
- [ ] Dry-run pipeline
- [ ] Ship (or resume)
- [ ] Verify readiness checklist
- [ ] Human: App Privacy questionnaire in ASC UI if Submit disabled
- [ ] Human: Submit for Review (unless auto-submit enabled)
1. Release manifest
version: "1.0"
whats_new: |
Initial release.
promotional_text: ""
refresh_assets: true
submit_for_review: false
whats_new required for validator even when ASC rejects it on first release (handle in metadata sync — see reference).
2. Orchestrator steps
Typical order:
- validate — manifest + required metadata fields
- tests — unit tests locally (
GregorTests-style, not full UI suite)
- bump —
MARKETING_VERSION + increment CURRENT_PROJECT_VERSION in project.yml, run xcodegen
- commit + tag —
v{version}, push branch + tag (triggers Cloud)
- wait-build — poll Xcode Cloud product or ASC builds API until
processingState: VALID
- assets (if
refresh_assets) — capture → compose → upload screenshots
- asc-sync — metadata, review detail, export compliance, attach build
- submit (optional) — create submission; often blocked by API role or left to human
./scripts/ship.sh ship releases/1.0.yaml --dry-run
./scripts/ship.sh ship releases/1.0.yaml
./scripts/ship.sh ship releases/1.0.yaml --resume
./scripts/ship.sh status --version 1.0
3. Build wait fallback
If find_ci_product() returns nothing (common right after first workflow setup), poll ASC builds:
GET /v1/builds?filter[app]={APP_ID}&include=preReleaseVersion&sort=-uploadedDate
Trap: build.attributes.version is the build number (e.g. 3), not marketing version. Match via included preReleaseVersions.attributes.version.
Local archive fallback when Cloud is broken: xcodebuild archive → exportArchive → xcrun altool --upload-app. Copy .p8 to ~/.appstoreconnect/private_keys/AuthKey_{KEY_ID}.p8 — altool ignores custom --apiKeyPath unless file is in that directory.
After upload, PATCH build: usesNonExemptEncryption: false (and set ITSAppUsesNonExemptEncryption = NO in Info.plist for future uploads).
Screenshots (non-obvious)
Deep capture / compose / ASC upload workflow: ios-app-store-screenshots.
Summary for ship pipelines: raw simulator capture → compose_store_assets.py (Pillow + scenes.yaml) → upload_store_assets.py --version X.Y. Use XCTest attachments + SCREENSHOT_BASENAME export (do not rely on xcodebuild shell env for launch args). Pin simulator names; gate DemoMode with #if DEBUG.
Demo / mock data (critical)
Store screenshots need content; App Store builds must not.
- Gate all demo vault seeding behind
#if DEBUG and launch args (e.g. -UITestDemo).
- Production: empty journal only (
JournalBootstrap.ensureDefaultJournal — directories, no notes).
- Review notes must describe real first-launch behavior (empty journal), not screenshot fixtures.
- Never symlink or ship bundled sample markdown in the app target.
Xcode Cloud
ci_post_clone.sh: install/run xcodegen generate when .xcodeproj is generated from spec.
Workflow project path must exactly match repo root project name (Gregor.xcodeproj). Renaming the project requires updating the workflow in Xcode → Product → Xcode Cloud → Manage Workflows — ASC stores containerFilePath; a stale name fails before CI scripts run. Do not commit symlinks for old names.
Confirm:
- Release workflow triggers on tag
v*
- Scheme = app target, action = Archive, distribution = App Store
Gregor.xcodeproj/xcshareddata/xcodecloud/manifest.json ids match after workflow creation (Xcode writes these)
Monitor: scripts/check_xcode_cloud.py --product "{AppName}" --wait 1200
ASC sync checklist (API-verifiable)
After asc-sync, confirm:
| Check | API hint |
|---|
| Build attached | appStoreVersions → build relationship |
| Export compliance | builds → usesNonExemptEncryption: false |
| Screenshots | appScreenshotSets with include=appScreenshots; assetDeliveryState.state: COMPLETE |
| Support URL | version localization |
| Privacy policy URL | appInfoLocalizations (not version localization) |
| Copyright | appStoreVersions |
| Content rights | apps.contentRightsDeclaration |
| Age rating | appInfos/.../ageRatingDeclaration |
| Review contact | appStoreReviewDetail — first/last name, phone in E.164, email |
Human-only in ASC UI: App Privacy nutrition labels (no stable public API), paid agreements, tax/banking.
Human gates (do not block automation on these)
- First GitHub ↔ Xcode Cloud connect
- App Privacy questionnaire
- Pressing Submit for Review (recommended even when API submit exists)
- App Review rejection iteration
Porting to a new app
- Create ASC app + version in UI (API cannot create apps).
- Fill age rating, export compliance questionnaire once.
- Wire constants, metadata YAML, release manifest.
- Copy/adapt
scripts/asc/* and orchestrator; rename product/scheme.
- Add UI screenshot test +
DemoMode (DEBUG-only).
- Create Xcode Cloud Release workflow; verify project path.
- First ship with
submit_for_review: false; human verifies ASC listing then submits.
Related skills
Additional resources