name: appstore-release description: Ship an iOS app (built with EAS/Expo) to App Store review end-to-end via the App Store Connect API — credentials, build attach, metadata, screenshots, pricing, age rating, content rights, review info, subscriptions, and the review submission. Use when the user says "publish/submit my app", "send it to App Review", "ship the iOS app", "fill out App Store Connect", "upload metadata/screenshots", or "automate App Store Connect". Parametric: works for any app given an ASC API key + app id + a config file.
App Store Release
Drive an entire iOS App Store submission from the App Store Connect API. Design goal: the user provides credentials + a config.json once, then the agent does everything that is automatable and only interrupts the user for the handful of things Apple genuinely does not expose via API. Do not ask the user to re-confirm things that are already in the config or have sensible defaults.
Operating principle (read this first)
- Config-driven, not prompt-driven. All app content lives in
config.json. Run commands; don't interview the user. - Only two things are UI-only (Apple has no API): App Privacy data-collection and first-time subscription attach + submit. The tooling detects and surfaces these with exact clicks. Everything else is scripted.
- Ask the user only when: config is missing a required field (tell them exactly which), a business decision isn't in config (subscription price, release timing), or before the single irreversible action (final submit). Otherwise proceed.
- Idempotent: every command is safe to re-run.
node cli.mjs releasecan be run repeatedly.
Architecture (SOLID, so you can extend it)
scripts/
cli.mjs single entrypoint / dispatcher (node cli.mjs <command>)
lib/ injected services, one responsibility each
env.mjs parse+validate env (creds, app id)
jwt.mjs ES256 token provider
client.mjs AscClient — auth + HTTP + uniform errors
discovery.mjs locate ASC resources by natural key (no hardcoded ids)
assets.mjs reserve→upload→commit binary uploader
config.mjs load + validate config, list what's missing
log.mjs structured results + machine-readable summary
context.mjs composition root (dependency injection)
commands/ one file per task; uniform contract (see _contract.md)
Add a capability = add a commands/x.mjs (meta + run(ctx)) and register it in cli.mjs.
Setup (one time) — see references/setup.md
- ASC API key (App Manager): download
AuthKey_XXXX.p8, note Key ID + Issuer ID. - App record exists in ASC (bundle id registered).
ASC_APP_ID= numeric id from the app's ASC URL. - EAS project configured,
eas logindone. - Fill
config.jsonfromreferences/config-template.json. Validate anytime: any command prints exactly which fields are missing.
Environment
export ASC_KEY_ID=... ASC_ISSUER_ID=... ASC_P8_PATH=/abs/AuthKey_XXXX.p8 # gitignore the .p8
export ASC_APP_ID=1234567890
export APPSTORE_CONFIG=/abs/config.json
cd scripts # then: node cli.mjs <command>
Runbook
1. Orient
node cli.mjs status # full read-only overview
node cli.mjs check # readiness + the UI-only steps still pending
2. Build the binary (EAS)
- If
eas buildfails with stale credentials ("provisioning profile expired / no certificate with serial…"):then setnode cli.mjs credentials # fresh dist cert + profile → credentials.json"credentialsSource":"local"on the eas.json production profile. - Build + submit the binary (export
EXPO_ASC_API_KEY_PATH/KEY_ID/ISSUER_ID+EXPO_APPLE_TEAM_ID+EXPO_APPLE_TEAM_TYPEfor non-interactive):Wait until the build is VALID (eas build -p ios --profile production --non-interactive --no-wait eas submit -p ios --profile production --id <buildId>node cli.mjs status).
3. Fill the entire listing in one shot
node cli.mjs release # attach build → metadata → pricing → content-rights →
# age-rating → category → review-info → screenshots →
# subscription → check
release runs the whole pipeline (idempotent) and ends with check, which prints the remaining UI-only steps. Screenshots must exist first (config.screenshots.dir) — see references/screenshots.md to generate them.
4. The two UI-only steps (hand off with exact clicks)
check lists these when they apply:
- App Privacy → Data Collection: declare the data types matching the app's privacy manifest, then Publish. ⚠️ If the binary ships
NSUserTrackingUsageDescriptionbut you declare no tracking, Publish is blocked → remove the key, rebuild (references/gotchas.md). - First-time subscription: version page → In-App Purchases and Subscriptions → Select → → Save → Add for Review → Submit. (The API can't attach a first subscription.)
5. Submit
node cli.mjs check # confirm nothing's missing
node cli.mjs submit --submit # app-only / updates (no first-time IAP)
For a first-time subscription, the user submits in the UI (step 4.2). Verify with status: version + subscription both WAITING_FOR_REVIEW.
Rejections
On rejection the version becomes editable again (REJECTED/METADATA_REJECTED/DEVELOPER_REJECTED). Read the resolution-center message, fix (metadata via release, or rebuild for binary issues), resubmit.
References
references/setup.md— key creation, app record, EAS, secrets hygiene.references/gotchas.md— every real pitfall (read before the first run).references/screenshots.md— generating exact-size PNGs (HTML → headless Chrome).references/config-template.json— per-app config (validated bylib/config.mjs).