Add CI to an Expo app in 10 minutes (GitHub Actions)
If your Expo app has no CI, every PR is a coin flip on whether the next person’s
npm install breaks the build. A minimal pipeline — lint, typecheck, and tests — takes
ten minutes to add and pays for itself on the first broken PR.
The workflow
.github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run check
Where npm run check is lint && typecheck && test — the same
command you run locally, so there are no surprises.
Make it one command
The trick that keeps CI honest is making local and CI run the same script:
{
"scripts": {
"lint": "eslint . --ext .ts,.tsx --max-warnings 0",
"typecheck": "tsc --noEmit",
"test": "jest --passWithNoTests",
"check": "npm run lint && npm run typecheck && npm test"
}
}
Gotchas
npm cineeds a committedpackage-lock.json. If you gitignore it, CI fails immediately — commit the lockfile.- Jest runs without a simulator.
jest-expoplus mocked native modules means tests run on the runner with no device. Mock anything native injest.setup.js. cache: npmis the difference between a 4-minute and a 90-second job.- Zero warnings.
--max-warnings 0turns “we’ll fix lint later” into “the build is red.” That’s the point.
CI is necessary but not sufficient — green lint/tsc/jest can still ship a blank screen or an unproven invariant. That’s what build gates are for; pair the two and your PRs actually mean something.
Get new posts
One email when a new post ships. No spam.
The CI workflow above (plus the two build gates and local release scripts) ships in the Expo Production Starter.