React Native (Expo) CI with GitHub Actions and EAS: automatic builds & internal distribution for testers — step by step
Apps mobile · By Kevin Marcondes · Published on · 9 min read
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/CDSee also
- Tutorial: set up mTLS between Traefik and Docker Compose services using smallstep CA and automatic certificates — step by step
- Tutorial: integrate Sentry into your GitHub Actions deploy pipeline — automatic source map upload & issue creation (PHP & JavaScript)
- Configure retention and compression for Docker container logs with journald + logrotate + rsyslog on a low-disk VPS
- Tutorial: Configure GitHub Actions for integration tests with PostgreSQL in ephemeral containers and automatic migration rollback — a practical guide