How to reduce duplication in your GitHub Actions workflows

19 August 2022 Comments

In 2019, GitHub published their own solution to run automated workflows called GitHub Actions, which allowed those hosting their code in GitHub, to be able to define and run their CI/CD pipelines in the same platform.

When it was released, one of the main pain points to use it was that defining pipelines required large yaml config files, where it was sometimes hard to avoid duplication.

However, during this time, and based on users’ feedback, GitHub has introduced several improvements on this regard.

Recently, I have been refactoring and improving a pipeline in one of my projects, and I wanted to share the different approaches I used to reduce duplication.

Those approaches are:

  • Matrix.
  • Composite actions.
  • Reusable workflows.

This article assumes you have certain knowledge on how GitHub Actions works. If that’s not the case, you probably want to take a look at its documentation.

Introducing the workflow

Let’s imagine we start with this continuous integration workflow for a PHP project:

name: Continuous integration

on:
  pull_request: null
  push:
    branches:
      - main
      - develop

jobs:
  coding-styles:
    runs-on: ubuntu-22.04
    env:
      extensions: 'openswoole, gd, intl'
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.0'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.0'
          tools: composer
          extensions: ${{ env.extensions }}
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Check coding styles
        run: composer coding-styles

  static-analysis:
    runs-on: ubuntu-22.04
    env:
      extensions: 'openswoole, gd, intl'
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.0'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.0'
          tools: composer
          extensions: ${{ env.extensions }}
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Static analysis
        run: composer static-analysis

  unit-tests-8-0:
    runs-on: ubuntu-22.04
    env:
      extensions: 'openswoole, gd, intl'
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.0'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.0'
          tools: composer
          extensions: ${{ env.extensions }}
          coverage: pcov
          ini-values: pcov.directory=module
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Unit tests
        run: composer test:unit

  unit-tests-8-1:
    runs-on: ubuntu-22.04
    env:
      extensions: 'openswoole, gd, intl'
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.1'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
          tools: composer
          extensions: ${{ env.extensions }}
          coverage: pcov
          ini-values: pcov.directory=module
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Unit tests
        run: composer test:unit
      - name: Publish coverage
        uses: actions/upload-artifact@v3
        with:
          name: coverage-unit
          path: |
            build/coverage-unit

  e2e-tests-8-0:
    runs-on: ubuntu-22.04
    env:
      extensions: 'openswoole, gd, intl'
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.0'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.0'
          tools: composer
          extensions: ${{ env.extensions }}
          coverage: pcov
          ini-values: pcov.directory=module
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: E2E tests
        run: composer test:e2e

  e2e-tests-8-1:
    runs-on: ubuntu-22.04
    env:
      extensions: 'openswoole, gd, intl'
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.1'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
          tools: composer
          extensions: ${{ env.extensions }}
          coverage: pcov
          ini-values: pcov.directory=module
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: E2E tests
        run: composer test:e2e
      - name: Publish coverage
        uses: actions/upload-artifact@v3
        with:
          name: coverage-e2e
          path: |
            build/coverage-e2e

  unit-mutation-tests:
    runs-on: ubuntu-22.04
    needs:
      - unit-tests-8-1
    env:
      extensions: 'openswoole, gd, intl'
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.1'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
          tools: composer
          extensions: ${{ env.extensions }}
          coverage: pcov
          ini-values: pcov.directory=module
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Download coverage
        uses: actions/download-artifact@v3
        with:
          name: coverage-unit
          path: build
      - name: Unit mutation tests
        run: composer mutation-tests:unit

  e2e-mutation-tests:
    runs-on: ubuntu-22.04
    needs:
      - e2e-tests-8-1
    env:
      extensions: 'openswoole, gd, intl'
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.1'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
          tools: composer
          extensions: ${{ env.extensions }}
          coverage: pcov
          ini-values: pcov.directory=module
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Download coverage
        uses: actions/download-artifact@v3
        with:
          name: coverage-unit
          path: build
      - name: E2E mutation tests
        run: composer mutation-tests:e2e

In human language, this is what the pipeline does:

  • Check coding styles.
  • Run a static analysis.
  • Run unit tests and generate code coverage.
  • Run E2E tests and generate code coverage.
  • Run mutation tests for the unit tests.
  • Run mutation tests for the E2E tests.

The first 4 jobs are all run in parallel, and the last two are run after the tests have finished (as they require the code coverage reports).

Also, for all the jobs, an environment needs to be set-up, with certain version of PHP (sometimes just 8.0, sometimes also 8.1) and some PHP extensions.

Now, let’s see how to improve all those duplicated steps.

Use a matrix

The first thing we can do is merge the jobs for all the PHP versions, and pass that as a matrix argument.

Also, coding styles and static analysis are both static code checks which require a very similar set-up. We can merge those two and pass them via matrix as well.

name: Continuous integration

on:
  pull_request: null
  push:
    branches:
      - main
      - develop

jobs:
  static-checks:
    runs-on: ubuntu-22.04
    env:
      extensions: 'openswoole, gd, intl'
    strategy:
      matrix:
        command: ['coding-styles', 'static-analysis']
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.0'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.0'
          tools: composer
          extensions: ${{ env.extensions }}
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Run ${{ matrix.command }} with composer
        run: composer ${{ matrix.command }}

  unit-tests:
    runs-on: ubuntu-22.04
    env:
      extensions: 'openswoole, gd, intl'
    strategy:
      matrix:
        php-version: ['8.0', '8.1']
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: ${{ matrix.php-version }}
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php-version }}
          tools: composer
          extensions: ${{ env.extensions }}
          coverage: pcov
          ini-values: pcov.directory=module
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Unit tests
        run: composer test:unit
      - name: Publish coverage
        if: ${{ matrix.php-version == '8.1' }}
        uses: actions/upload-artifact@v3
        with:
          name: coverage-unit
          path: |
            build/coverage-unit

  e2e-tests:
    runs-on: ubuntu-22.04
    env:
      extensions: 'openswoole, gd, intl'
    strategy:
      matrix:
        php-version: ['8.0', '8.1']
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.0'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.0'
          tools: composer
          extensions: ${{ env.extensions }}
          coverage: pcov
          ini-values: pcov.directory=module
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: E2E tests
        run: composer test:e2e
      - name: Publish coverage
        if: ${{ matrix.php-version == '8.1' }}
        uses: actions/upload-artifact@v3
        with:
          name: coverage-e2e
          path: |
            build/coverage-e2e

  unit-mutation-tests:
    runs-on: ubuntu-22.04
    needs:
      - unit-tests
    env:
      extensions: 'openswoole, gd, intl'
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.1'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
          tools: composer
          extensions: ${{ env.extensions }}
          coverage: pcov
          ini-values: pcov.directory=module
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Download coverage
        uses: actions/download-artifact@v3
        with:
          name: coverage-unit
          path: build
      - name: Unit mutation tests
        run: composer mutation-tests:unit

  e2e-mutation-tests:
    runs-on: ubuntu-22.04
    needs:
      - e2e-tests
    env:
      extensions: 'openswoole, gd, intl'
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup cache environment
        id: extcache
        uses: shivammathur/cache-extensions@v1
        with:
          php-version: '8.1'
          extensions: ${{ env.extensions }}
          key: coding-styles-extensions
      - name: Cache extensions
        uses: actions/cache@v2
        with:
          path: ${{ steps.extcache.outputs.dir }}
          key: ${{ steps.extcache.outputs.key }}
          restore-keys: ${{ steps.extcache.outputs.key }}
      - name: Use PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.1'
          tools: composer
          extensions: ${{ env.extensions }}
          coverage: pcov
          ini-values: pcov.directory=module
      - name: Install dependencies
        run: composer install --no-interaction --prefer-dist
      - name: Download coverage
        uses: actions/download-artifact@v3
        with:
          name: coverage-unit
          path: build
      - name: E2E mutation tests
        run: composer mutation-tests:e2e

With this change, we are down from 8 jobs to 5, with the only consideration that we now publish code coverage conditionally based on the PHP version.

Reuse steps with a composite action

The next more obvious thing is that there are a couple of steps that appear on each one of the jobs to set up the environment.

We can combine those into a local composite action that wraps all the individual steps and can be called as a whole by every job.

Local actions have to be located inside .github/actions, in a folder with the name we want, containing an action.yml file, which in our case, could look like this:

.github/actions/ci-setup/action.yml:

name: CI setup
description: 'Sets up the environment for jobs during CI workflow'

inputs:
  php-version:
    description: 'The PHP version to be setup'
    required: true
  php-extensions:
    description: 'The PHP extensions to install'
    required: false
    default: ''
  extensions-cache-key:
    description: 'The key used to cache PHP extensions'
    required: true

runs:
  using: composite
  steps:
    - name: Setup cache environment
      id: extcache
      uses: shivammathur/cache-extensions@v1
      with:
        php-version: ${{ inputs.php-version }}
        extensions: ${{ inputs.php-extensions }}
        key: ${{ inputs.extensions-cache-key }}
    - name: Cache extensions
      uses: actions/cache@v2
      with:
        path: ${{ steps.extcache.outputs.dir }}
        key: ${{ steps.extcache.outputs.key }}
        restore-keys: ${{ steps.extcache.outputs.key }}
    - name: Use PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: ${{ inputs.php-version }}
        tools: composer
        extensions: ${{ inputs.php-extensions }}
        coverage: pcov
        ini-values: pcov.directory=module
    - name: Install dependencies
      run: composer install --no-interaction --prefer-dist
      shell: bash

This action wraps the 4 steps that we have on every job. The only step we can’t add here is the checkout step, as we need the code to have been checked out first in order to find the action file itself.

With this local composite action, we can refactor the workflow to look like this:

name: Continuous integration

on:
  pull_request: null
  push:
    branches:
      - main
      - develop

jobs:
  static-checks:
    runs-on: ubuntu-22.04
    strategy:
      matrix:
        command: ['coding-styles', 'static-analysis']
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup
        uses: './.github/actions/ci-setup'
        with:
          php-version: '8.1'
          php-extensions: 'openswoole, gd, intl'
          extensions-cache-key: e2e-tests-${{ matrix.command }}
      - name: Run ${{ matrix.command }} with composer
        run: composer ${{ matrix.command }}

  unit-tests:
    runs-on: ubuntu-22.04
    strategy:
      matrix:
        php-version: ['8.0', '8.1']
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup
        uses: './.github/actions/ci-setup'
        with:
          php-version: ${{ matrix.php-version }}
          php-extensions: 'openswoole, gd, intl'
          extensions-cache-key: unit-tests-${{ matrix.php-version }}
      - name: Unit tests
        run: composer test:unit
      - name: Publish coverage
        if: ${{ matrix.php-version == '8.1' }}
        uses: actions/upload-artifact@v3
        with:
          name: coverage-unit
          path: |
            build/coverage-unit

  e2e-tests:
    runs-on: ubuntu-22.04
    strategy:
      matrix:
        php-version: ['8.0', '8.1']
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup
        uses: './.github/actions/ci-setup'
        with:
          php-version: ${{ matrix.php-version }}
          php-extensions: 'openswoole, gd, intl'
          extensions-cache-key: e2e-tests-${{ matrix.php-version }}
      - name: E2E tests
        run: composer test:e2e
      - name: Publish coverage
        if: ${{ matrix.php-version == '8.1' }}
        uses: actions/upload-artifact@v3
        with:
          name: coverage-e2e
          path: |
            build/coverage-e2e

  unit-mutation-tests:
    runs-on: ubuntu-22.04
    needs:
      - unit-tests
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup
        uses: './.github/actions/ci-setup'
        with:
          php-version: '8.1'
          php-extensions: 'openswoole, gd, intl'
          extensions-cache-key: unit-mutation-tests
      - name: Download coverage
        uses: actions/download-artifact@v3
        with:
          name: coverage-unit
          path: build
      - name: Unit mutation tests
        run: composer mutation-tests:unit

  e2e-mutation-tests:
    runs-on: ubuntu-22.04
    needs:
      - e2e-tests
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup
        uses: './.github/actions/ci-setup'
        with:
          php-version: '8.1'
          php-extensions: 'openswoole, gd, intl'
          extensions-cache-key: e2e-mutation-tests
      - name: Download coverage
        uses: actions/download-artifact@v3
        with:
          name: coverage-unit
          path: build
      - name: E2E mutation tests
        run: composer mutation-tests:e2e

Every job is now much shorter, with almost all duplicated code moved to the composite action.

Also, as a side effect, we got rid of defining the PHP extensions as an env var, since we now pass them as an arg to the action only in one place.

Reuse a whole workflow

But that’s not it. There’s still a lot of duplication between both tests jobs and both mutation tests jobs.

One way we can reduce even further the gap is by extracting them to reusable workflows.

They are similar to composite actions, with the difference that they do not wrap only a couple of steps, but they can have even multiple jobs that we then invoke from the main workflow.

Also, our reusable workflow can still use the composite action we created in previous step.

Let’s define our ci-test reusable workflow:

.github/workflows/ci-test.yml:

name: Tests

on:
  workflow_call:
    inputs:
      test-group:
        type: string
        required: true
        description: unit or e2e

jobs:
  tests:
    runs-on: ubuntu-22.04
    strategy:
      matrix:
        php-version: ['8.0', '8.1']
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup
        uses: './.github/actions/ci-setup'
        with:
          php-version: ${{ matrix.php-version }}
          php-extensions: 'openswoole, gd, intl'
          extensions-cache-key: ${{ inputs.test-group }}-tests-${{ matrix.php-version }}
      - name: Tests
        run: composer test:${{ inputs.test-group }}
      - name: Publish coverage
        if: ${{ matrix.php-version == '8.1' }}
        uses: actions/upload-artifact@v3
        with:
          name: coverage-${{ inputs.test-group }}
          path: |
            build/coverage-${{ inputs.test-group }}

  mutation-tests:
    runs-on: ubuntu-22.04
    needs:
      - tests
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup
        uses: './.github/actions/ci-setup'
        with:
          php-version: '8.1'
          php-extensions: 'openswoole, gd, intl'
          extensions-cache-key: ${{ inputs.test-group }}-mutation-tests
      - name: Download coverage
        uses: actions/download-artifact@v3
        with:
          name: coverage-${{ inputs.test-group }}
          path: build
      - name: ${{ inputs.test-group }} mutation tests
        run: composer mutation-tests:${{ inputs.test-group }}

And now, we can invoke this reusable workflow from our main CI workflow like this:

name: Continuous integration

on:
  pull_request: null
  push:
    branches:
      - main
      - develop

jobs:
  static-checks:
    runs-on: ubuntu-22.04
    strategy:
      matrix:
        command: ['coding-styles', 'static-analysis']
    steps:
      - name: Checkout code
        uses: actions/checkout@v3
      - name: Setup
        uses: './.github/actions/ci-setup'
        with:
          php-version: '8.1'
          php-extensions: 'openswoole, gd, intl'
          extensions-cache-key: e2e-tests-${{ matrix.command }}
      - name: Run ${{ matrix.command }} with composer
        run: composer ${{ matrix.command }}

  unit-tests:
    uses: './.github/workflows/ci-test.yml'
    with:
      test-group: unit

  e2e-tests:
    uses: './.github/workflows/ci-test.yml'
    with:
      test-group: e2e

This reduces the duplication to the bare minimum, allowing us to reuse the tests + mutation-tests logic both for unit tests and E2E tests, keeping the benefit of making the later depend on the former.

Final thoughts

You can see how the final result looks like in this example repository.

There are a couple of things to clarify from the examples above:

  • In here, we use local composite actions and reusable workflows.

    However, GitHub Actions supports loading them from a different repository, and therefore using them in multiple projects if needed.

    In the case of actions, it of course also allows to publish them in the marketplace so that you don’t have to reference them via repository name and path.

  • It may seem as if unit and mutation tests could have been simplified with a matrix, as we did with the static checks.

    However, that would not allow to make every mutation-tests job to depend only on its corresponding tests job, and they would have to wait for all the tests to finish.

    That’s why a reusable workflow is a better solution here.

  • It may also look like the unit-tests and e2e-tests jobs, which in the last version only invoke the ci-tests reusable workflow could have been merged using a matrix.

    However, GitHub Actions does not currently allow to use a matrix with reusable workflows.

    That’s why they are defined as two independent jobs.

  • The example used in this article is made-up, but tries to cover a bit of everything to justify all the strategies presented on it.

    If you want to see a real example, this has been based (on a very simplified way) on the continuous integration workflow from Shlink.