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.
What are 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:
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@v4with:node-version: ${{ matrix.version }}
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
When to use matrix strategies
There are many powerful use cases for matrices in GitHub Actions. Consider the following recipes you can try:
Run PHPUnit tests against several versions
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:
jobs:test:name: Test with PHP ${{ matrix.php }}runs-on: ubuntu-lateststrategy:matrix:php: ["7.4", "8.0", "8.1", "8.2", "8.3"]steps:- name: Checkout codeuses: actions/checkout@v3- name: Set up PHPuses: shivammathur/setup-php@v2with:php-version: ${{ matrix.php }}- name: Install dependenciesrun: composer install --prefer-dist --no-progress --no-suggest- name: Run PHPUnit testsrun: php vendor/bin/phpunit
Run slow tests concurrently by splitting them into chunks
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:
jobs:test:name: Run Cypress Tests - Matrix ${{ matrix.index }}runs-on: ubuntu-lateststrategy:matrix:index: [1, 2, 3, 4, 5]steps:- name: Checkout codeuses: actions/checkout@v2- name: Install dependenciesrun: npm install- name: Run Cypress testsrun: |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.
Run Jobs against Multiple Environments
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:
jobs:deploy:name: Deploy to Kubernetesruns-on: ubuntu-lateststrategy:matrix:namespace: [development, staging, production]steps:- name: Checkout codeuses: actions/checkout@v2- name: Set up kubectluses: azure/setup-kubectl@v1with:kubeconfig: ${{ secrets.KUBE_CONFIG_DATA }}- name: Apply manifestrun: kubectl apply -f path/to/manifest.yaml --namespace=${{ matrix.namespace }}
Benefits of matrix strategies
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.
Best Practices
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:
jobs:test:name: Run Testsruns-on: ubuntu-lateststrategy:matrix:environment: [development, staging, production]steps:- name: Checkout codeuses: actions/checkout@v2- name: Set up environmentrun: echo "Setting up environment for ${{ matrix.environment }}"- name: Run unit testsrun: npm run test:unitif: matrix.environment != 'production' # Only run unit tests for non-production environments- name: Run integration testsrun: npm run test:integrationif: 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.
Wrapping up
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!