Mobile CI/CD: Fastlane, GitHub Actions for iOS and Android, TestFlight and Play Store
Automate mobile app delivery — Fastlane setup for iOS and Android, GitHub Actions workflows, code signing with Match, TestFlight beta distribution, Play Store d
Mobile CI/CD: Fastlane, GitHub Actions for iOS and Android, TestFlight and Play Store
Mobile CI/CD is harder than web CI/CD. Code signing certificates, provisioning profiles, Apple Developer portal quirks, Android keystore management, and the app store review process all add complexity that web deployments don't have.
This guide covers the setup that automates iOS and Android builds from commit to beta distribution.
The Mobile Deployment Challenge
| Challenge | iOS | Android |
|---|---|---|
| Code signing | Certificates + provisioning profiles | Keystore file |
| Distribution | App Store Connect / TestFlight | Play Store / APK direct |
| Review time | 1–3 days (App Store) | 1–3 days (Play Store) |
| Beta testing | TestFlight (up to 10,000 testers) | Internal track → Closed/Open testing |
| Build machine | Requires macOS for iOS | Any OS for Android |
Fastlane: The Automation Layer
Fastlane is the standard tool for mobile deployment automation. It wraps common tasks (building, signing, uploading) into reusable "lanes."
# Install Fastlane
gem install fastlane
# or
brew install fastlane
# Initialize in your project root
cd YourApp
fastlane init
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
iOS: Fastfile Configuration
# ios/fastlane/Fastfile
default_platform(:ios)
platform :ios do
# Before any lane: ensure we have the right environment
before_all do
ensure_git_status_clean
xcversion(version: "~> 16.0") # Ensure correct Xcode version
end
# Lane: Sync certificates and provisioning profiles
lane :certificates do
match(
type: "appstore", # or "adhoc" for TestFlight, "development" for dev
app_identifier: "com.yourcompany.yourapp",
git_url: "https://github.com/yourorg/ios-certificates", # Private repo for certs
git_branch: "main",
readonly: is_ci, # CI only reads; devs can rotate
)
end
# Lane: Run tests
lane :test do
run_tests(
scheme: "YourApp",
device: "iPhone 16",
code_coverage: true,
xcargs: "-maximum-test-execution-time-allowance 300"
)
end
# Lane: Build and upload to TestFlight
lane :beta do
certificates # Sync signing assets
# Increment build number (must be unique and incrementing for App Store)
increment_build_number(
build_number: ENV["BUILD_NUMBER"] || latest_testflight_build_number + 1
)
# Build the app
build_app(
scheme: "YourApp",
workspace: "YourApp.xcworkspace",
configuration: "Release",
export_method: "app-store",
export_options: {
provisioningProfiles: {
"com.yourcompany.yourapp" => "match AppStore com.yourcompany.yourapp"
}
}
)
# Upload to TestFlight
upload_to_testflight(
api_key_path: "fastlane/app_store_connect_api_key.json",
skip_waiting_for_build_processing: false, # Wait for processing
distribute_external: true,
groups: ["Beta Testers"],
changelog: changelog_from_git_commits(
commits_count: 10,
pretty: "- %s"
)
)
# Notify Slack
slack(
message: "New iOS beta uploaded to TestFlight! Build #{get_build_number}",
slack_url: ENV["SLACK_WEBHOOK_URL"],
success: true
)
end
# Lane: Release to App Store
lane :release do
certificates
build_app(
scheme: "YourApp",
workspace: "YourApp.xcworkspace",
configuration: "Release",
export_method: "app-store"
)
upload_to_app_store(
api_key_path: "fastlane/app_store_connect_api_key.json",
submit_for_review: true,
automatic_release: false, # Manually release after approval
force: true,
precheck_include_in_app_purchases: false
)
end
error do |lane, exception|
slack(
message: "iOS #{lane} lane failed: #{exception.message}",
slack_url: ENV["SLACK_WEBHOOK_URL"],
success: false
)
end
end
iOS Code Signing: Match
Match stores all certificates and provisioning profiles encrypted in a Git repository, solving the "it works on my machine" signing problem.
# Setup Match (one-time, by team lead)
fastlane match init
# Choose: git (recommended)
# Enter: private repo URL for certificates
# Generate and store certificates
fastlane match development
fastlane match appstore
fastlane match adhoc
# On CI: just read existing certificates
fastlane match appstore --readonly
# Fastlane Match creates:
# - Development certificate + provisioning profile
# - AppStore certificate + provisioning profile
# All encrypted with MATCH_PASSWORD, stored in private git repo
# Any team member runs `fastlane match` to install locally
🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Android: Fastfile Configuration
# android/fastlane/Fastfile
default_platform(:android)
platform :android do
lane :test do
gradle(
task: "test",
project_dir: "android/"
)
end
lane :beta do
# Increment version code (must be higher than current Play Store version)
android_set_version_code(
version_code: ENV["BUILD_NUMBER"].to_i,
gradle_file: "android/app/build.gradle"
)
# Build signed AAB (Android App Bundle — required for Play Store)
gradle(
task: "bundle",
build_type: "Release",
project_dir: "android/",
properties: {
"android.injected.signing.store.file" => ENV["KEYSTORE_FILE"],
"android.injected.signing.store.password" => ENV["KEYSTORE_PASSWORD"],
"android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
"android.injected.signing.key.password" => ENV["KEY_PASSWORD"],
}
)
# Upload to Play Store internal testing track
upload_to_play_store(
track: "internal", # internal → alpha → beta → production
aab: "android/app/build/outputs/bundle/release/app-release.aab",
json_key: "fastlane/play_store_api_key.json",
release_status: "completed",
skip_upload_apk: true
)
end
lane :release do |options|
gradle(
task: "bundle",
build_type: "Release",
project_dir: "android/",
properties: {
"android.injected.signing.store.file" => ENV["KEYSTORE_FILE"],
# ... signing properties
}
)
upload_to_play_store(
track: options[:track] || "production",
rollout: "0.1", # 10% staged rollout
aab: "android/app/build/outputs/bundle/release/app-release.aab",
json_key: "fastlane/play_store_api_key.json",
)
end
end
GitHub Actions: Full CI/CD Pipeline
# .github/workflows/mobile-ci.yml
name: Mobile CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
BUILD_NUMBER: ${{ github.run_number }}
jobs:
# ─────────────────────────────────────────
# iOS Beta Build
# ─────────────────────────────────────────
ios-beta:
name: iOS Beta → TestFlight
runs-on: macos-15 # Must use macOS runner for iOS builds
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup Ruby (for Fastlane)
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- name: Setup Node.js (for React Native)
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Setup SSH key for Match certificates repo
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.MATCH_REPO_SSH_KEY }}
- name: Run iOS Fastlane beta lane
run: bundle exec fastlane ios beta
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APP_STORE_CONNECT_API_KEY_CONTENT: ${{ secrets.ASC_API_KEY_CONTENT }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
BUILD_NUMBER: ${{ env.BUILD_NUMBER }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
# ─────────────────────────────────────────
# Android Beta Build
# ─────────────────────────────────────────
android-beta:
name: Android Beta → Play Store Internal
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
bundler-cache: true
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Setup Java (for Android build)
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Install dependencies
run: npm ci
# Decode keystore from base64 secret
- name: Decode Android Keystore
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode \
> android/app/release.keystore
- name: Run Android Fastlane beta lane
run: bundle exec fastlane android beta
env:
KEYSTORE_FILE: "app/release.keystore"
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
PLAY_STORE_JSON_KEY: ${{ secrets.PLAY_STORE_JSON_KEY }}
BUILD_NUMBER: ${{ env.BUILD_NUMBER }}
Version Management
// scripts/bump-version.ts
// Bump version numbers across both platforms from one place
import { execSync } from 'child_process';
import { readFileSync, writeFileSync } from 'fs';
const newVersion = process.argv[2]; // e.g., "2.4.1"
if (!newVersion) throw new Error('Usage: ts-node bump-version.ts 2.4.1');
// Update package.json
const pkg = JSON.parse(readFileSync('package.json', 'utf8'));
pkg.version = newVersion;
writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
// iOS: update version in Info.plist via Fastlane
execSync(`cd ios && bundle exec fastlane run increment_version_number version_number:${newVersion}`, { stdio: 'inherit' });
// Android: update versionName in build.gradle
let gradle = readFileSync('android/app/build.gradle', 'utf8');
gradle = gradle.replace(/versionName "[\d.]+"/, `versionName "${newVersion}"`);
writeFileSync('android/app/build.gradle', gradle);
console.log(`Version bumped to ${newVersion}`);
Working With Viprasol
We set up mobile CI/CD pipelines for React Native and Flutter apps — Fastlane configuration, code signing automation with Match, GitHub Actions workflows, and TestFlight/Play Store delivery. Automated mobile builds eliminate manual release day operations.
→ Talk to our team about mobile CI/CD automation.
See Also
- React Native vs Flutter — choosing your mobile framework
- DevOps Best Practices — CI/CD principles for all platforms
- Mobile App Security — code signing and app security
- Feature Flags — gradual rollouts for mobile apps
- Web Development Services — mobile app development
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.