Build automated CI/CD pipelines for iOS and Android applications. Covers build automation, code signing, test orchestration, app store deployment, and beta distribution.
Mobile CI/CD Pipelines are the automated lifecycles that build, test, and deploy mobile applications across iOS and Android. They matter when a team ships multiple versions per day, maintains feature branches, runs regression suites, and needs to verify changes on real devices before release. The pipeline is the engine that ensures every commit to the main branch produces a new build artifact, runs tests, uploads to staging, and optionally deploys to production — all with minimal human intervention.
A mobile CI/CD pipeline begins with building the app binary. The build process must produce a .ipa for iOS and a .apk or .aab for Android, with consistent versioning, metadata, and signing.
For Android, a build.gradle file defines the build configuration:
android {
compileSdk 34
defaultConfig {
applicationId "com.example.app"
minSdk 24
targetSdk 34
versionCode 101
versionName "1.2.1"
signingConfig signingConfigs.release
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
signingConfigs {
release {
storeFile file('keystore.jks')
storePassword 'password'
keyAlias 'release-key'
keyPassword 'password'
}
}
}
The pipeline executes ./gradlew assembleRelease in a CI job, which generates app/build/outputs/apk/release/app-release.apk. For Android App Bundles, it runs bundleRelease, producing app/build/outputs/bundle/release/app-release.aab.
For iOS, the pipeline uses xcodebuild with precise configuration:
xcodebuild \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination 'platform=iOS Simulator,OS=17.0,name=iPhone 15' \
-derivedDataPath ./build \
-archivePath ./build/MyApp.xcarchive \
-archiveFormat xcarchive \
-archivePath ./build/MyApp.xcarchive \
archive
This generates a .xcarchive file containing the compiled binary, symbols, and metadata. The pipeline then exports the .ipa:
xcodebuild \
-exportArchive \
-archivePath ./build/MyApp.xcarchive \
-exportPath ./build/MyApp.ipa \
-exportOptionsPlist exportOptions.plist
The exportOptions.plist is a standard XML file:
<?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>AppStore_com.example.app.mobileprovision</string>
</dict>
<key>compileBitcode</key>
<true/>
<key>signingStyle</key>
<string>manual</string>
</dict>
</plist>
The pipeline must ensure that exportOptions.plist is checked into version control and updated with each release. If the method key is misspelled as methodType, the build fails silently: the .ipa is created, but the app is not uploaded to App Store Connect.
Once built, the pipeline runs automated tests. These include unit tests, integration tests, and end-to-end (E2E) tests on emulators and real devices.
For Android, the pipeline runs:
./gradlew testDebug connectedDebugAndroidTest
This executes JUnit tests in src/test and Espresso tests in src/androidTest. The output includes a test-results directory with XML files in JUnit format.
For iOS, the pipeline uses xctool or xcodebuild to run XCTest:
xcodebuild \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-destination 'platform=iOS Simulator,OS=17.0,name=iPhone 15' \
-testPlan TestPlan.xctestplan \
-derivedDataPath ./build \
-resultBundlePath ./build/MyApp-test-results.xcresult \
test
The TestPlan.xctestplan is a structured Xcode test plan that defines test suites, test cases, and environment variables. If the .xctestplan file is missing or malformed, the pipeline logs Test plan not found: TestPlan.xctestplan, but the build continues without failing.
For E2E testing, the pipeline uses Detox, a real-time end-to-end testing framework for React Native and native apps.
// detox.config.json
{
"testRunner": "jest",
"runnerConfig": {
"setupFilesAfterEnv": ["<rootDir>/tests/setup.js"],
"testMatch": ["**/*.test.js"]
},
"devices": [
{
"type": "iphone15",
"name": "iPhone 15",
"os": { "type": "ios", "version": "17.0" },
"launchArgs": { "launchArgs": ["-testPlan", "TestPlan.xctestplan"] }
}
],
"configurations": {
"ci": {
"binaryPath": "build/MyApp.app",
"device": "iPhone 15"
}
}
}
The pipeline executes npx detox build --configuration ci and npx detox test --configuration ci. Detox runs the app in a simulator, interacts with UI elements, and generates a test-results directory with screenshots, videos, and JSON logs.
Testing is the first place where the pipeline fails silently. If the simulator fails to launch due to an outdated iOS version, the pipeline logs Simulator failed to start: exit code 255, but the build continues. The solution: ensure the simulator is preloaded via xcrun simctl create and xcrun simctl boot.
After testing, the pipeline deploys the app to staging and production. The deployment process must handle versioning, metadata, and release notes.
For iOS, the pipeline uses altool to upload to App Store Connect:
xcrun altool \
--uploadApp \
--file ./build/MyApp.ipa \
--user "[email protected]" \
--password "@env:APP_STORE_PASSWORD" \
--verbose
The --user flag accepts an email or Apple ID. The --password flag supports a password stored in an environment variable, and @env:APP_STORE_PASSWORD resolves to the value of the APP_STORE_PASSWORD environment variable.
The pipeline waits for the upload to complete and then submits the app for review:
curl -X POST \
-H "Authorization: Bearer $APP_STORE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"app": {
"id": "1234567890",
"name": "MyApp",
"version": "1.2.1"
},
"reviewNotes": "Bug fixes and performance improvements.",
"submitForReview": true
}' \
https://itunesconnect.apple.com/transfer/api/v1/apps/1234567890/submissions
The pipeline must handle the case where the app.id is missing or malformed: the API returns 400 Bad Request with a detail field in the response body, but the pipeline logs only App submission failed without showing the JSON error.
For Android, the pipeline uploads the AAB to Google Play using gcloud CLI:
gcloud alpha android app-bundle upload \
--app-id com.example.app \
--bundle ./build/app-release.aab \
--track internal \
--release-notes "Fixes for login and profile editing." \
--version-code 101 \
--version-name 1.2.1
The --track flag accepts internal, beta, alpha, or production. If the track name is misspelled as internall, the upload succeeds but the app is not visible in the internal track.
The pipeline must also handle rollouts. For staged rollouts, it updates the track with a rollout percentage:
gcloud alpha android app-bundle update \
--app-id com.example.app \
--track beta \
--rollout 0.5 \
--version-code 101
This sets the rollout to 50% of users. The pipeline logs Rollout updated for beta track: 50%, but if the --rollout value is not a float, the API returns 400 Invalid value for rollout: "0.5", but the pipeline continues without validating the input.
The pipeline must support multiple environments: develop, staging, release, production. Each environment has its own configuration.
The pipeline uses branch-based deployment: when a PR is merged into main, it triggers a production release. When a feature branch is pushed, it triggers a staging build.
The pipeline uses a pipeline.yml file in the repository root:
name: Mobile CI/CD
on:
push:
branches:
- main
- develop
- 'feature/*'
paths:
- 'android/**'
- 'ios/**'
- 'package.json'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Build Android
run: |
./gradlew assembleRelease
./gradlew bundleRelease
- name: Build iOS
run: |
xcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Release -archivePath ./build/MyApp.xcarchive archive
xcodebuild -exportArchive -archivePath ./build/MyApp.xcarchive -exportPath ./build/MyApp.ipa -exportOptionsPlist exportOptions.plist
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: app-artifacts
path: |
build/*.apk
build/*.ipa
build/*.aab
This pipeline runs on every push to main, develop, or feature/*. The paths field ensures that only changes to mobile code trigger the pipeline.
The pipeline must handle the case where package.json is updated but android/app/src/main/res/values/strings.xml is not. The build runs, but the app version is not updated. The solution: use git diff --name-only HEAD~1 to detect changed files, and only build if mobile files have changed.
The pipeline silently fails when the exportOptions.plist is missing or has incorrect keys. The method key must be app-store, app-store-connect, or development. If it is appstore, the upload fails with Invalid method: appstore. The pipeline must validate the exportOptions.plist before uploading.
The pipeline fails when the simulator is not ready. The xcrun simctl list devices command returns a list of devices, but the pipeline assumes the device name is iPhone 15. If the device is named iPhone 15 Pro, the simulator fails to launch.
The pipeline fails when the gcloud CLI is not authenticated. The gcloud auth activate-service-account command must be run with a service account key file. If the key file is missing, gcloud returns Error: Unable to load service account key file, but the pipeline continues.
The pipeline fails when the app-release.aab is not built. The bundleRelease task may fail silently if the build.gradle file is missing or has syntax errors. The pipeline must check the return code of ./gradlew bundleRelease and fail if it is not zero.
The pipeline silently fails when the TestPlan.xctestplan file is not found. The xcodebuild command logs Test plan not found, but does not fail the job. The solution: use testPlan as a required input and validate the file exists before running the test.
The pipeline fails when the app-id in Google Play is not found. The gcloud alpha android app-bundle upload command returns 404 Not Found with a message field, but the pipeline logs only App upload failed. The solution: parse the JSON response and include the message in the log output.
A mobile CI/CD pipeline is not just a sequence of commands. It is a system that builds, tests, and deploys mobile apps with consistency and reliability. It depends on correct configuration, proper file formats, and robust error handling. The pipeline fails silently when a file is missing, a key is misspelled, or a command returns a non-zero exit code. The sharp edges are the small details: the exportOptions.plist must be correct, the TestPlan.xctestplan must be in place, the gcloud CLI must be authenticated. The pipeline is not a black box; it is a series of well-defined, observable steps, each with a clear signal of success or failure.
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