I'm a huge fan of open sourcing small utilities, but I never want to release something without automating new versions. As best as I can tell, projects always look more serious and stable if there’s multiple versions that have been published. In this post, I'm going to go over everything that I did to prepare and automate versioning and releasing an open source project. I've used the same base layout for several projects like actions-poetry (261 stars), typed-json-dataclass (79 stars), release-info-action (6 stars), and have even released company internal tools the same way, but today we'll be focused on my most recent utility, github-ratelimit-metrics and the combination of settings that I use for making releases as easy as possible for myself.
There’s three parts to making this work well:
We’ll be releasing these projects with semantic versioning (SemVer), which I appreciate as a consumer, because it let’s me easily understand whether or not I should expect an update to impact my usage.
So the first thing that we want to do is make sure that the repositories we want to configure this all on, have squash merging with the following default:
The default is relevant because you might accidentally forget to include a conventional commit header with a commit which will prevent the later automation from knowing how to update the semantic version.
Then configure your repository with a GitHub Action that uses amannn/action-semantic-pull-request. The README has the workflow that I pretty much copy paste into my projects.
This should help ensure that you always keep a conventional commit header in your PRs. Conventional commits are a specification that maps directly to knowing how you should change the numbers that are part of your semantic version. You can prefix a commit with something like feat:
which would correspond to a minor version bump, or fix:
which would correspond to a patch version bump.
Next, you want to configure semantic-release which is primarily a tool for automatically building and publishing packages to NPM, but with a little bit of configuration, it ends up being an amazing tool for general release management.
semantic-release has a large list of available plugins which will let you control what platform you publish to and what documentation to create.
I place the following content into a file called .releaserc
in the root of my repositories:
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/github"
]
}
The default branch for semantic-release is still master, so I switch it to using main. And then I use the commit-analyzer, release-notes-generator, and github plugins.
The commit-analyzer is what will read our conventional commit headers, the release-notes-generator will create a changelog and the github plugin will make releases on GitHub, using the generated changelog for the release description and will comment on related issues and related pull requests. These plugins together can be used for any kind of project. GitHub Actions, docker containers, etc.
One of the reasons that I really appreciate using semantic-release is for the commenting features. When semantic-release is executed, a comment is posted back to the pull request when a PR is included in a release, and if the pull request mentions fixing an issue, the tool will also comment on the issue letting watchers know that the fix has been released.
I love this feature for letting contributors know when their new features are available, or when bugs have been fixed. The contributors can instantly know which version they need to upgrade to to get the changes.
So how does this all get linked together? Let’s look at the release flow for github-ratelimit-metrics.
You can look at the full workflow here but since it’s large, we’ll break it down in this post.
Every workflow release job starts the same
- uses: actions/checkout@v3
- name: Generate release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
First we check out the repo so that we can get the .releaserc
, and then immediately run npx semantic-release
. GitHub Actions runners always have an LTS version of nodejs already installed which includes npm and npx. npx is a tool which will download and execute the latest version of an npm package. So executing npx semantic-release
will immediately execute semantic-release which will automatically pick up the .releaserc. And we pass in the GITHUB_TOKEN from the workflow which has all of the permissions that it needs by default.
If there’s a new version warranted by the commits since the last time this workflow was ran, then semantic-release will publish that to the GitHub releases. In the case of a Docker based project, now we can fetch that version so that we can tag the container.
- name: Get Latest Release
id: latest_version
uses: abatilo/release-info-action@v1.3.1
with:
owner: ${{ github.repository_owner }}
repo: ${{ github.event.repository.name }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prep docker tag
uses: docker/metadata-action@v3
id: metadata
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=${{ steps.latest_version.outputs.latest_tag }}
So let’s fetch the latest release version for this repo, and then we’ll pass the output of the latest tag into the docker/metadata-action action.
- name: Set docker cache tags
uses: int128/docker-build-cache-config-action@v1
id: cache
with:
image: ghcr.io/${{ github.repository }}/cache
- uses: docker/build-push-action@v2
id: build
with:
push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: ${{ steps.cache.outputs.cache-from }}
cache-to: ${{ steps.cache.outputs.cache-to }}
Then we can build our container using the tags and the container will get pushed.
And that’s all it takes! If you’re releasing a GitHub Action, you can skip all the parts about building a docker container. If you’re releasing a cross compiled go binary, you can run something like the goreleaser-action directly after the semantic-release step, and goreleaser will actually edit the release created by semantic-release with all of the compiled binaries. This way, you get all the comment automation and changelog automation from semantic-release and still get all of the binary compilation from goreleaser all on the same release.