GitHub Actions Tutorial — Dynamic Matrix

2 minute read

Dynamic Matrix

Instead of hard-coding matrix values in YAML, you can generate them at runtime. This is essential for monorepos, conditional testing, and pipelines that adapt to what changed.


fromJSON() Pattern

Use fromJSON() to convert a JSON string output into a matrix:


jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set.outputs.matrix }}
    steps:
      - id: set
        run: |
          echo 'matrix={"node":[18,20,22],"os":["ubuntu-latest","windows-latest"]}' >> "$GITHUB_OUTPUT"

  test:
    needs: setup
    strategy:
      matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: node --version


Generate Matrix from Changed Files

Build a matrix based on which packages changed in a monorepo:


jobs:
  detect:
    runs-on: ubuntu-latest
    outputs:
      packages: ${{ steps.changes.outputs.packages }}
      has-changes: ${{ steps.changes.outputs.has-changes }}
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Detect changed packages
        id: changes
        run: |
          CHANGED=$(git diff --name-only HEAD~1 HEAD | grep '^packages/' | cut -d/ -f2 | sort -u | jq -R . | jq -sc .)
          if [ "$CHANGED" = "[]" ]; then
            echo "has-changes=false" >> "$GITHUB_OUTPUT"
            echo 'packages=[]' >> "$GITHUB_OUTPUT"
          else
            echo "has-changes=true" >> "$GITHUB_OUTPUT"
            echo "packages=$CHANGED" >> "$GITHUB_OUTPUT"
          fi

  test:
    needs: detect
    if: needs.detect.outputs.has-changes == 'true'
    strategy:
      matrix:
        package: ${{ fromJSON(needs.detect.outputs.packages) }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: |
          cd packages/${{ matrix.package }}
          npm ci && npm test

Only changed packages get tested — no wasted CI minutes.


Generate Matrix from a Config File

Read matrix configuration from a JSON file in the repository:


jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.read.outputs.matrix }}
    steps:
      - uses: actions/checkout@v4
      - name: Read matrix config
        id: read
        run: |
          MATRIX=$(cat .github/matrix-config.json)
          echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"

  build:
    needs: setup
    strategy:
      matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
    runs-on: ${{ matrix.os }}
    steps:
      - run: echo "Building for ${{ matrix.os }} with ${{ matrix.compiler }}"

.github/matrix-config.json:

{
  "os": ["ubuntu-latest", "windows-latest"],
  "compiler": ["gcc", "clang"]
}

Conditional Matrix Expansion

Include different versions based on the branch:


jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set.outputs.matrix }}
    steps:
      - name: Generate matrix
        id: set
        run: |
          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
            echo 'matrix={"node":[18,20,22],"os":["ubuntu-latest","windows-latest","macos-latest"]}' >> "$GITHUB_OUTPUT"
          else
            echo 'matrix={"node":[20],"os":["ubuntu-latest"]}' >> "$GITHUB_OUTPUT"
          fi

  test:
    needs: setup
    strategy:
      matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm test

  • main branch: Full 9-job matrix (thorough testing)
  • Feature branches: Single-job matrix (fast feedback)

Common Mistakes

Mistake Fix
fromJSON() fails Output must be valid JSON — use jq to construct it
Empty matrix Dynamic matrix with empty array creates 0 jobs and doesn’t fail — add if: guard
Output truncated GitHub outputs have a 1 MB limit — keep matrix JSON small
Matrix keys don’t match Output JSON keys must match what the matrix: block expects

Quick Reference

Pattern Use Case
fromJSON() Convert step output to matrix
Changed files Test only affected packages
Config file Centralize matrix in .json file
Branch-conditional Full matrix on main, minimal on feature
jq Build JSON arrays from shell commands

← Previous: Fail-Fast & Parallel

Next: Matrix Patterns →

Updated: