WritingSetting Up CI/CD for React Native Apps with EAS Build and GitHub Actions — Clixo
6 min readreact-native, ci-cd, eas-build, github-actions, devops

Setting Up CI/CD for React Native Apps with EAS Build and GitHub Actions

How to configure a production-grade CI/CD pipeline for React Native using EAS Build and GitHub Actions — automated builds, testing, and store deployment.

Manually building and submitting React Native apps is a process that breaks under team pressure. One developer owns the signing certificates. Builds only run on their machine. A new hire cannot push a fix to production. This is not a deployment pipeline — it is a bottleneck with a name on it.

A proper CI/CD setup for React Native takes a day or two to configure correctly and saves orders of magnitude more time over the product lifecycle. Here is how to set one up using EAS Build and GitHub Actions.

Why EAS Build Over Self-Hosted Builds

The primary reason teams avoid self-hosted iOS builds is the infrastructure cost: you need a macOS machine, Xcode licenses, Apple provisioning certificates configured correctly, and build agents that stay current with OS and Xcode updates.

EAS Build moves this infrastructure to Expo's cloud. You get:

  • iOS and Android builds without a Mac for iOS
  • Automatic credential management (EAS Credentials handles certificates and provisioning profiles)
  • Build caching that speeds up subsequent builds
  • A defined build profile system (eas.json) that makes build configurations reproducible

The trade-off is per-build cost on paid plans and some loss of control over the build machine configuration. For most product teams, this trade-off is correct.

Project Prerequisites

Before setting up CI/CD:

  • Your project should be using Expo (managed or bare with EAS support)
  • EAS CLI installed: npm install -g eas-cli
  • An Expo account and the project linked: eas init
  • iOS credentials configured: eas credentials for iOS, which walks you through certificate and profile creation
  • Android keystore configured: eas credentials for Android

Store the EAS token as a GitHub Actions secret. In your Expo account dashboard, generate a token under Access Tokens. Add it to your GitHub repository's Settings → Secrets → Actions as EXPO_TOKEN.

Configuring eas.json

eas.json defines your build profiles. A standard configuration:

{
  "cli": {
    "version": ">= 7.0.0"
  },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal"
    },
    "preview": {
      "distribution": "internal",
      "ios": {
        "simulator": false
      }
    },
    "production": {
      "autoIncrement": true
    }
  },
  "submit": {
    "production": {
      "ios": {
        "appleId": "your-apple-id@example.com",
        "ascAppId": "your-app-store-connect-app-id",
        "appleTeamId": "YOUR_TEAM_ID"
      },
      "android": {
        "serviceAccountKeyPath": "./google-service-account.json",
        "track": "internal"
      }
    }
  }
}

Profile breakdown:

  • development: creates a development client build for testing with native modules. Distributed internally (not through the store).
  • preview: a production-like internal build for QA and stakeholder review. No simulator — installs on a real device.
  • production: the store release build. autoIncrement: true lets EAS increment the build number automatically on each build.

GitHub Actions Workflow

Create .github/workflows/build.yml:

name: EAS Build
 
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm test
 
  build-preview:
    needs: test
    if: github.ref == 'refs/heads/main'
    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 build --platform all --profile preview --non-interactive

Add a separate workflow for production releases triggered by a version tag:

name: Release
 
on:
  push:
    tags:
      - 'v*'
 
jobs:
  build-and-submit:
    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 build --platform all --profile production --non-interactive
      - run: eas submit --platform all --profile production --non-interactive

This workflow builds for both platforms and submits to the stores automatically on every version tag push.

Over-the-Air Updates with EAS Update

For JavaScript-only changes (no new native code), EAS Update lets you push updates to users without a store release:

- name: Push OTA update
  run: eas update --branch production --message "Fix: ${{ github.event.head_commit.message }}"

Add this step to your main branch workflow for fast iteration between full releases. Users receive the update on next app launch or background refresh.

The EAS Update channel system lets you target specific audiences: the production channel reaches all users, while a preview channel reaches only internal testers running the preview build.

Handling Secrets in CI

Your app likely needs API keys and environment-specific configuration. Do not commit these to source control. Options:

  • EAS Secrets: eas secret:create --name API_KEY --value yourvalue — available as environment variables during the EAS build
  • GitHub Actions secrets: available to your workflow scripts as ${{ secrets.MY_SECRET }}
  • app.config.js with environment variables: if you use app.config.js instead of app.json, you can read process.env during the build to inject configuration dynamically

Testing in CI

Before your builds, run:

npx jest --coverage --passWithNoTests
npx tsc --noEmit

TypeScript errors caught in CI are cheaper than TypeScript errors caught after a two-hour EAS build. Run type checking as a separate fast job that gates the build job.

For E2E testing on a CI build, Detox can run against your preview build on a remote iOS simulator or Android emulator via services like AWS Device Farm or BrowserStack App Automate. This is worth adding once your test suite is stable enough to not produce constant false failures.

The Pipeline in Summary

  • Every PR: run tests and type check
  • Every merge to main: build preview for iOS and Android, push OTA update to preview channel
  • Every version tag: build production for iOS and Android, submit to stores, push OTA update to production channel

This pipeline covers the full delivery workflow without requiring any engineer to own a local build machine or manage certificates manually.

If your team is setting up infrastructure for a React Native product and needs engineering support on the CI/CD and build system, Clixo handles the full delivery pipeline as part of our product builds.