From mobile-engineering
Step-by-step guide for automating the iOS App Store and Google Play release pipeline — code signing, build automation with Fastlane, TestFlight and internal track distribution, phased rollout, and the common pipeline failure modes.
How this skill is triggered — by the user, by Claude, or both
Slash command
/mobile-engineering:app-store-release-pipelineThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use when automating the app release process for the first time, diagnosing a signing failure in CI, designing a phased rollout, or establishing the review and release checklist for a mobile team.
Use when automating the app release process for the first time, diagnosing a signing failure in CI, designing a phased rollout, or establishing the review and release checklist for a mobile team.
Choose one signing approach before writing any CI config:
| Approach | Managed by | Best for |
|---|---|---|
| Xcode Automatic (local only) | Xcode + Apple Developer portal | Solo developer, no CI |
| match (Fastlane) | Git-encrypted cert repo | Teams + CI — recommended |
| Manual profiles | You | Legacy; avoid for new pipelines |
Fastlane match setup:
# One-time setup
fastlane match init
# Choose: git, S3, or Google Cloud Storage
# Sets MATCH_GIT_URL in CI secrets
fastlane match appstore # creates/renews App Store cert + profile
fastlane match development
match stores encrypted certificates in a private Git repo or cloud bucket — every CI runner decrypts on demand using MATCH_PASSWORD. No developer ever manages profiles manually.
# keystore (one-time, keep the .jks file outside the repo)
keytool -genkey -v -keystore release.jks \
-alias release -keyalg RSA -keysize 2048 -validity 10000
# Store in CI secrets:
# KEYSTORE_BASE64 = base64 -i release.jks
# KEY_ALIAS = release
# KEY_PASSWORD = ...
# STORE_PASSWORD = ...
build.gradle signing config reads from env at build time — the .jks file is never committed:
android {
signingConfigs {
release {
storeFile file(System.getenv("KEYSTORE_PATH") ?: "debug.jks")
storePassword System.getenv("STORE_PASSWORD")
keyAlias System.getenv("KEY_ALIAS")
keyPassword System.getenv("KEY_PASSWORD")
}
}
}
Fastfile structure:
platform :ios do
lane :beta do
match(type: "appstore")
increment_build_number(
build_number: ENV["BUILD_NUMBER"] || Time.now.to_i.to_s
)
build_app(
scheme: "MyApp",
configuration: "Release",
export_method: "app-store"
)
upload_to_testflight(
skip_waiting_for_build_processing: true,
changelog: changelog_from_git_commits(commits_count: 5)
)
end
lane :release do
beta
upload_to_app_store(
submit_for_review: false, # manual review trigger
automatic_release: false,
phased_release: true
)
end
end
platform :android do
lane :beta do
gradle(
task: "bundle",
build_type: "Release",
properties: {
"android.injected.signing.store.file" => ENV["KEYSTORE_PATH"],
...
}
)
upload_to_play_store(track: "internal")
end
end
name: Mobile Release
on:
push:
tags: ['v*']
jobs:
ios-beta:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with: { bundler-cache: true }
- name: Setup signing
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
run: bundle exec fastlane ios beta
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.ASC_KEY_CONTENT }}
android-beta:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore.jks
- run: bundle exec fastlane android beta
env:
KEYSTORE_PATH: keystore.jks
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
iOS TestFlight → App Store:
upload_to_testflight → internal testers (0 review delay).upload_to_app_store(phased_release: true) → starts at 1% day 1, 2% day 2, 5% day 3, 10% day 4, 20% day 5, 50% day 6, 100% day 7.Google Play tracks: internal → alpha → beta (% rollout configurable) → production.
| Platform | Version shown to user | Build/version code |
|---|---|---|
| iOS | CFBundleShortVersionString (semver) | CFBundleVersion (monotonically increasing integer) |
| Android | versionName (semver) | versionCode (monotonically increasing integer) |
Rule: versionCode / CFBundleVersion must be strictly increasing — the stores reject a build with a lower or equal build number. Use CI_BUILD_NUMBER (pipeline run number) as the build number.
.jks keystore or match certificates — the signing artifacts are the crown jewels; store them in secrets/encrypted storage only.match in CI.skip_waiting_for_build_processing: true on TestFlight uploads — the default waits up to 30 minutes for Apple processing, blocking the CI job; let it process asynchronously.npx claudepluginhub mcorbett51090/ravenclaude --plugin mobile-engineeringGuides completion of development work by verifying tests, detecting environment, and presenting structured options for merge, PR, or cleanup.
Enforces test-driven development: write failing test first, then minimal code to pass. Use when implementing features or bugfixes.
Guides creation and editing of skills using test-driven development with pressure scenarios and subagents to verify agent compliance.