CI Quality Gates
CI quality gates run the same lint, typecheck, format:check, and test scripts on every pull request - catching hook dependency mistakes and cross-package type breaks before EAS spends minutes on a native build.
Search across all documentation pages
CI quality gates run the same lint, typecheck, format:check, and test scripts on every pull request - catching hook dependency mistakes and cross-package type breaks before EAS spends minutes on a native build.
Quick-reference recipe card - copy-paste ready.
// package.json - scripts CI will call
{
"scripts": {
"lint": "expo lint",
"typecheck": "tsc --noEmit",
"format:check": "prettier --check .",
"test": "jest"
}
}# Local pre-push (same sequence as CI)
npm run format:check && npm run lint && npm run typecheck && npm run testWhen to reach for this:
main or release/*.eas build.tsc across apps/mobile and packages/*.type: build jobs.# .github/workflows/quality.yml
name: Quality Gates
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
group: quality-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
quality:
name: Lint, Format, Types, Tests
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Generate Expo types
run: npx expo customize tsconfig.json
- name: Prettier
run: npm run format:check
- name: ESLint
run: npm run lint
- name: TypeScript
run: npm run typecheck
- name: Unit tests
run: npm run test -- --ci --passWithNoTests# .eas/workflows/quality-gates.yml - same scripts on Expo infrastructure
name: Quality Gates
on:
pull_request:
branches: [main]
push:
branches: [main]
concurrency:
cancel_in_progress: true
group: ${{ workflow.filename }}-${{ github.ref }}
jobs:
quality:
name: Lint, Format, Types, Tests
steps:
- uses: eas/checkout
- uses: eas/use_npm_token
- uses: eas/install_node_modules
- name: Generate Expo types
run: npx expo customize tsconfig.json
- name: Format check
run: npm run format:check
- name: Lint
run: npm run lint
- name: Typecheck
run: npm run typecheck
- name: Unit tests
run: npm run test -- --ci --passWithNoTests
build_ios:
name: iOS build (after quality)
needs: [quality]
type: build
params:
platform: ios
profile: preview// turbo.json - monorepo fans out the same gates
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"lint": {
"dependsOn": ["^lint"]
},
"typecheck": {
"dependsOn": ["^typecheck"]
},
"test": {
"dependsOn": ["^test"]
},
"format:check": {
"outputs": []
}
}
}# .github/workflows/quality-monorepo.yml
name: Monorepo Quality
on:
pull_request:
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 turbo run format:check lint typecheck testWhat this demonstrates:
package.json script surface - developers and CI run identical commands.npm ci, Node 20, and concurrency cancellation to save minutes.npx expo customize tsconfig.json ensures .expo/types exists before tsc --noEmit.needs: [quality] blocks native builds on failure.expo lint wraps ESLint with eslint-config-expo - errors fail the job unless you pass --max-warnings intentionally.tsc --noEmit validates the entire TypeScript graph, including workspace packages imported by the app.prettier --check fails on formatting drift without mutating the runner filesystem.quality check before merge - policy enforcement, not suggestions.| Script | Command | Catches |
|---|---|---|
format:check | prettier --check . | Inconsistent quotes, imports, trailing commas |
lint | expo lint | Hooks deps, unused vars, RN a11y gaps |
typecheck | tsc --noEmit | Broken exports, bad route params, API types |
test | jest / vitest | Regressions in hooks, utils, reducers |
| Platform | Best for | Notes |
|---|---|---|
| GitHub Actions | PR checks on every push; open-source; monorepo turbo | Familiar YAML; free tier limits apply |
| EAS Workflows | Quality gate before type: build on Expo infra | Lives in .eas/workflows/; shares secrets with EAS Build |
| Both | Teams that want GH PR status and gated cloud builds | Keep scripts identical; duplicate YAML is acceptable |
format:check → lint → typecheck → test → (optional) eas build// apps/mobile/package.json
{
"scripts": {
"lint": "expo lint",
"typecheck": "tsc --noEmit",
"test": "jest"
}
}turbo run lint typecheck respects dependsOn: ["^lint"] so packages/ui lints before apps/mobile.engines and CI - RN tooling assumes Node 20 LTS.npm ci for pnpm install --frozen-lockfile and cache: pnpm on setup-node.Skipping expo customize before tsc - Typed routes fail with missing .expo/types on a clean CI checkout. Fix: Add an explicit step before typecheck, or commit generated types only if your team policy requires it (usually do not).
npm install instead of npm ci in CI - Lockfile drift produces "works on CI, fails locally" flakiness. Fix: Always npm ci when package-lock.json exists.
Running prettier --write in CI - Runner formats files that never get committed; main stays dirty. Fix: Use --check only in workflows.
Lint warnings allowed to merge - --max-warnings 0 is not the default everywhere; warnings accumulate. Fix: Treat warnings as errors in CI when adopting a zero-warning policy, or fix warnings before enabling the gate.
Quality job not required in branch protection - Developers merge with admin bypass while checks are red. Fix: GitHub → Settings → Branches → require the quality status check.
EAS build without needs: [quality] - Native builds start while lint is still failing. Fix: Chain needs so type: build jobs wait for the custom quality job.
| Alternative | Use When | Don't Use When |
|---|---|---|
| Husky + lint-staged only | Solo dev prototyping | You need enforceable gates reviewers can trust |
turbo run remote cache | Large monorepo CI is slow | Single-app repo with <30s checks already |
| Nx affected targets | Huge repo; only lint changed projects | Simple Expo app without Nx graph |
| EAS Build without prior lint | Emergency hotfix bypass (documented) | Normal feature development - too slow to fail |
lint → expo linttypecheck → tsc --noEmitformat:check → prettier --check .test → your unit test runnerCI should call these by name, not inline tool commands.
tsc validates exports, generics, and cross-package imports.packages/ui may not appear until the app imports it - tsc catches that..eas/workflows/quality-gates.yml.eas/checkout, eas/install_node_modules, then run: npm run <script> steps.needs: [quality] on downstream type: build jobs..expo/types exists after expo start; CI checkout is clean.npx expo customize tsconfig.json or expo export step that generates types before tsc.node_modules twice - slower for small repos.main.quality job name.[eas skip] / [skip eas] in commit messages for push triggers.engines.node in root package.json with CI node-version.npx turbo run test --filter=mobile
# or all packages:
npx turbo run testtest dependsOn: ["^test"] when packages must build/typecheck first.turbo fan-out grows; decrease when only running affected packages.npx expo-doctor as a step after install; fail on incompatible dependency versions.eslint-config-expo and the rules expo lint enforcesformat:check script and .prettierignoretypecheck validatesturbo run lint typecheck across workspacesStack versions: This page was written for Expo SDK 57 (
expo~57.0.4), React Native 0.86.0, and React 19.2.3.
Reviewed by Chris St. John·Last updated Jul 19, 2026