Setting Up React Native CI/CD with GitHub Actions and Fastlane

Published on December 16, 2025 • 14 min read

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:

  1. What would you like to use fastlane for? → "Automate beta distribution to TestFlight"
  2. Enter your Apple ID and password
  3. 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

  1. Go to Google Play Console → Setup → API access
  2. Create service account (link to Google Cloud)
  3. Grant "Release Manager" role
  4. Download JSON key
  5. Encode and add to GitHub Secrets
Security tip: Never commit secrets to Git. Always use GitHub Secrets or environment variables. One leaked certificate can compromise your entire release pipeline.

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 main branch 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

  1. Always test locally first - Don't debug in CI/CD, it's slow and wastes minutes
  2. Use caching aggressively - Cache node_modules, Pods, Gradle
  3. Run tests on every PR - Catch bugs before they hit main
  4. Separate test and deploy jobs - Run tests on every PR, deploy only on main
  5. Monitor your build times - Optimize if builds exceed 15-20 minutes
  6. Keep secrets secure - Use GitHub Secrets, never commit to Git
  7. 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.

Need help setting up CI/CD for your React Native app? At ThreadCode, we've configured CI/CD pipelines for 20+ production React Native apps. Let's automate your releases.