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 eliminates the manual steps that make mobile releases slow, error-prone, and stressful. A fully automated release pipeline can take a merge to main and produce a signed, tested, beta-distributed build in under 30 minutes — without a human touching it.
Release Train Model
Weekly Release Train:
Monday: Code freeze, branch cut (release/v2.5)
Tuesday: Automated regression tests, beta to QA
Wednesday: QA validation, bug fixes cherry-picked
Thursday: Release candidate to external beta testers
Friday: Submit to App Store + Play Store
Following Monday: Staged rollout begins
Day 1: 1% of users
Day 2: 5% of users
Day 3: 25% of users
Day 5: 50% of users
Day 7: 100% of users (if no regressions)
Hotfix Process:
Critical bug found → Hotfix branch from release
Automated tests → Expedited review request
Submit with "expedited review" flag (Apple)
Full rollout immediately after approval
Fastlane Automation
# fastlane/Fastfile - Complete release automation
platform :ios do
before_all do
setup_ci if ENV["CI"]
cocoapods(clean_install: true)
end
lane :prepare_release do |options|
version = options[:version]
# Bump version
increment_version_number(version_number: version)
increment_build_number(build_number: ENV["BUILD_NUMBER"] || latest_testflight_build_number + 1)
# Update changelog
stamp_changelog(section_identifier: version)
# Commit version bump
commit_version_bump(message: "[ci skip] Version bump to #{version}")
add_git_tag(tag: "ios/v#{version}")
push_to_git_remote
end
lane :build_and_sign do
# Automatic code signing with Match
sync_code_signing(
type: "appstore",
readonly: is_ci,
)
build_app(
scheme: "MyApp-Production",
configuration: "Release",
export_method: "app-store",
export_options: {
provisioningProfiles: {
"com.company.myapp" => "match AppStore com.company.myapp"
}
}
)
end
lane :distribute_beta do
build_and_sign
upload_to_testflight(
distribute_external: true,
groups: ["External Beta Testers"],
changelog: read_changelog(section_identifier: "Unreleased"),
)
slack(
message: "✅ iOS beta #{lane_context[:VERSION_NUMBER]} (#{lane_context[:BUILD_NUMBER]}) uploaded to TestFlight",
slack_url: ENV["SLACK_WEBHOOK_URL"],
)
end
end
Staged Rollout
# Automated staged rollout with monitoring
class StagedRollout:
STAGES = [
{"percentage": 1, "duration_hours": 24, "min_installs": 1000},
{"percentage": 5, "duration_hours": 24, "min_installs": 5000},
{"percentage": 25, "duration_hours": 48, "min_installs": 25000},
{"percentage": 50, "duration_hours": 48, "min_installs": 50000},
{"percentage": 100, "duration_hours": 0, "min_installs": 0},
]
def advance_rollout(self):
"""Automatically advance or halt rollout based on metrics."""
current_stage = self.get_current_stage()
metrics = self.get_crash_metrics()
# Halt criteria
if metrics.crash_rate > 0.02: # > 2% crash rate
self.halt_rollout(reason=f"Crash rate {metrics.crash_rate:.1%}")
self.alert("CRITICAL: Rollout halted due to elevated crash rate")
return
if metrics.anr_rate > 0.005: # > 0.5% ANR rate (Android)
self.halt_rollout(reason=f"ANR rate {metrics.anr_rate:.3%}")
return
# Advance criteria
stage = self.STAGES[current_stage]
if (self.hours_at_stage() >= stage["duration_hours"] and
self.installs_at_stage() >= stage["min_installs"]):
next_stage = current_stage + 1
self.set_rollout_percentage(self.STAGES[next_stage]["percentage"])
self.alert(f"Rollout advanced to {self.STAGES[next_stage]['percentage']}%")
Code Signing
iOS Code Signing:
Certificate → identifies the developer/company
Provisioning Profile → ties cert + app ID + devices
Types:
Development: Build for test devices
Ad Hoc: Build for specific UDIDs (up to 100)
App Store: Build for App Store distribution
Enterprise: Build for internal company distribution
Automation: Fastlane Match
Stores certs + profiles in encrypted Git repo
All CI machines pull same certs
No manual Xcode signing management
Android Code Signing:
Keystore → contains private key
Upload Key → signs the AAB you upload to Google
App Signing Key → Google re-signs for distribution
Best practice: Google Play App Signing
You keep upload key
Google keeps signing key
If upload key is compromised → reset without re-publishing
Anti-Patterns
| Anti-Pattern | Consequence | Fix |
|---|---|---|
| Manual version bumping | Duplicate version numbers, missed builds | Automated version management in CI |
| Sharing signing certs via email | Security risk, cert conflicts | Fastlane Match (encrypted repo) |
| 100% rollout immediately | No safety net for crashes | Staged rollout with monitoring gates |
| No crash monitoring integration | Find out about crashes from reviews | Crashlytics/Sentry alerts gate rollout |
| No release notes automation | Missing or inconsistent notes | Auto-generate from commit messages |
The goal of mobile release automation is to make releases boring. When releases are boring, they happen often. When they happen often, each release is small and low-risk.