Skip to main content

Expo EAS vs Fastlane vs Bitrise: React Native CI/CD 2026

·PkgPulse Team

Expo EAS vs Fastlane vs Bitrise: React Native CI/CD 2026

TL;DR

Shipping a React Native app to the App Store and Google Play every two weeks used to require a Mac, Xcode, Android Studio, and days of setup. CI/CD for mobile has matured significantly. Expo EAS (Expo Application Services) is the modern default for Expo-managed and bare React Native apps — cloud builds, over-the-air JavaScript updates, and App Store submission in one unified CLI. Fastlane is the automation layer — a Ruby-based tool that automates the repetitive steps (building, signing, screenshots, upload) and can run locally, in GitHub Actions, or any CI server. Bitrise is the mobile-first CI/CD platform — a hosted CI service with 300+ pre-built mobile steps, dedicated Mac machines, and tight integration with App Store Connect and Google Play. For Expo apps: EAS, no question. For custom native projects needing automation scripts: Fastlane. For teams that want managed CI infrastructure with mobile-specific features: Bitrise.

Key Takeaways

  • EAS Build runs in the cloud — no Mac required for iOS builds
  • EAS Update pushes JS-only changes in seconds — bypass App Store review for most updates
  • Fastlane deliver — upload builds + metadata + screenshots to App Store in one command
  • Bitrise has M1/M2 Mac machines — faster native builds than GitHub Actions
  • EAS pricing: 30 builds/month free, then $3/build (iOS) or $0.06/minute
  • Fastlane is free and open-source — you pay for whatever CI runs it
  • Bitrise Hobby plan: free for 1 app with 500 build minutes/month

Mobile CI/CD Challenges

Without CI/CD — manual release process:
  1. Pull latest code
  2. Run `cd ios && pod install`
  3. Open Xcode, update version + build number
  4. Set signing certificate (correct profile for production?)
  5. Archive → Validate → Upload to App Store Connect
  6. Wait 30 minutes for processing
  7. Repeat for Android (Gradle, keystore, Play Console)
  8. Track down why the signing certificate expired

  Reality: 2-4 hours per release, one developer blocked

With CI/CD:
  git push origin main → build triggers → both stores updated → done
  5 minutes of human time

Expo EAS: Cloud-Native Mobile CI/CD

EAS provides cloud build infrastructure, OTA updates, and store submission — designed specifically for Expo and React Native.

Installation

npm install -g eas-cli
eas login
eas build:configure  # Creates eas.json

eas.json Configuration

{
  "cli": {
    "version": ">= 7.0.0",
    "appVersionSource": "remote"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "ios": {
        "resourceClass": "m-medium"
      }
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview",
      "android": {
        "buildType": "apk"
      }
    },
    "production": {
      "channel": "production",
      "ios": {
        "resourceClass": "m-medium"
      },
      "android": {
        "buildType": "app-bundle"
      },
      "autoIncrement": true
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "developer@company.com",
        "ascAppId": "1234567890",
        "appleTeamId": "ABCD123456"
      },
      "android": {
        "serviceAccountKeyPath": "./google-play-key.json",
        "track": "internal"
      }
    }
  },
  "update": {
    "channel": "production"
  }
}

Build Commands

# iOS build (no Mac required — runs in EAS cloud)
eas build --platform ios --profile production

# Android build
eas build --platform android --profile production

# Both platforms simultaneously
eas build --platform all --profile production

# Development build (includes expo-dev-client for testing native modules)
eas build --platform ios --profile development

# Internal distribution (TestFlight alternative — direct IPA install)
eas build --platform ios --profile preview

Code Signing (Automatic)

# EAS manages certificates and provisioning profiles automatically
eas credentials

# Option: Let EAS manage everything (recommended)
eas build --platform ios --profile production
# → "Would you like us to automatically manage your credentials? y"
# → EAS creates and stores certificates in Expo's key storage

OTA Updates (EAS Update)

# Push JavaScript-only update (no rebuild needed)
# Users get the update on next app launch
eas update --channel production --message "Fix checkout bug"

# Preview channel for testing
eas update --channel preview --message "New feature preview"

# Rollback to previous update
eas update:roll-back-to-embedded --channel production
// expo-updates in your app — check and download updates
import * as Updates from "expo-updates";

async function checkForUpdates() {
  if (!__DEV__) {
    const update = await Updates.checkForUpdateAsync();

    if (update.isAvailable) {
      await Updates.fetchUpdateAsync();
      await Updates.reloadAsync();
    }
  }
}

// Automatic update on launch (configure in app.json)
// expo.updates.checkAutomatically: "ON_LOAD" | "ON_ERROR_RECOVERY" | "WIFI_ONLY" | "NEVER"

GitHub Actions with EAS

# .github/workflows/eas-build.yml
name: EAS Build

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"

      - name: Install dependencies
        run: npm ci

      - name: Setup EAS
        uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}

      - name: Build on EAS
        run: eas build --platform all --non-interactive --profile production

      - name: Submit to stores
        run: eas submit --platform all --non-interactive --profile production

EAS Update in CI/CD

# .github/workflows/ota-update.yml
name: OTA Update (JS-only changes)

on:
  push:
    branches: [main]
    paths:
      - "app/**"
      - "components/**"
      - "hooks/**"
      - "!**/*.native.*"  # Not native code

jobs:
  update:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: "npm"
      - run: npm ci
      - uses: expo/expo-github-action@v8
        with:
          eas-version: latest
          token: ${{ secrets.EXPO_TOKEN }}
      - run: eas update --channel production --message "${{ github.event.head_commit.message }}" --non-interactive

Fastlane: Automation Scripts for Native Builds

Fastlane is a collection of Ruby-based tools that automate iOS and Android release steps — certificate management, building, testing, and upload.

Installation

# macOS (required for iOS Fastlane)
brew install fastlane

# In your React Native project
cd ios && fastlane init
cd android && fastlane init

iOS Fastfile

# ios/fastlane/Fastfile
default_platform(:ios)

platform :ios do

  before_all do
    # Ensure we're on the right branch and code is clean
    ensure_git_branch(branch: "main")
    ensure_git_status_clean
  end

  desc "Sync certificates and profiles (Match)"
  lane :sync_certificates do
    match(
      type: "appstore",                # "development" | "adhoc" | "appstore"
      app_identifier: "com.company.myapp",
      git_url: "git@github.com:company/certificates.git",  # Private cert repo
      readonly: is_ci,                # Don't modify certs on CI
    )
  end

  desc "Build and upload to TestFlight"
  lane :beta do
    sync_certificates

    # Increment build number automatically
    increment_build_number(
      build_number: latest_testflight_build_number + 1,
      xcodeproj: "MyApp.xcodeproj"
    )

    # Build
    build_app(
      workspace: "MyApp.xcworkspace",
      scheme: "MyApp",
      configuration: "Release",
      export_method: "app-store",
    )

    # Upload to TestFlight
    upload_to_testflight(
      api_key_path: "./app_store_connect_api_key.json",
      skip_waiting_for_build_processing: true,
      notify_external_testers: false,
    )

    # Notify Slack
    slack(
      message: "iOS beta uploaded to TestFlight! ✅",
      slack_url: ENV["SLACK_WEBHOOK_URL"],
    )
  end

  desc "Submit to App Store"
  lane :release do
    sync_certificates

    increment_version_number(version_number: ENV["VERSION"])
    increment_build_number(build_number: Time.now.strftime("%Y%m%d%H%M"))

    build_app(
      workspace: "MyApp.xcworkspace",
      scheme: "MyApp",
      configuration: "Release",
    )

    deliver(
      api_key_path: "./app_store_connect_api_key.json",
      submit_for_review: true,
      automatic_release: false,
      force: true,               # Skip HTML report
      precheck_include_in_app_purchases: false,
      metadata_path: "./fastlane/metadata",
      screenshots_path: "./fastlane/screenshots",
    )
  end

  error do |lane, exception|
    slack(
      message: "Build failed in #{lane}: #{exception.message}",
      success: false,
      slack_url: ENV["SLACK_WEBHOOK_URL"],
    )
  end

end

Android Fastfile

# android/fastlane/Fastfile
default_platform(:android)

platform :android do

  desc "Build and upload to Google Play Internal Testing"
  lane :beta do
    gradle(
      task: "bundle",
      build_type: "Release",
      project_dir: "android/",
      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(
      track: "internal",
      aab: "./android/app/build/outputs/bundle/release/app-release.aab",
      json_key: "./google-play-key.json",
      skip_upload_metadata: true,
      skip_upload_screenshots: true,
    )
  end

end

Fastlane in GitHub Actions

# .github/workflows/fastlane-ios.yml
name: iOS Release

on:
  push:
    tags: ["v*.*.*"]

jobs:
  build:
    runs-on: macos-14  # M1 Mac runner
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: |
          npm ci
          cd ios && pod install

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: "3.2"
          bundler-cache: true  # Runs bundle install automatically

      - name: Run Fastlane
        run: cd ios && bundle exec fastlane beta
        env:
          MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
          ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
          ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
          ASC_KEY: ${{ secrets.ASC_KEY }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Bitrise: Mobile-First CI/CD Platform

Bitrise is a hosted CI/CD platform with dedicated mobile build machines, 300+ pre-built Steps, and tight integration with the mobile app stores.

bitrise.yml (Workflow Definition)

# bitrise.yml — Bitrise workflow configuration
format_version: "13"
default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git

app:
  envs:
    - NODEJS_VERSION: "20"
    - IOS_SCHEME: MyApp
    - ANDROID_MODULE: app
    - ANDROID_VARIANT: release

workflows:
  # Run on every PR — tests only
  pull-request:
    steps:
      - git-clone@8: {}
      - nvm@1:
          inputs:
            - node_version: $NODEJS_VERSION
      - npm@1:
          inputs:
            - command: ci
      - npm@1:
          inputs:
            - command: run test
      - npm@1:
          inputs:
            - command: run lint

  # iOS production build
  ios-release:
    meta:
      bitrise.io:
        machine_type_id: g2-m1.4core  # M1 Mac (4 vCPU, 12GB RAM)
        stack: osx-xcode-15
    steps:
      - git-clone@8: {}
      - nvm@1:
          inputs:
            - node_version: $NODEJS_VERSION
      - npm@1:
          inputs:
            - command: ci
      - cocoapods-install@2: {}
      - certificate-and-profile-installer@1: {}
      - xcode-archive@5:
          inputs:
            - project_path: ios/MyApp.xcworkspace
            - scheme: $IOS_SCHEME
            - distribution_method: app-store
            - export_method: app-store
            - automatic_code_signing: api-key
      - deploy-to-itunesconnect-application-loader@1:
          inputs:
            - api_key_path: $BITRISEIO_ASC_API_KEY_FILE_URL
            - api_key_id: $ASC_KEY_ID
            - api_key_issuer_id: $ASC_ISSUER_ID
      - slack@3:
          inputs:
            - webhook_url: $SLACK_WEBHOOK_URL
            - channel: "#releases"
            - message: "iOS build deployed to TestFlight ✅"

  # Android production build
  android-release:
    meta:
      bitrise.io:
        machine_type_id: elite
        stack: linux-docker-android-20
    steps:
      - git-clone@8: {}
      - nvm@1:
          inputs:
            - node_version: $NODEJS_VERSION
      - npm@1:
          inputs:
            - command: ci
      - android-build@1:
          inputs:
            - project_location: android
            - module: $ANDROID_MODULE
            - variant: $ANDROID_VARIANT
      - sign-apk@1:
          inputs:
            - android_app: $BITRISE_AAB_PATH
            - keystore_url: $BITRISEIO_ANDROID_KEYSTORE_URL
            - keystore_password: $ANDROID_KEYSTORE_PASSWORD
            - key_alias: $ANDROID_KEY_ALIAS
            - key_password: $ANDROID_KEY_PASSWORD
      - google-play-deploy@3:
          inputs:
            - package_name: com.company.myapp
            - track: internal
            - service_account_json_key_path: $BITRISEIO_GOOGLE_PLAY_KEY_URL

Feature Comparison

FeatureExpo EASFastlaneBitrise
Cloud builds❌ (runs on your CI)
Mac required✅ (for iOS)
OTA updates✅ EAS Update
Cert management✅ Automatic✅ MatchManual
Store submission✅ EAS Submit✅ deliver/supply
Screenshot automation✅ snapshot
Non-Expo appsBare RN only✅ Any native✅ Any native
Pricing (free tier)30 builds/monthFree (OSS)500 min/month
M1/M2 Mac machinesVia GitHub Actions
Pre-built mobile steps300+
Expo-specific✅ Best

When to Use Each

Choose Expo EAS if:

  • Your app is built with Expo (managed or bare workflow)
  • No Mac available — EAS builds iOS in the cloud without one
  • OTA JavaScript updates are important for your release process
  • You want the simplest possible setup: eas build, eas update, eas submit

Choose Fastlane if:

  • Full native project (not Expo) with custom build configurations
  • Screenshot automation for App Store listings is needed (fastlane snapshot)
  • You want automation scripts that run locally AND on any CI (GitHub Actions, CircleCI)
  • Complete ownership of the build process — no third-party CI dependency

Choose Bitrise if:

  • Dedicated mobile CI/CD with M1/M2 Mac machines without managing GitHub Actions MacOS runners
  • 300+ pre-built Steps reduce configuration time for common mobile workflows
  • Team needs a mobile-specific CI dashboard with build insights and test reports
  • Complex multi-step workflows (build → test → screenshot → upload → notify)

Methodology

Data sourced from official Expo EAS documentation (docs.expo.dev/eas), Fastlane documentation (fastlane.tools), Bitrise documentation (devcenter.bitrise.io), GitHub star counts as of February 2026, npm download statistics, pricing pages as of February 2026, and community discussions from the Expo Discord, r/reactnative, and the Bitrise community forums.


Related: RevenueCat vs Adapty vs Superwall for the in-app purchase infrastructure that ships inside these builds, or React Native MMKV vs AsyncStorage vs Expo SecureStore for local storage in the apps you're shipping.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.