[ANN] Easier release automation with "Release Please" CI tool

Release Please + Julia

I made a Julia fork of Release Please. It has been useful enough that I thought it is worth sharing!

For context, Release Please is a release automation tool. For every commit to a GitHub repository, it:

  1. looks at the commits since the last release,
  2. decides, based on Conventional Commits, whether a new release should be proposed,
  3. if so, opens a PR that bumps the version and updates the changelog. The maintainer can merge this release PR at their leisure - it will update automatically for new changes.

image

The version bump follows SemVer, with the major, minor, or patch change inferred from the commit messages. Once that PR is merged, Release Please creates the tag and GitHub release.

In this Julia version, I also have it set up to trigger JuliaRegistrator automatically, so there is no need to manually comment on a release commit. It removes a lot of the friction!

Conventional Commits

A “Conventional Commit” is a specific structure to commit messages. The standards are:

  • fix: for a bug fix
  • feat: for a new feature
  • docs: for documentation changes
  • refactor: for internal code changes that do not change behavior
  • test: for test changes
  • ci: for CI and workflow changes
  • chore: for maintenance work (used by dependabot)

Commits can also include a scope, such as feat(core): or fix(parser):, if you want to categorize changes more precisely.

Breaking changes should be marked explicitly, usually with ! (for example feat!: or feat(core)!:). Putting BREAKING CHANGE: in the commit body also works.

The scope part is optional, but it’s useful in larger repositories because it makes changelog entries easier to scan (it will automatically break it down by this). For release purposes, the main signals are still fix, feat, and explicit breaking changes. If you have a change that does too many things to describe in one commit, it’s probably a good indication to split things up into smaller commits.

Why use this instead of TagBot + Registrator?

The usual Julia workflow is some combination of JuliaRegistrator, TagBot, and manual version or changelog updates. That works, but I have found there is a lot of friction involved to create (or even remember to create) a release, and then comment on the GitHub commit after it gets merged.

Release Please makes releases represented as a PR. The version bump is there, the changelog update is there (generated by the commit history), and the merge itself is what triggers the release. I find this easier to inspect and review.

GitHub Workflow

This is the workflow I am using now instead of TagBot + JuliaRegistrator comments. Feel free to copy.

GitHub action file: .github/workflows/release-please.yml (note that you might need to change the branch name from main - in three places here)

name: release-please

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write
  issues: write

jobs:
  release-please:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 0

      - uses: actions/checkout@v6
        with:
          repository: MilesCranmerBot/release-please
          ref: 911ad18bdc3bb52bfef564cfbcb35f31aac01df3
          path: release-please-src

      - uses: actions/setup-node@v6
        with:
          node-version: '22'

      - name: Install release-please deps
        working-directory: release-please-src
        run: npm ci

      - name: Build release-please
        working-directory: release-please-src
        run: npm run compile

      - name: Create or update release PR
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          node release-please-src/build/src/bin/release-please.js release-pr \
            --token "$GITHUB_TOKEN" \
            --repo-url "https://github.com/${{ github.repository }}" \
            --target-branch main \
            --config-file release-please-config.json \
            --manifest-file .release-please-manifest.json

      - name: Create GitHub release
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          node release-please-src/build/src/bin/release-please.js github-release \
            --token "$GITHUB_TOKEN" \
            --repo-url "https://github.com/${{ github.repository }}" \
            --target-branch main \
            --config-file release-please-config.json \
            --manifest-file .release-please-manifest.json

Then, it needs a config for release please in release-please-config.json:

{
  "bootstrap-sha": "TODO: current HEAD commit before merging bootstrap PR",
  "packages": {
    ".": {
      "release-type": "julia",
      "include-component-in-tag": false,
      "bump-minor-pre-major": true,
      "bump-patch-for-minor-pre-major": true
    }
  }
}

If you have multiple packages in a single repository, you include the subfolder instead of ".", and also set include-component-in-tag to true as needed. Note that the bump-minor-pre-major stuff is so it will be compatible with 0.x.y style releases.

You need to also set a .release-please-manifest.json with the current version of your package(s):

{
  ".": "1.12.0"
}

What it looks like:

I backported a commit to my release-v1 compatibility branch: fix: enforce constraints in hall-of-fame (backport) (#593) · astroautomata/SymbolicRegression.jl@cc7e4c0 · GitHub. This uses the fix: prefix so will show up as a bug fix.

Fixes trigger new patch versions, and features trigger new minor versions. Thus, my CI (which I have a dedicated release-v1 workflow for) suggested making a new release:

image

This was automatically inferred to be a patch (1.13.1 → 1.13.2), since it only includes a bug fix, without any new features.

I merged that PR on this commit: chore(release-v1): release 1.13.2 (#597) · astroautomata/SymbolicRegression.jl@a8a11b9 · GitHub. release-please runs again, and sees that the release was just triggered. So it creates the tag, and also comments to let JuliaRegistrator know:

This kicks off the rest of the registration pipeline, creating 1.13.2.

I have it set up to automatically set the subdir argument of register as well, so that this should work (untested) for multi-package repositories as well!

Also, the upstreaming PR to release-please proper is here (feel free to :+1: so it gets merged faster…). But the fork should work too.

Happy releasing!

2 Likes

Awesome! I was already using this workflow, but with a few more manual steps. It always made more sense to me to start from a github release, rather than use tagbot. Great to see I can automate it now :slight_smile:

1 Like

The reason TagBot works the way it does is so that the tags can be immutable and so they reflect the state of the registry: it only makes the tag once the package is registered, so that the tag represents the version that actually got registered. Say you forgot a compat bounds and registration was blocked, then you fixed the compat bound and re-triggered registation. With the tagbot workflow, the tag & github release is only created once its registered. When you create a tag when you intend to release, that can be premature and then the tag needs to modified to point to the actually-registered commit. It can also be confusing to see a tag, try to install the package, but that version isn’t actually registered yet.

I agree; I’m a fan of workflows that automatically create the comment when you merge a version bump and things like that, and that part sounds useful to me. (I disagree with the ordering of the github release though).

3 Likes

You can ofcourse do the checks before you trigger the registration, in your own CI. Even if the registration fails for some reason it is fairly easy to correct.

But perhaps to make it more robust, we could have a dry run option in the registry. Do all the checks, but don’t actually make the registration. So you’re 100% sure it won’t fail. This is would be useful beyond this github action.

It can’t be completely infallible bc registration can get blocked for non-automated reasons, e.g. community review. This is most applicable to new packages but applies to new versions as well.

A community review within the 15 minute release window? Has that ever happened? (interrupting your own release does happen frequently)

Some data:

  • is:open is:pr label:"minor release" : 26,381
  • is:open is:pr label:"minor release" label:"AutoMerge: last run blocked by comment" : 67
  • is:pr label:"major release" is:closed: 1839
  • is:pr label:"major release" is:closed label:"AutoMerge: last run blocked by comment" : 10
1 Like

Yeah, I think that shows it does happen - and that’s not even counting the many times an author interrupts it, then resolves the issue, retriggers registration, and removes the block.

I think if we’re going to tag releases, it’s pretty important they match what’s in the registry, otherwise it leads to confusing and hard to debug issues. So since the registry is the source of truth, and the package author doesn’t unilaterally get to decide what goes in (unlike in some package ecosystems), tags should flow from there.