Mobile CI/CD Pipelines

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.

Building the Application

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:

Testing the Application

After 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:

Deploying to Test Environments

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

Deploying to Production

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.

Managing Configuration and Artifacts

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:

Handling Failures and Drift

Several silent failures often go unnoticed.

  xcrun simctl boot "iPhone 15"
  sleep 30
  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"

Versioning and Release Triggers

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:

For 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.

Summary of Core Commands and Files

TaskCommandFile/Output
Android build./gradlew assembleReleaseapp/build/outputs/apk/release/app-release.apk
iOS buildxcodebuild -workspace MyApp.xcworkspace -scheme MyApp -configuration Releasebuild/exports/MyApp.ipa
Firebase uploadcurl -X POST ...distributionId, status: uploaded
TestFlight uploadxcrun altool --uploadAppsubmissionId
Google Play releasecurl -X POST ...track: production
Versioninggit describe --tags --alwaysversionName, versionCode
Configuration updatesed, plutilapp.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.