Run Node.js Tests Across Multiple Versions with a GitHub Actions Matrix
Set up a GitHub Actions matrix strategy to run your Node.js test suite across multiple runtime versions in parallel, with per-version results, caching, and targeted reruns.
14 Jan 2026, 00:22 UTC

What you get at the end
You want every push to your repository to run your test suite against several Node.js versions at once, with a separate pass/fail result per version. GitHub Actions supports this with a matrix strategy: you define one job template and a list of values, and Actions fans it out into parallel jobs. This guide builds a workflow that tests a Node.js project on four runtime versions and shows you how to verify and adjust it.
Version note: the examples below use Node.js 14, 16, 18, and 20 to match a common legacy-support scenario. As of this writing, Node 14 and 16 are end-of-life and no longer receive security fixes, and newer LTS lines (20, 22) are the sensible defaults for new projects. Substitute the versions your project actually supports — the mechanics are identical.
Prerequisites
- A GitHub repository containing a
package.jsonwith a workingtestscript (e.g.,"test": "jest"ornode --test). - A lockfile (
package-lock.json) committed to the repo — this lets you usenpm cifor reproducible installs. - Permission to push to the default branch (or open a pull request) so you can add
.github/workflows/ci.yml. - Enough Actions minutes for parallel jobs: four matrix jobs consume roughly four times the minutes of a single job. On private repositories with a limited plan, check your usage under Settings → Billing.
Create the matrix workflow
Create the file .github/workflows/ci.yml in your repository root with the following content. You can do this locally and push, or through the GitHub web UI (Add file → Create new file), which will offer to commit it directly.
name: Node.js CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: [14, 16, 18, 20]
name: Test on Node ${{ matrix.node-version }}
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
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm testKey decisions in this file:
strategy.matrix.node-versionis the list Actions iterates over. Each entry becomes one job.fail-fast: falsekeeps the other version jobs running when one fails. The default istrue, which cancels the remaining jobs — fine for saving minutes, but it hides whether a failure is version-specific or universal. Leave it off while you are diagnosing version-specific breakage.cache: npminsetup-nodecaches the npm cache directory keyed on your lockfile, which noticeably speeds upnpm cion reruns.npm ciinstalls exactly what the lockfile specifies and fails if the lockfile andpackage.jsondisagree — preferable tonpm installin CI because it is reproducible.
Verify the run
Commit and push the file, then open the Actions tab in your repository. You should see:
- One workflow run named Node.js CI, containing four jobs labeled Test on Node 14, Test on Node 16, and so on. They start concurrently (subject to runner availability).
- In any job's log, the Use Node.js step prints the resolved version — expand it and confirm it matches the matrix entry, not some other default.
- The Run tests step shows your test runner's output and the job ends green. On pull requests, each matrix job appears as a separate required-or-optional check depending on your branch protection settings.
If you use branch protection, note that GitHub treats each matrix job as its own check name (e.g., Test on Node 18). If you later change the version list, update the required checks in branch protection too, or merges will block on a check that no longer exists.
When one version fails
A single red job does not require rerunning everything:
- Rerun just the failure: in the workflow run page, use Re-run failed jobs to retry only the red matrix entries. This is the fastest way to distinguish a flaky test from a real incompatibility.
- Confirm it is version-specific: if the same test fails on every version, the problem is your code or the workflow, not Node. If it fails on exactly one version, reproduce locally with that version (via
nvm use 14or a container) before changing the matrix. - Exclude or pin a version: if a version is genuinely unsupported, remove it from the list and document that in your README or
enginesfield inpackage.json. Do not silently drop it — theenginesfield is what users and tooling consult.
Limits and trade-offs
Matrix jobs multiply minute consumption and queue pressure. Four versions is usually a reasonable ceiling for a library; an application that deploys to one known runtime often only needs that one version in CI plus perhaps the next LTS. Also remember the matrix only changes the Node.js runtime — system dependencies, OS, and architecture stay fixed on ubuntu-latest. If you need OS coverage, add an os: [ubuntu-latest, windows-latest] matrix dimension, but expect the job count (and minutes) to multiply accordingly.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.