Skip to main content

Ralsina.Me — Roberto Alsina's website

Using tooling to automate releases

I have been work­ing on a bunch of per­son­al code projects in the past few months and as with all things we hu­mans do, try­ing to do a thing al­ways im­plies do­ing a num­ber of things you re­al­ly don't want to do.

You want a burg­er: you need to kill a cow, and light a fire, and what­not.

You want to write soft­ware: you need to re­lease it ev­ery once in a while.

Mind you, it's pos­si­ble to nev­er re­lease, and be hap­py that way, but if you want some­one else to use it, then re­leas­es are need­ed.

The bad news: re­leas­es are bor­ing and can be la­bo­ri­ous. Let's use as an ex­am­ple a tool I wrote called Hacé.

It's a com­mand line tool, so if I want to do a re­lease it in­volves:

  • Run­ning tests to make sure it's in a good state
  • Up­date the changel­og to have some da­ta of what's in the re­lease
  • Bump the ver­sion num­ber in files that have it (usu­al­ly at least source code, changel­og and meta­data)
  • Com­mit the ver­sion bump, and tag it with the same ver­sion
  • Build stat­ic ver­sions of the CLI (or cre­ate dock­er im­ages, what­ev­er)
  • Cre­ate source as­sets
  • Make the re­lease in GitHub so peo­ple can see it
  • Use the changel­og in the re­lease text

And if you do any of those things wrong, you get to google (a­gain) how to re­move a re­mote tag us­ing git and what­ev­er.

Here is a shell script that will do all of it, then I will ex­plain each step.

#!/bin/bash
set e

PKGNAME=$(basename "$PWD")
VERSION=$(git cliff --bumped-version |cut -dv -f2)

sed s/^version:.*$/version: "$VERSION"/ -i shard.yml
git add shard.yml
hace lint test
git cliff --bump -o
git commit -a -m "bump: Release v$VERSION"
git tag "v$VERSION"
git push --tags
hace static
gh release create "v$VERSION" \
    "bin/$PKGNAME-static-linux-amd64" \
    "bin/$PKGNAME-static-linux-arm64" \
    --title "Release v$VERSION" \
    --notes "$(git cliff -l -s all)"

When­ev­er I want to do a re­lease, I just run that script and it will do ev­ery­thing. Let's go over it bit by bit.

This runs us­ing bash, and will stop on any er­rors.

#!/bin/bash
set e

I want to use this same script on dif­fer­ent projects so I get PKG­NAME from the name of the fold­er it's run­ning in. I al­ways make that be the same as the name of the repos­i­to­ry.

PKGNAME=$(basename "$PWD")
VERSION=$(git cliff --bumped-version |cut -dv -f2)

The VERSION variable is obtained from ... git cliff? What is git cliff?

It's a changelog generator. That means it will create the changelog (later) from commit messages, but in this case what it's doing is suggesting what the next version number should be. To use git cliff you need to adopt "conventional commits", which means every commit message has a prefix like "feat:" or "fix:" or "docs:"

  • If those com­mits in­clude a "fix" this will be at least a patch re­lease.
  • If the com­mits since the pre­vi­ous re­lease in­clude a "feat" this will be at least a mi­nor re­lease.
  • If the com­mits in­clude a "feat!" this will be a ma­jor re­lease (the ! marks it as a break­ing change)

This way the re­lease I am mak­ing will be se­man­ti­cal­ly cor­rect ac­cord­ing to semver. Some peo­ple dis­like se­man­tic ver­sion­ing. That's ok, this is just one way to do things.

If you have com­mits that are not "con­ven­tion­al", cliff will ig­nore them and they will not be in the changel­og. I am forc­ing my­self to make all my com­mits con­ven­tion­al us­ing pre-­com­mit hooks but that's just my per­son­al pref­er­ence.

sed s/^version:.*$/version: "$VERSION"/ -i shard.yml
git add shard.yml

This is a Crystal program, and you are supposed to keep your version in the shard.yml metadata file, so this patches the file using good old sed, and then marks it as needing to be committed.

Usu­al­ly you al­so have the ver­sion set in your code, but for that I adopt­ed a so­lu­tion I saw in some­one else's code and get it at build time by in­sert­ing it in­to a con­stant at com­pi­la­tion time.

 VERSION = {{ `shards version #{__DIR__}`.chomp.stringify }}

Hacé is sort of a make re­place­men­t, so this runs tests and a lin­ter.

hace lint test

As mentioned before, git cliff is a changelog generator. This command updates the CHANGELOG.md file with the changes in the new release.

git cliff --bump -o

Now, a con­ven­tion­al com­mit with a ver­sion bump, cre­ate the git tag with the ver­sion num­ber, and push ev­ery­thing to GitHub:

git commit -a -m "bump: Release v$VERSION"
git tag "v$VERSION"
git push --tags

This cre­ates stat­ic bi­na­ries for Lin­ux in ARM and In­tel ar­chi­tec­tures. I wrote about the de­tails in an­oth­er ar­ti­cle.

hace static

Finally, we use the GitHub command line tool gh to:

  • Cre­ate the re­lease (with the right ver­sion)
  • Up­load the stat­ic bi­na­ries
  • GitHub au­to­mat­i­cal­ly cre­ates source ar­ti­facts
  • Use this re­lease's changel­og as notes for the re­lease, so users can see what's new
gh release create "v$VERSION" \
    "bin/$PKGNAME-static-linux-amd64" \
    "bin/$PKGNAME-static-linux-arm64" \
    --title "Release v$VERSION" \
    --notes "$(git cliff -l -s all)"

And that's it. While this does­n't make the re­lease process much faster (build­ing the stat­ic bi­na­ries takes a few min­utes) it does make it su­per re­li­able, and makes sure the changel­og is up to date and the re­lease is not some­thing like "v0.3.4: changes and bug­fix­es".

You can see how a re­lease looks like in the hacé re­leas­es page


Contents © 2000-2024 Roberto Alsina