A usual release of the spekt/testlogger project involves going through all the commits and meticulously crafting the CHANGELOG. It takes about half an hour and is boring enough to keep postponing the releases 🤭 We set out to automate this one.
Fortunately, Google’s release-please automates generating changelogs, creating release tags and GitHub releases. It’s supposed to be just adding a step to your GitHub action workflow; except I could not make sense of the state transitions 😕 Thus was born test-release-please experiments.
Lets deep dive into the state transitions to clarify the usage of this tool. And also talk a bit about releasing dotnet projects.
Release please
First things first, you need to add a single release-please action somewhere in your workflow. It must run on every push to the main branch. This action is stateful, so you don’t want concurrent workflows/jobs with same action.
Here are the steps along with examples:
- Setup
release-please
action in a GitHub workflow. See example. Now, keep making changes to the code as usual. - The GitHub workflow will compute the changes since last release and will create a PR like this. Note that the action parses conventional commit messages and GitHub tags to find the next semver release.
- The PR will be auto updated with every workflow run. E.g., a patch
release will be upgraded to a minor release if you have any commits with
feat: xyz
message. Note the PR has a labelautorelease: pending
. - Now you decide to release by merging the release PR.
- PR is merged and a workflow run is triggered.
- On this workflow run, the PR’s state changes to
autorelease: tagged
, the commit is tagged, and a release gets created. - See an example previous release PR and the corresponding release.
How this magic works?
In three keywords: GitHub actions, events, and labels/tags managing the state.
Blue phase: continuous commits result in changelog updates by parsing the conventional commit messages. A PR is created and stays up to date. It also serves like a mini dashboard of the release payload for next version.
Green phase: release-please tool learns that you’ve merged a PR with
autorelease: pending
label. It will cut a release and update the label in the
PR post that. Next run of the tool will happen in blue phase.
Dotnet releases
The release-please action knows which files to keep up-to-date in
the coding phase, e.g. CHANGELOG.md
and pyproject.toml
for python projects.
Now dotnet is not supported as a first class language. Fortunately, the tool
supports a generic strategy called simple
which can update the changelog and a
version.txt
file in the repo to depict current release version.
The workflow in spekt/testlogger had two requirements:
- We need the pre-release NuGet packages to be uploaded to a specific feed (MyGet).
- Release versions are uploaded to both MyGet and NuGet feeds.
- Version is managed in the workflow through a MSBUILD property. We must determine the next version dynamically inside the GitHub workflow.
You can see the solution in testlogger ci workflow.
We refactored the workflow into two jobs.
The version
job runs a bash script to find the next semver given the previous
release in version.txt
. See the example code below. Further, if
release-please
task detects we have a release cut, we force the build version
to be the tagged version.
version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: googleapis/release-please-action@v4
id: release
if: github.ref == 'refs/heads/master'
with:
token: ${{ secrets.GITHUB_TOKEN }}
release-type: simple
- name: Set default build number
run: |
# https://stackoverflow.com/questions/8653126/how-to-increment-version-number-in-a-shell-script
BUILD_VERSION=$(cat version.txt | awk -F. -v OFS=. '{$NF=$NF+1;print}')-pre.${{ github.run_number }}
echo "APP_BUILD_VERSION=${BUILD_VERSION}" >> $GITHUB_ENV
- name: Update build number
if: ${{ steps.release.outputs.release_created }}
run: |
RELEASE_VERSION=${{ steps.release.outputs.tag_name }}
echo "APP_BUILD_VERSION=${RELEASE_VERSION#v}" >> $GITHUB_ENV
- name: Final build version
run: |
echo ${{ env.APP_BUILD_VERSION }}
outputs:
build_version: ${{ env.APP_BUILD_VERSION }}
build:
needs: [version]
env:
APP_BUILD_VERSION: ${{ needs.version.outputs.build_version }}
# rest of the steps use APP_BUILD_VERSION env variable.
The build
job uses the output of version
job to set the package version. Now
with every commit, the release PR will stay up to date and pre-release packages
will use x.y.z-pre.n
versions. Once you merge the release PR, a package
version is generated with x.y.z
.
That’s all for this experiment. Thanks for reading!