This guide shows you how to build a fully automated CI/CD pipeline for a React Native app using GitHub Actions and Expo Application Services (EAS). Follow all eight steps and you'll have automated testing, builds, and over-the-air updates shipping to the App Store and Google Play — expect about 90 minutes to complete the initial setup.
What You'll Build
- A GitHub Actions workflow that runs lint and Jest tests on every pull request
- An EAS Build configuration that compiles iOS and Android binaries automatically on merge
- An over-the-air (OTA) update channel via EAS Update that pushes JS bundle changes without a store review
- Environment-specific secrets management so staging and production never share credentials
- A Slack notification step that posts build status to your team channel in real time
Prerequisites
- Node.js 20+ and pnpm 9 (or npm 10) installed locally
- An existing React Native project using Expo SDK 51 or later (bare or managed workflow)
- An Expo account — free tier is sufficient to start
- Apple Developer Program membership and a Google Play Console account for store distribution
- A GitHub repository with Actions enabled
- Basic familiarity with YAML syntax
Step 1: Install and Configure the EAS CLI
EAS CLI is Expo's official build and submit tool. It handles code signing, credentials storage, and cloud builds — eliminating the need to maintain Mac or Linux build machines locally.
npm install -g eas-cli
eas login
eas build:configure
The eas build:configure command generates an eas.json file in your project root. Open it and define three build profiles: development, preview, and production.
{
"cli": {
"version": ">= 10.0.0"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"channel": "preview"
},
"production": {
"channel": "production",
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}
Common pitfall: Skipping eas build:configure and writing eas.json manually often causes schema validation errors. Always run the command first, then edit the output.
Step 2: Store Credentials as GitHub Secrets
Never hardcode API keys or signing credentials in your repository. GitHub Actions reads secrets from your repository's Settings → Secrets and variables → Actions panel at runtime.
Add the following secrets:
EXPO_TOKEN— generate this from your Expo account dashboard under Access TokensSLACK_WEBHOOK_URL— your Slack Incoming Webhook URL (created in your Slack app settings)
Pro tip: Use GitHub Environments (not just plain secrets) to gate production deployments behind a required reviewer. This is a best practice aligned with supply chain security recommendations from the SLSA framework, widely adopted as of 2026.
Step 3: Write the Pull Request CI Workflow
Create .github/workflows/ci.yml in your repository. This workflow runs on every pull request targeting main or develop.
name: CI
on:
pull_request:
branches: [main, develop]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint
run: pnpm lint
- name: Run Jest tests
run: pnpm test --ci --coverage
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
Why does --frozen-lockfile matter? It prevents pnpm from silently updating your lockfile during CI, ensuring the exact dependency tree you tested locally is what runs in the pipeline. This has caught broken transitive dependency upgrades in many production React Native apps.
Step 4: Write the EAS Build and Deploy Workflow
Create .github/workflows/deploy.yml. This workflow triggers when code merges to main.
name: Deploy
on:
push:
branches: [main]
jobs:
build-and-submit:
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Set up Expo
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Build iOS and Android
run: eas build --platform all --profile production --non-interactive
- name: Submit to App Store and Play Store
run: eas submit --platform all --latest --non-interactive
Common pitfall: The --non-interactive flag is essential. Without it, EAS CLI prompts for user input and the workflow hangs indefinitely until it times out.
What if the EAS build fails with a credentials error?
Run eas credentials locally to verify your signing certificates and provisioning profiles are stored in Expo's credentials service. EAS manages credentials remotely by default, but the initial setup must happen on a machine that can authenticate with your Apple and Google accounts.
Step 5: Add EAS Update for Over-the-Air Deployments
OTA updates let you ship JavaScript and asset changes to users without waiting for an App Store review — typically bypassing a 24–72 hour review cycle. EAS Update supports this natively with Expo SDK 51+.
Add an update step to deploy.yml between the dependency install and the EAS build step:
- name: Publish OTA update
run: eas update --branch production --message "Release from ${{ github.sha }}"
Configure your app.json to enable updates:
{
"expo": {
"updates": {
"url": "https://u.expo.dev/YOUR_PROJECT_ID",
"enabled": true,
"fallbackToCacheTimeout": 0,
"checkAutomatically": "ON_LOAD"
},
"runtimeVersion": {
"policy": "sdkVersion"
}
}
}
Pro tip: Set fallbackToCacheTimeout: 0 in production. It forces the app to use a cached bundle immediately rather than waiting for a network fetch on launch — this measurably improves perceived startup time on slow connections common in regional markets across Australia and Southeast Asia.
Step 6: Separate Preview Builds for Staging
Your QA team and stakeholders should never test against production credentials. Add a second deploy job that triggers on pushes to develop and uses the preview EAS profile.
build-preview:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Build preview binary
run: eas build --platform all --profile preview --non-interactive
- name: Publish preview OTA update
run: eas update --branch preview --message "Preview from ${{ github.sha }}"
Distribute preview builds to testers using EAS's internal distribution — share the install link directly rather than going through TestFlight or the Play Console internal track.
Step 7: Add Slack Notifications for Build Status
Real-time notifications reduce the feedback loop from hours to minutes. Add this as a final step in both workflow files.
- name: Notify Slack
if: always()
uses: slackapi/[email protected]
with:
payload: |
{
"text": "Build ${{ job.status }} for ${{ github.repository }} on branch ${{ github.ref_name }} — commit ${{ github.sha }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK
The if: always() condition ensures the notification fires whether the job succeeded, failed, or was cancelled — so your team is never left wondering why a build went quiet.
Step 8: Validate the Full Pipeline End to End
With all files committed, verify the pipeline works before announcing it to your team.
- Open a new branch, make a trivial change (e.g. update a comment), and raise a pull request against
main. Theci.ymlworkflow should trigger automatically and complete in under three minutes for a typical React Native project. - Merge the pull request. The
deploy.ymlworkflow should trigger, run the EAS build (cloud build time is typically 10–20 minutes for both platforms), and post a Slack notification. - In your Expo dashboard, confirm the new build appears under Builds and the OTA update appears under Updates → production branch.
- Install the app from EAS's distribution link and verify the OTA update loads on launch.
Common pitfall: If the EAS submit step fails with a "missing App Store Connect API key" error, you need to add your App Store Connect API key to EAS credentials. Run eas credentials --platform ios locally and follow the prompts to upload the key.
Frequently Asked Questions
Does this pipeline work with Expo bare workflow projects?
Yes. EAS Build supports both managed and bare workflow projects as of Expo SDK 51. Bare workflow projects may require additional native configuration steps, but the GitHub Actions YAML shown here works without modification.
How is EAS Build different from building locally with Xcode or Android Studio?
EAS Build runs on Expo's cloud infrastructure, so you don't need a macOS machine to produce iOS binaries. It also manages code signing credentials centrally, which eliminates the "works on my machine" certificate issues that slow down most small teams.
What if the Jest tests pass locally but fail in CI?
The most common cause is a missing environment variable. Add a .env.test file to your repository (non-secret values only) or inject test-specific variables as GitHub Actions environment variables using the env: key on the test step.
Can I use this setup with Bitbucket Pipelines or GitLab CI instead of GitHub Actions?
The EAS CLI commands are identical regardless of CI provider. Replace the GitHub Actions YAML syntax with your provider's equivalent — the eas build, eas submit, and eas update commands remain unchanged.
How much does this pipeline cost to run?
GitHub Actions provides 2,000 free minutes per month on the free tier, and the Linux runners used here consume those minutes at 1× the rate. EAS Build offers a free tier with 30 build credits per month — enough for a small team shipping weekly. Production-scale teams typically pay $29–$99 USD per month for additional EAS credits as of mid-2026.
Next Steps
You now have a complete CI/CD pipeline that tests every PR, builds production binaries automatically, and pushes OTA updates without manual intervention. A few directions worth exploring next:
- Add Detox end-to-end tests to your CI workflow to catch UI regressions before they reach users
- Configure EAS Insights to track crash rates and ANR (App Not Responding) events by build version
- Set up branch protection rules in GitHub to require CI to pass before any merge — this prevents a single broken commit from blocking your entire team
If you're building a React Native app for your business and want a production-ready architecture from day one, the team at Lenka Studio works with SMBs across Australia, Singapore, Canada, and the US to deliver apps with CI/CD, design systems, and scalable backends already in place. Get in touch to talk through your project — we're happy to review your current setup and suggest what to prioritise first.




