| name | matrix-optimizer |
| description | Optimize GitHub Actions matrix strategies for testing across multiple versions, platforms, and configurations. Use when configuring matrix builds, testing multiple versions, cross-platform testing, or optimizing CI resource usage. Trigger words include "matrix strategy", "test matrix", "multiple versions", "cross-platform". |
Matrix Optimizer
Configure and optimize GitHub Actions matrix strategies for efficient multi-version and multi-platform testing.
Quick Start
Basic matrix for testing multiple Node.js versions:
strategy:
matrix:
node-version: [16, 18, 20]
Instructions
Step 1: Identify Matrix Dimensions
Common matrix dimensions:
- Language versions: Node.js, Python, Ruby, Go versions
- Operating systems: ubuntu, macos, windows
- Architectures: x64, arm64
- Dependency versions: Database versions, framework versions
- Feature flags: Different configuration options
Example dimensions:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [16, 18, 20]
# This creates 9 jobs (3 OS × 3 versions)
Step 2: Configure Matrix Strategy
Basic matrix:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node-version: [18, 20]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm test
Matrix with include:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node-version: [18, 20]
include:
# Add specific combination
- os: windows-latest
node-version: 20
# Add extra variables for specific combination
- os: ubuntu-latest
node-version: 20
experimental: true
Matrix with exclude:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [16, 18, 20]
exclude:
# Skip Node 16 on Windows
- os: windows-latest
node-version: 16
# Skip Node 16 on macOS
- os: macos-latest
node-version: 16
Step 3: Optimize for Cost and Speed
Fail-fast strategy:
strategy:
fail-fast: false # Continue all jobs even if one fails
matrix:
node-version: [16, 18, 20]
Max parallel jobs:
strategy:
max-parallel: 2 # Limit concurrent jobs
matrix:
node-version: [16, 18, 20]
Conditional matrix:
strategy:
matrix:
os: [ubuntu-latest]
# Add more OS only on main branch
${{ github.ref == 'refs/heads/main' && fromJSON('["macos-latest", "windows-latest"]') || fromJSON('[]') }}
Step 4: Use Matrix Variables
In job steps:
steps:
- name: Display matrix values
run: |
echo "OS: ${{ matrix.os }}"
echo "Version: ${{ matrix.node-version }}"
echo "Experimental: ${{ matrix.experimental }}"
In job configuration:
jobs:
test:
runs-on: ${{ matrix.os }}
continue-on-error: ${{ matrix.experimental == true }}
strategy:
matrix:
os: [ubuntu-latest]
node-version: [18, 20]
include:
- node-version: 21
experimental: true
Step 5: Name Jobs Clearly
jobs:
test:
name: Test on ${{ matrix.os }} with Node ${{ matrix.node-version }}
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
node-version: [18, 20]
Common Patterns
Language Version Matrix
Node.js:
strategy:
matrix:
node-version: [16, 18, 20]
steps:
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
Python:
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
Go:
strategy:
matrix:
go-version: ['1.20', '1.21', '1.22']
steps:
- uses: actions/setup-go@v4
with:
go-version: ${{ matrix.go-version }}
Cross-Platform Matrix
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
include:
# Platform-specific configurations
- os: ubuntu-latest
install-cmd: sudo apt-get install
- os: macos-latest
install-cmd: brew install
- os: windows-latest
install-cmd: choco install
steps:
- name: Install dependencies
run: ${{ matrix.install-cmd }} package-name
Database Version Matrix
strategy:
matrix:
postgres-version: [12, 13, 14, 15]
services:
postgres:
image: postgres:${{ matrix.postgres-version }}
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
Feature Flag Matrix
strategy:
matrix:
feature:
- name: baseline
flags: ''
- name: new-parser
flags: '--enable-new-parser'
- name: experimental
flags: '--enable-experimental'
steps:
- name: Run tests
run: npm test ${{ matrix.feature.flags }}
Optimization Strategies
Reduce Matrix Size
Before (12 jobs):
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node-version: [16, 18, 20, 21]
After (7 jobs):
matrix:
# Test all versions on Linux only
os: [ubuntu-latest]
node-version: [16, 18, 20, 21]
include:
# Test latest version on other platforms
- os: macos-latest
node-version: 21
- os: windows-latest
node-version: 21
Conditional Matrix Expansion
strategy:
matrix:
# Always test on Linux
os: [ubuntu-latest]
node-version: [18, 20]
include:
# Full matrix only on main branch or release tags
- ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') }}:
os: [macos-latest, windows-latest]
Parallel vs Sequential
High parallelism (faster, more expensive):
strategy:
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
steps:
- run: npm test -- --shard=${{ matrix.shard }}/8
Limited parallelism (slower, cheaper):
strategy:
max-parallel: 2
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
Caching Across Matrix
steps:
- uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-${{ matrix.node-version }}-
${{ runner.os }}-node-
Advanced Patterns
Dynamic Matrix from JSON
jobs:
setup:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
steps:
- id: set-matrix
run: |
# Generate matrix dynamically
MATRIX='{"include":[{"os":"ubuntu-latest","version":"18"},{"os":"macos-latest","version":"20"}]}'
echo "matrix=$MATRIX" >> $GITHUB_OUTPUT
test:
needs: setup
strategy:
matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
runs-on: ${{ matrix.os }}
steps:
- run: echo "Testing on ${{ matrix.os }} with version ${{ matrix.version }}"
Matrix with Outputs
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
outputs:
result-${{ matrix.os }}: ${{ steps.test.outputs.result }}
steps:
- id: test
run: echo "result=passed" >> $GITHUB_OUTPUT
Reusable Matrix Workflow
# .github/workflows/reusable-matrix.yml
on:
workflow_call:
inputs:
versions:
required: true
type: string
jobs:
test:
strategy:
matrix:
version: ${{ fromJSON(inputs.versions) }}
runs-on: ubuntu-latest
steps:
- run: echo "Testing version ${{ matrix.version }}"
# Caller workflow
jobs:
test:
uses: ./.github/workflows/reusable-matrix.yml
with:
versions: '["18", "20", "21"]'
Troubleshooting
Too many jobs:
- Use
excludeto remove unnecessary combinations - Test all versions on one OS, latest version on others
- Use conditional matrix expansion for PRs vs main
Jobs failing inconsistently:
- Set
fail-fast: falseto see all failures - Check for race conditions or timing issues
- Verify platform-specific dependencies
Slow matrix execution:
- Increase
max-parallelif budget allows - Optimize caching strategy
- Consider test sharding within jobs
Matrix not expanding:
- Verify JSON syntax in
fromJSON() - Check that matrix variables are properly referenced
- Ensure
includeandexcludesyntax is correct
Best Practices
- Start small: Begin with minimal matrix, expand as needed
- Test locally first: Verify one configuration works before expanding
- Use fail-fast: false: See all failures, not just first
- Name jobs clearly: Include matrix values in job names
- Cache effectively: Use matrix values in cache keys
- Optimize for PRs: Smaller matrix for PRs, full matrix for main
- Document matrix: Explain why each dimension is needed
- Monitor costs: Track runner minutes usage