Github reusable workflows and how to verify them

In december 2022 GitHub added the ability to share workflows between private repositories, opening the doors for Enterprises to make use of this feature. In this post I will describe how I implemented this in my APISMART Team and how I developed a system to verify changes to the reusable workflow. Basic knowledge of GitHub Actions is assumed.

A working example can be found here: reusable-workflows-with-ci and reusable-workflows-sample-project

Sections

Why reusable workflows

I’ll keep this short, since there are enough articles on the internet about this already. The main reason is to reduce the amount of code duplication. This in turn reduces the maintenance load of changing the workflow in dozens of repositories and merging dozens of Dependabot PRs for the same dependency. Additionally all repositories use the same standard to build and test their applications.

Implementation

The reusable workflow is usually stored in the .github repository of the organization. The workflow itself just executes a gradle clean build in order to build and test our applications. It makes use of the input parameters to allow each caller to specify which java version is needed. This allows even the older projects, which have not migrated to the latest java version, to use this.

name: reusable CI
on:
  workflow_call:
    inputs:
      java-version:
        required: true
        type: number

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 1
      - name: Set up JDK
        uses: actions/setup-java@v3
        with:
          distribution: 'temurin'
          java-version: ${{ inputs.java-version }}
          cache: 'gradle'
      - name: clean build
        run: ./gradlew clean build --no-daemon --info --stacktrace

The caller side is then very simple.

name: ci
on: [ pull_request ]

jobs:
  build:
    uses: <organization>/<repository>/.github/workflows/reusable-ci.yml@<version>
    with:
      java-version: 17

Verification

It was decided to always use the latest version, meaning that the version is main. This brings the risk that a change to the reusable workflow may break the builds of all our projects. An alternative would be to semantically version the reusable workflow and use a specific version. But this in turn would bring us back to having to merge dozens of Dependabot PRs for a single change.

Instead, a system was needed which would run the workflow on a test project in order to verify that it still works. First off, the reusable workflow was modified to allow checking out a different repository. This requires allowing to pass in the repository and an ssh key which has read access to that repository. Deploy keys are used to provide read access. See the guide on GitHub on how to create one. The public key is saved as deploy key in the test project and the private key is set as github action secret in the .github repository.

on:
  workflow_call:
    inputs:
      repository:
        required: false
        type: string
        default: ${{ github.repository }}
    secrets:
      SSH_KEY:
        required: false

Both values are then passed to the checkout action.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 1
          repository: ${{ inputs.repository }}
          ssh-key: ${{ secrets.SSH_KEY }}

Another workflow is added to the .github repository which uses the new input paratemers. It only runs when either the reusable workflow or itself is changed.

name: CI
on:
  pull_request:
    paths:
      - .github/workflows/ci.yml
      - .github/workflows/reusable-ci.yml

jobs:
  ci:
    uses: ./.github/workflows/reusable-ci.yml
    with:
      java-version: 17
      repository: <organization>/test-project
    secrets:
      SSH_KEY: ${{ secrets.SAMPLE_PROJECT_READ_KEY }}

And that’s it.
Now all our repositories use a shared workflow and we have a system in place that ensures that no accidents can occur.

Notes

An unpleasent change was that the name of the status check was changed from build to build/build. Which means that we had to adjust the required status check in every repository. I could not find any documentation on the naming of the status check but the assumption is that it is generated from the job name. In the case of reuseable jobs it is caller-job-name/callee-job-name. This can be avoided at the cost of getting billed for an extra minute on each workflow run which we decided was not worth the cost. For that add a second job with the name build and set the reuseable job as dependency with the needs keyword.

jobs:
  reuseable:
    uses: <organization>/<repository>/.github/workflows/reusable-ci.yml@<version>
    <ommitted>
  build:
    runs-on: ubuntu-latest
    needs: reuseable
    steps:
      - run: echo "workaround for better status check name"
  • sample PR of project using the reusable workflow: PR
  • sample PR of valid changes to reuseable worfklow: PR
  • sample PR of invalid changes to reuseable worfklow: PR