Automating Builds and Releases with Conventional Commits and Semantic Versioning
With multiple repositories where I manage package builds and releases through GitHub pipelines, I sought a reusable solution that could streamline my workflows across all repositories. Additionally, since I now use Conventional Commits across all the repositories I maintain, I wanted a solution that incorporates this convention into the release workflow. In this blog post, I’ll share my approach and provide an example of how to automatically create semantic versioned releases and generate changelogs based on your Conventional Commits.
The approach that I've implemented is powered by semantic-release, which automatically handles versioning based on Conventional Commits. By analyzing the commit history, it determines whether a release should be major, minor, or patch. The workflow then builds and tags a Docker image, and pushes it to GitHub Container Registry (GHCR).
First, we need a trigger
I set up a GitHub Action that triggers two events:
- Pushes to the
main
branch: This ensures that any merge to the main branch triggers the release and build process. - Manual triggers via
workflow_dispatch
: This allows me to manually start the process if needed.
Here's how the trigger section looks:
name: Release and Build our Docker Image
on:
push:
branches:
- main
workflow_dispatch:
Use semantic-release for Automated Versioning
The first job in this workflow handles the release process. The core of this job is semantic-release, which automates versioning based on the commit messages. This ensures that every change is tagged correctly following Semantic Versioning principles. Semantic-release also updates the changelog and pushes the new release details to GitHub. The jobs need some specific permissions so grant them carefully.
Here is the configuration part we need:
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4
with:
fetch-depth: 0
In addition to checking out the repository, semantic-release requires access to your repo’s history to analyze commits and generate the next version.
Next, I set up semantic-release by running it as part of the CI process:
- name: Run Semantic Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npx semantic-release
This ensures that every valid commit triggers a new release. Based on the commit messages, semantic-release determines the correct version (major, minor, or patch), tags the release, and updates the GitHub repository with the latest version.
Configure semantic-release in Your Repository
On top of the GitHub Action configuration, we need some configuration for semantic-release itself in our repository.
A good starting point to install all the required packages in your own codebase would be: https://github.com/semantic-release/semantic-release/blob/master/docs/usage/installation.md
releaserc.yml
Configuration
We need this file to configure semantic-release options or plugins that we need. To control how semantic-release operates, I used the following plugins in a releaserc.yml
file:
branches:
- main
plugins:
- "@semantic-release/commit-analyzer"
- "@semantic-release/release-notes-generator"
- "@semantic-release/changelog"
- "@semantic-release/github"
- "@semantic-release/git"
Each plugin serves a specific purpose:
- Commit Analyzer determines the type of release (major, minor, patch) based on commit messages.
- Release Notes Generator generates release notes automatically.
- Changelog updates the
CHANGELOG.md
file. - GitHub creates a new GitHub release.
- Git pushes the updated release files and tags to the repository.
The official documentation is very well written if you want to implement additional things.
Building and Pushing Docker Images
Once the release job is completed successfully, the next step is to build and push a Docker image to GHCR (GitHub Container Registry). This job depends on the release job, ensuring it only runs when a new release is created.
build-and-push-image:
needs: release
runs-on: ubuntu-latest
if: success()
Docker Login
Before building the image, I log in to the GitHub Container Registry using a GitHub token. This provides the necessary credentials for pushing the image later.
- name: Log in to the Container registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
Fetching the Latest Git Tag
To ensure that the Docker image is tagged correctly, I retrieve the latest Git tag generated by semantic-release:
- name: Get latest Git tag
id: version
run: |
latest_tag=$(git describe --tags `git rev-list
--tags --max-count=1`)
echo "Latest Git tag: $latest_tag"
echo "version=$latest_tag" >> $GITHUB_ENV
This step stores the latest version tag in the GitHub environment for use in subsequent steps.
Note: This is a trivial approach on how to get the latest tag from your repository history. You might need to change this part of the pipeline to fit your needs.
Setting Docker Image Tags
I use the docker/metadata-action
to set the image tags. This allows me to tag the image with both latest
and the newly created version.
- name: Set Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=raw,value=latest
type=raw,value=${{ env.version }}
Building and Pushing the Docker Image
Finally, the Docker image is built and pushed to GHCR using the following step:
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
This command builds the Docker image based on the latest code and pushes it to the registry with the specified tags (latest
and the specific version).
✨ The full GitHub Action can be found here: https://gist.github.com/xFuture603/4933f23cbe59fe9eba5fc832e84e4ada
Conclusion
By integrating Conventional Commits, and semantic-release, I’ve streamlined the process of building and releasing container images. Now, every push to the main
branch triggers an automatic version bump, release, and container image build, ensuring consistency and reducing manual overhead.
Sources
Conventional Commits - https://www.conventionalcommits.org/en/v1.0.0/
Semantic Release Configuration File Documentation - https://semantic-release.gitbook.io/semantic-release/usage/configuration#configuration-file
Working with Conventional Commits - https://xfuture-blog.com/working-with-conventional-commits/
Semantic Versioning - https://semver.org/
semantic-release - https://github.com/semantic-release/semantic-release
GitHub Action Pipeline Code - https://gist.github.com/xFuture603/4933f23cbe59fe9eba5fc832e84e4ada