Pixel-art image of a bull.

Shane Hull

Semantic Release (Please)

Published: 2024-02-10 Semantic Release (Please)

Using release-please to automate software releases with Semantic Commits (without the bloat).



In this post, I’ll give a quick how-to on using release-please as a tool to automate releases using Conventional Commits.

There are a few alternatives out there, but they are mostly bloated (either on their own terms or via a plugin ecosystem), highly opinionated, they force you into a certain workflow, or they are written with only a JS/TS stack in mind.

While unfortunately it’s still written in JS/TS, it’s nowhere near as opinionated, it’s super lightweight, and it doesn’t force you into some wacky workflow (other than the PR > release workflow, which I’ll explain).

The tool should be ran on every merge to the branch you want to release from. It will perform certain operations depending on the source branch and whether there is an open PR for a release.

For a release PR, it creates it’s own special branch that it understands. Only when one of these special branches are merged will it create a release.

Basically its: Merge to main > PR > Release, with the option to merge changes into your main branch as many times as you want before triggering the release.

Workflow for Merging a Feature Branch

  1. The Release-Please Job is Triggered:

    • The process begins when the ‘release-please’ job is triggered on a merge to the branch you want to release from.
  2. Checking for Feature/Fix Commits:

    • Determine if there are any ‘feature’ or ‘fix’ commits.
      • If there are no feature/fix commits, the process stops here (do nothing).
      • If there are feature/fix commits, proceed to the next step.
  3. Handling the Release Pull Request (PR):

    • Check for the existence of an existing branch + PR for a release. This is a special branch created by the tool.
      • If an existing release PR is found:
        • Update the changelog in the branch and the PR description with notes on latest feature/fix changes.
      • If no existing release PR is found:
        • Create a new release PR.
        • Add the label ‘autorelease: pending’ to the new PR.
        • Include any necessary changes for the changelog in this new PR.
  4. Finalizing the Feature Branch Merge:

    • At this point, if there were feature/fix commits merged into the main branch, a PR for a release will be prepared.
    • When ready to release, merge the prepared PR. This action triggers the next phase of the workflow.

Workflow for Merging a Release Branch

  1. The Release-Please Job is Triggered:

    • Similar to the feature branch, this process starts when the ‘release-please’ job is triggered for a release branch.
  2. Determining Branch Type:

    • Assess if the branch in question is a ‘release-please’ branch.
      • If it is not a release-please branch, the process ends here (do nothing).
      • If it is a release-please branch, proceed to create a release.

There is an official GitHub action available for us to use, which we’ll explore shortly, but first we need to add some configuration files.

Config and Manifest Files

Before we run release-please, we need an initial manifest file and a config file that configures one or more release targets.

Manifest File

.release-please-manifest.json:

{
  ".": "0.0.0"
}

The manifest file keeps track of the current version of each package in your repo.

For our purpose, we just have one target, and that is the root directory. You can, however, configure multiple targets to support a monorepo pattern.

Set it to 0.0.0 for now. The workflow will update and manage this for us once we create our first release.

Config File

release-please-config.json:

{
  "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
  "release-type": "go",
  "packages": {
    ".": {}
  }
}

Again, we just have one package, and the release-type is go. A full list of supported release types is available here.

Please consult the official documentation for more.

Now let’s get into the GitHub Action Workflow.

GitHub Actions Workflow

For this example, we’ll deploy a cli tool that is written in Go, and we’ll upload some assets (the Go binaries) to the release for completeness.

The path we’ll be building is cmd/shed, and we’ll use xgo to target multiple platforms, uploading the binaries to GitHub releases after it is created by release-please.

Here is the full workflow:

name: Build & Release

on:
  push:
    branches:
      - main

permissions:
  contents: write
  pull-requests: write

jobs:
  Release:
    runs-on: ubuntu-latest
    steps:
      - name: Release Please
        id: release-please
        uses: googleapis/release-please-action@v4
        with:
          release-type: go
          token: ${{secrets.GITHUB_TOKEN}}
      - uses: actions/checkout@v4
        if: ${{ steps.release-please.outputs.release_created }}
      - uses: actions/setup-go@v4
        if: ${{ steps.release-please.outputs.release_created }}
        with:
          go-version: 1.22
      - name: Build
        if: ${{ steps.release-please.outputs.release_created }}
        uses: crazy-max/ghaction-xgo@v3
        with:
          xgo_version: latest
          go_version: 1.22.x
          pkg: cmd/shed/
          dest: bin
          prefix: shed
          targets: linux/amd64,darwin/amd64,darwin/arm64,windows/amd64
          v: true
          ldflags: -s -w
      - name: Upload Release Binaries
        if: ${{ steps.release-please.outputs.release_created }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: gh release upload ${{ steps.release-please.outputs.tag_name }} ./bin/*

Let’s go through it step by step.

Permissions

Before we get into steps, the action requires some extra permissions defined in the workflow file:

permissions:
  contents: write
  pull-requests: write

We’ll also need to allow GitHub actions to create pull requests by allowing it in the repo’s settings via:

Settings > Actions > General > Allow GitHub Actions to create and approve pull requests

Steps

The first step is the release-please job. It’s pretty self-explanatory in terms of what to configure, and it just executes the workflow outline above.

It needs the default GITHUB_TOKEN (available in all pipelines).

There are no rules for when this step should run - we want it to run on every push to main, as defined in the global on rules at the top on the action.

- name: Release Please
  id: release-please
  uses: googleapis/release-please-action@v4
  with:
    release-type: go
    token: ${{secrets.GITHUB_TOKEN}}

All steps that follow the release-please step are only ran on a certain condition: if the release_created output from the release-please step is true.

if: ${{ steps.release-please.outputs.release_created }}

This output is only true when a release-please specific branch is merged to main (from a release PR), and a release is created by the tool.

The first conditional step is self explanatory, which just checks out main and sets up Go:

- uses: actions/checkout@v4
  if: ${{ steps.release-please.outputs.release_created }}
- uses: actions/setup-go@v4
  if: ${{ steps.release-please.outputs.release_created }}
  with:
    go-version: 1.22

The great thing about release-please is that none of these hefty steps need to run unless we actually release, so it’s a super quick pipeline in almost all cases.

At this point, we have a GitHub release with the changelog, description, etc, etc.. But, we don’t have any binaries for our cli tool.

So, the next step is to build the binaries with xgo:

- name: Build
  if: ${{ steps.release-please.outputs.release_created }}
  uses: crazy-max/ghaction-xgo@v3
  with:
    xgo_version: latest
    go_version: 1.22.x
    pkg: cmd/shed/
    dest: bin
    prefix: shed
    targets: linux/amd64,darwin/amd64,darwin/arm64,windows/amd64
    v: true
    ldflags: -s -w

You can do this however you like, or publish whatever you like as assets, but just roll with the example.

Next, we need to publish the binaries to GitHub releases. We’ll do this with the gh CLI tool, which is available on the runners by default:

- name: Upload Release Binaries
  if: ${{ steps.release-please.outputs.release_created }}
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  run: gh release upload ${{ steps.release-please.outputs.tag_name }} ./bin/*

At this point, we have a full release with a list of fixes, features and chores, complete with binaries for our platforms published as assets.

Screenshot of a GitHub release created by release-please.

Your First Release

By default, release-please will create the first release with the version number 1.0.0, which is not desired in any case..

To start at 0.0.1, create a commit like so before merging into main for the first time:

git commit -m "chore: release 0.0.1

Release-As: 0.0.1" --allow-empty

This will override the release version for the first PR, and subsequent releases are incremented correctly from there.

And that’s it, enjoy (please)!




<- back