Skip to content
7 min read·Lesson 6 of 8

Matrix Builds and Reusable Workflows

Fan out across versions/platforms with matrix strategies, then share entire pipelines via reusable workflows and workflow_call.

Two features make GitHub Actions scale beyond toy pipelines: matrix strategies (parallelism across versions) and reusable workflows (sharing pipelines across repos). This lesson teaches both.

Matrix Strategy Basics

Suppose you need to test a library on Node 18, 20, and 22, on both Linux and Windows. Writing six near-duplicate jobs would be painful. The matrix strategy fans out one job definition into multiple variants:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test

This produces 6 jobs (2 OSes × 3 Node versions). Each runs in parallel on its own runner.

Include and Exclude

Add specific combinations or trim the matrix:

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20, 22]
    include:
      - os: ubuntu-latest
        node: 20
        coverage: true            # extra variable only on this variant
    exclude:
      - os: windows-latest
        node: 18                  # skip this combination

Failure Behaviour

strategy:
  fail-fast: false            # default is true
  max-parallel: 4             # cap concurrency
  matrix: ...
  • fail-fast: true (default) cancels remaining variants the moment one fails — saves minutes
  • fail-fast: false lets every variant run to completion — better for "show me all the failures" CI
  • max-parallel throttles concurrency — useful when matrix variants compete for a shared resource (DB, API quota)

Dynamic Matrices

You can generate the matrix at runtime from JSON:

jobs:
  list-targets:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set.outputs.matrix }}
    steps:
      - id: set
        run: echo "matrix=$(./scripts/list-targets.js)" >> $GITHUB_OUTPUT

  build:
    needs: list-targets
    runs-on: ubuntu-latest
    strategy:
      matrix: ${{ fromJson(needs.list-targets.outputs.matrix) }}
    steps:
      - run: ./build.sh ${{ matrix.target }}

This pattern powers monorepo CI — list the changed packages dynamically, then fan a build job across them.

Reusable Workflows

A reusable workflow is a workflow file with on: workflow_call: as its trigger. It can be invoked from other workflows — including across repositories — with typed inputs and secrets.

Defining a reusable workflow

# .github/workflows/reusable-build.yml
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
      node-version:
        required: false
        type: number
        default: 20
    secrets:
      NPM_TOKEN:
        required: true
    outputs:
      image-tag:
        description: 'Built image tag'
        value: ${{ jobs.build.outputs.tag }}

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.tag.outputs.tag }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: npm ci
      - id: tag
        run: echo "tag=ghcr.io/acme/app:${{ github.sha }}" >> $GITHUB_OUTPUT

Calling it from another workflow

jobs:
  build:
    uses: acme-org/shared-workflows/.github/workflows/reusable-build.yml@v3
    with:
      environment: staging
      node-version: 22
    secrets:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh "${{ needs.build.outputs.image-tag }}"

You can pass secrets: inherit to forward all the caller's secrets verbatim — convenient but blunt. For sensitive cases, name each secret explicitly so the called workflow's authors can audit what it has access to.

Composite Action vs Reusable Workflow

ConcernComposite actionReusable workflow
ScopeSteps inside one jobEntire workflow (multiple jobs)
Runs onThe caller's runnerIts own runner
Can use other actionsYesYes
Can call other reusable workflowsNoYes (up to 4 nesting levels)
Best for"Set up project" sub-recipes"Build and deploy" full pipelines

The Internal Platform Pattern

A common shape at large orgs: a platform-workflows repo holds reusable workflows for "build a Node service", "build a Go service", "deploy to k8s", etc. Every application repo invokes those reusable workflows from a tiny CI file. Centralising the pipelines means upgrades, security improvements, and policy changes happen once.

Versioning Reusable Workflows

Pin reusable workflows the same way you pin actions — to a tag or SHA, not @main. The reusable workflow can break your pipeline if it changes; pinning gives you control over when to absorb the change.

Key Takeaways

  • Matrix strategies fan a single job into N variants across versions, OSes, or any custom axes.
  • Use include: and exclude: to add or remove specific combinations from the matrix.
  • fail-fast: false keeps other matrix variants running after one fails — useful for full-coverage CI.
  • Reusable workflows (workflow_call) let one workflow invoke another with typed inputs and secrets.
  • Composite actions reuse steps within a job; reusable workflows reuse entire pipelines.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →