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

Without Matrix: Write 6 nearly-identical jobs 😫 Node 18 Node 20 Node 22 Ubuntu Windows Job 1 ⚡ Job 2 ⚡ Job 3 ⚡ Job 4 ⚡ Job 5 ⚡ Job 6 ⚡ ⚡ All 6 run in PARALLEL Total time = slowest single job, not 6× slower
A 2×3 matrix generates 6 parallel jobs from one definition. The more combinations, the more value.

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

fail-fast: true (default) Node 18 ✗ Node 20 ⊘ Node 22 ⊘ One fails → ALL cancelled immediately fail-fast: false (recommended) Node 18 ✗ Node 20 ✓ Node 22 ✓ One fails → others complete → see full picture
fail-fast: false tells you WHICH environments are broken, not just "something failed."

🏋️ 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

ScenarioMatrix VariablesJobs Generated
Library supporting multiple Nodenode: [18, 20, 22]3
Cross-platform CLIos: [ubuntu, windows, macos]3
Both combinedos × node: 3 × 39
Mono-repo servicesservice: [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

  1. How many jobs does a matrix with os: [a, b] and node: [18, 20, 22] generate?
  2. What does fail-fast: false do and why is it usually preferred?
  3. How do you reference a matrix variable inside a step?
  4. How would you add coverage collection to ONLY the Node 20 job?
Reveal answers
  1. 6 jobs (2 × 3 = 6 combinations, all parallel).
  2. Lets all matrix jobs complete even if one fails, so you see the full picture of which environments are broken. Default true cancels siblings on first failure.
  3. ${{ matrix.variable-name }} — e.g., ${{ matrix.node-version }}
  4. Use include: to add a coverage: true variable to the Node 20 entry, then if: matrix.coverage on 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.