GitHub Actions Matrix Builds

A matrix build runs the same job multiple times with different combinations of values — like different programming language versions, operating systems, or database types. Instead of writing three separate jobs to test on Node.js 18, 20, and 22, you write one job with a matrix and GitHub creates all three automatically.

The Problem Matrix Solves

Without matrix builds, testing across multiple environments means copying and pasting jobs:

Without matrix (repetitive and hard to maintain):

jobs:
  test-node18:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with: { node-version: '18' }
      - run: npm test

  test-node20:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with: { node-version: '20' }
      - run: npm test

  test-node22:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with: { node-version: '22' }
      - run: npm test

With matrix builds, one job definition produces all three:

With matrix (clean and scalable):

jobs:
  test:
    strategy:
      matrix:
        node-version: [18, 20, 22]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm test

How Matrix Works

The strategy.matrix block defines lists of values. GitHub creates one job instance for each value in the list. The matrix context inside the job provides the current iteration's value.

matrix:
  node-version: [18, 20, 22]

GitHub generates:
  Job 1 → node-version = 18
  Job 2 → node-version = 20
  Job 3 → node-version = 22

All three run in parallel.

Multi-Dimensional Matrix

Add multiple variables to create every possible combination:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node-version: [18, 20]

GitHub generates 6 jobs — one for each combination:

ubuntu-latest  + node 18
ubuntu-latest  + node 20
windows-latest + node 18
windows-latest + node 20
macos-latest   + node 18
macos-latest   + node 20

Use ${{ matrix.os }} in runs-on and ${{ matrix.node-version }} in the setup step:

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node-version: [18, 20]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm test

Excluding Specific Combinations

Some combinations may not be valid or useful. The exclude key removes specific combinations from the matrix:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node-version: [18, 20]
    exclude:
      - os: macos-latest
        node-version: 18

This removes the macOS + Node 18 combination, leaving 5 jobs instead of 6. Use this when a combination is known to be incompatible or redundant.

Including Extra Combinations

The include key adds specific combinations that are not part of the main matrix, or adds extra variables to existing combinations:

strategy:
  matrix:
    node-version: [18, 20]
    include:
      - node-version: 22
        experimental: true    ← Extra variable only for this entry

Now Node 22 is also tested, and only that job has the experimental variable set to true. You can use this in a condition later:

steps:
  - name: Run tests
    run: npm test
    continue-on-error: ${{ matrix.experimental == true }}

Node 22 tests are allowed to fail without stopping the overall job.

Controlling Failure Behavior with fail-fast

By default, GitHub cancels all remaining matrix jobs the moment any one of them fails. This is called fail-fast and it saves runner minutes when an early failure reveals a fundamental problem.

Disable fail-fast when you want every combination to run to completion regardless of failures:

strategy:
  fail-fast: false
  matrix:
    node-version: [18, 20, 22]

With fail-fast: false, a failure on Node 18 does not cancel the Node 20 and Node 22 jobs. You get complete results for all versions.

Controlling Parallelism with max-parallel

By default, all matrix jobs run simultaneously. Limit how many run at once using max-parallel:

strategy:
  max-parallel: 2
  matrix:
    node-version: [18, 20, 22]

With max-parallel: 2, only two jobs run at once. The third waits until one of the first two finishes. Use this when the target system cannot handle too many simultaneous connections.

Practical Matrix Example — Python Test Matrix

name: Python Test Matrix

on: [push, pull_request]

jobs:
  test:
    strategy:
      fail-fast: false
      matrix:
        python-version: ['3.10', '3.11', '3.12']
        os: [ubuntu-latest, windows-latest]
        exclude:
          - os: windows-latest
            python-version: '3.10'

    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

      - run: pip install -r requirements.txt

      - run: pytest --tb=short

This produces 5 test jobs: 3 Python versions on Ubuntu, plus Python 3.11 and 3.12 on Windows.

Matrix Build Benefits at a Glance

Without Matrix          | With Matrix
------------------------|---------------------------
One job per version     | One job definition, N runs
Hard to add versions    | Add a value to the list
Parallel by hand only   | Parallel automatically
Inconsistent structure  | Identical structure per job

Leave a Comment

Your email address will not be published. Required fields are marked *