Running Node.js Tests Across Versions with a Single GitHub Actions Matrix
Learn how to replace duplicated Node.js CI workflows with a single GitHub Actions matrix, control parallelism, and verify the setup locally or in the UI.
01 Sept 2026, 18:57 UTC

The problem: duplicated workflows for each Node version
When a library or application must stay compatible with several Node.js releases (e.g., 14, 16, 18, 20), many teams create a separate workflow file for each version. This leads to repetitive YAML, easy drift when a step changes, and harder‑to‑read CI logs.
Thesis: a matrix strategy keeps the definition in one place while giving you parallel feedback and controllable resource use
By declaring a strategy.matrix block you list the Node versions once. GitHub Actions expands the matrix into distinct jobs, runs them in parallel (subject to your account’s concurrency limits), and reports each result individually. You can throttle parallelism with max-parallel to stay within minute budgets.
Worked example: a CI job that tests four Node versions
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14, 16, 18, 20]
# Limit to two concurrent jobs to avoid hitting concurrency caps
max-parallel: 2
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up 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 test suite
run: npm test
Place this file at .github/workflows/ci.yml in the default branch. No additional permissions are required beyond the default read token for checkout and the write token that Actions automatically provides for logging.
Verifying the matrix works locally with act
- Install
act(e.g.,brew install acton macOS or follow the official docs). - From the repository root, run:
act -j test
This simulates the workflow and prints the Node version used in each job. You should see lines like:
[Test] Node version: v14.21.3
[Test] Node version: v16.20.0
...
If you prefer to verify in GitHub:
- Push a commit to trigger the workflow.
- Open the Actions tab, select the latest run, and click the matrix expansion (shows
node-version: 14,16, etc.). - Inside each expanded job, check the step “Set up Node.js …” – the output should display the expected version.
Trade‑off and limitation: controlling minutes and concurrency
Every matrix combination creates a separate job, so a large matrix can quickly consume your account’s concurrent job slots and increase billed minutes. The example above uses max-parallel: 2 to cap parallelism; you can also:
- Reduce the matrix size (e.g., test only LTS versions).
- Move heavy integration tests to a separate workflow that runs on a schedule.
- Monitor usage via the Usage tab of a workflow run and adjust
max-parallelor matrix entries accordingly.
Secrets and environment variables are shared across all matrix entries. If you need a version‑specific secret, guard it with a conditional step:
- name: Use version‑specific token
if: matrix.node-version == '18'
run: echo "$MY_TOKEN"
env:
MY_TOKEN: ${{ secrets.NODE_18_TOKEN }}
Actionable closing: adopt the matrix, watch the usage
Start by copying the example workflow into your repository, adjust the Node list to match your support matrix, and set max-parallel to a value that fits your concurrency limit (check your account settings under Settings → Actions → General → Workflow permissions). After the first run, open the Usage tab to confirm total minutes stay within expectations. If you notice the job queue backing up, lower max-parallel or split out heavy tests. This approach gives you a single source of truth for Node version testing while keeping resource consumption predictable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.