Production engineering guide for mobile ci cd pipelines covering patterns, implementation strategies, and operational best practices.
Mobile CI/CD pipelines automate the build, test, and deployment of mobile applications across iOS and Android, ensuring consistent, reliable releases from code commit to production. They matter most when teams ship frequently—multiple times per day—and need to validate changes across real devices, simulators, and configurations, catching regressions early and enabling feature flags, canary releases, and rollbacks with confidence.
A mobile CI/CD pipeline begins with artifact generation: compiling source code into installable binaries—APKs for Android, IPA files for iOS. The build step must handle versioning, code signing, asset bundling, and configuration per environment.
For Android, the build is orchestrated via Gradle. A build.gradle file defines the android block with buildTypes, flavorDimensions, and productFlavors. A typical build.gradle snippet:
android {
compileSdk 34
defaultConfig {
applicationId "com.example.app"
minSdk 21
targetSdk 34
versionCode 100
versionName "1.2.0"
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
debuggable true
}
}
flavorDimensions "environment"
productFlavors {
staging {
dimension "environment"
applicationIdSuffix ".staging"
}
production {
dimension "environment"
applicationIdSuffix ".prod"
}
}
signingConfigs {
release {
keyAlias 'app-key'
storeFile file('keystore.jks')
storePassword 'password123'
keyPassword 'password123'
}
}
}
Running the build in CI:
./gradlew assembleStagingRelease assembleProductionRelease
This generates app-staging-release.apk and app-production-release.apk in app/build/outputs/apk/.
For iOS, Xcode projects use xcodebuild. A pipeline command to build a release IPA:
xcodebuild -workspace MyApp.xcworkspace -scheme Production -configuration Release -archivePath build/MyApp.xcarchive
The archive contains all assets and metadata. To export the IPA:
xcodebuild -exportArchive -archivePath build/MyApp.xcarchive -exportPath build/exports -exportOptionsPlist ExportOptions.plist
The ExportOptions.plist file 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.app</key>
<string>MyApp-Production.mobileprovision</string>
</dict>
<key>uploadBitcode</key>
<true/>
<key>compileBitcode</key>
<true/>
</dict>
</plist>
Artifacts must be versioned and tagged. The build process should compute a version string from Git metadata: git describe --tags --long --dirty outputs v1.2.0-12-gabc1234. This version is embedded in the app and stored in a build manifest.
A build.manifest.json file is generated per build:
{
"app": {
"name": "MyApp",
"platform": "android",
"version": "1.2.0-12-gabc1234",
"buildId": "20231027-081532",
"commit": "abc1234567890abcdef1234567890abcdef12345678",
"branch": "main",
"timestamp": "2023-10-27T08:15:32Z"
},
"artifacts": [
{
"type": "apk",
"path": "app/build/outputs/apk/staging/release/app-staging-release.apk",
"sha256": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
}
]
}
This manifest is uploaded to a blob storage endpoint via curl:
curl -X PUT \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ARTIFACT_TOKEN" \
-d @build.manifest.json \
"https://artifacts.example.com/builds/20231027-081532"
Testing must be fast, deterministic, and scalable. The pipeline runs unit, integration, and UI tests across multiple configurations.
Unit tests are run via the test runner provided by the platform. In Android, ./gradlew test executes JUnit and Espresso tests in src/test/ and src/androidTest/.
For iOS, xctest is driven by xcodebuild:
xcodebuild -workspace MyApp.xcworkspace -scheme Test -destination 'platform=iOS Simulator,name=iPhone 14,OS=17.0' test
The destination string selects a simulator by name and OS version. The output includes a .xctestrun file specifying test configuration:
<?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>testables</key>
<array>
<string>MyAppTests</string>
</array>
<key>environment</key>
<dict>
<key>TEST_ENV</key>
<string>ci</string>
</dict>
</dict>
</plist>
UI tests are executed using tools like Appium or Detox, both of which run on a server that communicates with simulators or real devices.
Appium requires a appium server running on port 4723. A test script in JavaScript:
const wd = require('wd');
const { Builder } = wd;
async function runUITest() {
const driver = await new Builder()
.withCapabilities({
platformName: 'iOS',
deviceName: 'iPhone 14',
app: '/path/to/MyApp.ipa',
automationName: 'XCUITest'
})
.usingRemote('http://localhost:4723/wd/hub')
.build();
try {
await driver.elementByAccessibilityId('loginButton').click();
await driver.elementByXPath('//XCUIElementTypeTextField[@name="username"]').sendKeys('alice');
await driver.elementByXPath('//XCUIElementTypeButton[@name="submit"]').click();
const success = await driver.elementByXPath('//XCUIElementTypeStaticText[@name="Welcome, alice"]').text();
console.log('Test passed:', success);
} catch (err) {
console.error('Test failed:', err.message);
} finally {
await driver.quit();
}
}
runUITest();
The pipeline invokes this script via node test/ui.js. The Appium server logs test execution, including screenshots and video on failure.
Test results are collected as XML and JSON. JUnit XML is expected by CI systems like Jenkins or GitHub Actions:
<testsuite name="LoginTest" tests="3" failures="1" errors="0" time="4.2">
<testcase name="user_can_login" classname="LoginTest" time="1.5">
<failure message="Expected welcome message to be visible">Expected element to be present</failure>
</testcase>
<testcase name="user_cannot_login_with_invalid_email" classname="LoginTest" time="0.8"/>
</testsuite>
The pipeline uploads these reports and attaches them to the build. A common failure: test execution time exceeds timeout, causing the pipeline to fail silently. The xcodebuild command may not fail on test failures, requiring explicit validation:
if ! xcodebuild -scheme Test -destination 'platform=iOS Simulator,name=iPhone 14,OS=17.0' test; then
echo "Test suite failed" >&2
exit 1
fi
Deployment involves uploading binaries to app stores and pushing updates to internal testing channels.
To submit a build to Apple’s App Store, the pipeline uses altool or transporter:
/usr/bin/altool --uploadApp -f MyApp.ipa -u [email protected] -p password123
altool returns a submissionId. The pipeline waits for the upload to complete via polling:
while true; do
result=$(curl -s -H "Authorization: Bearer $TOKEN" \
"https://api.appstoreconnect.apple.com/v1/submissions/$submissionId")
status=$(echo "$result" | jq -r '.data.attributes.status')
if [ "$status" = "processing" ]; then
sleep 30
elif [ "$status" = "finished" ]; then
echo "Upload succeeded"
break
else
echo "Upload failed: $status"
exit 1
fi
done
For Android, the pipeline uses gplaycli or google-play-deploy:
gplaycli --json --service-account-key-file key.json --track=beta --release-notes-file=release-notes.txt --apk=app-staging-release.apk
The gplaycli tool authenticates via a service account key file in JSON format:
{
"type": "service_account",
"project_id": "myapp-123456",
"private_key_id": "abc123def456",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...",
"client_email": "[email protected]",
"client_id": "123456789012345678901",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token"
}
The pipeline monitors the rollout via gplaycli polling until the track is fully promoted.
For internal testing, the pipeline uploads builds to a staging environment:
curl -X POST \
-H "Authorization: Bearer $INTERNAL_TOKEN" \
-F "[email protected]" \
-F "track=internal" \
-F "notes=Fix login flow and dark mode" \
https://internal-api.example.com/apps/upload
The internal-api returns a buildId and downloadUrl. The pipeline sends notifications via Slack or email.
All pipelines are driven by a pipeline.yaml file at the repository root:
name: Mobile CI/CD
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Java
uses: actions/setup-java@v3
with:
java-version: '17'
- name: Build Android
run: |
./gradlew assembleStagingRelease assembleProductionRelease
- name: Build iOS
run: |
xcodebuild -workspace MyApp.xcworkspace -scheme Production -configuration Release -archivePath build/MyApp.xcarchive
xcodebuild -exportArchive -archivePath build/MyApp.xcarchive -exportPath build/exports -exportOptionsPlist ExportOptions.plist
- name: Upload Artifacts
run: |
curl -X PUT -H "Content-Type: application/json" -H "Authorization: Bearer $ARTIFACT_TOKEN" -d @build.manifest.json https://artifacts.example.com/builds/$(date +%Y%m%d-%H%M%S)
test:
needs: build
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Install Appium
run: brew install appium
- name: Start Appium Server
run: appium &> appium.log &
- name: Run UI Tests
run: node test/ui.js
- name: Upload Test Reports
run: |
curl -X POST -H "Authorization: Bearer $TEST_TOKEN" -F "[email protected]" https://test-reports.example.com
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Deploy to Google Play
run: |
gplaycli --json --service-account-key-file key.json --track=beta --release-notes-file=release-notes.txt --apk=app-production-release.apk
- name: Deploy to App Store
run: |
/usr/bin/altool --uploadApp -f MyApp.ipa -u [email protected] -p password123
gradlew build completes without error, but build.gradle is misconfigured. The pipeline must verify output directories exist and artifacts are non-empty.xcodebuild output and captures simulator logs via xcrun simctl log.CFBundleIdentifier or missing Info.plist keys. The pipeline validates the IPA using unzip -l MyApp.ipa.gplaycli fails to upload APK, but the pipeline does not retry. A robust pipeline includes exponential backoff and retry logic.build.manifest.json version doesn’t match the version in the app’s Info.plist. The pipeline validates this via plutil -p Info.plist.assembleDebug and test on every pull request, posting status checks to Git.main, the pipeline creates a Git tag, builds production artifacts, and deploys to staging and production.pipeline.yaml uses matrix to deploy to staging, beta, and production with different app IDs, signing configs, and release notes.The pipeline is the heartbeat of mobile engineering—its logs, artifacts, and configuration form a shared contract across teams, ensuring every release is repeatable, observable, and accountable.
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