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