GitHub Actions Matrix Build Across Node.js Versions
A matrix strategy that runs your test suite on multiple Node.js versions in parallel. Essential for libraries that support older Node releases.
A GitHub Actions matrix strategy that runs the same test suite against multiple Node.js versions in parallel. This is the standard CI pattern for npm libraries. With three Node versions, the workflow runs three jobs in parallel and you get a clean grid of pass/fail in the PR check UI.
Tested on GitHub Actions ubuntu-latest, Node 20/22/24.
When to Use This
- Publishing an npm library that supports multiple Node LTS versions
- Testing a CLI tool that users run on different machines
- Validating a polyfill or compat shim across Node versions
- Catching regressions when bumping engines.node in package.json
Don't use this when you're building an app that you control the runtime for (test only the version you deploy with — matrix doubles your CI minutes).
Code
# .github/workflows/test.yml
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
node-version: [20, 22, 24]
name: Test on Node ${{ matrix.node-version }}
steps:
- uses: actions/checkout@v4
- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm testThe name: line interpolates the matrix value into the job display name, so the GitHub UI shows Test on Node 20, Test on Node 22, etc. Without it, all jobs would just show test and you couldn't tell them apart.
Usage
A more advanced matrix that combines Node versions with multiple operating systems:
strategy:
fail-fast: false
matrix:
node-version: [20, 22, 24]
os: [ubuntu-latest, macos-latest, windows-latest]
exclude:
# Don't bother testing Node 20 on Windows — too slow
- node-version: 20
os: windows-latest
runs-on: ${{ matrix.os }}This creates 9 jobs minus 1 excluded = 8 parallel runs. The exclude block lets you skip combinations that aren't worth the CI time.
Pitfalls
fail-fast: falseis essential for matrix builds. Without it, one failing job cancels the others and you can't tell if other versions also broke. Always set it to false unless you're optimizing for CI minutes.- Matrix jobs count separately against your CI quota. A 3x3 matrix burns 9x the minutes. Be aware on private repos with limited quotas.
actions/setup-nodecache is per-Node-version. That's correct behavior — different Node versions need different binaries — but it means your cache hits are lower than a single-version build.- The matrix key name is arbitrary. You can call it
nodeinstead ofnode-version, but you have to reference it consistently with${{ matrix.node }}. - Use
includefor specific extra combinations that don't fit the cartesian product. Example: test the latest version on every OS, but only one OS for older versions.
Related Snippets & Reading
- Next.js CI Workflow — for app-side single-version testing
- Cache npm Dependencies(coming soon) — works alongside matrix builds
- GitHub Actions matrix strategies — official docs
Frequently Asked Questions
When do you need a matrix build for Node.js versions?
Whenever you publish a library or tool that other projects depend on. Your users may run any LTS version, so your CI must verify the code works on all of them. Apps usually only need to test the version they deploy with.
What's the difference between fail-fast: true and fail-fast: false?
fail-fast: true (the default) cancels every job in the matrix the moment one fails. fail-fast: false lets every job finish, so you see all failing combinations in one run. For matrix builds, false is almost always what you want.