Build CI/CD pipelines that handle the unique challenges of mobile development: signing, provisioning, app store submission, beta distribution, and multi-platform builds. Covers Fastlane, Xcode Cloud, build optimization, and release management.
Mobile CI/CD Pipelines are automated workflows that build, test, and deploy mobile applications—iOS and Android—on every code change, enabling consistent, reliable, and rapid releases. They matter when teams ship multiple versions per week, maintain multiple app variants (e.g., free vs. premium), or integrate with feature flags, dynamic config, or native code libraries. They are essential for reducing merge conflicts, catching regressions early, and ensuring production readiness across device types and OS versions.
The foundation of any mobile CI/CD pipeline is the build step. The build must compile both Android and iOS artifacts, embed configuration, and produce distributable packages.
On Android, a build starts with ./gradlew assemble and is extended with variant-specific tasks:
./gradlew assembleDebug assembleRelease
The build.gradle file defines buildTypes, flavorDimensions, and productFlavors. For example:
android {
buildTypes {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')
}
release {
signingConfig signingConfigs.release
applicationIdSuffix ".release"
versionNameSuffix "-release"
}
}
productFlavors {
free {}
premium {}
}
}
This produces four APKs: app-free-debug.apk, app-premium-debug.apk, app-free-release.apk, app-premium-release.apk.
On iOS, the build uses xcodebuild with a derived data path:
xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -destination 'generic/platform=iOS' -derivedDataPath ./build
The BuildSettings in the .xcodeproj file determine the output format. To generate an .ipa file:
xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -archivePath ./build/MyApp.xcarchive -exportPath ./build/exports
Then archive and export:
xcodebuild -exportArchive -archivePath ./build/MyApp.xcarchive -exportPath ./build/exports -exportOptionsPlist exportOptions.plist
The exportOptions.plist defines export behavior:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>com.example.myapp</key>
<string>MyApp_Distribution</string>
</dict>
<key>compileBitcode</key>
<true/>
<key>signingStyle</key>
<string>manual</string>
</dict>
</plist>
The pipeline must validate that the build output includes:
app/build/outputs/apk/release/app-release-unsigned.apk, app/build/outputs/bundle/release/app-release.aabbuild/exports/MyApp.ipa, build/exports/MyApp.appAfter building, the pipeline runs a suite of tests. These include unit, integration, and UI tests.
For Android, ./gradlew test runs unit tests in JUnit, Espresso, and Robolectric. The build.gradle file configures test variants:
android {
testOptions {
unitTests {
includeAndroidResources true
testCoverageEnabled true
}
animationsDisabled true
}
}
Test reports are generated in build/reports/. The pipeline collects them and uploads to a central artifact store.
For iOS, the pipeline runs:
xcodebuild -workspace MyApp.xcworkspace -scheme MyAppUITests -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.4' test
This runs UI tests defined in XCTest, producing .xcresult files. The pipeline archives these results and extracts failure details:
xcrun xcresulttool get --path MyApp.xcresult --output ./test-results
The output includes a summary.json with pass/fail counts, test durations, and screenshots for failed tests.
A critical failure mode is missing test dependencies. The pipeline must ensure that:
local.properties specifies sdk.dir and android.useAndroidX=truePodfile is locked (pod install --repo-update), and simulator is preloaded with required data.Once tests pass, the pipeline deploys the build to staging or test environments.
For Android, the pipeline uploads the APK or AAB to Firebase App Distribution:
curl -X POST \
-H "Authorization: Bearer $FIREBASE_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @app-release.aab \
"https://firebaseremoteconfig.googleapis.com/v1/projects/myproject/distributions"
The request includes metadata:
{
"releaseNotes": "Fix login flow, update onboarding screens",
"versionName": "1.2.3",
"versionCode": 103,
"track": "beta",
"status": "draft"
}
Firebase returns a distributionId, which the pipeline uses to promote the build to production.
For iOS, the pipeline uploads the .ipa to TestFlight:
xcrun altool --uploadApp --file MyApp.ipa \
--user "[email protected]" \
--password "@env:TESTFLIGHT_PASSWORD" \
--verbose
The altool command returns a submissionId. The pipeline waits for the submission to complete and then submits the build for review:
xcrun altool --submitApp --submissionId 123456 \
--user "[email protected]" \
--password "@env:TESTFLIGHT_PASSWORD" \
--verbose
The final step is releasing to production, triggered either manually or automatically.
For Android, the pipeline uploads the AAB to Google Play:
curl -X POST \
-H "Authorization: Bearer $GOOGLE_PLAY_TOKEN" \
-H "Content-Type: application/json" \
-d @play-release.json \
"https://playdeveloperapi.googleapis.com/v3/applications/com.example.myapp/edits"
The play-release.json includes:
{
"tracks": [
{
"track": "production",
"versionCodes": [103],
"releaseNotes": [
{
"language": "en",
"text": "Stability improvements, bug fixes, performance tuning."
}
]
}
]
}
The pipeline waits for the edit to be committed and the rollout to complete.
For iOS, the pipeline promotes the TestFlight build to production:
xcrun altool --promoteApp --submissionId 123456 \
--track "production" \
--user "[email protected]" \
--password "@env:TESTFLIGHT_PASSWORD" \
--verbose
The pipeline monitors the rollout via the TestFlight API:
curl -H "Authorization: Bearer $TESTFLIGHT_TOKEN" \
"https://api.testflight.apple.com/v1/submissions/123456"
The response includes status, rolloutStatus, and releaseNotes.
The pipeline must manage configuration files and artifacts across environments.
A common pattern is to embed app.json or Info.plist with environment-specific values. The pipeline modifies these files before building:
sed -i 's|https://api.example.com|https://staging.api.example.com|g' app.json
# Update Info.plist for iOS
plutil -replace CFBundleShortVersionString -string "1.2.3" Info.plist
plutil -replace CFBundleVersion -string "103" Info.plist
The pipeline ensures that:
versionCode and versionName.artifacts/android/app-release.aabartifacts/ios/MyApp.ipaartifacts/versions/1.2.3/Several silent failures often go unnoticed.
app-release.aab exists but app/build/outputs/bundle/release/app-release.aab is not created due to a Gradle task misconfiguration. The build logs show :app:assembleRelease but the AAB is missing.simctl: xcrun simctl boot "iPhone 15"
sleep 30
versionCode is incremented manually, but the pipeline uses 100 for 1.0.0 and 101 for 1.1.0, leading to out-of-order releases..p12 certificate is not available. The pipeline must verify: ls -la /secrets/ios-signing.p12
security list-keychains
security default-keychain -s login.keychain
security unlock-keychain -p $KEYCHAIN_PASSWORD login.keychain
xcrun simctl create "iPhone 15" "iPhone 15" "17.4"
The pipeline uses a versioning strategy based on Git metadata.
The versionName is derived from the Git tag:
VERSION_NAME=$(git describe --tags --always --dirty)
The versionCode is a monotonically increasing number stored in versionCode.txt:
VERSION_CODE=$(cat versionCode.txt)
echo $((VERSION_CODE + 1)) > versionCode.txt
The pipeline triggers on:
git push origin maingit push origin feature/*git tag v1.2.3For feature branches, the pipeline deploys to a staging environment with a unique URL:
echo "https://staging.myapp.com/${BRANCH_NAME}"
The URL is embedded in the build manifest and made available in the CI logs.
| Task | Command | File/Output |
|---|---|---|
| Android build | ./gradlew assembleRelease | app/build/outputs/apk/release/app-release.apk |
| iOS build | xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release | build/exports/MyApp.ipa |
| Firebase upload | curl -X POST ... | distributionId, status: uploaded |
| TestFlight upload | xcrun altool --uploadApp | submissionId |
| Google Play release | curl -X POST ... | track: production |
| Versioning | git describe --tags --always | versionName, versionCode |
| Configuration update | sed, plutil | app.json, Info.plist |
The pipeline is complete when:
A mobile CI/CD pipeline is not a sequence of steps; it is a contract between developers and production. It ensures that every change, from a typo to a major refactoring, is validated, tested, and delivered with confidence.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy