[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. But the fork should work too.

Happy releasing!

20 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).

7 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.

1 Like

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.

3 Likes

Not sure if you meant this, but tags are not technically immutable. For example, in GitHub Actions it is standard practice to move a major tag such as v1 forward to the latest compatible v1.x.y release (it is even documented as such: Managing custom actions - GitHub Docs). So I do not think immutability of tags, in the strict sense, should be the reason here, since true immutability really requires a commit SHA. But that being said, I agree it is still useful for tags to reflect the registry state!

I guess if this is worrisome, I think you could still use TagBot on top - if you comment out the ā€˜Create GitHub release’ step, it should (?) just work as normal with a TagBot workflow set up, with the usual 15min delay.

For me I think the main benefit is you don’t need to make the commit comment, edit the CHANGELOG.md, figure out if you should be making a major/minor/patch, make a new PR to bump the version info, have the release notes be nicely organized, etc.

Another advantage is release-please can handle pre-release versions - which the Julia registry cannot. Therefore TagBot is limited to only those tags which Julia registry can handle. This lets one generate alpha versions to be consumed by downstream users/applications who use the GitHub version directly:

I don’t encounter this situation regularly enough for this to pose a bottleneck, but I guess another alternative is to have the local CI check for toml issues before submitting so that the Julia registry CI isn’t the one to catch it. e.g., the registry’s CI checks could be used as a required step for the release-please PR to be merged in the first place.

But in the rare case this does happen, note that one can do

git tag -f v1.3.2 HEAD
git push origin -f v1.3.2

to update the GitHub releases after fixing things

I guess it might be nice to have the release-please workflow automatically check-in after 25min or so, similar to tagbot… Maybe someone else could try this :slight_smile:

2 Likes

Yeah, what I meant is: TagBot allows a workflow that should never require mutating tags.

1 Like

I think the most sensible thing would be to gate registration by a workflow that performs the same checks by the registry: General/.github/workflows/automerge.yml at master Ā· JuliaRegistries/General Ā· GitHub

Therefore, a release-please PR would only be merged after the registration CI is happy.

Which is probably what we ought to be doing anyways, rather than relying on the public registry backstop CI as the only validator. (And even when the registry does catch some issues, it still requires an awkward PR to fix the compat entries in main for an already updated Project.toml)

1 Like

Amazing, I’ll definitely be giving this a try!
Thanks!

1 Like

They can be made so, and that’s important for supply chain security:

2 Likes

I could see this working with the current system if it just opened an issue (and kept it up to date) with a protected example of what the user should post to invoke JuliaRegistrator with a nice release note.

Also, note TagBot has gotten better at changelogs recently:

It sounds like this tool already creates the comment programmatically, which is nice

1 Like

Yeah! chore(release-v1): release 1.13.2 (#597) Ā· astroautomata/SymbolicRegression.jl@a8a11b9 Ā· GitHub

It also supports subdir packages - this bot will comment subdir=... in the Registrator trigger. So it should handle multiple projects in a single repo too.

@giordano the TagBot automation can be kept if desired - just comment out the GitHub release step.

There’s a lot of configurability in release-please: GitHub - googleapis/release-please-action: automated releases based on conventional commits Ā· GitHub. Setting the option skip-github-release to false will also work.

Here’s the full list of options:

input description
token A GitHub secret token, the action defaults to using the special secrets.GITHUB_TOKEN
release-type If specified, defines the release strategy to use for the repository. Reference Release types supported
path create a release from a path other than the repository’s root
target-branch branch to open pull release PR against (detected by default)
config-file Path to the release-please config in the repository. Defaults to release-please-config.json
manifest-file Path to the release-please versions manifest. Defaults to .release-please-manifest.json
repo-url GitHub repository name in the form of <owner>/<repo>. Defaults to the repository the action is running in.
github-api-url Override the GitHub API URL.
github-graphql-url Override the GitHub GraphQL URL
fork If true, send the PR from a fork. This requires the token to be a user that can create forks (e.g. not the default GITHUB_TOKEN)
include-component-in-tag If true, add prefix to tags and branches, allowing multiple libraries to be released from the same repository
proxy-server Configure a proxy server in the form of <host>:<port> e.g. proxy-host.com:8080
skip-github-release If true, do not attempt to create releases. This is useful if splitting release tagging from PR creation.
skip-github-pull-request If true, do not attempt to create release pull requests. This is useful if splitting release tagging from PR creation.
skip-labeling If true, do not attempt to label the PR.
changelog-host The proto://host where commits live. Defaults to ${{ github.server_url }} (usually https://github.com)
versioning-strategy The versioning strategy to use. Defaults to default
release-as The version to release as.
3 Likes

Hi, I’m the author of JuliaRegisterChangelog, where I wanted to accomplish a similar thing as Release Please. I leave my post here

My workflow works in the following way:

  • On every new commit triggers only if Project.toml has been modified
  • Check if the version has changed since the last commit and proceed only if it has
  • Generate a changelog automatically using Conventional Commits and conventional-changelog
  • Trigger JuliaRegistrator commenting on the latest commit to register the package and correctly formats the release notes

I leave here also the GitHub repo GitHub - alex180500/JuliaRegisterChangelog: Automatic changelog and registering of a Julia package using Conventional Commits and JuliaRegistrator Ā· GitHub

2 Likes

Cool! Sorry for not seeing your package earlier. Just so I understand, is your package a layer on top of google’s release-please, or is it a rewrite of release-please-like functionality entirely in Julia?

1 Like

Don’t worry about it! I wanted to achieve the changelog with conventional commit and the registration with the Registrator automatically. But at the same time I wanted to be the one triggering the release so whenever I want a new release, I just make a commit with an update of the Project.toml and the changelog + registration is done automatically (and then tagbot creates my tag but that is a separate workflow).

All the workflow is done using the conventional commit npm package (+ git diff and grep to check if the last commit actually was a version bump). Then I use github action script to comment the commit that is inspected.

1 Like