GitHub Actions — Node.js CI

A production-ready GitHub Actions workflow that runs your Node.js test suite against Node 18, 20, and 22 using a build matrix — so you catch version-specific regressions before they reach production.

🛠 Paste this YAML into the validator to check it instantly.

Open Validator →

Overview

This workflow triggers on every push to main and on all pull requests targeting main. It uses GitHub Actions' build matrix to run the same job three times in parallel — once for each active Node.js LTS version (18, 20, and 22). This approach catches version-specific API changes or deprecations early, without needing to maintain separate workflow files per version.

The workflow checks out the code, installs the correct Node.js version using the official actions/setup-node action (with npm caching enabled), installs dependencies with npm ci, runs the build step if present, and finally executes the test suite. It's intentionally minimal — add linting, coverage reporting, or artifact uploads on top as needed.

Save this file as .github/workflows/ci.yml (or any .yml name) inside your repository. The .github/workflows/ directory must exist at the root of your repo.

Full YAML Copy-paste ready

.github/workflows/ci.yml
name: Node.js CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18.x, 20.x, 22.x]
    steps:
      - uses: actions/checkout@v4
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm run build --if-present
      - run: npm test

Key sections explained

The on trigger block

The on key defines which events activate the workflow. Here, two events are configured: push to the main branch and pull_request targeting main. The branches filter under each event means the workflow only runs for changes to or targeting main — pushes to feature branches won't trigger it unless a PR is opened against main.

If you want CI to run on every branch push (not just main), remove the branches filter entirely, or replace it with a wildcard: branches: ['**'].

The strategy.matrix block

The strategy.matrix key instructs GitHub Actions to run the job multiple times with different variable values. Here, node-version: [18.x, 20.x, 22.x] produces three parallel jobs. Each job gets a different value for matrix.node-version, which is then referenced in the step via ${{ matrix.node-version }}. The .x suffix tells setup-node to resolve the latest patch release within that major version (e.g. 20.x might resolve to 20.19.1).

By default, if one matrix job fails, all remaining jobs continue running. Add fail-fast: false under strategy to keep this behavior explicit, or set it to true to cancel all jobs immediately on the first failure.

npm ci vs npm install

This workflow uses npm ci rather than npm install. The difference matters in CI: npm ci installs exactly what's in package-lock.json (failing if it's out of sync with package.json), never updates the lockfile, and deletes node_modules before installing. This produces a clean, deterministic install every time — which is exactly what you want in CI. npm install may silently update the lockfile or resolve differently across runs.

The cache: 'npm' option in setup-node caches the npm dependency cache directory (keyed by package-lock.json), so subsequent runs with unchanged dependencies are much faster.

The --if-present flag

npm run build --if-present runs the build script from package.json only if it exists. Without this flag, the step would fail with an error if no build script is defined — which would break the workflow for projects (like pure Node.js CLIs or libraries) that don't have a build step. It's a safe default: it runs your build if you have one, and silently succeeds if you don't.

Tips & variations

Add a Node.js version to the matrix

When Node.js releases a new LTS version, just append it to the array: node-version: [18.x, 20.x, 22.x, 24.x]. GitHub Actions will automatically add a fourth parallel job — no other changes needed.

Cache with a Yarn or pnpm lockfile

If your project uses Yarn or pnpm, change cache: 'npm' to cache: 'yarn' or cache: 'pnpm', and replace the npm ci step with yarn install --frozen-lockfile or pnpm install --frozen-lockfile respectively.

Upload a coverage report

After the npm test step, you can upload coverage to Codecov by adding:

additional step
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

Pin action versions

This workflow uses @v4 tags for actions. For maximum supply-chain security, pin to a full commit SHA (e.g. actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683). Tools like Dependabot (see the Dependabot example) can keep these pins up to date automatically.