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.
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:
PATCH increments for bug fixes and small improvementsMINOR for backward-compatible feature additionsMAJOR for breaking changesThe 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.
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.
Each platform requires a distinct build process. The pipeline runs two parallel jobs: one for iOS and one for Android.
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.
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).
Before deployment, the pipeline runs a test suite. The test phase includes unit, integration, and UI tests.
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 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.
The pipeline deploys the .ipa and .aab to the respective app stores: Apple App Store and Google Play.
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"
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.
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.
xcodebuild outputs Build Succeeded but the .ipa file is missing. The cause is a missing exportArchive step or incorrect archivePath.versionCode in build.gradle is not updated across builds, causing the Play Store to treat two releases as identical.altool returns status: FAILURE but the errorMessage is not parsed. The error is often a missing or incorrectly named provisioning profile.gcloud android publishing tracks edit commit command is run before uploadMetadata, causing the new version to be deployed with old metadata.connectedAndroidTest reports 3 failures, but the pipeline does not fail the job unless failureCount > 3.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.
| Purpose | Command / File | Key Flags / Keys |
|---|---|---|
| iOS build | xcodebuild | -scheme, -configuration, -archivePath |
| iOS export | xcodebuild -exportArchive | -exportOptionsPlist, -exportPath |
| Android build | ./gradlew assembleRelease | -PbuildVariant, build.gradle |
| Android testing | ./gradlew connectedAndroidTest | -PtestVariant, test-results.xml |
| App Store upload | altool --uploadApp | --user, --password, --outputDirectory |
| App Store metadata | altool --uploadMetadata | --metadataPath, metadata/ directory |
| Play Store edit | gcloud android publishing tracks edit create | --track, --package-name |
| Play Store commit | gcloud 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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy