Your app runs on Node 18, 20, and 22. On Linux and maybe Windows. You need to test all combinations — but not by copy-pasting jobs. Matrix builds generate parallel jobs from a single definition.
The Problem: Combinatorial Testing
Matrix Syntax
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node-version: [18, 20, 22]
fail-fast: false # Don't cancel others if one fails
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
${{ matrix.VARIABLE }} is replaced with each combination value. GitHub auto-generates one job per combination. You write one job definition, get N parallel executions.
Advanced Matrix Features
include: Add Specific Combinations
strategy:
matrix:
node-version: [18, 20, 22]
include:
# Add an extra variable to one specific combo
- node-version: 20
coverage: true # Only collect coverage on Node 20
exclude: Remove Specific Combinations
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
exclude:
# Don't test Node 18 on macOS (not needed)
- os: macos-latest
node-version: 18
fail-fast: Control Failure Behavior
🏋️ Exercise: Matrix CI Pipeline
Add a matrix build to your CI that tests across Node 18, 20, and 22:
# Update the test job in .github/workflows/ci.yml
test:
name: 🧪 Test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
fail-fast: false
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
- name: Report
run: echo "✅ Tests passed on Node ${{ matrix.node-version }}" >> $GITHUB_STEP_SUMMARY
After pushing: Go to Actions and you'll see three parallel "Test" jobs — one for each Node version. The build job waits for ALL three to pass.
Real-World Matrix Patterns
| Scenario | Matrix Variables | Jobs Generated |
|---|---|---|
| Library supporting multiple Node | node: [18, 20, 22] | 3 |
| Cross-platform CLI | os: [ubuntu, windows, macos] | 3 |
| Both combined | os × node: 3 × 3 | 9 |
| Mono-repo services | service: [api, web, worker] | 3 |
| Test sharding (speed) | shard: [1, 2, 3, 4] | 4 |
Cost awareness: A 3×3 matrix = 9 parallel jobs = 9× the runner minutes. Use matrices for what adds value (catching real compatibility issues), not just for fun.
max-parallel: 3 can limit concurrency if you're concerned about costs.
🧠 Recall Check
- How many jobs does a matrix with
os: [a, b]andnode: [18, 20, 22]generate? - What does
fail-fast: falsedo and why is it usually preferred? - How do you reference a matrix variable inside a step?
- How would you add coverage collection to ONLY the Node 20 job?
Reveal answers
- 6 jobs (2 × 3 = 6 combinations, all parallel).
- Lets all matrix jobs complete even if one fails, so you see the full picture of which environments are broken. Default
truecancels siblings on first failure. ${{ matrix.variable-name }}— e.g.,${{ matrix.node-version }}- Use
include:to add acoverage: truevariable to the Node 20 entry, thenif: matrix.coverageon the coverage step.
Matrix builds complete the CI picture — you can now validate your code across any combination of environments in parallel. Next: we shift from CI to CD — deployment concepts and strategies.