Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 44

Matrix Builds for Multi-Environment Testing

Recommended resources

Hello! Welcome back to our module on CI/CD Pipeline Mastery.

In our last lesson, we focused on orchestrating workflows by creating dependencies between jobs with needs and sharing data with artifacts. You learned how to build sequential pipelines where the output of one job becomes the input for the next.

Today, we'll tackle a common challenge in DevOps: how do you test your application across multiple environments without duplicating your entire CI pipeline? This lesson addresses that question directly, focusing on the learning outcome: Implement matrix builds in GitHub Actions to test across multiple environments or versions.

You already write GitHub Actions workflows, but mastering matrix strategies will significantly enhance your ability to create efficient, scalable, and maintainable pipelines—moving you closer to writing them "by heart." We'll explore how to run a single job definition across various combinations of operating systems, language versions, or other parameters, a technique that is fundamental to robust continuous integration.

1. What is a Matrix Build and Why Use It?

Imagine you need to ensure your application works correctly on Ubuntu and Windows, and is compatible with both Node.js v16 and v18. Without a matrix, you might create four separate, nearly identical jobs:

  • test-ubuntu-node16
  • test-ubuntu-node18
  • test-windows-node16
  • test-windows-node18

This approach is repetitive and hard to maintain. A matrix build solves this by letting you define your variables (like OS and Node.js version) and automatically generating a job for each combination. It's a powerful application of the Don't Repeat Yourself (DRY) principle in your CI/CD pipelines.

The following article gives a great definition of matrix builds and compares it to a for loop in programming, which is a useful mental model.

Matrix Builds with GitHub Actions

Let's start with a clear definition from the article 'Matrix Builds with GitHub Actions' on the Blacksmith blog.

Please read the section 'What are matrix builds?'. It clearly explains the core concept and its benefits.

Matrix builds are versatile and can be used for many scenarios beyond just OS and language versions.

Use Cases for Matrix Builds
This diagram illustrates several powerful use cases for matrix builds, including cross-platform testing, verifying language compatibility, testing against different database versions, and even sharding large test suites to run them in parallel.

2. The Basic Syntax: strategy and matrix

To implement a matrix, you add a strategy block to your job definition. Inside that, you define your matrix with one or more variables, each with a list of values.

Here is a basic structure:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16, 18, 20] # This is our first matrix variable
    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }} # Access the variable here
    - run: npm ci
    - run: npm test

This workflow will automatically create and run three parallel jobs, one for each value in the node-version array. Notice how we access the current value for each job using the context expression ${{ matrix.node-version }}.

The following video provides a concise demonstration of creating a matrix and accessing its variables.

GitHub Actions - Reusable workflows - Matrix Strategies

This video from Mickey Gousset, titled 'GitHub Actions - Reusable workflows - Matrix Strategies', clearly demonstrates the setup and execution of a matrix build.

Watch from 00:43 to 01:35 and then from 04:40 to 07:16. The first part explains the concept, and the second part shows a live demo of adding a strategy: matrix block, using the matrix variable, and viewing the parallel jobs in the Actions tab.


3. Testing Across Multiple Operating Systems

One of the most common use cases for a matrix is testing across different operating systems. This is achieved by defining an os variable in your matrix and using it in the runs-on field of the job.

jobs:
  build:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    runs-on: ${{ matrix.os }} # Dynamically set the runner OS
    steps:



      # ... your build and test steps ...

This will run your job three times, once on each specified operating system. Let's watch a demonstration of this.

GitHub Actions Tutorial - Basic Concepts and CI/CD Pipeline with Docker

In the 'GitHub Actions Tutorial' video by TechWorld with Nana, which we've referenced before, there is an excellent segment that focuses specifically on creating a matrix for multiple operating systems.

Please watch from 22:40 to 24:39. This section demonstrates how to add a matrix for different OS versions and shows the resulting parallel execution in the GitHub UI.

When the workflow runs, you will see a job for each combination in your matrix, clearly labeled in the GitHub Actions UI.

GitHub Actions Matrix Build Example
This image shows the GitHub Actions UI for a successful matrix build. On the left, you can see multiple jobs spawned from a single job definition, each targeting a different combination of OS, version, and environment. The green checkmarks indicate that all tests passed across all configurations.

4. Fine-Tuning Your Matrix: include, exclude, and fail-fast

Sometimes, you need more control than a simple combination of all variables provides. GitHub Actions offers keywords to refine your matrix:

  • include: Adds a specific combination to the matrix, potentially with extra variables. This is useful for testing a beta version on only one OS or adding a special configuration.
  • exclude: Removes a specific combination from the matrix. This is perfect for skipping known-incompatible setups (e.g., an old library version on a new OS).
  • fail-fast: By default, if any job in a matrix fails, GitHub Actions immediately cancels all other in-progress and queued jobs in that matrix. You can set fail-fast: false to allow all jobs to complete regardless of failures, which can be useful for gathering comprehensive test results.

The following article provides clear examples for all these scenarios.

GitHub Actions Matrix Strategy: Basics, Tutorial & Best Practices

The article 'GitHub Actions Matrix Strategy' from Codefresh offers an excellent tutorial on these advanced controls.

Read the sections 'Expanding or Adding Matrix Configurations' (for include), 'Excluding Matrix Configurations' (for exclude), and 'Handling Failures' (for fail-fast and continue-on-error).


5. Your Turn: Create a Multi-Environment Test Workflow

Now, let's apply what you've learned.

Task:
Create a GitHub Actions workflow that tests a simple Node.js application across multiple environments. You can create a new repository or add this to an existing one with a package.json file.

Requirements:

  1. Name the workflow file multi-env-test.yml.
  2. The workflow should have a single job named test.
  3. The test job must use a matrix to run on two operating systems: ubuntu-latest and windows-latest.
  4. The matrix should also test against two Node.js versions: 18 and 20.
  5. This would normally create four jobs. Exclude the combination of windows-latest and Node.js version 18.
  6. The job steps should:
    • Check out the code.
    • Set up the correct Node.js version using the matrix variable.
    • Run npm install to install dependencies.
    • Run npm test (or echo "Running tests..." if you don't have a test script).
Click here to see the solution
name: Multi-Environment Test

on: [push]

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
        node-version: [18, 20]
        exclude:
          - os: windows-latest
            node-version: 18
    
    runs-on: ${{ matrix.os }}

    steps:
    - name: Check out code
      uses: actions/checkout@v4

    - name: Set up Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v4
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: npm install

    - name: Run tests
      run: npm test

This configuration will generate three jobs:

  • ubuntu-latest / node 18
  • ubuntu-latest / node 20
  • windows-latest / node 20

Conclusion

Great job! You've now mastered one of the most powerful features in GitHub Actions for creating efficient and comprehensive CI pipelines.

Key Takeaways:

  • Matrix builds automate running a job across multiple configurations using the strategy: matrix syntax.
  • This approach follows the DRY principle, making workflows more maintainable and scalable.
  • You can create matrices for any variable, such as operating system (runs-on: ${{ matrix.os }}), language version, or cloud environment.
  • Use include and exclude to fine-tune your test matrix and fail-fast: false to ensure all jobs run even if one fails.

In our next lesson, we will continue on the theme of writing reusable and maintainable CI/CD code. We'll explore Jenkins Shared Libraries and GitHub Actions reusable workflows, two different but related approaches to reducing code duplication across many pipelines and repositories.