GitHub Actions matrices

Greg Foster
Greg Foster
Graphite software engineer


Note

This guide explains this concept in vanilla Git. For Graphite documentation, see our CLI docs.


For many teams, continuous integration (CI) tools like GitHub Actions are an essential piece of the software delivery lifecycle. As such, it is important that the CI workflows be robust enough to handle a myriad of use cases while still being ergonomic enough to be accessible. One feature that GitHub Actions offers to accomplish this is matrix strategies.

The matrix strategy is a feature that allows you to create a single job definition and use it to run multiple jobs depending on some input variables. You can provide several variables, and GitHub runs instances of your job for each combination of these variables, up to a maximum of 256 jobs per workflow run.

Your basic matrix definition might look like this:

Terminal
jobs:
example_matrix:
strategy:
matrix:
os: [ubuntu-22.04, ubuntu-20.04]
version: [10, 12, 14]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.version }}

Credit: GitHub Docs

In this case, there would be six jobs run:

  • ubuntu-22.04, node 10
  • ubuntu-22.04, node 12
  • ubuntu-22.04, node 14
  • ubuntu-20.04, node 10
  • ubuntu-20.04, node 12
  • ubuntu-20.04, node 14

There are many powerful use cases for matrices in GitHub Actions. Consider the following recipes you can try:

If you need to ensure your code works against several language versions, you can define these in the matrix and run your tests against each of them:

Terminal
jobs:
test:
name: Test with PHP ${{ matrix.php }}
runs-on: ubuntu-latest
strategy:
matrix:
php: ["7.4", "8.0", "8.1", "8.2", "8.3"]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Run PHPUnit tests
run: php vendor/bin/phpunit

You might have a case where you need to run a large test suite that takes a long time. You can reduce the time developers are waiting on CI by running tests in parallel. This is straightforward with matrices:

Terminal
jobs:
test:
name: Run Cypress Tests - Matrix ${{ matrix.index }}
runs-on: ubuntu-latest
strategy:
matrix:
index: [1, 2, 3, 4, 5]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: npm install
- name: Run Cypress tests
run: |
npm run cypress:run --spec "cypress/integration/test${{ matrix.index }}.spec.js"

If you don't want to split your tests manually, there are tools that can automate this for you, leading to a rather refined parallel testing setup.

Beyond testing, matrices can be leveraged to automate many kinds of repetitive jobs. Consider a case where you might want to apply a Kubernetes manifest to several namespaces:

Terminal
jobs:
deploy:
name: Deploy to Kubernetes
runs-on: ubuntu-latest
strategy:
matrix:
namespace: [development, staging, production]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up kubectl
uses: azure/setup-kubectl@v1
with:
kubeconfig: ${{ secrets.KUBE_CONFIG_DATA }}
- name: Apply manifest
run: kubectl apply -f path/to/manifest.yaml --namespace=${{ matrix.namespace }}

Matrices give you the ability to reduce duplication by creating generic job definitions that can serve multiple purposes. If the alternative is writing and maintaining multiple job definitions just to test different language versions, you can see how matrices are an ergonomic improvement.

They also give you a simple mechanism to run jobs in parallel, which can be a huge time-saver in CI. This is important because any time spent waiting for pipelines to run is potentially lost productive time.

Matrix strategies are powerful, but there are still some things to be aware of when using them. Be mindful that for each variable you introduce, the number of jobs that are run is increased. This can dramatically increase CI costs and runtime if you're not careful.

Another thing you should take advantage of is the compounding benefit of using matrices alongside other advanced GitHub Actions syntax, such as conditional expressions. Consider this snippet:

Terminal
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
strategy:
matrix:
environment: [development, staging, production]
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up environment
run: echo "Setting up environment for ${{ matrix.environment }}"
- name: Run unit tests
run: npm run test:unit
if: matrix.environment != 'production' # Only run unit tests for non-production environments
- name: Run integration tests
run: npm run test:integration
if: matrix.environment == 'staging' # Only run integration tests for the staging environment

While the same base job definition is used for all the jobs in the matrix, you can leverage conditional expressions to alter the behavior in some jobs depending on the matrix variables. This means you can still have a degree of fine control without needing to resort to fully duplicating the job definitions.

The GitHub Actions matrix strategy is a powerful tool that can help you supercharge your workflow automation. Any case where you have multiple similar jobs that need to run is a potential candidate for a matrix strategy. Be on the lookout for where you can leverage this powerful pattern!

Graphite
Git stacked on GitHub

Stacked pull requests are easier to read, easier to write, and easier to manage.
Teams that stack ship better software, faster.

Or install our CLI.
Product Screenshot 1
Product Screenshot 2