Using tooling to automate releases
I have been working on a bunch of personal code projects in the past few months and as with all things we humans do, trying to do a thing always implies doing a number of things you really don't want to do.
You want a burger: you need to kill a cow, and light a fire, and whatnot.
You want to write software: you need to release it every once in a while.
Mind you, it's possible to never release, and be happy that way, but if you want someone else to use it, then releases are needed.
The bad news: releases are boring and can be laborious. Let's use as an example a tool I wrote called Hacé.
It's a command line tool, so if I want to do a release it involves:
- Running tests to make sure it's in a good state
- Update the changelog to have some data of what's in the release
- Bump the version number in files that have it (usually at least source code, changelog and metadata)
- Commit the version bump, and tag it with the same version
- Build static versions of the CLI (or create docker images, whatever)
- Create source assets
- Make the release in GitHub so people can see it
- Use the changelog in the release text
And if you do any of those things wrong, you get to google (again) how to remove a remote tag using git and whatever.
Here is a shell script that will do all of it, then I will explain 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)"
Whenever I want to do a release, I just run that script and it will do everything. Let's go over it bit by bit.
This runs using bash, and will stop on any errors.
#!/bin/bash
set e
I want to use this same script on different projects so I get PKGNAME from the name of the folder it's running in. I always make that be the same as the name of the repository.
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 commits include a "fix" this will be at least a patch release.
- If the commits since the previous release include a "feat" this will be at least a minor release.
- If the commits include a "feat!" this will be a major release (the ! marks it as a breaking change)
This way the release I am making will be semantically correct according to semver. Some people dislike semantic versioning. That's ok, this is just one way to do things.
If you have commits that are not "conventional", cliff will ignore them and they will not be in the changelog. I am forcing myself to make all my commits conventional using pre-commit hooks but that's just my personal preference.
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.
Usually you also have the version set in your code, but for that I adopted a solution I saw in someone else's code and get it at build time by inserting it into a constant at compilation time.
VERSION = {{ `shards version #{__DIR__}`.chomp.stringify }}
Hacé is sort of a make replacement, so this runs tests and a linter.
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 conventional commit with a version bump, create the git tag with the version number, and push everything to GitHub:
git commit -a -m "bump: Release v$VERSION"
git tag "v$VERSION"
git push --tags
This creates static binaries for Linux in ARM and Intel architectures. I wrote about the details in another article.
hace static
Finally, we use the GitHub command line tool gh
to:
- Create the release (with the right version)
- Upload the static binaries
- GitHub automatically creates source artifacts
- Use this release's changelog as notes for the release, 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 doesn't make the release process much faster (building the static binaries takes a few minutes) it does make it super reliable, and makes sure the changelog is up to date and the release is not something like "v0.3.4: changes and bugfixes".
You can see how a release looks like in the hacé releases page