| name | mobile-ci-cd-overview |
| description | 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. |
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.
- 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