Automating Mobile App Releases with Fastlane: iOS & Android CI/CD
A comprehensive guide to setting up Fastlane for automated iOS TestFlight and Android Play Store deployments with GitHub Actions, code signing, and version management.
Why Fastlane?
Mobile releases are notoriously painful. Code signing certificates expire, provisioning profiles get out of sSNc, version numbers need manual bumping, screenshots need regenerating, and deploying to TestFlight takes 15 manual steps. Fastlane automates all of it.
Before Fastlane, our mobile team spent 4-6 hours per release on manual tasks. After: a merge to main triggers a full release to TestFlight and Play Store in under 45 minutes, fully unattended.
Pipeline Architecture
Project Setup
# Install Fastlane
gem install fastlane
# Initialize in your project root
cd ios && fastlane init
cd android && fastlane init
Code Signing with Match
Code signing is the #1 source of iOS CI failures. Fastlane's match action solves this by storing all certificates and profiles in a private Git repository, encrypted with a passphrase. Every machine — developer laptops and CI runners — fetches signing credentials from the same source.
# ios/fastlane/Matchfile
git_url("https://github.com/yourorg/ios-signing")
storage_mode("git")
type("appstore")
app_identifier(["com.yourcompany.app"])
username("apple@yourcompany.com")
# ios/fastlane/Fastfile
lane :release do
match(type: "appstore", readonly: true)
increment_build_number
gym(
scheme: "YourApp",
export_method: "app-store",
output_directory: "./build"
)
pilot(
skip_waiting_for_build_processing: true,
changelog: changelog_from_git_commits
)
slack(
message: "🚀 iOS build #{lane_context[SharedValues::BUILD_NUMBER]} uploaded!"
)
end
Android Lane
# android/fastlane/Fastfile
lane :release do
gradle(
task: "bundle",
build_type: "Release",
properties: {
"android.injected.signing.store.file" => ENV["KEYSTORE_PATH"],
"android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
"android.injected.signing.store.password" => ENV["STORE_PASSWORD"]
}
)
supply(
track: "internal",
aab: "app/build/outputs/bundle/release/app-release.aab"
)
end
GitHub Actions Workflow
# .github/workflows/release.yml
on:
push:
branches: [main]
jobs:
ios-release:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Install Fastlane
run: gem install fastlane
- name: Run Fastlane release
run: cd ios && fastlane release
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
APP_STORE_CONNECT_API_KEY: ${{ secrets.ASC_KEY }}
android-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Fastlane release
run: cd android && bundle exec fastlane release
env:
KEYSTORE_PATH: ${{ secrets.KEYSTORE_PATH }}
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
The Result
What used to take half a day of an engineer's time now runs every time code merges. Our mobile team ships to TestFlight and the internal Play Store track on every merge to main. Full production releases happen weekly with a single label on the release PR. Engineering time saved: approximately 20 hours per month.