Fastlane: Stop Deploying React Native Apps Manually
Introduction
Deploying React Native apps manually is tedious. You build the app, sign it, take screenshots, write changelogs, upload to TestFlight, submit to App Store, then repeat for Android. Each release can take hours.
I use Fastlane to automate this entire process. One command handles building, signing, and uploading. No more clicking through Xcode or dealing with expired certificates.
Here's how I set it up for my projects.
What Fastlane Does
Fastlane handles the entire deployment pipeline:
Automated builds - Compile your app for iOS and Android with one command.
Code signing - Manage certificates and provisioning profiles automatically with Match.
Screenshots - Generate App Store and Play Store screenshots programmatically.
Beta deployment - Push builds to TestFlight and Google Play Internal Testing instantly.
Store submission - Upload builds directly to App Store Connect and Play Console.
Changelog management - Handle release notes across multiple languages and platforms.
Everything that takes hours manually happens in minutes automatically.
Installation
Install Fastlane with Homebrew:
brew install fastlane
Initialize in your React Native project:
cd ios
fastlane init
cd ../android
fastlane init
Fastlane asks what you want to automate. Choose "Automate beta distribution" for TestFlight/Play Console setup.
iOS Setup
Basic Fastfile
Create or update ios/fastlane/Fastfile:
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
increment_build_number(xcodeproj: "YourApp.xcodeproj")
build_app(scheme: "YourApp")
upload_to_testflight
end
desc "Deploy a new version to the App Store"
lane :release do
increment_build_number(xcodeproj: "YourApp.xcodeproj")
build_app(scheme: "YourApp")
upload_to_app_store
end
end
Using Match for Code Signing
Match stores your certificates and profiles in a private Git repo. No more manual certificate management.
Initialize Match:
cd ios
fastlane match init
Choose "git" as storage and provide a private Git repo URL. Match encrypts everything.
Update your Fastfile:
lane :beta do
match(type: "appstore")
increment_build_number(xcodeproj: "YourApp.xcodeproj")
build_app(scheme: "YourApp")
upload_to_testflight
end
Match handles signing automatically. Your team uses the same certificates. No more "certificate expired" surprises.
Automated Screenshots
Generate screenshots with Snapshot:
fastlane snapshot init
This creates Snapfile and UI test files. Configure devices and languages:
# Snapfile
devices([
"iPhone 15 Pro Max",
"iPhone 15",
"iPad Pro (12.9-inch)"
])
languages([
"en-US",
"de-DE",
"es-ES"
])
scheme("YourAppUITests")
Add to your lane:
lane :screenshots do
snapshot
upload_to_app_store(skip_binary_upload: true)
end
Run fastlane screenshots to generate and upload screenshots automatically.
Android Setup
Basic Fastfile
Create or update android/fastlane/Fastfile:
default_platform(:android)
platform :android do
desc "Deploy to Play Store Internal Testing"
lane :beta do
gradle(
task: "bundle",
build_type: "Release"
)
upload_to_play_store(
track: "internal",
aab: "app/build/outputs/bundle/release/app-release.aab"
)
end
desc "Deploy to Play Store Production"
lane :release do
gradle(
task: "bundle",
build_type: "Release"
)
upload_to_play_store(
track: "production",
aab: "app/build/outputs/bundle/release/app-release.aab"
)
end
end
Google Play Service Account
To use Fastlane with Play Console, you need a service account:
- Go to Google Cloud Console
- Create a service account
- Download the JSON key
- Store it as
android/fastlane/google-play-key.json - Enable Google Play Android Developer API
- Grant the service account access in Play Console
Update your Fastfile:
lane :beta do
gradle(
task: "bundle",
build_type: "Release"
)
upload_to_play_store(
track: "internal",
json_key: "fastlane/google-play-key.json",
aab: "app/build/outputs/bundle/release/app-release.aab"
)
end
Managing Changelogs
Fastlane handles changelogs automatically. Create changelog files:
iOS:
ios/fastlane/metadata/en-US/release_notes.txt
ios/fastlane/metadata/de-DE/release_notes.txt
Android:
android/fastlane/metadata/android/en-US/changelogs/42.txt
android/fastlane/metadata/android/de-DE/changelogs/42.txt
The number in Android changelogs is the version code. Fastlane uploads these automatically with your build.
Running Fastlane Locally
Deploy to TestFlight:
cd ios
fastlane beta
Deploy to Play Store internal testing:
cd android
fastlane beta
Deploy to production:
cd ios
fastlane release
cd android
fastlane release
That's it. Fastlane builds, signs, and uploads everything.
GitHub Actions Integration
Automate deployment on every release tag. Create .github/workflows/deploy.yml:
name: Deploy
on:
push:
tags:
- 'v*'
jobs:
deploy-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.0
bundler-cache: true
- name: Install dependencies
run: |
cd ios
bundle install
pod install
- name: Deploy to TestFlight
env:
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
run: |
cd ios
fastlane beta
deploy-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: 3.0
bundler-cache: true
- name: Set up JDK
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'adopt'
- name: Install dependencies
run: |
cd android
bundle install
- name: Deploy to Play Store
env:
GOOGLE_PLAY_JSON_KEY: ${{ secrets.GOOGLE_PLAY_JSON_KEY }}
run: |
echo "$GOOGLE_PLAY_JSON_KEY" > android/fastlane/google-play-key.json
cd android
fastlane beta
Add secrets to your GitHub repo:
FASTLANE_USER- Your Apple IDFASTLANE_PASSWORD- App-specific password from AppleMATCH_PASSWORD- Password for Match encryptionMATCH_GIT_BASIC_AUTHORIZATION- Base64 encoded Git credentials for Match repoGOOGLE_PLAY_JSON_KEY- Contents of your Google Play service account JSON
Now every time you push a tag like v1.2.3, GitHub Actions automatically builds and deploys to both stores.
Version Bumping
Automate version bumps with lanes:
# iOS
lane :bump_version do
increment_version_number(
bump_type: "patch" # or "minor" or "major"
)
increment_build_number
commit_version_bump(
message: "Version bump",
xcodeproj: "YourApp.xcodeproj"
)
push_to_git_remote
end
# Android - add to build.gradle
def versionMajor = 1
def versionMinor = 2
def versionPatch = 3
def versionCode = versionMajor * 10000 + versionMinor * 100 + versionPatch
android {
defaultConfig {
versionCode versionCode
versionName "${versionMajor}.${versionMinor}.${versionPatch}"
}
}
Run fastlane bump_version before deploying.
Common Lanes I Use
Here's my complete setup for most projects:
platform :ios do
lane :beta do
match(type: "appstore")
increment_build_number
build_app(scheme: "YourApp")
upload_to_testflight(
skip_waiting_for_build_processing: true
)
slack(message: "iOS beta deployed to TestFlight")
end
lane :release do
match(type: "appstore")
increment_build_number
build_app(scheme: "YourApp")
upload_to_app_store(
submit_for_review: true,
automatic_release: false,
force: true
)
slack(message: "iOS submitted for review")
end
lane :screenshots do
snapshot
upload_to_app_store(
skip_binary_upload: true,
skip_metadata: true
)
end
end
platform :android do
lane :beta do
gradle(
task: "clean bundle",
build_type: "Release"
)
upload_to_play_store(
track: "internal",
json_key: "fastlane/google-play-key.json"
)
slack(message: "Android beta deployed to Internal Testing")
end
lane :release do
gradle(
task: "clean bundle",
build_type: "Release"
)
upload_to_play_store(
track: "production",
json_key: "fastlane/google-play-key.json"
)
slack(message: "Android deployed to Production")
end
end
Troubleshooting
Code signing fails on iOS - Run fastlane match nuke distribution to reset certificates, then fastlane match appstore to regenerate.
Upload fails with 2FA - Generate an app-specific password at appleid.apple.com and use that instead of your Apple ID password.
Android upload fails - Make sure your service account has "Release Manager" role in Play Console and the API is enabled in Google Cloud.
Build fails in CI - Check that all secrets are set correctly and your runner has enough disk space. iOS builds need ~20GB free.
Frequently Asked Questions
Does Fastlane work with Expo?
It does, but you need to eject first. Fastlane requires access to native iOS/Android code, which Expo's managed workflow doesn't expose. After ejecting, you can use Fastlane normally.
Can I deploy to both stores simultaneously?
Yes, but I recommend separate lanes. iOS and Android releases rarely happen at exactly the same time due to review processes.
How do I handle different app variants (staging, production)?
Create separate lanes with different schemes/build types. Use environment variables to switch between configurations.
What if I already have certificates?
Run fastlane match import to import existing certificates into Match. Or use cert and sigh actions instead of Match.