Testing your code on just one version of Node.js, Python, or Ruby is risky. What if your code breaks on Python 3.9 but works on 3.12? What if there are Windows-specific bugs you don't catch on Linux? GitHub Actions Matrix Strategy solves this elegantly.
The Problem: Multi-Version Compatibility
Imagine this common scenario:
# Simple workflow jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '20' - run: npm test
Problems: - β Only tests on Node.js 20 - β Only tests on Ubuntu - β Users with Node 16 or 18 may have issues - β Windows or macOS-specific bugs go undetected
To test 3 Node versions Γ 3 OSes = 9 combinations, you'd need 9 separate jobs. Too much duplicated code!
The Solution: Matrix Strategy
Matrix Strategy lets you define multiple test dimensions in a single job:
jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] node-version: [16, 18, 20] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm test
Result: 9 jobs running in parallel automatically! π
How Matrix Works
Basic Syntax
strategy: matrix: # Each key defines a dimension language: [node, python, go] version: [latest, stable]
GitHub Actions creates a cartesian product of all combinations: - language: node, version: latest - language: node, version: stable - language: python, version: latest - language: python, version: stable - language: go, version: latest - language: go, version: stable
Total: 3 Γ 2 = 6 jobs
Accessing Matrix Values
Use ${{ matrix.variable }} to access the current value:
steps: - name: Print current combination run: | echo "Testing ${{ matrix.language }} version ${{ matrix.version }}"
Practical Examples
1. Multi-Version Node.js Testing
name: Node.js CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [16.x, 18.x, 20.x, 21.x] steps: - uses: actions/checkout@v4 - name: Setup Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Install dependencies run: npm ci - name: Run tests run: npm test - name: Run lint run: npm run lint
Result: Tests on Node 16, 18, 20, and 21 simultaneously.
2. Cross-Platform Testing
name: Cross-Platform Tests on: [push, pull_request] jobs: test: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] python-version: ['3.9', '3.10', '3.11', '3.12'] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip pip install -r requirements.txt - name: Run tests run: pytest
Result: 3 OSes Γ 4 Python versions = 12 parallel jobs
3. Matrix with Multiple Dimensions
strategy: matrix: os: [ubuntu-latest, windows-latest] node-version: [18, 20] database: [postgres, mysql, sqlite] include: # Add specific combination - os: macos-latest node-version: 20 database: postgres steps: - name: Start database run: | echo "Starting ${{ matrix.database }} on ${{ matrix.os }}"
Result: (2 Γ 2 Γ 3) + 1 = 13 jobs
Advanced Features
1. Include - Add Specific Combinations
strategy: matrix: os: [ubuntu-latest] node-version: [18, 20] include: # Test Node 16 only on Ubuntu - os: ubuntu-latest node-version: 16 # Test Node 20 on macOS too - os: macos-latest node-version: 20 # Add extra variables for specific combination - os: ubuntu-latest node-version: 20 experimental: true
2. Exclude - Remove Unwanted Combinations
strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] node-version: [16, 18, 20] exclude: # Node 16 not supported on Windows - os: windows-latest node-version: 16 # Save runners - skip macOS for Node 18 - os: macos-latest node-version: 18
Result: (3 Γ 3) - 2 = 7 jobs instead of 9
3. Fail-Fast (Default: true)
strategy: fail-fast: false # Continue other jobs even if one fails matrix: version: [1, 2, 3, 4, 5]
fail-fast: true (default): Cancels all jobs if one fails
fail-fast: false: Runs all independently
4. Max-Parallel - Limit Concurrent Executions
strategy: max-parallel: 3 # Only 3 jobs at a time matrix: version: [1, 2, 3, 4, 5, 6, 7, 8]
Useful for: - Saving CI minutes - Avoiding external API rate limiting - Limiting resource usage
Real-World Use Cases
1. Multi-Version NPM Library
name: NPM Package CI on: [push, pull_request] jobs: test: strategy: matrix: node-version: [16, 18, 20, 22] os: [ubuntu-latest, windows-latest, macos-latest] exclude: # Node 22 still experimental - os: windows-latest node-version: 22 runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm ci - run: npm test - run: npm run build - name: Test installation run: | npm pack npm install -g *.tgz
2. Application with Multiple Databases
strategy: matrix: database: - type: postgres version: '14' port: 5432 - type: postgres version: '15' port: 5432 - type: mysql version: '8.0' port: 3306 - type: mariadb version: '10.11' port: 3306 steps: - name: Start ${{ matrix.database.type }} run: | docker run -d \ -p ${{ matrix.database.port }}:${{ matrix.database.port }} \ ${{ matrix.database.type }}:${{ matrix.database.version }} - name: Run migrations env: DATABASE_URL: ${{ matrix.database.type }}://localhost:${{ matrix.database.port }}/test run: npm run migrate - name: Run tests run: npm test
3. Multi-Architecture Build
strategy: matrix: include: - arch: amd64 os: ubuntu-latest - arch: arm64 os: ubuntu-latest - arch: armv7 os: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Build for ${{ matrix.arch }} run: | docker buildx build \ --platform linux/${{ matrix.arch }} \ -t myapp:${{ matrix.arch }} \ .
Naming and Identification
Dynamic Job Name
jobs: test: name: Test on ${{ matrix.os }} with Node ${{ matrix.node-version }} strategy: matrix: os: [ubuntu-latest, windows-latest] node-version: [18, 20] runs-on: ${{ matrix.os }}
Result in UI: - Test on ubuntu-latest with Node 18 - Test on ubuntu-latest with Node 20 - Test on windows-latest with Node 18 - Test on windows-latest with Node 20
Performance and Costs
Runtime Consumption
Example: Matrix with 12 combinations, each running 5 minutes.
fail-fast: true (if none fail): 5 minutes Γ 12 = 60 runner minutes
fail-fast: true (if one fails at 2min): ~2-5 minutes (cancels others)
fail-fast: false: Always 60 minutes
Optimizations
- Use aggressive caching:
- uses: actions/cache@v4 with: path: ~/.npm key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
- Prioritize important combinations:
include: - os: ubuntu-latest node-version: 20 priority: high # Run this first
- Use max-parallel to control cost:
strategy: max-parallel: 2 # Max 2 jobs at a time on free accounts
Debugging Matrix
See All Combinations
jobs: debug: strategy: matrix: os: [ubuntu, windows, macos] version: [1, 2, 3] steps: - name: Print matrix run: | echo "OS: ${{ matrix.os }}" echo "Version: ${{ matrix.version }}" echo "Runner: ${{ runner.os }}"
Test One Combination
Use workflow_dispatch with inputs:
on: workflow_dispatch: inputs: os: type: choice options: [ubuntu-latest, windows-latest, macos-latest] node-version: type: choice options: [16, 18, 20] jobs: test: runs-on: ${{ inputs.os }} steps: - uses: actions/setup-node@v4 with: node-version: ${{ inputs.node-version }}
Best Practices
- β Always test LTS + latest versions
- β Use fail-fast: false in PRs to see all issues
- β Document which combinations are mandatory
- β Use exclude to save on non-critical combinations
- β Name jobs descriptively
- β Avoid very large matrices (>20 combinations)
- β Don't duplicate logic - use matrix
Conclusion
Matrix Strategy transforms:
# From 100+ lines of duplicated code test-node-16-ubuntu: ... test-node-18-ubuntu: ... test-node-20-ubuntu: ... test-node-16-windows: ... # ... etc
Into:
# 20 elegant lines strategy: matrix: os: [ubuntu, windows, macos] node: [16, 18, 20]
Benefits: - β±οΈ Time savings - parallel testing - π― More coverage - multiple combinations easily - π§ Simple maintenance - one place to update - π° Cost control - max-parallel and exclude
Already using Matrix Strategy? Share in the comments how many combinations you test!