How to make a Dynamic Matrix in GitHub Actions

Do you want split monorepo for each package? Instead of 20 workflows with copy-paste steps, you can define just one with a static matrix for packages.

Yet, nothing in real life is static but rather dynamic. A new package can be added, old can be removed. How could we automate this even more with a dynamic matrix?

In the last post we talked about monorepo split with GitHub Actions.

Today we'll look on a rather general idea for any GitHub Action - dynamic matrix.

Static Matrix

We've already talked about the use case for the split of many packages into many repositories. Instead of repeating each workflow with a different package, we can use a static matrix.

A typical static matrix looks like this:

jobs:
    monorepo_split:
        runs-on: ubuntu-latest

        strategy:
            matrix:
                package:
                    # list your packages here
                    - coding-standard
                    - phpstan-rules

After you define the strategy, add steps that use ${{ matrix.package }}.

        # ...
        steps:
            -   uses: actions/checkout@v2

            -
                uses: symplify/github-action-monorepo-split@master
                env:
                    GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
                with:
                    package-directory: 'packages/${{ matrix.package }}'
                    split-repository-organization: 'symplify'
                    split-repository-name: '${{ matrix.package }}'

For each item in package: a new workflow run will be triggered. So, in this case - matrix triggers 3 parallel runs.


Matrix is an Array

The above could be written in PHP like:

$matrix = [
    'packages' => [
        'coding-standard',
        'phpstan-rules',
    ]
];

foreach ($matrix['packages'] as $package) {
    $packageDirectory = 'packages/' . $package;
    $splitRepositoryName = $package;
    // ... do the magic
}

Is there new package? Add it to the matrix:

            matrix:
                package:
                    # list your packages here
                    - coding-standard
                    - phpstan-rules
+                   - easy-coding-standard

That's it!


Any static opens up the door to troubles... How easy do you think is to forget to extend the workflow after adding a new package?

From Static Array to JSON

GitHub Actions are ready to make our life easier. Since April 2020, there is a fromJson() function to help us. What does it do?

It converts a json to an array, like this:

            matrix:
-                 package:
-                   - coding-standard
-                   - phpstan-rules
+                 package: ${{ fromJson(["coding-standard", "phpstan-rules"]) }}

You are probably thinking, "Well, that's just terrible, Tomas". Thank you, and you're right.

  • But where is JSON, there is... an endpoint.
  • And where is an endpoint, there is a space for dynamic output.

The Symplify\MonorepoBuilder is using this trick to get all the packages from /packages directory in handy json format:

vendor/bin/monorepo-builder packages-json

[
    "coding-standard",
    "phpstan-rules"
]

This version is not final, but very roughly the command above would be written like this:

-           matrix: ${{ fromJson(["coding-standard", "phpstan-rules"]) }}
+           matrix: ${{ fromJson(vendor/bin/monorepo-builder packages-json) }}

From Json to Fully Dynamic Matrix

If we run the command above as it is, it would fail. Setting up a matrix is like the setUp() method in a PHPUnit test case - there is zero code executed before it.

  • That's why we use setUp() method to actually set what we need first.
  • Then run test() method.

We have to do the same here:

  • In the 1st step, we create the JSON with all packages
  • In the 2nd step, we use this JSON as input for the matrix, that will create a standalone run for each package

How does the Workflow look Like?

jobs:
    # phase 1
    provide_packages_json:
        runs-on: ubuntu-latest

        steps:
            # git clone + use PHP + composer install
            -   uses: actions/checkout@v2
            -   uses: shivammathur/setup-php@v2
                with:
                    php-version: 7.4
            -   run: composer install --no-progress --ansi

            # here we create the json, we need the "id:" so we can use it in "outputs" bellow
            -
                id: set-matrix
                run: echo "::set-output name=matrix::$(vendor/bin/monorepo-builder packages-json --names)"

        # here, we save the result of this 1st phase to the "outputs"
        outputs:
            matrix: ${{ steps.set-matrix.outputs.matrix }}

    # phase 2
    split_monorepo:
        # this means, wait for the "provide_packages_json" phase 1 to finish
        needs: provide_packages_json

        runs-on: ubuntu-latest
        strategy:
            # ↓ the real magic happens here - create dynamic matrix from the json
            matrix:
                package: ${{ fromJson(needs.provide_packages_json.outputs.matrix) }}

        steps:
            # ...

That's it!

This way, we'll never forget to split nor test a new package. Never.


Check fully functional .github/workflows/split_monorepo.yaml in Symplify monorepo for more details.

Is this something niche? Not really. This week, Kamil has added this approach to Sylius too.

Use for ~~Monorepo Split~~ Anything Dynamic!

This fromJson() trick is not something exclusive to monorepo package splitting. It's just one of the possible use cases.

The primary use case is already in your mind.

  • What is repeated static in your CI?
  • What lists can be delegated to simple command that already knows the data?
  • How could you use the dynamic matrix in your workflows today?

Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!