React Native (Expo) CI with GitHub Actions and EAS: automatic builds & internal distribution for testers — step by step

Apps mobile · By · Published on · 9 min read

React Native (Expo) CI with GitHub Actions and EAS: automatic builds & internal distribution for testers — step by step
Photo: Rafael Minguet Delgado / Pexels

CI pipeline for React Native (Expo) with GitHub Actions and EAS: automate builds and internal distribution for testers. This tutorial shows how to set up eas.json profiles, GitHub secrets, and a YAML workflow that kicks off preview builds on PRs and, on main merges, submits to Play (internal track) and TestFlight. At the end we validate the flow and cover common errors.

What we'll build: pipeline overview

The goal is to trigger automated EAS (Expo Application Services) builds for every PR and for pushes to the main branch. For PRs we produce internal preview builds for quick validation. On merges to main we not only build but also automatically submit artifacts to internal channels: Google Play internal track and TestFlight for iOS.

EAS performs the remote build while GitHub Actions orchestrates the process. This keeps runners lightweight, focusing on tests, linting and versioning. The outcome: direct links or delivery to testers, reducing friction between code and validation.

Summary flow:

  • Open PR: triggers an internal preview build (Android and iOS) on EAS for quick testing.
  • Merge to main: triggers an internal build and submits to Google Play (internal track) and TestFlight.
  • Testers receive the app through official channels or via an internal EAS link (when applicable).

Prerequisites and project organization

Before starting, make sure the project and accounts are ready. You will need valid store and EAS access. Do not store personal credentials in the repo or unsecured places.

  • A GitHub repository with your Expo app (SDK compatible with EAS).
  • An Expo account with EAS enabled and the project linked.
  • Identifiers set: android.package and ios.bundleIdentifier in app.json/app.config.js.
  • Access to Google Play Console (API integration permissions).
  • Access to App Store Connect (active API key for submissions).
  • GitHub Actions enabled on the repository.

Define build profiles and channels in eas.json

We’ll use three profiles: development (for dev client), preview (internal distribution for QA) and production (store releases). Preview is the focus for testers. Also configure channels to organize updates/releases.

On Android you’ll typically use AAB for store submission and optionally APK for direct installs among technical testers. For iOS, TestFlight is the standard internal distribution channel.

{
  "cli": { "version": ">= 3.13.0" },
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "channel": "development",
      "ios": { "simulator": false },
      "android": { "buildType": "apk" }
    },
    "preview": {
      "distribution": "internal",
      "channel": "preview"
    },
    "production": {
      "channel": "production"
    }
  },
  "submit": {
    "internal": {
      "android": { "track": "internal" },
      "ios": { "ascAppId": "SEU_APP_ID_ASC" }
    },
    "production": {
      "android": { "track": "production" }
    }
  }
}

GitHub secrets and store permissions

Create GitHub secrets for the workflow. Never commit private keys, service account JSONs or tokens. Use GitHub Secrets and restrict access (protected environments).

  • EXPO_TOKEN: Expo/EAS access token for non-interactive builds.
  • GOOGLE_SERVICE_ACCOUNT_KEY: Google Play service account JSON (permissioned to upload to the internal track).
  • ASC_ISSUER_ID, ASC_KEY_ID, ASC_PRIVATE_KEY: App Store Connect API credentials for TestFlight submissions.
  • App build variables (if any): keep them as secrets and inject them at build time.

GitHub Actions workflow: build and internal distribution

The YAML below orchestrates the flow: installs dependencies, runs tests, authenticates with EAS and triggers preview builds for Android and iOS. On pushes to main it submits to Play (internal track) and TestFlight.

If you want to understand triggers and Actions best practices, see this guide on automatic pipelines: How to set up an automatic deploy pipeline. Note: the job runs on GitHub-hosted runners while the build itself runs remotely on EAS.

name: ci-expo-eas

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

jobs:
  build-and-distribute:
    runs-on: ubuntu-latest
    env:
      EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}
      EXPO_APPLE_APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
      EXPO_APPLE_APP_STORE_CONNECT_KEY_ID: ${{ secrets.ASC_KEY_ID }}
      EXPO_APPLE_APP_STORE_CONNECT_PRIVATE_KEY: ${{ secrets.ASC_PRIVATE_KEY }}
      GOOGLE_SERVICE_ACCOUNT_KEY: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_KEY }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Install deps
        run: yarn install --frozen-lockfile

      - name: Run tests (optional)
        run: yarn test --if-present

      - name: Install EAS CLI
        run: npm i -g eas-cli

      - name: Build preview (Android + iOS)
        run: eas build --platform all --profile preview --non-interactive

      - name: Submit to Google Play (internal track)
        if: github.ref == 'refs/heads/main'
        run: eas submit -p android --profile internal --non-interactive

      - name: Submit to TestFlight
        if: github.ref == 'refs/heads/main'
        run: eas submit -p ios --profile internal --non-interactive

      - name: List latest builds
        run: eas build:list --limit 5

Distribution to testers: options and when to use them

After builds are published, choose the internal distribution method that fits your team. For Android, the Play internal track scales well for QA. For iOS, TestFlight is the standard for internal/external testing. For technical teams, an internal APK (preview) can be handy for quick validation.

For Android-only CI and signing details, see: Android CI with GitHub Actions: secure keystore. For formal store publications, see: How to publish an Android app to Google Play and How to publish an iOS app on the App Store.

Platform Profile Purpose Artifact Distribution
Android preview Quick internal QA AAB (default) or APK (optional) EAS link or Google Play (internal track)
iOS preview Internal QA IPA TestFlight (internal or external groups)
Android/iOS development Dev client for in-progress features APK/IPA dev client Internal install via EAS link/QR
Android/iOS production Public release AAB/IPA Stores (stable tracks)

How to confirm it worked

After the workflow runs, validate that builds were created and submissions progressed. Do not share build links outside the authorized tester group. If you see permission errors, review credentials and store roles.

  • Check the EAS dashboard to confirm jobs finished successfully.
  • Verify in the Google Play Console that the artifact landed in the internal track.
  • In App Store Connect, check build processing and TestFlight availability.
  • Test installation on a real device (one Android and one iOS).
# List recent builds and their states
npx eas-cli build:list --limit 10 --json

# Submission statuses
npx eas-cli submit:status --platform android
npx eas-cli submit:status --platform ios

Common troubleshooting

Frequent failures in this flow are usually related to authentication, app identifiers and store permissions. Below are common symptoms and practical fixes.

  • Authentication failed (You are not authenticated): check EXPO_TOKEN and ensure the token has access to the project.
  • Android rejected by Play (permission denied): confirm the service account can manage artifacts for the correct app.
  • Missing bundle/package: set android.package and ios.bundleIdentifier in app.json/app.config.js.
  • iOS: invalid App Store Connect key: verify issuer ID, key ID and the private key content.
  • Internal iOS build won’t install outside TestFlight: signature restrictions; use TestFlight for testers.
  • Native changes not reflected: clean generated native folders and rebuild after critical native config changes.

Security and CI/CD best practices for mobile

Protect credentials and reduce the attack surface. For sensitive projects, use protected environments, require dual review for secrets, and avoid leaking sensitive data in logs. See this dedicated checklist: CI/CD security checklist.

If job volume grows, consider autoscaling self-hosted runners for heavy test stages (not for EAS builds themselves): Auto-scale self-hosted runners.

  • Store secrets only in GitHub Environments with approvals.
  • Rotate keys periodically and disable unused credentials.
  • Avoid printing sensitive variables in logs.
  • Restrict who can trigger workflows on the main branch.

Frequently asked questions

Does EAS build on my GitHub Actions runner?

No. GitHub Actions only orchestrates the process. The actual build runs on EAS servers. The runner only needs Node, the EAS CLI and credentials to start remote jobs and, when applicable, submit to stores.

Can I distribute Android builds without Google Play?

Yes — by generating an APK in an internal (preview) profile and sharing the EAS build link. For a more scalable QA flow with group control, prefer the Play Console internal track.

How should I version builds between internal and production releases?

Keep version, android.versionCode and ios.buildNumber consistent in the app config. Automate increments with CI scripts to ensure internal builds don’t conflict with production.

Can I run E2E tests (Detox) in this pipeline?

Yes, but run them before triggering the EAS Build in separate jobs, using emulators/simulators on the runners. Only start the EAS build after the test suite passes.

Which app environment variables should I expose in CI?

Expose only the minimum required. Use GitHub secrets and secure variables in EAS when applicable. Never commit .env files. Read environment variables at build time.

Do I need paid store accounts for TestFlight and internal track?

You need valid accounts and appropriate permissions in App Store Connect and Google Play Console. Policies and requirements may change, so verify each store’s dashboard before configuring the pipeline.

Want to adapt this pipeline to your app?

If you need help adjusting profiles, versioning or pipeline security, get in touch to discuss your scenario.

Talk about mobile CI/CD

See also