Notes on seeking wisdom and crafting software

Release please

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

Release me state transition
Release please action works in two phases. In the phase 1 (blue), the action runs as part of a GitHub workflow and ensures a “release PR” reflects the set of changes for next release. And in phase 2 (green), the user merges the “release PR” and a GitHub release is created automatically by the action.

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:

  1. Setup release-please action in a GitHub workflow. See example. Now, keep making changes to the code as usual.
  2. 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.
  3. 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 label autorelease: pending.
  4. Now you decide to release by merging the release PR.
  5. PR is merged and a workflow run is triggered.
  6. On this workflow run, the PR’s state changes to autorelease: tagged, the commit is tagged, and a release gets created.
  7. 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:

  1. We need the pre-release NuGet packages to be uploaded to a specific feed (MyGet).
  2. Release versions are uploaded to both MyGet and NuGet feeds.
  3. 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!