Skip to main content
Mobile Launcher
Deployment

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.yml

That'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.

  1. Go to expo.dev/settings/access-tokens
  2. Click "Create token"
  3. Name it (e.g., github-actions)
  4. Copy the token, you won't see it again

Step 2: Add the Token to GitHub Secrets

  1. Go to your GitHub repository
  2. Navigate to SettingsSecrets and variablesActions
  3. Click "New repository secret"
  4. Name: EXPO_TOKEN
  5. Value: Paste the token from Step 1
  6. 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:

Yaml
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-interactive

How the Workflow Triggers

EventWhat Happens
Pull Request to mainRuns validate only (type check, lint, tests). No build.
Push to mainRuns validate, then triggers a production build on EAS.
Manual dispatchYou 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:

CheckCommandWhat It Catches
Type checkyarn tsc --noEmitTypeScript errors, missing types
Lintyarn lintCode style issues, potential bugs
Testsyarn test --ciBroken 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:

  1. Sets up Node.js and installs dependencies
  2. Authenticates with EAS using your EXPO_TOKEN
  3. Triggers a cloud build on Expo's servers
  4. 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:

Yaml
      - 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 \
            --latest

Required Secrets for Auto-Submit

For this to work, you need additional secrets:

SecretWhere to Get ItUsed For
EXPO_TOKENexpo.dev/settings/access-tokensEAS authentication
EXPO_APPLE_APP_SPECIFIC_PASSWORDappleid.apple.com → App-Specific PasswordsiOS 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 OTACannot Update OTA
JavaScript code (bug fixes, UI changes)Native modules (new permissions, new SDK)
Images and assetsapp.json configuration changes
Styling and layoutNew native dependencies

Configuration

Ensure your app.json has updates configured:

Json
{
  "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:

Yaml
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-interactive

Usage: 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:

BranchTriggerAction
mainPushValidate + Production build
developPR to mainValidate only (type check, lint, tests)
hotfix/*PushOTA update to production
Manual dispatchGitHub Actions UIBuild any profile for any platform

Typical Release Flow

  1. Develop features on feature branches
  2. Open PR to main → CI validates automatically
  3. Merge to main → CI builds and optionally submits
  4. Critical bug found → Push to hotfix/fix-name → OTA update goes live

Common Issues

IssueSolution
EXPO_TOKEN not foundVerify the secret name matches exactly in GitHub Settings → Secrets → Actions.
Build triggers but fails on EASCheck the build logs at expo.dev. Common cause: dependency mismatch or missing native config.
OTA update not showingVerify runtimeVersion matches between your build and update. Run eas update:list to check deployed updates.
Validate passes locally but fails in CICI uses --frozen-lockfile, make sure your yarn.lock is committed and up to date.
Build takes too longEAS 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 iOSCheck that EXPO_APPLE_APP_SPECIFIC_PASSWORD is set and your eas.json has valid appleId, ascAppId, and appleTeamId.

Next Steps