GitHub Actions Day 2: Matrix Workflows

December 2, 2019

This is day 2 of my GitHub Actions Advent Calendar. If you want to see the whole list of tips as they're published, see the index.

One of the biggest advantages to having a CI/CD system is that it lets you build and test with multiple configurations efficiently. Building and testing on your machine before you push is certainly necessary, but it's rarely sufficient. After all, you probably only have one version of node installed. But building on a variety of platforms will give you confidence and insight that your changes work across the entire ecosystem that you support.

One of the earliest CI systems that introduced this notion of building across multiple configurations was Mozilla Tinderbox. It was revolutionary - and when I worked on AbiWord, I was responsible for our Tinderbox setup. We had a lab full of machines so that we could test the Motif build and the GTK build, and so that we could test against different dependencies (this was around the time of the dreaded libc5 to libc6 migration) and even different C++ compilers.

Back then a big part of my job was maintaining this lab full of expensive computers. So it's no surprise that one of my favorite parts of GitHub Actions is the matrix workflow functionality, which lets me quickly run multiple builds to support a variety of configurations.

I still write native code, so I still need deal with building with different compilers and different dependencies. But now I don't need a lab full of machines, I can just use a matrix workflow setup in GitHub Actions.

Matrix workflows might look a little overwhelming at first, but it's really just simple variable substitution. You define a set of variables, and a set of values that should be assigned to each of those variables. GitHub Actions will then execute a workflow with all the different expansions of those variables.

This becomes powerful very quickly - lets say have three different variables that you want to test for. In my case, I want to test with two different C compilers (gcc and clang), three different SSL backends (OpenSSL, GnuTLS and NSS), and two different Kerberos backends (MIT and Heimdal). To test all of these different combinations, that's 2 * 3 * 2 = 12 different configurations.

But instead of having to define twelve different jobs -- or worse, having to set up twelve different machines in a lab, like in the bad old days -- I can just specify a matrix with three variables. If I specify a matrix in a job, I'll actually get twelve jobs running with the different permutations:

matrix:
  cc: [gcc, clang]
  curl: [openssl, gnutls, nss]
  kerberos: [libkrb5, heimdal]

Now in my job, I can just reference each of these variables using the matrix context. For example, ${{ matrix.cc }} will expand to the current value of the cc variable.

Here's an example workflow that installs each of these dependencies and runs my autoconf setup and then runs make:

When you run this workflow, you can see quickly how it expands to 12 different jobs. On the left of the workflow run, you can see each of them. So that simple workflow expanded out very quickly.

When you open up the steps on one of the runs, you can see that indeed we were able to install our dependencies. If I open up the build (clang, openssl, libkrb5) job, I'm in fact running clang (as reported by ${CC} --version), the OpenSSL version of libcurl (as reported by curl-config) and MIT krb5 (as reported by krb5-config).

Workflow Demonstration

So you can see that you can build out powerful workflows with multiple configurations just with a few lines of a matrix definition in your workflow.