GitHub Actions matrix build (2026)
strategy.matrix to your job listing the versions you want, set
fail-fast: false so one failing leg doesn't cancel the rest, and interpolate
${{ matrix.version }} into your setup step. GitHub runs every combination in
parallel. Copy-paste workflow below.
The workflow
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ["20", "22", "24"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm run lint --if-present
- run: npm test
Why each part matters
strategy.matrix
The matrix turns one job definition into one run per value — here three jobs, on Node 20, 22
and 24, all in parallel. Add a second key (e.g. os: [ubuntu-latest, windows-latest])
and GitHub expands the cross-product automatically: 3 versions × 2 OSes = 6 jobs, no
extra YAML.
fail-fast: false
By default GitHub cancels every other matrix leg the moment one fails. That hides whether a bug
is version-specific. Setting fail-fast: false lets all legs finish, so you see at a
glance that, say, only Node 24 is broken — not that "CI is red".
matrix value in the setup step
node-version: ${{ matrix.node }} is the whole point — each leg installs its own
version. Forget this interpolation and all three legs run on the same default version, so the
matrix tests nothing.
cache: npm
The official setup actions cache dependencies for you — cache: npm on
setup-node (or cache: pip on setup-python) keys on your
lockfile and restores node_modules's download cache across runs. No separate
actions/cache step needed.
Python and Go
The shape is identical — swap the setup action and the matrix key:
strategy:
fail-fast: false
matrix:
python: ["3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: pip
- run: pip install -e ".[test]"
- run: pytest
For Go, use actions/setup-go@v5 with go-version: ${{ matrix.go }}
(caching is on by default).
Common mistakes
- Hard-coding the version — leaving
node-version: "20"instead of${{ matrix.node }}means every leg runs the same version; the matrix becomes a no-op that just wastes runner minutes. - Leaving fail-fast on — one flaky leg cancels the others, so you never learn which versions actually pass.
- Quoting versions wrong — write
"20", not20. YAML reads bare3.10as the number3.1, silently testing the wrong version. - Skipping the lockfile —
npm ci(notnpm install) and a committed lockfile are what make the cache key stable and the build reproducible.
Going further
For a complete, ready-to-run project — app, Dockerfile, docker-compose with Postgres, and a CI workflow already wired and green — the SysBuild Pro Pack ships six production stacks (FastAPI, Django, Node/Express, Next.js, Go and Rust), each with a working GitHub Actions pipeline you can extend into a matrix, for $29 one-time.