← Back to all posts

Fastlane: Stop Deploying React Native Apps Manually

EhsanBy Ehsan
7 min read
React NativeFastlaneDeploymentCI/CDiOSAndroidAutomationApp StorePlay StoreDevOps

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:

  1. Go to Google Cloud Console
  2. Create a service account
  3. Download the JSON key
  4. Store it as android/fastlane/google-play-key.json
  5. Enable Google Play Android Developer API
  6. 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 ID
  • FASTLANE_PASSWORD - App-specific password from Apple
  • MATCH_PASSWORD - Password for Match encryption
  • MATCH_GIT_BASIC_AUTHORIZATION - Base64 encoded Git credentials for Match repo
  • GOOGLE_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.

Resources