Detox CI Integration: Running React Native E2E Tests in GitHub Actions and GitLab

Detox CI Integration: Running React Native E2E Tests in GitHub Actions and GitLab

Running Detox tests in CI requires matching simulator/emulator availability to your runner OS, caching dependencies, and handling the slow parts (app builds, simulator boot) efficiently. This guide covers complete CI configurations for GitHub Actions and GitLab CI for both iOS and Android.

Key Takeaways

  • iOS requires macOS runners — this is non-negotiable; GitHub's macOS runners cost ~10x Linux.
  • Build the app once, test multiple times — cache the .app and .apk artifacts across test runs to avoid rebuild costs.
  • Android emulators on Linux need KVM or nested virtualization — configure the emulator action correctly or tests will hang on boot.
  • Detox artifacts capture screenshots and video — configure artifactsLocation and upload with actions/upload-artifact for debugging failures.
  • Parallel execution with multiple simulators requires splitting test files and launching independent simulator instances.

Getting Detox running locally is straightforward. Getting it running reliably in CI requires understanding a handful of platform constraints, caching strategies, and failure modes that aren't obvious until you hit them.

iOS CI Requirements

iOS simulators only run on macOS. No Linux equivalent exists. This means:

  • GitHub Actions: use macos-latest or macos-14 runners (Apple Silicon)
  • GitLab CI: use a macOS runner (self-hosted or a paid cloud provider)
  • Self-hosted Mac minis: popular for teams running many iOS builds

macOS runner costs on GitHub Actions are approximately 10x Linux runner costs. Optimizing your Detox iOS pipeline — caching builds, running only on PR or merge, parallelizing — has real cost impact.

Android CI Requirements

Android emulators run on Linux but require hardware acceleration:

  • GitHub Actions: the reactivecircus/android-emulator-runner action handles KVM setup
  • Self-hosted Linux: enable KVM (sudo kvm-ok to verify)
  • Cloud CI (CircleCI, GitLab, etc.): use machine executors (not Docker containers) — containers don't support KVM

GitHub Actions: iOS

# .github/workflows/detox-ios.yml
name: Detox iOS E2E

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

jobs:
  e2e-ios:
    runs-on: macos-14
    timeout-minutes: 60

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install applesimutils
        run: |
          brew tap wix/brew
          brew install applesimutils

      - name: Cache Detox build
        uses: actions/cache@v4
        id: detox-build-cache
        with:
          path: ios/build
          key: detox-ios-${{ hashFiles('ios/**', 'package-lock.json') }}
          restore-keys: |
            detox-ios-

      - name: Install CocoaPods
        if: steps.detox-build-cache.outputs.cache-hit != 'true'
        run: |
          cd ios && pod install --repo-update

      - name: Build iOS app for Detox
        if: steps.detox-build-cache.outputs.cache-hit != 'true'
        run: npx detox build --configuration ios.sim.debug

      - name: Run Detox tests
        run: npx detox test --configuration ios.sim.debug --headless --record-logs all --take-screenshots failing

      - name: Upload test artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: detox-ios-artifacts
          path: artifacts/
          retention-days: 7

Key decisions in this config:

Cache the build directory (ios/build). The xcodebuild step takes 10–20 minutes. Cache it keyed on iOS and package changes — only rebuild when something relevant changes.

--headless flag runs tests without opening a simulator window. Required in CI environments without a display.

--record-logs all --take-screenshots failing captures diagnostic information. You want screenshots of failed states and device logs when debugging CI failures that don't reproduce locally.

Always upload artifacts (if: always()) — you need the screenshots and logs even (especially) when tests fail.

GitHub Actions: Android

# .github/workflows/detox-android.yml
name: Detox Android E2E

on:
  pull_request:
    branches: [main]

jobs:
  e2e-android:
    runs-on: ubuntu-latest
    timeout-minutes: 60

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Setup Java
        uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'

      - name: Setup Android SDK
        uses: android-actions/setup-android@v3

      - name: Cache Gradle
        uses: actions/cache@v4
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: gradle-${{ hashFiles('android/**/*.gradle*', 'android/gradle-wrapper.properties') }}

      - name: Cache Detox APK
        uses: actions/cache@v4
        id: apk-cache
        with:
          path: android/app/build/outputs/apk
          key: detox-android-${{ hashFiles('android/**', 'package-lock.json') }}

      - name: Build Android APK
        if: steps.apk-cache.outputs.cache-hit != 'true'
        run: npx detox build --configuration android.emu.debug

      - name: Run Android Emulator + Detox tests
        uses: reactivecircus/android-emulator-runner@v2
        with:
          api-level: 31
          arch: x86_64
          profile: Pixel_4
          avd-name: Pixel_4_API_31
          script: npx detox test --configuration android.emu.debug --headless --record-logs all

      - name: Upload test artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: detox-android-artifacts
          path: artifacts/
          retention-days: 7

The reactivecircus/android-emulator-runner action handles:

  • AVD creation
  • KVM setup
  • Emulator boot (waits for fully booted state)
  • Cleanup after tests

Without it, you'd need to manage all of this manually — the action is worth using.

GitLab CI: iOS

# .gitlab-ci.yml
detox-ios:
  stage: test
  tags:
    - macos
    - xcode
  only:
    - merge_requests
    - main
  timeout: 60 minutes
  cache:
    key:
      files:
        - ios/Podfile.lock
        - package-lock.json
    paths:
      - ios/build/
      - node_modules/
  before_script:
    - npm ci
    - brew tap wix/brew && brew install applesimutils
    - cd ios && pod install && cd ..
  script:
    - npx detox build --configuration ios.sim.debug
    - npx detox test --configuration ios.sim.debug --headless --record-logs all --take-screenshots failing
  artifacts:
    when: always
    paths:
      - artifacts/
    expire_in: 7 days

GitLab's tags key selects which runner executes the job. You need a macOS runner registered with those tags for iOS jobs.

Configuring Detox Artifacts

Tell Detox where to store artifacts in your detox.config.js:

module.exports = {
  // ... apps, devices, configurations ...

  artifacts: {
    rootDir: 'artifacts',
    pathBuilder: './e2e/artifactsPathBuilder.js', // optional custom naming
    plugins: {
      instruments: { enabled: false },
      log: {
        enabled: true,
        keepOnlyFailedTestsArtifacts: false,
      },
      uiHierarchy: 'enabled',
      screenshot: {
        shouldTakeAutomaticSnapshots: true,
        keepOnlyFailedTestsArtifacts: true,
        takeWhen: {
          testDone: false,
          testFailed: true,
        },
      },
      video: {
        enabled: false, // enable for debugging flaky tests
      },
    },
  },
};

For most CI runs, screenshots on failure + logs is the right balance. Video is extremely helpful for debugging flaky tests but generates large files.

Parallel Test Execution

Running tests in parallel requires:

  1. Multiple simulators or emulators
  2. Test file sharding
  3. Separate Detox workers
# GitHub Actions matrix for parallel iOS execution
jobs:
  e2e-ios:
    runs-on: macos-14
    strategy:
      matrix:
        shard: [1, 2, 3]
    steps:
      # ... setup steps ...
      - name: Run Detox tests (shard ${{ matrix.shard }} of 3)
        run: |
          npx detox test \
            --configuration ios.sim.debug \
            --headless \
            --shard-index ${{ matrix.shard }} \
            --shard-count 3

Detox's built-in sharding splits test files evenly across shards. With 3 shards and 30 test files, each shard runs 10 files. The total wall-clock time drops to ~1/3 of the serial execution time.

Important: each shard needs its own simulator. With matrix execution, each job gets a fresh runner with its own simulator. Sharding on a single machine with multiple simultaneous simulators requires explicit simulator boot and Detox worker configuration.

Handling Simulator Boot Time

Simulators take 30–60 seconds to boot. On CI, you're paying for this every run. Strategies to minimize it:

Keep simulators booted (self-hosted runners only): boot the simulator as part of the runner startup script, not as part of the job. Jobs then find a warm simulator.

Use --reuse flag: detox test --reuse tells Detox to reuse an already-running app instead of reinstalling. Only valid when you know the binary hasn't changed.

Cache simulator state: GitHub Actions doesn't support simulator state caching, but self-hosted runners can use a pre-configured simulator image.

Environment Variables in Tests

Pass environment variables to the app under test using launch arguments:

// detox.config.js
configurations: {
  'ios.sim.ci': {
    device: 'simulator',
    app: 'ios.debug',
    behavior: {
      init: {
        exposeGlobals: true,
      },
    },
  },
},
// In tests
await device.launchApp({
  launchArgs: {
    API_URL: process.env.API_URL || 'https://staging.api.example.com',
    MOCK_AUTH: 'true',
  },
});

For CI, set environment variables in the workflow and they'll flow through to the test process.

Failing Fast and Retry Logic

Detox has a built-in retry mechanism for flaky tests:

npx detox test --configuration ios.sim.debug --retries 2

With --retries 2, failed tests are retried up to 2 times before marking as definitively failed. This handles intermittent infrastructure flakiness (slow emulator, network blip) without masking real failures.

Don't set retries higher than 2. If a test fails consistently on retry, it's either genuinely failing or fundamentally flaky — fix it instead of retrying.

Pull Request vs. Full Suite Strategy

Running the full Detox suite on every commit is expensive. A tiered approach:

Trigger Tests to run
PR opened/updated Smoke tests (critical path only, ~10 tests)
Merge to main Full E2E suite
Scheduled (nightly) Full suite + real device tests

Implement this with path filtering and branch conditions in your CI config:

on:
  pull_request:
    paths:
      - 'src/**'
      - 'e2e/**'
      - 'ios/**'
      - 'android/**'

This skips the E2E workflow entirely for documentation or config-only changes.

Debugging CI-Only Failures

Tests that pass locally but fail in CI are usually caused by:

  1. Different simulator version — match your CI simulator exactly to local development
  2. Missing applesimutils — install on the CI runner
  3. Build cache stale — clear the cache and rebuild
  4. Animations enabled — CI simulators may have animations enabled; disable them with UIAnimationDragCoefficient=0 in simulator settings or via the Detox init configuration
  5. Network conditions — your staging API may be unavailable or slow in CI; use mock adapters for network-dependent tests

The artifacts (screenshots + logs) from failed CI runs should tell you what was on screen at the time of failure. If you're flying blind without them, the first step is configuring artifact upload.

Read more

Start now free