Mobile Release Automation

Automate the entire mobile release lifecycle from build to app store submission. Covers Fastlane pipelines, code signing automation, beta distribution, staged rollouts, release trains, and the patterns that make mobile releases predictable and fast.

Mobile Release Automation is the process of orchestrating the build, test, deployment, and release of mobile apps—iOS and Android—through a repeatable, predictable, and consistent pipeline. It matters most when teams ship multiple versions per week, require rollbacks, manage multiple app variants (e.g., staging, prod, internal), or need to release to app stores with precise timing and metadata.

Planning the Release Pipeline

A release pipeline begins with a versioning strategy that defines how code changes translate into release artifacts. The most common approach uses semantic versioning (SemVer): MAJOR.MINOR.PATCH (e.g., 2.1.4), where:

The pipeline triggers on git push to main, a git tag with a version, or a manual event. Each trigger starts a job that produces a build artifact: an .ipa for iOS and an .aab (Android App Bundle) for Android.

Triggering the Pipeline

The pipeline is usually triggered by a git push to main or a git tag. A git tag v1.2.3 creates a release candidate. The pipeline consumes the tag’s commit SHA and generates release metadata.

git tag v1.2.3
git push origin v1.2.3

The CI system (e.g., GitHub Actions, GitLab CI, CircleCI) reads the git describe --tags output to derive the version. The git describe command outputs a version string like v1.2.3-4-gabc1234, where 4 is the number of commits since the last tag, and abc1234 is the commit hash.

Building the Artifacts

Each platform requires a distinct build process. The pipeline runs two parallel jobs: one for iOS and one for Android.

iOS Build

The iOS build is driven by xcodebuild, the command-line tool included with Xcode. The build produces an .ipa file and optionally an .xcarchive.

xcodebuild \
  -scheme "MyApp" \
  -destination "generic/platform=iOS" \
  -archivePath "build/MyApp.xcarchive" \
  -configuration Release \
  -derivedDataPath "build/derivedData" \
  -archivePath "build/MyApp.xcarchive" \
  -allowProvisioningUpdates \
  -UseModernBuildSystem=YES

The archivePath output is used to generate the final .ipa:

xcodebuild \
  -exportArchive \
  -archivePath "build/MyApp.xcarchive" \
  -exportPath "build/MyApp.ipa" \
  -exportOptionsPlist "export-options.plist"

The export-options.plist file controls the export behavior. It includes keys such as:

<?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-AppStore.mobileprovision</string>
  </dict>
  <key>compileBitcode</key>
  <true/>
  <key>signingCertificate</key>
  <string>Apple Distribution</string>
  <key>uploadBitcode</key>
  <true/>
  <key>uploadSymbols</key>
  <true/>
</dict>
</plist>

The method key determines the upload context: app-store, ad-hoc, or development. The provisioningProfiles dictionary maps bundle identifiers to provisioning profile filenames.

Android Build

The Android build uses ./gradlew, the Gradle wrapper, with a build.gradle file defining tasks.

./gradlew assembleRelease -PbuildVariant=release

The buildVariant property selects a flavor and build type. The output is an Android App Bundle (*.aab) and APKs (if needed).

android {
    compileSdk 34

    defaultConfig {
        applicationId "com.example.myapp"
        minSdk 21
        targetSdk 34
        versionCode 123
        versionName "1.2.3"
    }

    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            signingConfig signingConfigs.release
        }
    }

    flavorDimensions "env"
    productFlavors {
        staging {
            applicationIdSuffix ".staging"
            versionNameSuffix "-staging"
        }
        prod {
            applicationIdSuffix ".prod"
            versionNameSuffix "-prod"
        }
    }

    signingConfigs {
        release {
            storeFile file("keystore.jks")
            storePassword "password"
            keyAlias "mykey"
            keyPassword "password"
        }
    }
}

The versionCode is a monotonically increasing integer used by the Google Play Store to determine update priority. The versionName is the user-visible version string (e.g., 1.2.3).

Testing the Artifacts

Before deployment, the pipeline runs a test suite. The test phase includes unit, integration, and UI tests.

iOS Testing

Unit and integration tests are run via xcodebuild with the test action:

xcodebuild \
  -scheme "MyApp" \
  -destination "platform=iOS Simulator,OS=17.0,name=iPhone 15" \
  -testPlan "Tests.xctestplan" \
  -configuration Debug \
  -resultBundlePath "build/MyApp.xcresult"

The testPlan file is an .xctestplan file that defines the test suite structure. It includes a list of test targets, each with a testSuite and testCases.

The pipeline collects results and generates a report. The xcresult directory contains an xcresult file that can be opened in Xcode or converted to JSON:

xcrun xcresulttool list --path build/MyApp.xcresult

The xcresulttool command is part of Xcode’s command-line tools and outputs structured test results.

Android Testing

Android tests are run using ./gradlew connectedAndroidTest:

./gradlew connectedAndroidTest \
  -PtestVariant=release \
  -PtestDevice="emulator-5554" \
  --continue

The testVariant key selects the build variant (e.g., staging), and testDevice specifies the emulator or device identifier.

The test results are written to app/build/reports/androidTests and include a test-results.xml file that conforms to JUnit XML format:

<testsuite name="com.example.myapp.LoginTest" tests="3" failures="1" errors="0" time="2.45">
  <testcase classname="com.example.myapp.LoginTest" name="testLoginSuccess" time="0.82"/>
  <testcase classname="com.example.myapp.LoginTest" name="testLoginFailure" time="1.11">
    <failure message="Expected user to be logged in" type="AssertionError">
      <![CDATA[Expected: true
Actual: false
    ]]>
    </failure>
  </testcase>
  <testcase classname="com.example.myapp.LoginTest" name="testLoginWithInvalidCredentials" time="0.52"/>
</testsuite>

The pipeline parses this file and marks the job as failed if any test fails or if the total time exceeds a threshold.

Deploying to App Stores

The pipeline deploys the .ipa and .aab to the respective app stores: Apple App Store and Google Play.

Apple App Store

Deployment uses altool, the command-line tool included with Xcode, or the transporter command-line tool.

altool \
  --uploadApp \
  --file "build/MyApp.ipa" \
  --user "[email protected]" \
  --password "secret" \
  --outputDirectory "build/altool-output"

The altool command returns a status field indicating success or failure:

{
  "status": "SUCCESS",
  "message": "Upload successful",
  "appVersion": "1.2.3",
  "appBuildNumber": "123",
  "appIdentifier": "com.example.myapp"
}

If the status is FAILURE, the error is logged and the job fails. The altool tool also supports --uploadMetadata to update metadata (e.g., app description, screenshots, changelog).

Metadata is provided in a metadata directory with files:

metadata/
├── en-US/
│   ├── description.txt
│   ├── title.txt
│   ├── subtitle.txt
│   ├── keywords.txt
│   └── releaseNotes.txt
├── screenshots/
│   ├── iphone-6s.png
│   ├── iphone-15.png
│   └── ipad.png
└── icons/
    └── 1024x1024.png

The pipeline runs altool --uploadMetadata after the .ipa upload:

altool \
  --uploadMetadata \
  --file "build/MyApp.ipa" \
  --metadataPath "metadata" \
  --user "[email protected]" \
  --password "secret"

Google Play

Deployment uses gcloud, the Google Cloud CLI, or the google-play-deploy script.

gcloud android publishing tracks edit create \
  --track=beta \
  --package-name=com.example.myapp \
  --title="Beta Release v1.2.3"

This creates a new edit session. The pipeline uploads the .aab file:

gcloud android publishing bundles upload \
  --track=beta \
  --package-name=com.example.myapp \
  --bundle-path=build/MyApp.aab

The upload command returns a JSON response:

{
  "track": "beta",
  "versionCode": 123,
  "versionName": "1.2.3",
  "status": "completed",
  "userFraction": 0.1,
  "releaseNotes": [
    {
      "language": "en",
      "text": "Fixed login crash on iOS 17.0"
    }
  ]
}

The pipeline then commits the edit:

gcloud android publishing tracks edit commit \
  --track=beta \
  --package-name=com.example.myapp \
  --version-code=123

The commit command returns a status of pending, completed, or failed. If failed, the job logs the errorMessage and halts.

Managing Release Variants and Rollbacks

A single release pipeline handles multiple release variants: staging, preview, production, and internal.

Each variant is built from a different git branch, and the pipeline uses the branch name to determine the buildVariant and environment.

For example, a git push origin feature/new-login triggers a staging build, which produces MyApp-staging.aab and MyApp-staging.ipa.

Rollbacks are triggered by tagging an existing release with a rollback-v1.2.3 tag:

git tag rollback-v1.2.3
git push origin rollback-v1.2.3

The pipeline detects the rollback prefix and deploys the tagged version to the production track, updating the versionCode and versionName accordingly.

Common Failure Modes

The sharp edge: the pipeline assumes that the versionCode and versionName are consistent across the build and deployment steps. If versionCode is 123 in build.gradle but the .aab contains 124, the Play Store treats the release as a new version, even though the pipeline expected it to be a patch.

Summary of Key Commands and Files

PurposeCommand / FileKey Flags / Keys
iOS buildxcodebuild-scheme, -configuration, -archivePath
iOS exportxcodebuild -exportArchive-exportOptionsPlist, -exportPath
Android build./gradlew assembleRelease-PbuildVariant, build.gradle
Android testing./gradlew connectedAndroidTest-PtestVariant, test-results.xml
App Store uploadaltool --uploadApp--user, --password, --outputDirectory
App Store metadataaltool --uploadMetadata--metadataPath, metadata/ directory
Play Store editgcloud android publishing tracks edit create--track, --package-name
Play Store commitgcloud android publishing tracks edit commit--version-code, --track

The release pipeline is not a sequence of jobs—it is a system of interlocking expectations, where the failure of one step silently breaks the entire release process.

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.