Mobile CI/CD Basics
10 examples to understand PR checks, release pipelines, and nightly builds for Expo SDK 57 apps - 7 basic and 3 intermediate.
Search across all documentation pages
10 examples to understand PR checks, release pipelines, and nightly builds for Expo SDK 57 apps - 7 basic and 3 intermediate.
CI/CD for React Native differs from web: you ship native binaries (store builds) and optionally OTA JavaScript bundles (OTA Updates Basics). Cloud builds via EAS Build Basics keep signing keys off laptops.
npx create-expo-app@latest ShipApp --template blank-typescript
cd ShipApp
npx expo install eas-cli --save-dev
npx eas-cli@latest build:configureStandardize scripts CI will call:
{
"scripts": {
"lint": "expo lint",
"typecheck": "tsc --noEmit",
"test": "jest",
"format:check": "prettier --check ."
}
}Tooling: These examples target Expo SDK 57 (
expo~57.0.4), React Native 0.86.0, and React 19.2.3. E2E snippets assume dev or preview builds - not Expo Go alone.
Mobile teams run three distinct automation lanes with different speed, cost, and blast-radius goals.
┌─────────────────────────────────────────────────────────────┐
│ PR checks │ Every PR │ 2–8 min │ Block merge │
├──────────────────┼──────────────┼──────────┼───────────────┤
│ Release pipeline │ Tag / main │ 20–60 min│ Store + OTA │
├──────────────────┼──────────────┼──────────┼───────────────┤
│ Nightly │ Cron 02:00 │ 30–90 min│ Drift signal │
└─────────────────────────────────────────────────────────────┘| Lane | Triggers | Typical jobs | Blocks |
|---|---|---|---|
| PR checks | pull_request | lint, tsc, Jest | Merge to main |
| Release | push tag v*, manual | EAS build, Maestro smoke, submit | Store promotion |
| Nightly | schedule: cron | Full matrix build, contract tests | Nothing - alerts only |
Related: CI Quality Gates - script surface PR checks call
PR automation answers: "Is this diff safe to merge?" - not "Is this ready for the App Store?"
# .github/workflows/pr-checks.yml
name: PR Checks
on:
pull_request:
branches: [main]
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npx expo customize tsconfig.json
- run: npm run format:check
- run: npm run lint
- run: npm run typecheck
- run: npm run test -- --ciquality check - policy, not honor systemnpm run lint && npm run typecheck && npm testRelated: Mobile Testing Basics - pyramid layers PR checks cover
Release automation answers: "Build and ship the version we tagged."
# .github/workflows/release.yml
name: Release
on:
push:
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- run: npm ci
- run: npm run typecheck && npm run test -- --ci
- run: eas build --profile production --platform all --non-interactive// eas.json (excerpt)
{
"build": {
"production": {
"autoIncrement": true,
"channel": "production"
}
}
}v2.4.0) - tag ↔ binary traceabilityEXPO_TOKEN robot account - never personal passwords in CI (GitHub Actions + EAS)production profile maps to store signing credentials in EASRelated: Build Profiles & Flavors -
previewvsproduction
When PR volume is high, individual green checks hide cross-branch integration failures. Nightlies run on a schedule without blocking merges.
# .github/workflows/nightly.yml
name: Nightly Integration
on:
schedule:
- cron: "0 6 * * 1-5" # weekdays 06:00 UTC
workflow_dispatch:
jobs:
nightly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { ref: main }
- uses: expo/expo-github-action@v8
with:
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- run: npm ci
- run: npx expo-doctor
- run: npm run test -- --ci
- run: eas build --profile preview --platform android --non-interactive#mobile-ci) on failure - do not block daytime mergesnpx expo-doctor - catches SDK skew nobody touched in today's PRsNot every PR needs a native binary. Split fast gates from optional preview builds.
| Signal | PR check only | + Preview build |
|---|---|---|
Copy change in Text | ✓ | ✗ |
New native module (expo-camera) | lint/tsc | ✓ |
| Navigation refactor | unit tests | ✓ (Maestro smoke) |
| Config plugin change | tsc + doctor | ✓ |
# Label-gated preview - saves EAS credits
on:
pull_request:
types: [labeled]
jobs:
preview:
if: contains(github.event.pull_request.labels.*.name, 'needs-preview')
# ... eas build --profile previewpaths filter for native-impacting dirsneeds-previewJS-only fixes ship via eas update inside the release pipeline - native changes still need eas build.
# .eas/workflows/release-ota.yml (excerpt)
jobs:
quality:
steps:
- uses: eas/checkout
- uses: eas/install_node_modules
- run: npm run typecheck && npm run test -- --ci
update_production:
needs: [quality]
type: update
params:
channel: production
message: ${{ github.sha }}runtimeVersion must match the store binary users installed - Runtime Version Policyeas update --roll-back - Rollback RunbookThe anti-pattern: QA tests a laptop build; stores receive a different cloud build.
❌ Laptop build → QA sign-off → CI rebuild → Store
✅ EAS build ID abc123 → QA sign-off → eas submit --id abc123 → Store# Promote the exact build QA tested
eas submit --platform ios --id <BUILD_ID> --non-interactive
eas submit --platform android --id <BUILD_ID> --non-interactiveeas submit --latest is convenient but risky if a newer build queued after QARelated: EAS Submit - App Store Connect and Play Console handoff
Turborepo monorepos should not run mobile CI when only the marketing site changed.
on:
pull_request:
paths:
- "apps/mobile/**"
- "packages/ui/**"
- "package-lock.json"# Or path filter action
- uses: dorny/paths-filter@v3
id: changes
with:
filters: |
mobile:
- 'apps/mobile/**'
- 'packages/**'
- if: steps.changes.outputs.mobile == 'true'
run: npm run typecheck -w apps/mobileturbo run lint typecheck test --filter=mobile fans out with cache - GitHub Actions + EASAdvanced teams chain quality → build → E2E → submit in one workflow file.
# .eas/workflows/store-release.yml (sketch)
name: Store Release
on:
push:
tags: ["v*"]
jobs:
quality:
steps:
- uses: eas/checkout
- uses: eas/install_node_modules
- run: npm run lint && npm run typecheck && npm run test -- --ci
build_ios:
needs: [quality]
type: build
params: { platform: ios, profile: production }
smoke_ios:
needs: [build_ios]
type: maestro
params:
build_id: ${{ needs.build_ios.outputs.build_id }}
flow_path: [".maestro/smoke.yml"]
submit_ios:
needs: [smoke_ios]
type: submit
params:
platform: ios
build_id: ${{ needs.build_ios.outputs.build_id }}needs chains enforce ordering - submit cannot run if Maestro failedMobile releases rarely ship alone. Document deploy dependencies in a release calendar.
Week 12 release train
Tue - Backend API v3 deploys to staging (contract tests green)
Wed - eas build production (native + embedded JS)
Thu - eas update production (JS hotfix if needed, same runtimeVersion)
Fri - eas submit after Maestro + manual QA
Mon - Staged rollout 10% → 50% → 100%| Change type | Ships via | Backend dependency |
|---|---|---|
| Button color | OTA | None |
| New API field (optional) | OTA | Backend live first |
| New native module | Store build | Feature flag off until binary live |
| Breaking API | Store + backend same window | Coordinate freeze |
| Anti-pattern | Why it fails |
|---|---|
eas build on every PR commit | Burns credits; 20-minute feedback loops |
| Skipping tests on release tags | "Hotfix" tags ship regressions |
| Laptop archive to TestFlight | Non-reproducible; signing drift |
| OTA without channel discipline | Staging bundle hits production users |
| One pipeline for everything | PR waits 45 minutes; team disables CI |
PR: lint, format, tsc, unit tests, optional contract tests. Release: EAS build, E2E smoke, submit, OTA to production channel.
Weekdays for active teams; weekly for small apps. Increase frequency after SDK upgrades or monorepo migrations.
EAS Workflows honor [eas skip] on push triggers. Prefer label-gated previews over routine skip tokens - branch protection should still require PR checks.
Many teams use GitHub Actions for PR checks and EAS Workflows for build/test/submit chains. Keep npm scripts identical across both.
eas.jsonStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026