mobile-release
Skill15 starsChanged 23 days agoApache-2.0View on GitHub
Use when versioning or shipping a Kotlin Multiplatform app or shared library to Google Play and the App Store - "cut a release", "bump the version", versionCode, versionName, CFBundleShortVersionString, CFBundleVersion, release tags, semver for a KMP library, Fastlane lanes, TestFlight, Play internal track, staged or phased rollout, release checklist, signing keys, match, Play service account.
4 other projects use a version of this.
Adopted by
Most recently taken up by
- wrsilva/reis-mobiletheir own version, first seen Nov 8, 2026
- SWEStash/swe-workflow-skillstheir own version, first seen Nov 8, 2026
- nimadorostkar/Claude-Skills-collectiontheir own version, first seen Oct 13, 2026
- RSSNext/Folotheir own version, first seen Oct 8, 2026
Other versions4 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 4
- RSSNext/Folo★ 39ksame name
- nimadorostkar/Claude-Skills-collection★ 33same name
- wrsilva/reis-mobile★ 8same name
- SWEStash/swe-workflow-skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- android-to-kmp-migrationSkill
- compose-design-system-componentSkill
- compose-multiplatform-localizationSkill
- compose-multiplatform-navigationSkill
- compose-multiplatform-screenSkill
- figma-to-mobile-uiSkill
- kmp-api-endpointSkill
- kmp-build-checkSkill
- kmp-build-failure-triageSkill
- kmp-ci-github-actionsSkill
- kmp-code-reviewSkill
- kmp-crash-triageSkill
- kmp-data-layerSkill
- kmp-dev-machine-cleanupSkill
- kmp-expect-actualSkill
- kmp-feature-moduleSkill
- kmp-flaky-test-triageSkill
- kmp-gradle-version-catalogSkill
- kmp-koin-diSkill
- kmp-module-healthSkill
- kmp-room-databaseSkill
- kmp-secure-storageSkill
- kmp-swiftui-interopSkill
- kmp-test-coverage-backfillSkill
- kmp-testing-patternsSkill
- kmp-xcframework-distributionSkill
- mobile-accessibility-test-idsSkill
- mobile-bug-fixSkill
- mobile-feature-implementSkill
- mobile-feature-planSkill
- mobile-performance-fixSkill
- mobile-platform-paritySkill
- mobile-security-reviewSkill
The file
We split this skill into the rules it gives an agent
3 rules. 0 are common, 1 is distinctive, and 2 are very specific to this project.
0
Common
Most of the 4 edited versions we read keep these.
1
Distinctive
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 80
Put app.versionName and app.versionCode in gradle.properties.
- Very specific: line 112
If an Android-only tag used the bare v1.5.0, an SPM range such as .upToNextMajor(from: "1.0.0") could resolve to it and fetch a …
- Distinctive: line 189
Create annotated tags locally.
“Common” is measured: kept by at least 6 in 10 of the 4 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Mobile release
One version number, read by both apps; one tag scheme that package managers cannot misread; one checklist that is the same every time. Most release incidents in a two-platform app are not code bugs — they are numbers that moved on one platform and not the other, a tag that resolved to the wrong artifact, or a signing secret that ended up somewhere it should not be.
When to use
- Preparing a release of the Android app, the iOS app, or both.
- Setting up versioning so Android and iOS cannot drift apart.
- Choosing tag names in a repo that ships more than one artifact.
- Writing or fixing Fastlane lanes for Play and TestFlight / App Store.
- Planning a staged (Play) or phased (App Store) rollout and what to monitor.
- Versioning a shared KMP library consumed through Maven or Swift Package Manager.
When not to use
- The CI workflow YAML that runs these lanes: use
kmp-ci-github-actions. - Building and publishing an XCFramework / Swift package: use
kmp-xcframework-distribution. - A crash spike after release: use
kmp-crash-triage. - Security review of the release build (obfuscation, cert pinning, debug flags): use
mobile-security-review.
Step 0 — Read the project first
grep -rnE 'versionCode|versionName|applicationIdSuffix|versionNameSuffix|productFlavors' --include=build.gradle.kts .
grep -nE '^app\.|version' gradle.properties 2>/dev/null
grep -rnE 'MARKETING_VERSION|CURRENT_PROJECT_VERSION' iosApp --include='*.pbxproj' --include='*.xcconfig' 2>/dev/null
plutil -p iosApp/iosApp/Info.plist 2>/dev/null | grep -E 'CFBundle(ShortVersionString|Version)'
git tag -l | sort -V | tail -20 # existing tag scheme
ls fastlane Gemfile Gemfile.lock 2>/dev/null && cat fastlane/Fastfile 2>/dev/null
ls CHANGELOG.md 2>/dev/null
ls Package.swift 2>/dev/null # does this repo ship a Swift package?Record: where each version number lives today, existing tag format, flavours/build configurations, whether Fastlane exists and its lanes, whether the repo ships a Swift package. Follow an existing scheme if it is consistent; propose changes only for real defects (drift, ambiguous tags).
Workflow
1. Version policy
Apps and libraries both use semver MAJOR.MINOR.PATCH for the user-facing version:
| Change | App versionName / CFBundleShortVersionString | Library version |
| Bug fix only, no new screens or API calls | PATCH + 1 | PATCH + 1 |
| New feature, backwards compatible | MINOR + 1, PATCH = 0 | MINOR + 1 (new public API) |
| Breaking: removed flow, data migration, new minimum backend contract | MAJOR + 1 | MAJOR + 1 (any public API/ABI break) |
| Internal/test build only | unchanged | -SNAPSHOT / pre-release |
Build numbers are separate:
- Android
versionCode: an integer,+1for every artifact uploaded to Play, including re-uploads of the sameversionName. Never reused, never decreased. Play's maximum is 2100000000. - iOS
CFBundleVersion: must increase for every upload of the sameCFBundleShortVersionString. Using the AndroidversionCodevalue for both keeps them trivially in step.
For libraries: a published version is immutable. Never re-publish the same version with different bytes — bump the patch. Check public API changes before choosing the bump; the binary-compatibility-validator plugin (or the ABI validation built into newer Kotlin Gradle plugin versions) makes breaks visible in review.
2. One source of truth, read by both builds
Put app.versionName and app.versionCode in gradle.properties. Android reads them in
build.gradle.kts; iOS gets them through a generated xcconfig and, in release lanes, as xcargs.
Full snippets — Gradle, signing from env vars, xcconfig sync script, CI drift guard — are in
references/version-single-source.md.
Two rules carry most of the value:
versionNameandversionCodemove together, in the same commit. Store ordering uses the code; humans, crash dashboards and any update-prompt or minimum-version logic use the name. Many apps also send the build number to their backend in a request header. Bump one without the other and those systems disagree about which build is installed.- Keep
versionNameclean semver in every flavour. DistinguishstagingfromproductionwithapplicationIdSuffix(Android) and a per-configurationPRODUCT_BUNDLE_IDENTIFIER(iOS), not withversionNameSuffix.
Done when: changing the two lines in gradle.properties (plus the sync script) changes the version
shown by both apps.
3. Tag scheme
Swift Package Manager resolves versions only from tags that are valid semver, optionally prefixed
with v. Anything else is invisible to it. So in any repository that ships a Swift package, the
bare tags belong to the package:
| Artifact | Tag | Example |
| Swift package / XCFramework (SPM) | vX.Y.Z or X.Y.Z (match what consumers already use) | v2.0.3 |
| Android app | android-vX.Y.Z | android-v1.4.0 |
| iOS app | ios-vX.Y.Z | ios-v1.4.0 |
| Maven/Gradle library from a repo that also ships SPM | android-vX.Y.Z (or maven-v…) | android-v1.5.0 |
| Anything else | <platform>-vX.Y.Z | web-v3.1.0 |
If an Android-only tag used the bare v1.5.0, an SPM range such as .upToNextMajor(from: "1.0.0")
could resolve to it and fetch a Package.swift pointing at the wrong binary. Use a hyphen, not a
slash (android-v1.4.0, not android/v1.4.0): slashes are treated as paths by some tools and
UIs. Use annotated tags (git tag -a). Tags are immutable contracts — to fix a bad release, ship
X.Y.Z+1; do not delete and re-push a tag.
Audit:
git tag -l 'v[0-9]*.[0-9]*.[0-9]*' # SPM-visible
git tag -l 'android-v*' 'ios-v*' # app tags4. Fastlane lanes
Copy assets/Fastfile.example to fastlane/Fastfile, pin fastlane in
a Gemfile (run lanes with bundle exec fastlane …), and adjust the TODOs. Lanes:
| Lane | Does |
android internal | gradle → signed bundleRelease → upload_to_play_store(track: "internal") |
android promote_production rollout:0.1 | promotes the internal build to production as a staged rollout |
android update_rollout rollout:0.5 | raises the rollout fraction; 1 completes it |
ios beta | setup_ci → match(readonly: true) → build_app with the shared version → upload_to_testflight |
ios release | submits the TestFlight build for review with phased_release: true |
For Kotlin Multiplatform iOS builds, the Xcode build phase usually runs Gradle to build the Kotlin
framework — the machine running build_app needs a JDK on PATH.
5. Signing secrets never in git
- Android: upload keystore and passwords come from CI secrets (keystore as base64, decoded to a
temp dir at build time). The Play service-account JSON is a secret passed as
json_key_data. Enrol in Play App Signing so a lost upload key can be reset. - iOS: certificates and profiles via
matchin a private repo (or cloud storage), encrypted withMATCH_PASSWORD; CI usesreadonly: trueso it never creates or revokes certificates. Authenticate with an App Store Connect API key (app_store_connect_api_key), not an Apple ID password with 2FA. .gitignore:*.jks,*.keystore,*.p8,*.p12,*.mobileprovision, service-account JSON,fastlane/report.xml,fastlane/README.mdif generated.
Stop and ask the user before creating, rotating or deleting any keystore, certificate,
provisioning profile or API key, and before running match without readonly.
6. Release checklist
Copy this into the release issue or PR and tick it off.
- [ ] Release branch / commit chosen; CI green on it (both platforms)
- [ ] Version bumped in gradle.properties (name + code together); iOS xcconfig synced
- [ ] CHANGELOG.md updated (user-facing notes; links to issues/PRs)
- [ ] Store "what's new" text written for Play and App Store / TestFlight
- [ ] Crash-free baseline recorded for the current production version (users and sessions)
- [ ] Android: internal track build smoke-tested on a real device
- [ ] iOS: TestFlight build smoke-tested on a real device
- [ ] R8 mapping and dSYMs uploaded to the crash reporter (or archived)
- [ ] Tags created: android-vX.Y.Z / ios-vX.Y.Z (pushed only after approval)
- [ ] Play: staged rollout started (e.g. 10 %) · App Store: phased release on
- [ ] Monitoring window: crash-free rate, ANR rate, top new crashes, reviews, backend errors
- [ ] Rollout expanded, or halted with a fix planRollout and monitoring. Play staged rollouts take an explicit fraction you raise over time and can be halted. App Store phased release spreads an update over 7 days to users with automatic updates (1 %, 2 %, 5 %, 10 %, 20 %, 50 %, 100 %) and can be paused for up to 30 days in total; users can still update manually. Compare the new version's crash-free users and sessions with the baseline you recorded before expanding. Google Play's core-vitals "bad behaviour" thresholds are a 1.09 % user-perceived crash rate and 0.47 % user-perceived ANR rate overall (8 % per phone model) — check the current values in Play Console before relying on them.
7. Commit and tag
chore(release): bump to 1.4.0 (42)Create annotated tags locally. Push commits and tags, upload to stores and start rollouts only after the user confirms — each of those is visible to users or irreversible.
Quick reference
| Item | Android | iOS |
| User-facing version | versionName | CFBundleShortVersionString ← MARKETING_VERSION |
| Build number | versionCode (int, +1 per upload) | CFBundleVersion ← CURRENT_PROJECT_VERSION (grows per upload) |
| Side-by-side environments | applicationIdSuffix | per-configuration PRODUCT_BUNDLE_IDENTIFIER |
| Tag | android-vX.Y.Z | ios-vX.Y.Z (bare vX.Y.Z reserved for SPM) |
| Upload | upload_to_play_store (supply) | upload_to_testflight (pilot) |
| Store release | track_promote_to: "production" + rollout | upload_to_app_store (deliver) + phased_release |
| Signing | upload keystore from CI secrets | match readonly + App Store Connect API key |
Verify
Report each check as PASS, FAIL or NOT RUN with the reason. Never mark a check PASS that you
did not run.
grep -E '^app\.version(Name|Code)=' gradle.properties # both present
./gradlew :androidApp:assembleRelease --dry-run # config resolves (no signing needed for --dry-run)
bash scripts/sync-ios-version.sh && git diff --exit-code iosApp/Configuration/Version.xcconfig
xcodebuild -project iosApp/iosApp.xcodeproj -scheme iosApp -showBuildSettings -configuration Release \
| grep -E ' (MARKETING_VERSION|CURRENT_PROJECT_VERSION) =' # macOS: matches gradle.properties
bundle exec fastlane lanes # lanes parse and are listed
ruby -c fastlane/Fastfile # Ruby syntax
git tag -l 'v[0-9]*' 'android-v*' 'ios-v*' # scheme consistent
git ls-files | grep -iE '\.(jks|keystore|p8|p12|mobileprovision)$' # expect no outputUploads, promotions and submissions are verified only by their real run — report them NOT RUN
unless the user asked you to run them and you saw them succeed.
Common mistakes
| Mistake | Why it bites | Fix |
Bumping versionName without versionCode (or the reverse) | Store, crash dashboards and backend update checks disagree about the build | Both in one commit, from one file |
versionNameSuffix per flavour | The version is no longer plain semver; version comparisons break | applicationIdSuffix only |
| iOS version typed by hand in Xcode | Drifts from Android within a release or two | xcconfig generated from gradle.properties; xcargs in release lanes |
Reusing a versionCode / CFBundleVersion | Upload rejected | +1 per upload, including re-uploads |
Bare vX.Y.Z tags for Android in an SPM repo | SPM resolves the Android tag and fetches the wrong binary | Prefix every non-SPM tag: android-v… |
android/v1.4.0 slash tags | Some tools and UIs treat them as paths | Hyphen: android-v1.4.0 |
| Deleting and re-pushing a tag | Consumers pinned to it now get different bytes | Ship a patch version |
Keystore, .p8 or service-account JSON committed | Permanent leak in history; rotation required | Secrets in CI only; .gitignore; scan with git ls-files |
match without readonly on CI | CI can create or revoke certificates for the whole team | readonly: true; setup_ci |
| Runtime signature check lists only the upload key | Play-signed builds fail the check in production | Include the Play app signing certificate |
| 100 % rollout on day one | A crash reaches every user before you see it | Staged / phased rollout against a recorded baseline |
| No mapping file / dSYMs kept | Production stack traces cannot be symbolicated | Upload or archive them in the release job |
Related skills
kmp-ci-github-actions— the release workflow that runs these lanes with protected secrets.kmp-xcframework-distribution— Swift package and XCFramework publishing (the bare-semver tags).kmp-crash-triage— investigating a crash spike during rollout.mobile-security-review— release build hardening.kmp-build-check— the both-platform check before cutting the release.- External: fastlane docs · App Store Connect CLI skills · Google Play CLI skills.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

