Mobile CI/CD Overview
Instructions
A mobile CI/CD pipeline reliably turns a commit into a signed, store-ready artifact. Builds are slow, signing is finicky, and store APIs are rate-limited. A pragmatic pipeline invests in caching, parallelism, and guard rails more than in clever automation.
1. Pipeline Shape
Commit -> PR checks (lint, unit, integration)
-> Build artifact (debug)
-> UI smoke on emulator
-> Merge to main
-> Build artifact (release, signed)
-> Upload to internal/TestFlight/Play Internal
-> Optional: promote to production with staged rollout
Keep PR lane fast (under 10 minutes). Promote heavy work (device cloud, E2E, size analysis) to the main lane.
2. Branching
- Trunk-based with short-lived branches works best for mobile. Long release branches invite merge pain and diverging metadata.
- Release branches only when multiple versions must be supported simultaneously (enterprise distribution).
- Tag every production release on the merge commit (
v2.5.0).
- Protect
main with required checks, linear history, and signed commits.
3. Build Matrices
- iOS: one macOS runner per job. Match Xcode version to the project's
required_xcode_version. Keep SDK up to date.
- Android: Linux runners; use cached Gradle caches and AVD images.
- Flutter: matrix of iOS + Android builds; share analyzer/test job.
- React Native: similar matrix; use Hermes; cache Metro.
- Avoid matrices that fan out to 20 jobs for a PR. Most issues repro on one target.
4. Caching
Caching is where mobile pipelines win or lose.
- Gradle: cache
~/.gradle/caches, ~/.gradle/wrapper, keyed on **/*.gradle*, **/gradle-wrapper.properties, gradle/libs.versions.toml.
- CocoaPods: cache
Pods and ~/Library/Caches/CocoaPods, keyed on Podfile.lock.
- SwiftPM: cache
~/Library/Developer/Xcode/DerivedData/*/SourcePackages, keyed on Package.resolved.
- Yarn/npm: cache
~/.yarn/cache or ~/.npm keyed on lockfile. Enable Yarn zero-install if appropriate.
- Dart/pub: cache
~/.pub-cache, keyed on pubspec.lock.
- Xcode DerivedData: cache selectively; often cheaper to rebuild than to invalidate.
5. Signing
- Keep signing assets out of the repo. Use CI secrets (
.p12, keystore, provisioning profiles, App Store Connect API key JSON).
- Prefer App Store Connect API keys over Apple ID-based
altool. They are session-safe and scriptable.
- Prefer signing by Google Play (upload key in CI, app signing key with Google).
- Use fastlane match or equivalent to centralize provisioning profiles and certs for iOS across engineers and CI.
# GitHub Actions snippet - iOS signing
- name: Import certs
run: |
echo "$IOS_DIST_CERT_P12_BASE64" | base64 -d > cert.p12
security create-keychain -p "" build.keychain
security import cert.p12 -k build.keychain -P "$IOS_DIST_CERT_PASSWORD" -T /usr/bin/codesign
- name: Download provisioning profile
run: echo "$IOS_PROVISIONING_PROFILE_BASE64" | base64 -d > app.mobileprovision
6. Artifacts
- Upload debug builds to Firebase App Distribution or TestFlight internal for quick QA.
- Store
.ipa, .aab, mapping.txt (Android), dSYM (iOS) for every release with 90-day retention at minimum.
- dSYM and mapping files must be uploaded to the crash reporter on every release; without them stack traces are unreadable.
7. Store Upload
- Fastlane:
pilot (TestFlight), supply (Play). Battle-tested.
- Gradle Play Publisher: Play upload via Gradle tasks.
- Xcode Cloud: first-class Apple pipeline; good for iOS-only teams.
- Codemagic / Bitrise / AppCircle: mobile-focused hosted CI with prebuilt store steps.
Automate internal/TestFlight on every main merge. Automate production only when staged rollout and halt criteria are owned by the pipeline.
8. Environments and Flavors
- Use build flavors / schemes / productFlavors to separate
dev, staging, prod bundles with distinct bundle IDs.
- Separate Firebase / analytics projects per flavor. Never mix staging and prod analytics.
- Secrets come from the CI environment, not from committed files. Use
gradle.properties readers or xcconfig with CI injection.
9. Versioning
- Version code / build number must strictly increase. Compute from commit count (
git rev-list --count HEAD) or CI run number.
- Version name from a tag or
package.json/pubspec.yaml/Info.plist/build.gradle.
- Fail the build if version code is not greater than the last uploaded artifact.
10. Observability for the Pipeline
- Track build duration, cache hit rate, test duration, and failure reasons.
- Alert when PR lane exceeds a budget (e.g., 15 minutes) consistently.
- Keep a rolling dashboard of flaky jobs; quarantine or fix.
11. Anti-Patterns
- Self-hosted runners without auto-scaling. Either accept queueing or invest in elasticity.
- Signing secrets stored in plain repo files, even in private repos.
- Single giant job that does lint, build, test, and upload. Split so failures are isolated.
- Version number bumped manually. Automate.
- No dSYM/mapping upload step. Crashes will be unreadable and ownership lost.
- Running real device tests on every PR. Sample on PR, full matrix on main.
Checklist
1---2name: mobile-ci-cd-overview3description: Mobile CI/CD fundamentals - branching, build matrices, signing, artifact caching, store uploads, and common pipeline shapes across Xcode Cloud, GitHub Actions, Bitrise, and Codemagic. Use when designing or rebuilding a mobile pipeline.4---56# Mobile CI/CD Overview78## Instructions910A mobile CI/CD pipeline reliably turns a commit into a signed, store-ready artifact. Builds are slow, signing is finicky, and store APIs are rate-limited. A pragmatic pipeline invests in caching, parallelism, and guard rails more than in clever automation.1112### 1. Pipeline Shape1314```15Commit -> PR checks (lint, unit, integration)16 -> Build artifact (debug)17 -> UI smoke on emulator18 -> Merge to main19 -> Build artifact (release, signed)20 -> Upload to internal/TestFlight/Play Internal21 -> Optional: promote to production with staged rollout22```2324Keep PR lane fast (under 10 minutes). Promote heavy work (device cloud, E2E, size analysis) to the main lane.2526### 2. Branching2728- **Trunk-based** with short-lived branches works best for mobile. Long release branches invite merge pain and diverging metadata.29- **Release branches** only when multiple versions must be supported simultaneously (enterprise distribution).30- Tag every production release on the merge commit (`v2.5.0`).31- Protect `main` with required checks, linear history, and signed commits.3233### 3. Build Matrices3435- **iOS**: one macOS runner per job. Match Xcode version to the project's `required_xcode_version`. Keep SDK up to date.36- **Android**: Linux runners; use cached Gradle caches and AVD images.37- **Flutter**: matrix of iOS + Android builds; share analyzer/test job.38- **React Native**: similar matrix; use Hermes; cache Metro.39- Avoid matrices that fan out to 20 jobs for a PR. Most issues repro on one target.4041### 4. Caching4243Caching is where mobile pipelines win or lose.4445- **Gradle**: cache `~/.gradle/caches`, `~/.gradle/wrapper`, keyed on `**/*.gradle*`, `**/gradle-wrapper.properties`, `gradle/libs.versions.toml`.46- **CocoaPods**: cache `Pods` and `~/Library/Caches/CocoaPods`, keyed on `Podfile.lock`.47- **SwiftPM**: cache `~/Library/Developer/Xcode/DerivedData/*/SourcePackages`, keyed on `Package.resolved`.48- **Yarn/npm**: cache `~/.yarn/cache` or `~/.npm` keyed on lockfile. Enable Yarn zero-install if appropriate.49- **Dart/pub**: cache `~/.pub-cache`, keyed on `pubspec.lock`.50- **Xcode DerivedData**: cache selectively; often cheaper to rebuild than to invalidate.5152### 5. Signing5354- Keep signing assets out of the repo. Use CI secrets (`.p12`, keystore, provisioning profiles, App Store Connect API key JSON).55- Prefer **App Store Connect API keys** over Apple ID-based `altool`. They are session-safe and scriptable.56- Prefer **signing by Google Play** (upload key in CI, app signing key with Google).57- Use **fastlane match** or equivalent to centralize provisioning profiles and certs for iOS across engineers and CI.5859```yaml60# GitHub Actions snippet - iOS signing61- name: Import certs62 run: |63 echo "$IOS_DIST_CERT_P12_BASE64" | base64 -d > cert.p1264 security create-keychain -p "" build.keychain65 security import cert.p12 -k build.keychain -P "$IOS_DIST_CERT_PASSWORD" -T /usr/bin/codesign66- name: Download provisioning profile67 run: echo "$IOS_PROVISIONING_PROFILE_BASE64" | base64 -d > app.mobileprovision68```6970### 6. Artifacts7172- Upload debug builds to Firebase App Distribution or TestFlight internal for quick QA.73- Store `.ipa`, `.aab`, `mapping.txt` (Android), `dSYM` (iOS) for every release with 90-day retention at minimum.74- dSYM and mapping files must be uploaded to the crash reporter on every release; without them stack traces are unreadable.7576### 7. Store Upload7778- **Fastlane**: `pilot` (TestFlight), `supply` (Play). Battle-tested.79- **Gradle Play Publisher**: Play upload via Gradle tasks.80- **Xcode Cloud**: first-class Apple pipeline; good for iOS-only teams.81- **Codemagic / Bitrise / AppCircle**: mobile-focused hosted CI with prebuilt store steps.8283Automate internal/TestFlight on every main merge. Automate production only when staged rollout and halt criteria are owned by the pipeline.8485### 8. Environments and Flavors8687- Use **build flavors / schemes / productFlavors** to separate `dev`, `staging`, `prod` bundles with distinct bundle IDs.88- Separate Firebase / analytics projects per flavor. Never mix staging and prod analytics.89- Secrets come from the CI environment, not from committed files. Use `gradle.properties` readers or `xcconfig` with CI injection.9091### 9. Versioning9293- Version code / build number must strictly increase. Compute from commit count (`git rev-list --count HEAD`) or CI run number.94- Version name from a tag or `package.json`/`pubspec.yaml`/`Info.plist`/`build.gradle`.95- Fail the build if version code is not greater than the last uploaded artifact.9697### 10. Observability for the Pipeline9899- Track build duration, cache hit rate, test duration, and failure reasons.100- Alert when PR lane exceeds a budget (e.g., 15 minutes) consistently.101- Keep a rolling dashboard of flaky jobs; quarantine or fix.102103### 11. Anti-Patterns104105- Self-hosted runners without auto-scaling. Either accept queueing or invest in elasticity.106- Signing secrets stored in plain repo files, even in private repos.107- Single giant job that does lint, build, test, and upload. Split so failures are isolated.108- Version number bumped manually. Automate.109- No dSYM/mapping upload step. Crashes will be unreadable and ownership lost.110- Running real device tests on every PR. Sample on PR, full matrix on main.111112## Checklist113114- [ ] Trunk-based branching with required PR checks.115- [ ] PR lane runs under 10 minutes with caches warm.116- [ ] Release lane signs artifacts using secrets from CI storage.117- [ ] dSYMs and mapping files uploaded to the crash reporter per release.118- [ ] Store uploads automated to internal/TestFlight on main merge.119- [ ] Version code/build number auto-computed and monotonic.120- [ ] Separate flavors for dev/staging/prod with isolated analytics.121- [ ] Pipeline metrics (duration, flake rate) are monitored.122- [ ] Cache keys include the lockfiles that govern their invalidation.