Setting Up React Native CI/CD with GitHub Actions and Fastlane
Manual builds are a developer's nightmare. Every release becomes a multi-hour process: running tests, building for iOS, building for Android, signing, uploading to TestFlight, uploading to Play Store, praying nothing breaks. After setting up CI/CD pipelines for 20+ React Native apps, I'm sharing the exact setup that works in production.
By the end of this guide, you'll have a fully automated pipeline that builds, tests, and deploys your React Native app to both iOS and Android with a single git push. No more manual builds. Ever.
Why CI/CD Matters for React Native
Here's what proper CI/CD gives you:
- Automated builds: Push to main, get builds on TestFlight and Play Store
- Consistent releases: No more "works on my machine" issues
- Time savings: 3-4 hours of manual work → 0 hours
- Faster iteration: Deploy beta builds multiple times per day
- Catch bugs early: Automated tests run on every PR
- Team scalability: Any developer can trigger releases
The Stack: GitHub Actions + Fastlane
GitHub Actions: Free CI/CD built into GitHub. 2,000 free minutes/month for private repos (plenty for most projects).
Fastlane: Ruby-based automation tool that handles all the iOS/Android complexity (code signing, screenshots, metadata, uploads).
This combination is battle-tested, free for small teams, and scales to enterprise apps with millions of users.
Prerequisites
Before starting, you'll need:
- React Native app (0.70+) with iOS and Android projects
- Apple Developer account ($99/year)
- Google Play Developer account ($25 one-time)
- GitHub repository
- Basic terminal/command line knowledge
Part 1: Setting Up Fastlane
Install Fastlane
# Install Fastlane via RubyGems
sudo gem install fastlane -NV
# Or using Homebrew (macOS)
brew install fastlane
Initialize Fastlane for iOS
cd ios
fastlane init
Fastlane will ask you questions. Choose:
- What would you like to use fastlane for? → "Automate beta distribution to TestFlight"
- Enter your Apple ID and password
- Select your app from the list (or create new)
This creates ios/fastlane/Fastfile and ios/fastlane/Appfile.
Configure iOS Fastfile
Edit ios/fastlane/Fastfile:
default_platform(:ios)
platform :ios do
desc "Push a new beta build to TestFlight"
lane :beta do
# Increment build number (based on TestFlight latest)
increment_build_number(xcodeproj: "YourApp.xcodeproj")
# Build the app
build_app(
scheme: "YourApp",
export_method: "app-store",
export_options: {
provisioningProfiles: {
"com.yourcompany.yourapp" => "match AppStore com.yourcompany.yourapp"
}
}
)
# Upload to TestFlight
upload_to_testflight(
skip_waiting_for_build_processing: true,
skip_submission: true,
distribute_external: false
)
end
desc "Run tests"
lane :test do
run_tests(
scheme: "YourApp",
devices: ["iPhone 15 Pro"]
)
end
end
Initialize Fastlane for Android
cd android
fastlane init
Choose "Automate beta distribution to Google Play" when prompted.
Configure Android Fastfile
Edit android/fastlane/Fastfile:
default_platform(:android)
platform :android do
desc "Push a new beta build to Play Store"
lane :beta do
# Increment version code
gradle(
task: "clean bundleRelease",
properties: {
"android.injected.signing.store.file" => ENV["KEYSTORE_PATH"],
"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 test track)
upload_to_play_store(
track: 'internal',
release_status: 'draft',
aab: 'app/build/outputs/bundle/release/app-release.aab'
)
end
desc "Run tests"
lane :test do
gradle(task: "test")
end
end
Part 2: Code Signing Setup
iOS Code Signing (Match)
Fastlane Match stores certificates in a private Git repo (encrypted). This is the cleanest approach for teams.
# Initialize Match
cd ios
fastlane match init
Choose "git" and provide a private repo URL (create one on GitHub for certificates).
# Generate certificates and profiles
fastlane match appstore
fastlane match development
Update your Fastfile to use Match:
lane :beta do
# Sync certificates
match(type: "appstore", readonly: true)
increment_build_number(xcodeproj: "YourApp.xcodeproj")
build_app(scheme: "YourApp", export_method: "app-store")
upload_to_testflight
end
Android Code Signing
Generate a keystore for release builds:
cd android/app
keytool -genkey -v -keystore release.keystore -alias release \
-keyalg RSA -keysize 2048 -validity 10000
IMPORTANT: Never commit this keystore to Git. Store it securely and reference it via environment variables.
Update android/app/build.gradle:
android {
...
signingConfigs {
release {
if (project.hasProperty('RELEASE_STORE_FILE')) {
storeFile file(RELEASE_STORE_FILE)
storePassword RELEASE_STORE_PASSWORD
keyAlias RELEASE_KEY_ALIAS
keyPassword RELEASE_KEY_PASSWORD
}
}
}
buildTypes {
release {
signingConfig signingConfigs.release
...
}
}
}
Part 3: GitHub Actions Workflows
Create iOS Workflow
Create .github/workflows/ios-deploy.yml:
name: iOS Build & Deploy
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-ios:
runs-on: macos-14
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Install CocoaPods
run: |
cd ios
bundle install
bundle exec pod install
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: ios
- name: Run tests
run: |
cd ios
bundle exec fastlane test
- name: Build and deploy to TestFlight
if: github.ref == 'refs/heads/main'
env:
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
FASTLANE_PASSWORD: ${{ secrets.FASTLANE_PASSWORD }}
FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: ${{ secrets.FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD }}
run: |
cd ios
bundle exec fastlane beta
Create Android Workflow
Create .github/workflows/android-deploy.yml:
name: Android Build & Deploy
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-android:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: android
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run tests
run: |
cd android
bundle exec fastlane test
- name: Decode keystore
if: github.ref == 'refs/heads/main'
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 --decode > android/app/release.keystore
- name: Build and deploy to Play Store
if: github.ref == 'refs/heads/main'
env:
KEYSTORE_PATH: ${{ github.workspace }}/android/app/release.keystore
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
PLAY_STORE_JSON_KEY_DATA: ${{ secrets.PLAY_STORE_JSON_KEY_DATA }}
run: |
cd android
bundle exec fastlane beta
Part 4: GitHub Secrets Configuration
Navigate to GitHub repo → Settings → Secrets and variables → Actions. Add these secrets:
iOS Secrets
- MATCH_PASSWORD: Password for encrypted certificates repo
- MATCH_GIT_BASIC_AUTHORIZATION: Base64 encoded GitHub PAT for Match repo access
- FASTLANE_USER: Your Apple ID email
- FASTLANE_PASSWORD: Your Apple ID password
- FASTLANE_APPLE_APPLICATION_SPECIFIC_PASSWORD: App-specific password from appleid.apple.com
Android Secrets
- ANDROID_KEYSTORE_BASE64: Your keystore file encoded in base64
- KEYSTORE_PASSWORD: Keystore password
- KEY_ALIAS: Key alias
- KEY_PASSWORD: Key password
- PLAY_STORE_JSON_KEY_DATA: Service account JSON from Google Play Console
Encoding Keystore for GitHub Secrets
# Encode keystore to base64
base64 -i android/app/release.keystore -o keystore.txt
# Copy contents and add to GitHub Secrets
cat keystore.txt
Getting Google Play Service Account JSON
- Go to Google Play Console → Setup → API access
- Create service account (link to Google Cloud)
- Grant "Release Manager" role
- Download JSON key
- Encode and add to GitHub Secrets
Part 5: Testing Your Pipeline
Test Locally First
# Test iOS build locally
cd ios
bundle exec fastlane beta
# Test Android build locally
cd android
bundle exec fastlane beta
Fix any issues before pushing to GitHub Actions.
Test on GitHub Actions
Push to a feature branch and create a PR:
git checkout -b test-ci-cd
git add .
git commit -m "Add CI/CD pipeline"
git push origin test-ci-cd
This triggers the workflows, but won't deploy (only runs tests). Check the Actions tab for results.
Deploy to Production
Merge to main to trigger full deployment:
git checkout main
git merge test-ci-cd
git push origin main
Watch the magic happen in the Actions tab. Within 20-30 minutes, you'll have builds on TestFlight and Play Store internal track.
Part 6: Advanced Configuration
Version Bumping
Automate version bumps with Fastlane:
# iOS - in Fastfile
lane :bump_version do
increment_version_number(
bump_type: "patch" # or "minor", "major"
)
end
# Android - in Fastfile
lane :bump_version do
increment_version_code(
gradle_file_path: "app/build.gradle"
)
end
Slack Notifications
Get notified when builds complete:
# Add to Fastfile
after_all do |lane|
slack(
message: "Successfully deployed new build!",
channel: "#mobile-releases",
slack_url: ENV["SLACK_WEBHOOK_URL"]
)
end
error do |lane, exception|
slack(
message: "Build failed: #{exception.message}",
channel: "#mobile-releases",
slack_url: ENV["SLACK_WEBHOOK_URL"],
success: false
)
end
Multiple Environments
Set up staging and production environments:
lane :staging do
match(type: "adhoc")
build_app(
scheme: "YourApp-Staging",
export_method: "ad-hoc"
)
firebase_app_distribution(
app: ENV["FIREBASE_APP_ID_STAGING"]
)
end
lane :production do
match(type: "appstore")
build_app(
scheme: "YourApp-Production",
export_method: "app-store"
)
upload_to_testflight
end
Part 7: Troubleshooting Common Issues
iOS: Certificate Issues
If you get certificate errors:
# Regenerate certificates
fastlane match nuke development
fastlane match nuke appstore
fastlane match development
fastlane match appstore
Android: Gradle Build Fails
Increase memory for Gradle in android/gradle.properties:
org.gradle.jvmargs=-Xmx4096m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
GitHub Actions: Out of Minutes
macOS runners use 10x minutes. Optimize by:
- Only running on
mainbranch for deployments - Using caching for dependencies
- Running tests on faster Linux runners when possible
Real-World Results
After implementing this pipeline for our clients:
- Manual build time: 3-4 hours → 0 hours
- Release frequency: Weekly → Daily
- Build failures: 30% → 5% (caught in CI before release)
- Time to fix bugs: 2-3 days → 4 hours (rapid iteration)
- Team velocity: 2x improvement (devs focus on features, not builds)
Cost Breakdown
Here's what this setup costs monthly:
- GitHub Actions: Free (2,000 minutes/month for private repos)
- Additional minutes: $0.08/minute for macOS runners (if you exceed free tier)
- Fastlane: Free (open source)
- Apple Developer: $99/year
- Google Play Developer: $25 one-time
Total: Effectively free for most small-to-medium teams.
Best Practices
- Always test locally first - Don't debug in CI/CD, it's slow and wastes minutes
- Use caching aggressively - Cache node_modules, Pods, Gradle
- Run tests on every PR - Catch bugs before they hit main
- Separate test and deploy jobs - Run tests on every PR, deploy only on main
- Monitor your build times - Optimize if builds exceed 15-20 minutes
- Keep secrets secure - Use GitHub Secrets, never commit to Git
- Document your pipeline - Add README explaining setup for new team members
Conclusion: Automate Everything
Setting up CI/CD for React Native takes a day of work, but saves hundreds of hours over a project's lifetime. Manual builds are error-prone, time-consuming, and don't scale as your team grows.
With this setup, you can:
- Deploy to TestFlight and Play Store with a single
git push - Run automated tests on every code change
- Ship bug fixes in hours, not days
- Onboard new developers without teaching them the build process
- Sleep better knowing your releases are consistent and reliable
The initial setup might seem complex, but follow this guide step-by-step and you'll have a production-ready CI/CD pipeline by the end of the day. Your future self will thank you.