RunsOn RunsOn

The matrix strategy in GitHub Actions

Understand the matrix strategy in GitHub Actions and how to use it to run jobs across multiple configurations.

Matrix strategy

The matrix strategy runs jobs across multiple configurations - different OS versions, language versions, or other parameters. This tests your application across environments without duplicating workflows.

How the matrix strategy works

Define configurations in the strategy block using the matrix key. GitHub Actions automatically creates and runs a job for each combination.

Benefits of using a matrix strategy

  • Efficiency: Automatically generates test runs across multiple configurations
  • Simplicity: Reduces redundancy and keeps workflows maintainable
  • Coverage: Tests across multiple environments, architectures, and software versions

Example: compiling across multiple architectures using docker

This example builds and pushes Docker images for multiple architectures (amd64, arm64) using GitHub Actions.

Workflow setup

Using docker/build-push-action with a matrix strategy:

name: Build and push Docker images

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        platform:
          - linux/arm64/v8
          - linux/amd64

    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

      - name: Set up Docker buildx
        id: buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to DockerHub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          file: Dockerfile
          push: true
          tags: REPO/APP:MY_TAG
          platforms: ${{ matrix.platform }}

Explanation of the workflow

  1. Trigger: The workflow triggers on a push to the main branch.
  2. Matrix strategy: Defines a matrix of platforms to build a docker image for (linux/amd64 and linux/arm64/v8). GitHub Actions will create two jobs, one for each architecture.
  3. Docker actions:
    • Set up Docker buildx: Prepares the Docker buildx builder, which is a part of Docker CLI starting from 19.03 and supports building multi-architecture images.
    • Login to DockerHub: Logs into DockerHub to allow pushing the images.
    • Build and push: Builds the Docker image and pushes it to DockerHub.

This setup ensures that your Docker images are built for both linux/amd64 and linux/arm64/v8 architectures automatically whenever changes are pushed to the main branch.

Advanced features of the matrix strategy

Excluding configurations

In complex workflows, you might not need to run a job for every possible combination in a matrix. GitHub Actions allows you to explicitly exclude certain configurations using the exclude keyword. This is particularly useful when certain combinations are known to be incompatible or unnecessary.

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20]
    exclude:
      - os: ubuntu-latest
        node: 18

In this example, the job will run for all combinations except for Ubuntu with Node 18.

Including additional configurations

Conversely, you can include additional configurations that are not part of the standard matrix combinations using the include keyword. This allows for testing against specific versions or settings without creating a completely separate workflow.

strategy:
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20]
    include:
      - os: macos-latest
        node: 20
        additional: "experimental feature"

Here, an additional job for macOS with Node 20 and an experimental feature is added to the matrix.

Dynamic matrix generation

For projects that require a dynamic configuration, you can generate the matrix based on the output of a previous step or job. This is achieved by setting the matrix via expressions and using outputs from previous steps.

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - id: set-matrix
        run: |
          echo "matrix={\"include\": [{\"os\": \"ubuntu-latest\", \"node\": \"20\"}]}" >> "$GITHUB_OUTPUT"

  test:
    needs: build
    strategy:
      matrix: ${{fromJson(needs.build.outputs.matrix)}}
    runs-on: ${{ matrix.os }}
    steps:
      - run: npm test

This setup dynamically creates a matrix for the test job based on the output from the build job.

Fail-fast behavior

By default, GitHub Actions uses a fail-fast strategy for matrix jobs. This means if one job in the matrix fails, all other jobs that are still running will be cancelled. This can save time and resources when an issue is detected early in one of the configurations. However, you might want to disable this behavior if you need results from all matrix jobs, regardless of individual failures.

strategy:
  fail-fast: false
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [18, 20]

Setting fail-fast to false ensures that all jobs complete, providing a full set of results for analysis.

Max parallel jobs

To manage resource consumption and possibly improve build times if using self-hosted runners, you can limit the number of jobs that run in parallel using the max-parallel key. This is particularly useful when you have a large matrix and a limited number of runners available.

strategy:
  max-parallel: 2
  matrix:
    os: [ubuntu-latest, windows-latest]
    node: [14, 16, 18, 20]

In this configuration, no more than two jobs will run at the same time, even if more runners are available. This helps in managing the load on the available infrastructure and can be adjusted based on the specific needs of the project or the CI environment.

Using the matrix context and strategy.job-index variable

GitHub Actions provides a matrix context that can be used within a job to access the current matrix configuration. Additionally, the strategy.job-index variable is available to identify the current job’s index within the matrix strategy. This can be particularly useful for tasks that need to reference the position or order of the job.

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node: [18, 20]
    runs-on: ${{ matrix.os }}
    steps:
      - name: Display matrix and index
        run: |
          echo "Running on ${{ matrix.os }} with Node.js ${{ matrix.node }}"
          echo "This is matrix job #${{ strategy.job-index }}"

In this example, each job will output which operating system and Node.js version it is using, along with its index in the matrix. This index is zero-based, so the first job in the matrix will have an index of 0.

The strategy.job-index variable is especially useful for parallel testing scenarios where you might want to split tests evenly or assign specific tests to certain matrix jobs based on their index.