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.
Last updated: August 2026

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:

  1. Adding Properties to an Existing Combination: If the keys in include match an existing permutation, GitHub Actions adds the extra keys/values to that specific job's matrix context.
  2. Adding a Completely New Standalone Combination: If the keys in include do 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:

  1. Cartesian product of all base matrix keys.
  2. exclude rules are applied to prune matching permutations.
  3. include rules 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 step name: or job name: 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 [] for strategy.matrix, the downstream job will fail with a workflow execution error. Always handle empty change sets by conditionally skipping downstream jobs using if: ${{ needs.detect-changes.outputs.matrix-data != '' && needs.detect-changes.outputs.matrix-data != '[]' }}.
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D