Skip to main content
GitHub Actions intermediate Lesson 3 of 5

GitHub Actions: Matrix Builds & Strategy

Run tests across multiple OS, runtime versions, and configurations simultaneously using matrix strategy—reduce feedback time without duplicating workflow YAML.

Manually duplicating jobs to test Node 18, 20, and 22 on Ubuntu and macOS means 6 copies of the same YAML. Matrix strategy generates all combinations automatically from a variable list.

Learning outcomes

By the end you can:

  • define a matrix to test multiple versions/OS combinations
  • include and exclude specific combinations
  • set fail-fast and max-parallel options
  • use matrix variables in steps

1) Why matrix builds?

Testing a Node.js library across Node 18/20/22 on Ubuntu and macOS requires 6 identical job definitions—unless you use a matrix.

A matrix strategy tells GitHub Actions to run a job once per combination of values you define.

2) Basic matrix

name: CI

on: [push, pull_request]

jobs:
  test:
    name: Test (Node ${{ matrix.node }} on ${{ matrix.os }})
    runs-on: ${{ matrix.os }}

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

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node ${{ matrix.node }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: npm

      - run: npm ci
      - run: npm test

This creates 6 parallel jobs: ubuntu+18, ubuntu+20, ubuntu+22, macos+18, macos+20, macos+22.

3) Matrix with fail-fast and max-parallel

strategy:
  fail-fast: false          # don't cancel other jobs when one fails
  max-parallel: 4           # run at most 4 jobs at once (rate limiting)
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
    python: ["3.10", "3.11", "3.12"]

4) Include: add extra properties to specific combinations

include adds extra variables to a combination or creates a new one:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20]
    include:
      # Add coverage upload only for ubuntu+20
      - os: ubuntu-latest
        node: 20
        upload_coverage: true
      # Add a completely new combination
      - os: macos-latest
        node: 22
        upload_coverage: false

Use in steps:

- name: Upload coverage
  if: matrix.upload_coverage == true
  uses: codecov/codecov-action@v4

5) Exclude: skip specific combinations

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest, macos-latest]
    node: [18, 20, 22]
    exclude:
      # Skip Node 18 on macOS (not supported by our setup)
      - os: macos-latest
        node: 18

6) Passing matrix outputs between jobs

jobs:
  build:
    strategy:
      matrix:
        platform: [linux/amd64, linux/arm64]
    runs-on: ubuntu-latest
    outputs:
      image_tag: ${{ steps.meta.outputs.tags }}
    steps:
      - id: meta
        name: Docker meta
        uses: docker/metadata-action@v5
        with:
          images: myregistry/myapp
          flavor: |
            suffix=-${{ matrix.platform == 'linux/arm64' && 'arm64' || 'amd64' }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          platforms: ${{ matrix.platform }}
          tags: ${{ steps.meta.outputs.tags }}
          push: true

7) Real-world example: Go multi-version CI

name: Go CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    name: Test Go ${{ matrix.go }} on ${{ matrix.os }}
    runs-on: ${{ matrix.os }}

    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
        go: ["1.21", "1.22", "1.23"]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Go ${{ matrix.go }}
        uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Download dependencies
        run: go mod download

      - name: Run tests
        run: go test ./... -race -coverprofile=coverage.txt

      - name: Upload coverage (Go 1.23, Ubuntu only)
        if: matrix.go == '1.23' && matrix.os == 'ubuntu-latest'
        uses: codecov/codecov-action@v4
        with:
          file: coverage.txt

Next steps

  • Reusable workflows: share full workflows across repositories
  • Deployment workflows: OIDC, environments, and approval gates
  • Composite actions: package multiple steps into one reusable action

Frequently Asked Questions

How many parallel jobs does a matrix create?
One job per combination. A matrix of 3 OS values × 3 Node versions creates 9 parallel jobs. GitHub's free tier has concurrency limits, so very large matrices may queue.
What does fail-fast do?
fail-fast: true (the default) cancels all remaining matrix jobs as soon as one fails. Set it to false when you want to see results from all combinations even if one fails.