Run all your tests concurrently

When using GitHub actions we usually chain all the checks, but when some tests take a long time, having the CI fail at the first check, can consume a lot of time from our developers, who need to fix this first step, re-run the CI and then see if it fails anywhere else.

Instead, we can make all the checks run at the same time, and let our developers see all the failing cases, using a matrix

Example with chained tests

A generic GitHub action for a javascript PR could look like the following:

on:
  pull_request:

jobs:
  test-code:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm install
      - run: npm run eslint
      - run: npm run prettier
      - run: npm run test
      - run: npm run e2e
      - run: npm run build

So every test is quite linear and it is executed one after the other.

stateDiagram
    direction LR
    [*] --> install
    install --> eslint
    eslint --> prettier
    prettier --> test
    test --> e2e
    e2e --> build
    build -->  [*]

But if the pull request fails in two points, for example, in eslint and e2e, the users won’t see the second case. They will only see that eslint is failing.

stateDiagram
    classDef errorEvent fill:#f00,color:white,font-weight:bold,stroke-width:2px,stroke:yellow

    direction LR
    [*] --> install
    install --> eslint
    eslint -->  error

    class error errorEvent

They will have to fix the eslint test, push their changes and wait for the CI again.

If we are working on a big project, where one of the early steps consumes a lot of time, this could mean that our developers will let this run and forget about it until it’s too late, and they have moved their attention somewhere else.

Using a matrix to see all the checks at once

What we can do, instead, is use a matrix strategy. This allows us to run all the checks simultaneously and detect every failing case at once.

concurrency

A modified example of the previous action would be the following:

on:
  pull_request:

jobs:
  test-code:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false # Important
      matrix:
        command: 
          - eslint
          - prettier
          - test
          - e2e
          - build
    steps:
      - uses: actions/checkout@v4
      - run: npm install
      - run: npm run ${{ matrix.command }}

This will make all the tests run concurrently and, in the case that one of them fails, we will be able to visualize them without having to necessarily fix them one by one.

stateDiagram
    direction LR
    classDef errorEvent fill:#f00,color:white,font-weight:bold,stroke-width:2px,stroke:yellow
    [*] --> install
    install --> eslint
    eslint -->  error
    install --> prettier
    prettier --> [*]
    install --> test
    test --> [*]
    install --> e2e
    e2e --> error
    install --> build
    build -->  [*]

    class error errorEvent

This allows your developers to work on fixing every failing check at once, instead of having to push and wait.

fixing

Combining tests into a final step

You’ll probably don’t want to have X amounts of required status checks in your branch protection rules, else you will end up with a very populated configuration that will look like this (and in your case it could be way worse):

If you add a new step, remove one, or simply want to copy the rules of main into release-v1 branch, you’ll have to do a lot of manual work (which could be prone to errors).

Instead, there is a simple solution that we can apply!

We make a new job which needs the matrix job:

on:
  pull_request:

jobs:
  test-code:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false # Important
      matrix:
        command: 
          - eslint
          - prettier
          - test
          - e2e
          - build
    steps:
      - uses: actions/checkout@v4
      - run: npm install
      - run: npm run ${{ matrix.command }}

  conclude:
    runs-on: ubuntu-latest
    name: Checks passed
    needs: [test-code] # It will only run if test-code was completed
    steps:
      - run: echo 'Good job! All the tests passed 🚀'

This allows us to only require the job conclude as our required status check.

conclude will only run if all the jobs inside the matrix are completed successfully. If one of them fails, the job is skipped and the PR won’t be mergeable.

stateDiagram
    direction LR
    final: Checks Passed
    state Matrix {
    direction LR
    [*] --> install
    install --> eslint
    eslint -->  [*]
    install --> prettier
    prettier --> [*]
    install --> test
    test --> [*]
    install --> e2e
    e2e --> [*]
    install --> build
    build -->  [*]
    }
    [*] --> Matrix
    Matrix --> final

Now we can add or remove checks from the matrix without having to change the required status checks in the branch protection rules.