Skip to content

The matrix strategy in GitHub Actions

One powerful feature within GitHub Actions is the matrix strategy. This strategy enables you to run jobs across multiple versions of languages, operating systems, or other varying parameters. It’s particularly useful for ensuring that your application works across different environments without needing to manually duplicate workflows.

How the matrix strategy works

The matrix strategy in GitHub Actions allows you to define a set of different configurations (like operating system versions, programming language versions, etc.) and GitHub Actions will automatically create and run a job for each combination of these configurations. This is done within the strategy block of a workflow file, using the matrix key.

Benefits of using a matrix strategy

  • Efficiency: Automatically generates test runs across multiple configurations.
  • Simplicity: Reduces redundancy and keeps your workflows readable and maintainable.
  • Coverage: Increases the test coverage by testing across multiple environments, architectures, software versions, etc.

Example: compiling across multiple architectures using docker

Let’s consider a scenario where you need to build and push Docker images for multiple architectures using GitHub Actions. This is a common requirement for ensuring that your Docker images are compatible with different hardware architectures like amd64, arm64, etc.

Workflow setup

We can use the docker/build-push-action GitHub Action, which simplifies building and pushing Docker images. Here’s how you can set up a workflow with a matrix strategy to build for multiple architectures:

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@v4
- 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.