Section 5 - Releasing the reusable workflow
Last updated
Last updated
Now that we've defined a reusable test-build-deploy
workflow, the next step is to create a release workflow. This allows us to version the reusable workflow, ensuring that each workflow referencing it has a stable version to rely on.
To achieve this, we will set up a release workflow similar to the example in Chapter 1, Section 3: Building a Workflow. However, this workflow will be adapted to handle multiple releases within a single repository, allowing us to host and release multiple reusable workflows.
The release workflow will be similar to what we did in Chapter 1, Section 3. However, instead of handling a single release, we will set up a system that releases each reusable workflow in the repository independently.
Currently, the repository only hosts the test-build-deploy
workflow, but we want to ensure the release workflow can handle future workflows as well. This will prevent the need to create separate release workflows for each reusable workflow, which would be inefficient.
In traditional programming, repetitive actions are handled with loops, and GitHub Actions provides a similar capability using the . By using a matrix, we can automate the release process for each reusable workflow using a single workflow file.
The release process will consist of two jobs:
Job 1: workflows-to-release
– This job builds the matrix values (a list) of the reusable workflows ready for release.
Job 2: release
– This job performs the release of the reusable workflows that have new changes.
First, let's create release.yaml
under the .github/workflows/
directory in the github-
repository.
Since this is a release workflow, we will configure triggers so the workflow runs on push
and pull_request
events targeting the main
branch.
The matrix job will monitor for any changes to reusable workflows. If a reusable workflow file is modified, we will extract the name of the file and generate matrix values (a list of workflow names) to use in the matrix strategy of the release job.
When creating matrix values, you are essentially generating an array that can be iterated over. For our use case, this array will contain the names of the reusable workflows ready for release. These values will be passed into the matrix strategy of the release job.
To distinguish reusable workflows from other files, we'll ensure that all reusable workflows are prefixed with reusable-
, allowing us to filter only the relevant files.
Here’s how the job will work:
Identify all the files that have changed.
If a changed file is prefixed with reusable-
, extract the file name.
Place the name into the matrix value list.
Now, let's put it all together in the release.yaml
workflow file:
Let's walk through each step:
Step 1: actions/checkout@v4
Checks out the repository code.
Step 2: Get changed files
Uses the tj-actions/changed-files
action to gather the list of files that were modified.
Step 3: Get changed workflows under /workflows
Filters the list of changed files to identify workflows prefixed with reusable-
. This step ensures we’re only tracking reusable workflows for release. You'll also notice filtering for custom actions (prefixed with .github/actions/
)—this is in preparation for Chapter 4, where we will discuss custom actions and their releases.
We collect the names of all workflows or actions ready for release and store them in $GITHUB_OUTPUT.values
Lastly, we place the list of changed workflows (or actions) into job.outputs.names
so that it can be utilized in the matrix strategy for the release job.
This job will be very similar to the one in Chapter 1, Section 3. However, there are two main differences:
The strategy will be a matrix, allowing us to release each workflow independently.
We will also create a major version tag (v1
) for each release. This will allow users to track the major version and avoid frequent updates when there are only minor or patch changes. For example, if we release version v1.0.0
, we will also create a v1
tag. If we later release a patch version v1.0.1
, we will override the v1
tag with this version.
Here is the release job:
Lastly, since we are commenting on pull requests in the "Comment on PR" step, pushing tags, and creating releases, we need to ensure that the workflow has the appropriate write permissions for both pull requests and repository contents. This will allow the workflow to comment on PRs, push tags, and create releases. We can set these permissions at the top of the workflow file as follows:
Matrix strategy: Allows the release workflow to run for each reusable workflow, passing in a list of workflows to release.
Get bump version from PR labels: Extracts the version bump (e.g., patch, minor, major) based on labels, defaulting to a patch if no label is found.
Bump version and push tag: Increments the version and pushes the tag.
Create major version tag: Creates a major version tag (e.g., v1
).
Override or push major tag: Updates or creates the major tag.
Comment on PR: Comments on the pull request with the current and next release versions.
Create or update major GitHub release: Creates or updates a major GitHub release.
Create a GitHub release: Creates the GitHub release for the specific version.
To do this, we’ll remove everything under the jobs section in the test-build-deploy.yaml workflow and reference the reusable workflow instead. Here’s how you can do it:
This setup will call the reusable workflow and execute all its jobs. In this case, we don’t need to pass any parameters because we’re fine with the default values. However, if you wanted to override a default parameter, you could do it by using the with
keyword. For example:
Here, the image-name parameter is overridden with the value user-mgmt, instead of using the default ${{ github.event.repository.name }}.
After committing this change, the workflow will run, and you’ll see that all three jobs defined in the reusable workflow are executed. This greatly simplifies the logic, as we no longer need to define separate workflows for each service. Instead of copy-pasting similar workflows across services, we can centralize the logic in one place and reuse it across multiple projects.
In this chapter, I talked about versioning and tagging major versions (like v1, v2) this is with the goal of making it easier for users to adopt updates without needing to change their workflows every time there’s a patch or minor release. It's a common pattern in many community actions.
But recent events have highlighted a risk: version tags in public GitHub Actions are not immutable.
Patch versions (v1.1.2) shouldn’t change
Minor and major versions (v1, v1.1) should be backward-compatible
it ultimately depends on trusting the maintainers. And in open-source, bad actors can compromise that trust.
A malicious actor gained access and repointed version tags (like v45) to code that exfiltrated secrets from workflows — affecting any project that used it.
It’s all about the tradeoff between usability and security.
If you’re working in sensitive environments or want to be extra cautious, the best practice is to pin actions to a specific commit SHA:
Now that we’ve successfully released our reusable workflow, let’s put it to use in our services. To demonstrate this, we’ll update the test-build-deploy.yaml workflow to reference the reusable workflow instead of duplicating its logic. As an example, we’ll update the for the user-management-service.
You can follow the same steps to update the workflow to reference the reusable workflow in the same manner.
While maintainers should follow — meaning:
A popular action, that I reference in this chapter: , was recently compromised, this has now been addressed, but it's important to be aware of what happened.
Here’s a great write-up on what happened: