Skip to content
compiler.dev

React Native GitHub Actions Workflow Template

Last updated

This is a working .github/workflows/ci.yml for a React Native app. A fast JavaScript job runs lint, type check and Jest on every push. A second job builds the Android app with Gradle caching. iOS is covered in the variants because it needs macOS.

The workflow

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  js:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4

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

      - run: npm ci
      - run: npm run lint
      - run: npx tsc --noEmit
      - run: npm test -- --ci --maxWorkers=2

  android:
    needs: js
    runs-on: ubuntu-latest
    timeout-minutes: 40
    steps:
      - uses: actions/checkout@v4

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

      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: 17

      - uses: gradle/actions/setup-gradle@v4
        with:
          cache-read-only: ${{ github.ref != 'refs/heads/main' }}

      - run: npm ci
      - run: ./gradlew assembleDebug --no-daemon
        working-directory: android

What the non-obvious parts do

  • Two jobs on purpose. The JavaScript checks finish in minutes and catch most mistakes. The Android build is slower, so it waits for them with needs: js.
  • cache: npm caches npm downloads keyed on package-lock.json. See caching npm. If you use Yarn or pnpm, change the cache option and install command; see Yarn and pnpm.
  • --ci --maxWorkers=2 makes Jest fail on obsolete snapshots instead of writing them, and limits workers so memory use stays predictable. Raise maxWorkers on bigger machines.
  • npm ci before Gradle. The Android build reads native modules from node_modules, so install JavaScript dependencies first.
  • working-directory: android runs the wrapper where the gradlew file lives.
  • setup-gradle caches Gradle dependencies and the wrapper. Only main saves the cache, to avoid many near-duplicate entries. Details in Gradle caching.
  • Debug build. assembleDebug needs no signing keys, so it is safe on pull requests.

Variants

iOS on macOS

  ios:
    needs: js
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: bundle install && bundle exec pod install
        working-directory: ios
      - run: xcodebuild -workspace App.xcworkspace -scheme App -configuration Debug -sdk iphonesimulator build CODE_SIGNING_ALLOWED=NO
        working-directory: ios

Replace App with your workspace and scheme. macOS minutes cost several times Linux minutes, so run this on main or on labelled pull requests. See macOS runners for iOS CI and speed up Xcode builds.

Expo and EAS

For Expo projects, run npx expo-doctor and Jest here, and let EAS Build handle native builds.

End-to-end tests

Detox or Maestro need emulators and are slow. Run them nightly or shard them; see test sharding.

Release builds

Decode signing keys from secrets at build time and upload the APK or AAB as an artifact.

Run it on faster machines

Android Gradle builds use every core and are the longest step. Change one line on each Linux job:

    runs-on: [self-hosted, Linux, compiler-ci]

The job runs on a fresh 8 vCPU machine from compiler.dev, with fallback to your original runner. macOS is in early access. See speed up React Native on GitHub Actions and pricing.

FAQ

Do I need Watchman?

No, not for Jest in CI. Use --watchman=false if Jest warns about it.

Why is my Android build so slow the first time?

Gradle downloads dependencies and compiles native code. Caches fix later runs.

Can I build iOS on Linux?

No. iOS builds need macOS and Xcode.

Should I run Metro bundling in CI?

Only for release builds. For checks, lint, types and Jest are enough.