CI/CD Setup
Automate builds, testing, and OTA updates with GitHub Actions and EAS
CI/CD Setup
Automate your build, test, and deployment pipeline with GitHub Actions and EAS. Once configured, every push to main can automatically validate your code and trigger a production build.
Quick Start
Already familiar with GitHub Actions? Here's the minimum setup:
# 1. Generate an Expo access token
# Go to https://expo.dev/settings/access-tokens → Create token
# 2. Add it to your GitHub repo secrets
# Repo → Settings → Secrets → Actions → New secret → EXPO_TOKEN
# 3. Copy the workflow file into your repo
mkdir -p .github/workflows
# Add the workflow YAML below to .github/workflows/eas-build.ymlThat's it. Once the workflow file and secret are in place, GitHub Actions will automatically validate PRs and build on push to main.
Full Guide
Part 1: GitHub Actions Setup
Step 1: Generate an Expo Access Token
EAS needs an access token to build your app from CI.
- Go to expo.dev/settings/access-tokens
- Click "Create token"
- Name it (e.g.,
github-actions) - Copy the token, you won't see it again
Step 2: Add the Token to GitHub Secrets
- Go to your GitHub repository
- Navigate to Settings → Secrets and variables → Actions
- Click "New repository secret"
- Name:
EXPO_TOKEN - Value: Paste the token from Step 1
- Click "Add secret"
Never hardcode your Expo token in the workflow file or anywhere in your repo. Always use GitHub Secrets.
Step 3: Add the Workflow File
Create the file .github/workflows/eas-build.yml in your repository:
name: EAS Build & Validate
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
inputs:
platform:
description: "Platform to build"
required: true
default: "all"
type: choice
options:
- all
- ios
- android
profile:
description: "Build profile"
required: true
default: "production"
type: choice
options:
- development
- preview
- production
jobs:
validate:
name: Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Type check
run: yarn tsc --noEmit
- name: Lint
run: yarn lint
- name: Run tests
run: yarn test --ci --coverage
build:
name: Build
needs: validate
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
- name: Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build
run: |
eas build \
--platform ${{ github.event.inputs.platform || 'all' }} \
--profile ${{ github.event.inputs.profile || 'production' }} \
--non-interactiveHow the Workflow Triggers
| Event | What Happens |
|---|---|
Pull Request to main | Runs validate only (type check, lint, tests). No build. |
Push to main | Runs validate, then triggers a production build on EAS. |
| Manual dispatch | You choose platform and profile from the GitHub Actions UI. |
Part 2: Understanding the Pipeline
Validate Job
The validate job runs on every PR and push. It catches issues before they reach production:
| Check | Command | What It Catches |
|---|---|---|
| Type check | yarn tsc --noEmit | TypeScript errors, missing types |
| Lint | yarn lint | Code style issues, potential bugs |
| Tests | yarn test --ci | Broken functionality, regressions |
If any check fails, the workflow stops and the build is skipped.
Build Job
The build job only runs on push to main or manual dispatch (not on PRs). It:
- Sets up Node.js and installs dependencies
- Authenticates with EAS using your
EXPO_TOKEN - Triggers a cloud build on Expo's servers
- Builds both iOS and Android (or just one, if manually dispatched)
The actual compilation happens on EAS servers, your GitHub Actions runner just triggers it.
Build times: EAS builds typically take 10–20 minutes per platform. The GitHub Actions job itself finishes in a few minutes, it just triggers the build and exits.
Part 3: Auto-Submit to Stores
You can extend the workflow to automatically submit builds to the App Store and Google Play after a successful build.
Add Submit Step
Add this after the build step in your workflow:
- name: Submit to stores
if: github.event.inputs.profile == 'production' || github.event_name == 'push'
run: |
eas submit \
--platform ${{ github.event.inputs.platform || 'all' }} \
--profile production \
--non-interactive \
--latestRequired Secrets for Auto-Submit
For this to work, you need additional secrets:
| Secret | Where to Get It | Used For |
|---|---|---|
EXPO_TOKEN | expo.dev/settings/access-tokens | EAS authentication |
EXPO_APPLE_APP_SPECIFIC_PASSWORD | appleid.apple.com → App-Specific Passwords | iOS App Store upload |
Add EXPO_APPLE_APP_SPECIFIC_PASSWORD to your GitHub Secrets the same way you added EXPO_TOKEN.
For Google Play auto-submit, your eas.json must reference a service account key. In CI, store the key content as an EAS Secret rather than a file. See the Google Play guide for details.
Part 4: Over-the-Air (OTA) Updates
Sometimes you need to fix a bug instantly without waiting for App Store review. EAS Update lets you push JavaScript and asset changes directly to users.
What Can Be Updated OTA
| Can Update OTA | Cannot Update OTA |
|---|---|
| JavaScript code (bug fixes, UI changes) | Native modules (new permissions, new SDK) |
| Images and assets | app.json configuration changes |
| Styling and layout | New native dependencies |
Configuration
Ensure your app.json has updates configured:
{
"expo": {
"updates": {
"url": "https://u.expo.dev/[your-project-id]"
},
"runtimeVersion": {
"policy": "appVersion"
}
}
}Runtime version ensures users only receive updates compatible with their native build. The appVersion policy ties the runtime version to your version field in app.json.
Push an OTA Update
To push a hotfix to all users on the production channel:
eas update --branch production --message "Fix login crash on Android"This update goes live immediately, no store review required.
Automate Hotfix Deploys
Add a hotfix trigger to your workflow that runs eas update when you push to a hotfix/* branch:
name: EAS Update (Hotfix)
on:
push:
branches:
- "hotfix/**"
jobs:
update:
name: Push OTA Update
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
- name: Setup EAS
uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Push update
run: |
eas update \
--branch production \
--message "${{ github.event.head_commit.message }}" \
--non-interactiveUsage: Create a branch like hotfix/fix-login-crash, push your fix, and the OTA update goes live automatically.
Part 5: Branch Strategy
Here's a recommended branching strategy that works well with this CI/CD setup:
| Branch | Trigger | Action |
|---|---|---|
main | Push | Validate + Production build |
develop | PR to main | Validate only (type check, lint, tests) |
hotfix/* | Push | OTA update to production |
| Manual dispatch | GitHub Actions UI | Build any profile for any platform |
Typical Release Flow
- Develop features on feature branches
- Open PR to
main→ CI validates automatically - Merge to
main→ CI builds and optionally submits - Critical bug found → Push to
hotfix/fix-name→ OTA update goes live
Common Issues
| Issue | Solution |
|---|---|
EXPO_TOKEN not found | Verify the secret name matches exactly in GitHub Settings → Secrets → Actions. |
| Build triggers but fails on EAS | Check the build logs at expo.dev. Common cause: dependency mismatch or missing native config. |
| OTA update not showing | Verify runtimeVersion matches between your build and update. Run eas update:list to check deployed updates. |
| Validate passes locally but fails in CI | CI uses --frozen-lockfile, make sure your yarn.lock is committed and up to date. |
| Build takes too long | EAS free tier has queue times. Upgrade to a paid plan for priority builds, or use --platform to build one platform at a time. |
| Auto-submit fails for iOS | Check that EXPO_APPLE_APP_SPECIFIC_PASSWORD is set and your eas.json has valid appleId, ascAppId, and appleTeamId. |
Next Steps
- App Store Deployment, Set up your Apple Developer Account and App Store Connect
- Google Play Deployment, Set up your Play Console and testing tracks