3.4 Matrix Strategies & Multi-Platform Builds
Key Takeaways
- The strategy.matrix configuration generates a multi-dimensional Cartesian product of job permutations, running each permutation as a distinct, isolated job across operating systems, runtimes, or configurations.
- The include keyword allows augmenting existing matrix combinations with extra keys or adding completely new standalone combinations to the execution matrix.
- The exclude keyword selectively prunes specific unwanted or unsupported combinations from the Cartesian product matrix.
- By default, fail-fast: true cancels all in-progress and queued matrix jobs if any single combination fails; setting fail-fast: false allows all permutations to execute to completion.
- Dynamic matrices can be generated at runtime by executing an upstream setup job that emits a JSON array string to $GITHUB_OUTPUT and parsing it with ${{ fromJSON(...) }} in strategy.matrix.
Matrix Strategies & Multi-Platform Builds
Enterprise software must frequently support multiple operating systems, architecture targets, runtime versions, and database engines. Rather than duplicating job definitions across dozens of YAML files, GitHub Actions provides the matrix execution strategy (strategy.matrix). A matrix automatically generates a multi-dimensional array of job permutations, executing each combination in parallel across isolated runners.
1. Dimensional Matrix Generation & Cartesian Products
When you specify arrays of variables under strategy.matrix, GitHub Actions calculates the Cartesian product of all supplied dimensions. Every permutation becomes a separate job execution in the workflow graph.
jobs:
cross-platform-test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- name: Setup Node ${{ matrix.node-version }} on ${{ matrix.os }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
Visualizing the Matrix Combination Tree
+-----------------------------------------------------------------------------+
| MATRIX CARTESIAN PRODUCT TREE |
| |
| [ MATRIX ROOT ] |
| | |
| +----------------------+----------------------+ |
| | | | |
| [ ubuntu-latest ] [ windows-latest ] [ macos-latest ] |
| / | \ / | \ / | \ |
| v18 v20 v22 v18 v20 v22 v18 v20 v22 |
| | | | | | | | | | |
| Job 1 Job 2 Job 3 Job 4 Job 5 Job 6 Job 7 Job 8 Job 9 |
| |
| Total Permutations: 3 x 3 = 9 Distinct Jobs |
+-----------------------------------------------------------------------------+
Inside each matrix job instance, the variable values for that specific permutation are accessed using ${{ matrix.<key_name> }}.
2. Fine-Tuning Permutations with include and exclude
Real-world build matrices rarely require a pure Cartesian product. Certain OS/runtime combinations may be unsupported, or specific configurations may require extra environment flags.
Pruning Combinations with exclude
The exclude key removes specific combinations generated by the Cartesian product:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]
exclude:
# Exclude Node 18 on macOS (unsupported legacy combination)
- os: macos-latest
node-version: 18
# Exclude Node 22 on Windows
- os: windows-latest
node-version: 22
Result: 9 total initial permutations - 2 excluded = 7 executing jobs.
Augmenting & Expanding with include
The include key serves two distinct purposes depending on whether the specified keys match an existing Cartesian combination:
- Adding Properties to an Existing Combination: If the keys in
includematch an existing permutation, GitHub Actions adds the extra keys/values to that specific job'smatrixcontext. - Adding a Completely New Standalone Combination: If the keys in
includedo not match any existing permutation, GitHub Actions appends a new job permutation to the matrix.
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [20, 22]
include:
# 1. Adds 'experimental: true' ONLY to ubuntu-latest + node 22
- os: ubuntu-latest
node: 22
experimental: true
# 2. Adds 'experimental: false' to all other standard combinations
- os: ubuntu-latest
node: 20
experimental: false
- os: windows-latest
node: 20
experimental: false
- os: windows-latest
node: 22
experimental: false
# 3. Standalone addition: Adds a macOS Canary build not in main dimensions
- os: macos-latest
node: 23
experimental: true
custom-flag: '--enable-canary'
[!IMPORTANT] Execution Order of Matrix Processing: GitHub Actions evaluates matrix configurations in the following strict order:
- Cartesian product of all base matrix keys.
excluderules are applied to prune matching permutations.includerules are applied to inject additional keys or append standalone combinations.
3. Matrix Execution Control: fail-fast & max-parallel
Matrix strategies provide controls for error tolerance and resource management.
1. fail-fast: true (Default) vs fail-fast: false
fail-fast: true(Default): If any single job in the matrix fails, GitHub Actions immediately sends cancellation signals to all other currently running and queued matrix jobs in that strategy. This saves billing minutes during early test failures.fail-fast: false: If one matrix job fails, all other matrix jobs continue running to completion. This is essential for test suites and multi-OS validation, where developers need to see which specific operating systems or versions passed and which failed.
strategy:
fail-fast: false # Ensure all 9 OS/Node combinations run even if one fails
max-parallel: 4 # Limit concurrency to 4 simultaneous running jobs
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
2. max-parallel Concurrency Throttling
By default, GitHub Actions attempts to run as many matrix jobs in parallel as your account tier allows. Setting max-parallel: <number> restricts the maximum number of matrix jobs that can run concurrently. This prevents:
- Exhausting self-hosted runner pool capacity.
- Overwhelming external test databases or third-party APIs with concurrent connections.
- Consuming all available organization runner slots.
4. Dynamic Matrix Generation via JSON & fromJSON()
In advanced enterprise pipelines (such as monorepos or multi-service microservice architectures), static matrices in YAML are insufficient. You may only want to test the specific sub-projects or container images that changed in a given Pull Request.
GitHub Actions supports Dynamic Matrices by allowing strategy.matrix to receive a JSON object parsed via ${{ fromJSON(...) }} from an upstream setup job output.
+-----------------------------------------------------------------------------+
| DYNAMIC MATRIX ARCHITECTURE |
| |
| [ Job 1: setup-matrix ] |
| - Inspects git diff / modified directories |
| - Constructs JSON array string: '["auth-service", "billing-service"]' |
| - Writes to $GITHUB_OUTPUT: matrix_config={"include":[...]} |
| | |
| v |
| [ Job 2: test-matrix ] |
| - needs: setup-matrix |
| - strategy: matrix: ${{ fromJSON(needs.setup-matrix.outputs.matrix_json) }}|
| - Dynamically spawns N jobs matching modified services |
+-----------------------------------------------------------------------------+
Complete End-to-End Dynamic Matrix YAML Example:
name: Enterprise Monorepo Dynamic Matrix
on:
pull_request:
branches: [main]
jobs:
detect-changes:
name: Compute Dynamic Matrix
runs-on: ubuntu-latest
outputs:
matrix-data: ${{ steps.build-matrix.outputs.matrix }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Calculate Changed Services
id: build-matrix
run: |
# In a real workflow, this script queries git diff or directory changes
# Emitting dynamic JSON structure containing matrix arrays
DYNAMIC_JSON='{"include":[{"service":"auth","runtime":"node:20","test_cmd":"npm test"},{"service":"payment","runtime":"golang:1.22","test_cmd":"go test ./..."},{"service":"reporting","runtime":"python:3.11","test_cmd":"pytest"}]}'
echo "matrix=${DYNAMIC_JSON}" >> "$GITHUB_OUTPUT"
execute-tests:
name: "Test: ${{ matrix.service }} (${{ matrix.runtime }})"
needs: detect-changes
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.detect-changes.outputs.matrix-data) }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Execute Service Test Suite
run: |
echo "Testing ${{ matrix.service }} with runtime ${{ matrix.runtime }}"
echo "Command: ${{ matrix.test_cmd }}"
5. Exam Traps & Real-World Matrix Gotchas
- macOS Billing Multipliers: On GitHub-hosted runners, Windows jobs consume 2x and macOS jobs consume 10x standard Linux minutes. A 3x3 matrix spanning Linux, Windows, and macOS will consume runner quota significantly faster than Linux-only matrices.
- Matrix Variables in Step Names: Always include
${{ matrix.<key> }}in the stepname:or jobname:so that individual job results are easily distinguishable in the GitHub Actions UI and log transcripts. - Empty Dynamic Matrix Error: If an upstream job produces an empty JSON array
[]forstrategy.matrix, the downstream job will fail with a workflow execution error. Always handle empty change sets by conditionally skipping downstream jobs usingif: ${{ needs.detect-changes.outputs.matrix-data != '' && needs.detect-changes.outputs.matrix-data != '[]' }}.
A workflow executes a test matrix across 6 operating system and database combinations. During execution, one combination fails after 20 seconds. By default, what action does GitHub Actions take regarding the remaining 5 combinations?
A workflow author specifies a matrix with os: [ubuntu-latest, windows-latest] and node: [18, 20]. Under the include: key, they add - { os: macos-latest, node: 22 }. How does GitHub Actions evaluate this include entry?
An enterprise monorepo workflow dynamically generates a list of microservices to test. Job setup calculates a JSON array string and writes it to $GITHUB_OUTPUT as service_matrix. Which configuration correctly passes this dynamic list to the downstream job test?