| name | ship-mobile-app-store-release |
| description | Prepares and ships iOS App Store and Google Play releases — code signing (certs/provisioning, upload vs app-signing keystores), marketing-version/build-number bumps, Fastlane/EAS/Gradle build lanes, TestFlight/Play-track uploads, phased rollout, store metadata, and review-rejection remediation. |
| when_to_use | Cutting a mobile release — fixing signing or keystores, automating builds with Fastlane/EAS/Gradle, uploading to TestFlight or Play tracks, staged/phased rollout, store listing/metadata, or fixing an App Store/Play review rejection. Distinct from deploy-release (server/web deploys), release-notes (changelog prose), and cicd-pipeline-author (the CI workflow that calls these lanes). |
When to Use
Reach for this skill when the artifact is a store binary (.ipa/.aab) headed for Apple or Google review, not a server rollout:
- "Set up code signing / fix
No profiles for 'com.x' were found / rotate an expired cert"
- "Generate or recover an Android keystore; enroll in Play App Signing; upload key vs app-signing key confusion"
- "Bump the version + build number and ship to TestFlight / Play internal track"
- "Wire up Fastlane (
gym/pilot/supply) or EAS (eas build/eas submit) in CI"
- "Do a phased / staged rollout at 1% → 100%, then halt it"
- "Upload screenshots, description, what's-new, age rating"
- "Our build got rejected for Guideline 4.3 / 5.1.1 / IAP — fix and resubmit"
NOT this skill:
- Deploying a server, web app, or container to prod (blue-green, canary pods) → deploy-release
- Writing the human-facing changelog / what's-new prose → release-notes (this skill only places the text)
- Authoring the CI YAML that runs these lanes (job graph, caching, secrets injection) → cicd-pipeline-author
- Storing the keystore password / App Store Connect API key safely → secrets-management
Steps
-
Set versioning first — two independent numbers, never reused. Marketing version (user-visible) vs build number (uniqueness). Bump the build number on every upload even within the same marketing version; stores reject duplicates.
| Field | iOS (Info.plist) | Android (build.gradle) | Rule |
|---|
| Marketing version | CFBundleShortVersionString | versionName | SemVer 1.4.0, free-form |
| Build number | CFBundleVersion | versionCode | Must strictly increase per upload. iOS: any string with increasing numerics. Android: a single integer, monotonic, max 2.1B |
Default: derive build number from CI run number or commit count (git rev-list --count HEAD) so it auto-increments and is reproducible. Use Fastlane increment_build_number / increment_version_code — never hand-edit and forget.
-
iOS signing — prefer Fastlane match, not Xcode "Automatically manage signing", for CI. You need a Distribution certificate (.p12) + an App Store provisioning profile bound to the explicit App ID and that cert.
match stores certs/profiles encrypted in a private git repo (or S3/GCS), so every machine/CI agent shares one cert instead of each minting a new one (Apple caps you at 2–3 distribution certs).
- Authenticate CI with an App Store Connect API key (
.p8 + key id + issuer id) via app_store_connect_api_key — not your Apple ID password / 2FA, which breaks unattended.
- Match entitlements to enabled capabilities: Push →
aps-environment: production in the release entitlements; App Groups, Sign in with Apple, Associated Domains must each be toggled on the App ID and present in the profile, or the build is signed but the feature silently fails.
bundle exec fastlane match appstore
bundle fastlane match appstore --
Common Errors
- Reusing a build number.
ERROR ITMS-90186 / "Version already exists" (iOS) or Version code N has already been used (Play). Always increment per upload, even for the same marketing version.
- Each CI agent minting its own distribution cert. Hits Apple's 2–3 cert cap, then nothing can sign. Use
match --readonly so agents fetch one shared cert.
- Apple ID + password (not API key) in CI. Breaks on 2FA prompts. Use an App Store Connect API key (
.p8).
- Committing the keystore or its passwords. Anyone with the repo can sign as you. Keystore + passwords go to a secrets store;
.gradle properties out of VCS.
- Treating the upload key as the app-signing key. With Play App Signing, Google re-signs; the key in your keystore is only the upload key. Don't panic-rotate the wrong one — resetting the upload key is a support flow, not a config change.
- Shipping
.apk to a new Play app. Rejected — new apps require .aab. Use bundleRelease, not assembleRelease.
- Entitlement enabled in Xcode but not on the App ID / profile. Signs fine, feature dead at runtime (push silently drops, App Group reads empty). Toggle the capability on the App ID and regenerate the profile.
aps-environment: development in a store build. Push notifications work in debug, fail in production. Release entitlements must use production.
- Missing a required screenshot size. Submission blocked with no obvious cause. Supply every required device dimension for the platform.
- 100% rollout on day one. A crash hits every user at once with no staged escape. Start at 1% and ramp.
- Expecting an instant rollback. Neither store has one. iOS needs an expedited re-review; Play halt only stops new installs. Always keep a hotfix lane ready.
- Login-walled app with no demo credentials. Auto-reject under 5.1.1/2.1. Reviewer can't get past the login. Put working creds in Review Notes.
Verify
- Versioning: the build number is strictly greater than the last accepted upload (check App Store Connect / Play Console history); marketing version is correct.
- iOS signing:
codesign -dvv <App>.app shows the Distribution cert and the explicit App ID; the embedded profile is the App Store profile and not expired (security cms -D -i embedded.mobileprovision).
- Android signing:
jarsigner -verify -verbose app-release.aab (or apksigner verify on a built APK) passes with the upload key; the app is enrolled in Play App Signing.
- Entitlements:
codesign -d --entitlements - <App>.app lists every capability the app uses, with aps-environment: production for a store build.
- Upload landed: the build appears in TestFlight Internal / the target Play track and finishes processing without an email rejection (ITMS-* / Play pre-launch report clean).
- Metadata complete: what's-new, all required screenshot sizes, age/content rating, and the privacy/Data-safety form are filled — the submit button is enabled with no blocking warnings.
- Rollout staged: production is at the intended start percentage (e.g. 1%), not 100%, and the phased/staged toggle is on.
- Post-release watch: crash-free sessions and ANR are visible on a dashboard, and you've confirmed the halt/pause control works (locate it, don't trigger it).
Done = a binary with a unique, increasing build number is correctly signed (distribution cert + production entitlements / upload key + Play App Signing), uploaded to the intended track with complete metadata, rolling out at a staged percentage, and the crash-free monitor + halt path are both confirmed available.