Next.js CI Workflow with Lint, Typecheck, and Build
A clean GitHub Actions workflow for Next.js projects: install, lint, typecheck, build. Cached, fast, and PR-ready.
A starter GitHub Actions workflow for Next.js apps that runs install, lint, typecheck, and build on every push and pull request. Each step is isolated so a lint failure doesn't hide a typecheck failure. Cached at the package-manager level for ~30s install times on warm runs.
Tested on Next.js 16, Node 24 LTS, GitHub Actions runners (ubuntu-latest).
When to Use This
- Every Next.js repo, public or private
- As a baseline before adding deployment steps
- As a PR gate that blocks merging on red builds
- Combined with
concurrencyto cancel superseded runs on the same branch
Don't use this when you need a build matrix across multiple Node versions (use the matrix-node-versions snippet instead) or when builds take more than 10 minutes (split lint/typecheck/build into parallel jobs).
Code
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Typecheck
run: npx tsc --noEmit
- name: Build
run: npm run buildThe concurrency block cancels any in-progress run when you push a new commit to the same branch — saves CI minutes on rapid-fire pushes.
Usage
Drop the file at .github/workflows/ci.yml and push. The workflow runs automatically on every push to main and dev, and on every PR opened against them. The status badge appears in PR checks within a minute.
To make CI a required check before merge:
- GitHub repo → Settings → Branches → Branch protection rules
- Add
main(anddev) → Require status checks to pass before merging - Search for
CI / ciand add it
Pitfalls
npm ciis mandatory in CI, notnpm install.cienforces the lockfile and is faster.installcan mutate the lockfile, which breaks reproducibility.- Cache key is based on the lockfile path. If you have multiple lockfiles in a monorepo, point
cache-dependency-pathat the specific one. tsc --noEmitignoresnext-env.d.tsissues. That's fine — Next regenerates it on build. Don't waste time fighting it.- Build runs Next.js's own type check anyway. If you only need one of the two, drop the explicit
tsc --noEmitstep. I keep it because it fails faster than build. timeout-minutes: 10is intentional. Without it, a stuck build burns through your CI quota.
Related Snippets & Reading
- Cache npm Dependencies in GitHub Actions(coming soon) — deeper caching strategy
- Matrix Build Across Node Versions(coming soon) — for libraries that test against multiple Node versions
- Conditional Skip on Docs-Only Changes(coming soon) — save CI minutes when only Markdown changed
Frequently Asked Questions
Why run lint, typecheck, and build as separate steps in CI?
Separate steps give you isolated failure reports. If lint fails, the typecheck and build still run so you see every problem in one CI run. Bundling them into a single command makes you fix issues one at a time across multiple pushes.
Should you cache node_modules or use the actions/setup-node cache?
Use actions/setup-node with the cache option set to npm or pnpm. It caches the package manager store, not node_modules, which is faster and cache-key correct across different platforms and Node versions.