GitHub Actions Node CI
This workflow runs your lint, type check, tests and build on every pull request and every push to main, with npm's cache so repeat runs install fast. Save it as .github/workflows/ci.yml, make sure the four npm scripts exist in package.json, and push.
Updated
Files
name: CI
on:
push:
branches: [main]
pull_request:
# A new push to the same branch or pull request cancels the run still in progress.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test
- run: npm run buildSave with your agent
Connect the Sniptide MCP server once, then ask your agent to keep this template in your library. It reads the files from the JSON version of this page and saves them as a private snippet you can edit and file into a collection.
claude mcp add --transport http sniptide https://app.sniptide.com/api/mcpRead https://sniptide.com/templates/github-actions-node-ci.json and save its files as a private Sniptide snippet titled "GitHub Actions Node CI".What each part does
Part | What it does | When to change it |
|---|---|---|
on | Runs on pull requests and on pushes to main. Pushes to other branches run through their pull request, so nothing runs twice. | Add workflow_dispatch: to start runs by hand. |
concurrency | Cancels a run that is still going when a newer push arrives on the same branch. | Rarely. |
permissions | Gives the job read access to the code and nothing else. | Only when a step needs more, such as writing a comment. |
timeout-minutes | Stops a stuck job after 15 minutes instead of the default 6 hours. | When your build legitimately takes longer. |
setup-node | Installs the Node version named in .nvmrc and caches npm's download cache by the lockfile. | Use node-version: 24 if you don't keep an .nvmrc. |
npm ci | Installs exactly what the lockfile says, and fails if the lockfile is out of date. | Use the variant below for pnpm. |
Checks | One step per script, so the log shows which one failed. | Remove the scripts your project doesn't have. |
To block merging when a check fails, open the repository's settings, add a branch ruleset for main and require the test job.
Variant: pnpm
pnpm has to be installed before setup-node can cache it. pnpm/action-setup reads the version from packageManager in package.json.
steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v5
with:
node-version-file: .nvmrc
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test
- run: pnpm buildVariant: several Node versions
A library that supports more than one Node version can test them all with a matrix. Each version runs as its own job.
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [20, 22, 24]
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm testfail-fast: false keeps the other versions running when one fails, so you see every failure at once.
Variant: tests that need Postgres
A service container starts Postgres next to the job. The health check makes the steps wait until it accepts connections.
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U app"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgres://app:app@localhost:5432/app_test
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm testThe same database runs on your machine with the Docker Compose Postgres template.