Automating releases with release-please
This year I began to enjoy creating OSS in my spare time, because agentic coding with AI became practical.
- ainame/swift-slack-client – a Swift client that plays the same role as Slack Bolt
- ainame/xcodeproj-cli – a CLI version (no Docker needed) of giginet/xcodeproj-mcp-server/
- ainame/tuzuru – a static site generator I built to host this blog
- ainame/swift-displaywidth – a library that calculates character widths so tuzuru can show tables in the terminal
Releasing new versions is still hard work. Recently I have been building CLIs, so tagging in Git is not enough. As soon as I tag, I want to run the steps that publish the tool. To make that easier I introduced release-please into ainame/tuzuru
, and I would like to share how it went.
Before release-please
When I develop a tool with pushing straight to main
, I created a simple script for releasing scripts/release.sh
. With the GitHub Actions workflow, it worked like this:
- Run
scripts/release.sh
on my local- Build and test a tool
- If it succeeds, update the files that hold the version, and git tag and apush
- GitHub notices the tag and builds
- Create the universal binary for macOS
- Create the statically linked binary for Linux
- Create the GitHub Release and upload the pre-built binaries
- Publish the npm package that wraps the binary
- Update the Homebrew formula
This flow worked fine, but once the tool felt stable I wanted to switch to PR-based development. I tried to build my own flow with Claude Code and friends, but the default token that GitHub Actions provides has limits, so the approach did not land nicely. I looked for existing examples that solved the same problem, and that is when I found release-please.
What is release-please?
It is a CLI tool written in TypeScript. If you want to use it on GitHub Actions, you can just call release-please-action
in your workflow. I took me a while to understand how it works.
I tried it and figured it out. I’m going to share what I learned here. (I have not read through the source code though, so please let me know if I got something wrong.)
How it works
As the README suggests, you may start putting f.github/workflows/release-please.yml
to your project. With the workflow like below, release-please runs every time something is merged into main
.
on:
push:
branches:
- main
permissions:
contents: write
issues: write
pull-requests: write
name: release-please
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
# this assumes that you have created a personal access token
# (PAT) and configured it as a GitHub action secret named
# `MY_RELEASE_PLEASE_TOKEN` (this secret name is not important).
token: ${{ secrets.MY_RELEASE_PLEASE_TOKEN }}
# this is a built-in strategy in release-please, see "Action Inputs"
# for more options
release-type: simple
release-please recognises the current release state from PR labels and switches between two behaviours:
- It builds a release PR based on Conventional Commits.
- It finds the commits for the next release. If there is even one user-facing change such as
feat
orfix
, it opens a PR. - It adds the
autorelease: pending
label to the PR. - It updates version strings in the codebase based on
release-please-config.json
and.release-please-manifest.json
. - The release PR is updated every time something merges into
main
(including updates toCHANGELOG.md
).
- It finds the commits for the next release. If there is even one user-facing change such as
- When a PR with the
autorelease: pending
label is merged, it creates a Git tag and a GitHub Release page.- At that point you can trigger any workflow on the release published event.
- Because the Release page already exists you can, for example, build binaries and upload them there.
The key is to manage the pending state through the PR label. This design reminded me of Renovate, which also uses resources on GitHub as its state.
https://github.com/ainame/tuzuru/pull/64
The release PR gathers all user-facing changes on every mereges.
Quirks
It looks neat, but release-please comes with a few habits.
- It decides the next version from Conventional Commits (a release with only
fix
becomes a patch release, and so on). - It strongly encourages a linear Git history (squash merge). If you merge
main
into the PR with a merge commit, the changelog becomes messy. - You need a personal access token or a GitHub App token to get around the limits of the default GitHub Actions token.
If you are not used to these things, they may feel troublesome. I had never used Conventional Commits before, and it looked like a hassle. But I’m sold. it is still far better than writing useless commit messages. And perhaps, if you’re doing PR-based developement and allowing only squash merge, you only care the PR title to use conventional commits?
Wrapping up
After some trial and error, I managed to bring release-please into ainame/tuzuru
, and I am happy with the streamlined release flow. Let’s give release-please a go and have a fun with coding!