Design a mobile release process that balances velocity with stability. Covers staged rollouts, feature flags, crash monitoring, hotfix workflows, app store review strategies, and the mobile-specific failure modes that web teams never think about.
Mobile Release Management: Ship Fast Without Bricking Phones
Ship new features, bug fixes, and performance improvements to mobile apps with confidence across iOS and Android. This is not just deploying to a server. It is orchestrating binary builds, testing, staged rollouts, in-app updates, and rollback mechanisms—all while ensuring users never see a broken or crashed app. Release management in mobile engineering is the discipline of turning a code commit into a live, stable, and performant app on every device, with minimal downtime and zero bricking.
A mobile release begins with a consistent, repeatable build process. The build must produce a binary (APK or IPA) that includes all dependencies, assets, and configuration, and must be uniquely versioned and identifiable.
Use gradle build for Android and xcodebuild for iOS, each with a fixed set of flags. For Android, run:
./gradlew assembleRelease \
-PbuildConfigVersionCode=1023 \
-PbuildConfigVersionName=2.3.1 \
-PbuildConfigFlavor=prod \
--build-cache
This generates app-release.apk in app/build/outputs/apk/release/. The buildConfigVersionCode is an integer used by the Android package manager to determine update priority and compatibility. versionName is a string shown to users. The flavor key selects a build variant (e.g., prod, staging, dev) from buildTypes and productFlavors in build.gradle.
For iOS, use:
xcodebuild -workspace MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-destination 'generic/platform=iOS' \
-derivedDataPath ./build \
-archivePath ./build/MyApp.xcarchive \
-archiveFormat xcarchive \
-exportMethod app-store \
-exportPath ./build/outputs \
-allowProvisioningUpdates
This produces MyApp.ipa and a .xcarchive, which contains the built binary, symbols, and metadata. The exportMethod app-store ensures the IPA is ready for submission to the App Store. The destination generic/platform=iOS enables building for all iOS devices.
Versioning must be consistent across platforms. Use the same versionName in both build.gradle and Info.plist. The CFBundleShortVersionString key in Info.plist holds versionName, and CFBundleVersion holds versionCode. When deploying, verify that both match:
<key>CFBundleShortVersionString</key>
<string>2.3.1</string>
<key>CFBundleVersion</key>
<string>1023</string>
A mismatch between versionName and versionCode causes confusion during rollout and increases the risk of users on outdated builds.
After building, distribute the app binaries to the relevant channels: App Store, Play Store, internal distribution, and in-app updates.
For Apple App Store, use altool to upload the .xcarchive:
./altool --uploadApplication --file ./build/MyApp.xcarchive \
--username "[email protected]" \
--password "secret" \
--verbose
This sends the archive to Apple’s servers. The upload returns a submissionId, which can be used to check the status of the submission with:
./altool --showSubmissionStatus --submissionId 123456789 \
--username "[email protected]" \
--password "secret" \
--verbose
The response includes status, progress, and error fields. Common failure modes include:
Error 1003: App file is not valid.Error 1012: Provisioning profile mismatch.Error 1021: Missing icon assets.For Google Play, use gcloud CLI to upload and promote the APK:
gcloud android app-bundles upload \
--app-bundle ./app/build/outputs/bundle/release/app-release.aab \
--track=beta \
--version-code=1023 \
--release-notes="Bug fixes and performance improvements" \
--device-sets=phone,tablet \
--staged
The --staged flag uploads but does not promote the APK to beta. To promote, run:
gcloud android app-bundles promote \
--track=beta \
--version-code=1023 \
--from-track=staging \
--staged
The track field determines which rollout channel receives the release: draft, alpha, beta, production. Promoting from staging to beta triggers a rollout to all users in the beta track.
In-app updates allow users to update without waiting for the App Store or Play Store. Use a manifest file to define release metadata and rollout strategy.
The manifest is a JSON file served from a CDN or backend API, typically at /api/releases/latest. Example:
{
"version": "2.3.1",
"versionCode": 1023,
"releaseNotes": "Fixed login crash and improved image loading.",
"downloadUrl": "https://cdn.myapp.com/2.3.1/MyApp.ipa",
"installUrl": "myapp://install?version=2.3.1",
"minAppVersion": "2.0.0",
"rollout": {
"strategy": "percentage",
"percentage": 20,
"targetDevices": ["iPhone", "iPad"],
"delay": 3600
},
"changelog": [
"Fixed crash on login screen",
"Improved image loading with caching",
"Added dark mode support"
],
"optional": true,
"installMode": "background"
}
The installMode field can be foreground, background, or manual. The background mode downloads the update in the background and installs it on next app launch. The rollout object defines how the release is phased. The percentage field determines how many users receive the update immediately; delay is the number of seconds before the next batch is exposed.
When the app checks for updates, it fetches this manifest and compares versionCode with the currently installed version. If versionCode is higher, it triggers the update.
For Android, use AppUpdateManager API with the following code:
val appUpdateManager = AppUpdateManagerFactory.create(context)
val appUpdateInfoTask = appUpdateManager.appUpdateInfo
appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->
if (appUpdateInfo.updateAvailable() && appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
val updateRequest = AppUpdateManagerFactory.create(context)
.beginFlexibleUpdate(appUpdateInfo)
.setInstallerPackageName("com.myapp")
.setAdditionalExts(listOf("com.myapp.update"))
.build()
appUpdateManager.startUpdateFlowForResult(updateRequest, REQUEST_CODE_FLEXIBLE_UPDATE)
}
}
The beginFlexibleUpdate method starts a flexible (background) update. The setAdditionalExts ensures the app can handle custom update extensions. A failure mode: if updateAvailable() returns true but isUpdateTypeAllowed(AppUpdateType.FLEXIBLE) returns false, the app skips the update.
A mobile release can fail silently. Implement health checks and rollback procedures to catch issues early.
Each release includes a healthCheck endpoint. When a new version is deployed, it calls:
POST /api/health/check
Content-Type: application/json
{
"version": "2.3.1",
"buildId": "abc123",
"environment": "production"
}
The endpoint returns a 200 status with a JSON body:
{
"status": "healthy",
"checks": [
{
"name": "database_connection",
"status": "ok",
"details": "Connected to PostgreSQL at db.prod.myapp.com:5432"
},
{
"name": "api_latency",
"status": "warning",
"details": "Average response time 420ms (threshold: 300ms)",
"threshold": 300,
"actual": 420
},
{
"name": "crash_rate",
"status": "critical",
"details": "Crash rate increased by 12% over last 10 minutes",
"threshold": 5,
"actual": 12
}
]
}
The status field is one of healthy, warning, critical. The checks array contains test results. A critical check fails the health check, triggering a rollback.
Rollback is initiated by sending a POST /api/rollbacks request:
POST /api/rollbacks
Content-Type: application/json
{
"targetVersion": "2.3.0",
"reason": "High crash rate in 2.3.1",
"triggeredBy": "healthCheck",
"notes": "Crash rate increased from 3% to 12% in 10 minutes."
}
The system responds with:
{
"rollbackId": "r12345",
"status": "started",
"startedAt": "2024-05-10T14:32:11Z",
"targetVersion": "2.3.0",
"reason": "High crash rate in 2.3.1"
}
The rollback process then:
currentVersion in the database.installMode: "background" and installUrl: "myapp://rollback?version=2.3.0".Each rollout step logs to a release_logs table:
CREATE TABLE release_logs (
id BIGSERIAL PRIMARY KEY,
release_id TEXT NOT NULL,
step TEXT NOT NULL,
status TEXT CHECK (status IN ('pending', 'started', 'success', 'failed', 'cancelled')),
started_at TIMESTAMP WITH TIME ZONE,
completed_at TIMESTAMP WITH TIME ZONE,
error TEXT,
metadata JSONB
);
A step is one of build, upload_apple, upload_google, promote_google, check_health, trigger_rollback. When a step fails, status = 'failed' and error contains the full error message. A typical error text:
Error: Failed to upload to App Store. Status code 400. Response body: {"error":"invalid_app","message":"The app file is not valid for upload."}
Rollouts are not just “deploy to production.” They are phased, with increasing risk and visibility.
A full rollout uses a sequence of stages:
https://staging.myapp.com.Each phase is defined by a rollout_plan table:
CREATE TABLE rollout_plan (
id BIGSERIAL PRIMARY KEY,
release_id TEXT NOT NULL,
stage TEXT NOT NULL,
percentage INT NOT NULL,
duration_hours INT,
target_devices TEXT[],
min_app_version TEXT,
health_check_threshold INT,
triggered_by TEXT,
status TEXT CHECK (status IN ('pending', 'in-progress', 'completed', 'failed'))
);
The stage can be staging, internal_beta, public_beta, phased_10, phased_30, phased_70, final. The duration_hours field controls how long the phase runs before advancing.
After each phase, the system checks the healthCheck endpoint. If the crash_rate is above health_check_threshold, it pauses the rollout until resolved.
A failure in a phased rollout is common: users in the 30% phase report a crash on the profile screen, but the health check shows crash_rate at 9%—above the threshold of 7%. The system responds by:
release_logs.This is mobile release management: a system of build, deployment, phased rollout, health checks, and automated rollback, all working together to ship fast and avoid bricking phones.
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