Mobile CI/CD: Fastlane, GitHub Actions, and App Store Deployment
TL;DR: This guide covers mobile CI/CD setup. Learn Fastlane, GitHub Actions, test automation, and deploying to App Store and Play Store.
Introduction
Mobile app development requires efficient build and deployment pipelines. With apps needing to target iOS and Android, often across multiple environments (development, staging, production), manual builds are unsustainable. A robust CI/CD pipeline automates testing, building, and deployment, reducing errors and accelerating release cycles.
This guide covers the essential components of mobile CI/CD: Fastlane for automation, GitHub Actions for CI/CD orchestration, testing integration, and App Store deployment. Whether you’re deploying to TestFlight, Google Play, or the App Store, these tools streamline the entire process.
Fastlane: Mobile Automation
Fastlane is the industry standard for mobile build automation. It provides tools for building, testing, and deploying iOS and Android apps. Fastlane lanes define repeatable workflows that can be executed from the command line.
Installation
# Install Fastlane via Ruby
sudo gem install fastlane
# Or via Homebrew (macOS)
brew install fastlane
# Navigate to your project and initialize
fastlane init
The initialization process creates a fastlane directory with a Fastfile where you’ll define your automation lanes.
Fastfile Structure
default_platform(:ios)
platform :ios do
desc "Build and test the app"
lane :build do
# Run tests
scan
# Build the app
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp"
)
end
desc "Run unit tests"
lane :test do
scan(
scheme: "MyApp",
clean: true
)
end
desc "Deploy to TestFlight"
lane :deploy_testflight do
# Get the latest build number
increment_build_number
# Build
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp"
)
# Upload to TestFlight
upload_to_testflight(
skip_waiting_for_build_processing: true,
distribute_external: true,
groups: ["Testers"]
)
end
desc "Deploy to App Store"
lane :deploy_appstore do
# Ensure code signing is configured
match(type: "appstore")
# Build and upload
build_app(
workspace: "MyApp.xcworkspace",
scheme: "MyApp"
)
# Submit for review
deliver(
skip_screenshots: true,
skip_metadata: true,
force: true
)
end
end
Android Configuration
platform :android do
desc "Build Android debug APK"
lane :build do
gradle(
task: "assembleDebug"
)
end
desc "Deploy to Google Play (Internal)"
lane :deploy_playstore do
gradle(
task: "assembleRelease"
)
supply(
track: "internal",
apk: "app/build/outputs/apk/release/app-release.apk"
)
end
end
Match for Code Signing
Fastlane Match manages certificates and provisioning profiles:
lane :setup do
match(
type: "development",
readonly: true
)
end
GitHub Actions CI/CD
GitHub Actions provides cloud-based CI/CD with generous free tiers for open source and reasonable pricing for private repositories.
Basic iOS CI Workflow
name: Mobile CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm test
- name: Run linting
run: npm run lint
build-ios:
name: Build iOS
needs: test
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Cache Ruby gems
uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
- name: Install Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
- name: Install dependencies
run: |
cd ios
bundle install
- name: Build iOS
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
run: bundle exec fastlane build
build-android:
name: Build Android
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Android SDK
uses: android-actions/setup-android@v3
- name: Build Android
run: |
cd android
./gradlew assembleDebug
Scheduled Builds and Releases
name: Release Pipeline
on:
push:
tags:
- 'v*'
jobs:
release:
name: Create Release
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
- name: Install dependencies
run: bundle install
- name: Build and Deploy
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
run: bundle exec fastlane release
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: true
Test Automation
Unit Testing in CI
- name: Run tests
run: |
npm test -- --coverage
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
Snapshot Testing
lane :snapshot do
snapshot(
scheme: "MyApp",
devices: ["iPhone 14", "iPhone SE", "iPad Pro 11"]
)
end
App Store Deployment
Version Management
# Auto-increment build number
lane :prepare_release do
# Get latest build from TestFlight
latest_build = latest_testflight_build_number
new_build = latest_build + 1
increment_build_number(
build_number: new_build
)
# Update version if needed
increment_version_number(
version_number: "1.2.0"
)
end
Play Store Deployment
lane :deploy_playstore do
gradle(
task: "assembleRelease",
build_type: "Release"
)
supply(
track: "internal",
apk: "app/build/outputs/apk/release/app-release-unsigned.apk",
json_key_data: ENV["PLAY_STORE_JSON_KEY"]
)
end
Best Practices
Secrets Management
Never commit secrets to GitHub. Use:
- GitHub Secrets for sensitive values
- Fastlane Match for certificates
- Environment files loaded at runtime
Caching
- name: Cache Node modules
uses: actions/cache@v4
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
Parallel Jobs
Run independent jobs in parallel to reduce total CI time:
jobs:
test-ios:
runs-on: macos-latest
steps: [...]
test-android:
runs-on: ubuntu-latest
steps: [...]
Conclusion
Mobile CI/CD requires:
- Fastlane - Automation for builds and deployment
- GitHub Actions - Cloud CI/CD orchestration
- Test automation - Quality assurance with every change
- App Store deployment - Automated release to stores
Implementing these tools transforms mobile development from manual builds to automated, reliable releases. Start with basic builds, add testing, then expand to full deployment automation.
Modern Mobile CI/CD in 2026
AI-Powered Testing
# AI-assisted visual testing
- name: Percy Visual Tests
uses: percy/cli@latest
with:
files: ['**/*.png']
ignore: ['**/android/**']
# AI test generation
- name: AI Test Generation
uses: appmap/appmap-agent@v1
with:
- name: Generate AI tests
run: |
# Use AI to generate test cases
appmap record -- open MyApp.app
appmap test generate --ai
Mobile-Specific GitHub Actions
# Matrix testing across devices
jobs:
test:
strategy:
matrix:
device: ["iPhone 15", "iPhone SE", "iPad Pro"]
os-version: ["17.0", "16.0"]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Run XCTests
run: xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=${{ matrix.device }},OS=${{ matrix.os-version }}'
Environment-Specific Deployments
# Fastlane environment configuration
lane :deploy do
environment = ENV['DEPLOY_ENV']
case environment
when 'development'
build_app(scheme: 'MyApp-Dev')
deploy_to_testflight(skip_waiting_for_build_processing: true)
when 'staging'
build_app(scheme: 'MyApp-Staging')
deploy_to_testflight
when 'production'
match(type: 'appstore')
build_app(scheme: 'MyApp')
upload_to_app_store
end
end
App Store Connect API Integration
# Using App Store Connect API for metadata
lane :update_metadata do
app_store_connect_api_key
# Update app metadata
deliver(
metadata_path: "./metadata",
app_identifier: "com.myapp.app",
skip_screenshots: true,
overwrite_screenshots: false
)
end
Firebase App Distribution
# Deploy to Firebase App Distribution
lane :deploy_firebase do
gradle(task: "assembleRelease")
firebase_app_distribution(
app: "1:123456789:android:abc123",
groups: "testers",
firebase_token: ENV["FIREBASE_TOKEN"],
release_notes: "New features in this release!"
)
end
Codemagic Alternative
# codemagic.yaml for alternative CI/CD
workflows:
ios-workflow:
name: iOS Workflow
max_build_duration: 60
environment:
flutter: stable
xcode: latest
cocoapods: default
scripts:
- name: Set up code signing
script: |
keychain initialize
app-store-connect fetch-signing-files "com.myapp" --type IOS_APP_STORE
keychain add-certificates
- name: Flutter build
script: flutter build ios --release
- name: Deploy to App Store
script: |
app-store-connect publish \
--path build/ios/ipa/*.ipa
RevenueCat Integration
# Configure RevenueCat in CI
lane :configure_revenuecat do
# Set up RevenueCat API key
ENV["REVENUECAT_API_KEY"] = "sk_live_xxx"
# Create offering
api_key = ENV["REVENUECAT_API_KEY"]
# RevenueCat API calls for managing offerings
end
Build Metrics and Monitoring
# Track build times and metrics
- name: Build Metrics
run: |
echo "Build Time: ${{ github.event.inputs.build_time }}"
echo "Build Status: ${{ job.status }}"
- name: Send to Datadog
uses: DataDog/datadog-metric-action@v1
with:
api_key: ${{ secrets.DATADOG_API_KEY }}
metrics: |
- metric: mobile.build.time
type: gauge
value: ${{ github.event.inputs.build_time }}
tags: ["env:production"]
Comments