As part of an internal improvement, my team converted most of our internal JVM libraries to be published in a BOM (Bill of Materials) format. Previously, each of our over 60 libraries was published individually. This transition to a BOM was a huge step forward for our developer experience, as now we only need to upgrade a single version instead of updating each library’s version individually.
However, while the BOM simplifies upgrades, it introduces a new challenge: tracking changes between different BOM versions. For example, what exactly changed between version 4.0.1 and 4.0.2?
Since we use GitLab, we decided to enhance our release process by providing better release information for each library. After considering our options, we found that GitLab changelog feature was the best approach for our use case.
I drew inspiration from a project I like, Dependabot for GitLab, which uses changelog APIs to create awesome release notes like the one below.
Additionally, it automatically updates the CHANGELOG.md file, as shown below:
How to Add Release Notes to a Project
GitLab’s changelog feature relies on adding a commit trailer to the commit message. This tells GitLab what to include in the release notes. Since there’s no default value, you need to manually add the changelog trailer to each commit you want to appear in the release notes.
You can do this by appending changelog: value
to the end of your commit message, like this:
Your commit title
Your commit message
changelog: fix
If you’re using the command line, you can achieve this with:
git commit --message "Fixed log format" --trailer "changelog: fix"
To start the process of adding a release notes to your project, you have to do the following steps.
Steps to Add Release Notes to Your Project
1. Add a changelog_config.yml
This step is only required if you want to customize the release note categories or template. The default configuration from GitLab’s documentation looks like this:
{% if categories %}
{% each categories %}
### {{ title }} ({% if single_change %}1 change{% else %}{{ count }} changes{% end %})
{% each entries %}
- [{{ title }}]({{ commit.web_url }})\
{% if author.credit %} by {{ author.reference }}{% end %}\
{% if merge_request %} ([merge request]({{ merge_request.web_url }})){% end %}
{% end %}
{% end %}
{% else %}
No changes.
{% end %}
The default categories include:
- `added`: New feature
- `fixed`: Bug fix
- `changed`: Feature change
- `deprecated`: New deprecation
- `removed`: Feature removal
- `security`: Security fix
- `performance`: Performance improvement
- `other`: Other
To customize categories or the template, create a changelog_config.yml
file in the .gitlab
folder. Below is an example from the Dependabot project:
---
categories:
breaking: "💥 Breaking changes"
security: "⚠️ Security updates"
feature: "🚀 New features"
improvement: "🔬 Improvements"
fix: "🐞 Bug Fixes"
dependency: "📦 Dependency updates"
ci: "🔧 CI changes"
chore: "🧹 Maintenance"
doc: "📄 Documentation updates"
other: "❓ Other changes (changelog not specified)"
template: |
{% if categories %}
{% each categories %}
### {{ title }} ({% if single_change %}1 change{% else %}{{ count }} changes{% end %})
{% each entries %}
- [{{ title }}]({{ commit.reference }}) by {{ author.reference }}.\
{% if merge_request %} See merge request {{ merge_request.reference }}{% end %}
{% end %}
{% end %}
{% else %}
No changes with `changelog` in the commit message detected
{% end %}
If your versioning tags differ from X.Y.Z
or vX.Y.Z
, you need to customize the tag regex. For example, if your tags follow version-X.Y.Z
, you can add the following:
tag_regex: '^version-(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)$'`
the tag regex is used by Gitlab when you create a new release to check for the commits between the two versions.
2. Publishing Release Pipeline
To automate releases, you need a pipeline that creates a tag (or just a release) based on a version and a commit reference. Here’s an example configuration:
publish-gitlab-release:
stage: release
image: registry.gitlab.com/dependabot-gitlab/ci-images/release-cli:0.18
variables:
RELEASE_NOTES_FILE: release_notes.md
needs:
- job: publish-bom
artifacts: true
rules:
- if: $CI_COMMIT_TAG
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_PIPELINE_TRIGGERED != "true" && $CI_COMMIT_MESSAGE !~ /Add changelog for version/
script:
- echo "running publish-gitlab-release for $TAG_NAME"
- .gitlab/ci/script/changelog.sh
interruptible: false
release:
tag_name: '$TAG_NAME'
description: $RELEASE_NOTES_FILE
name: '$TAG_NAME'
ref: '$CI_COMMIT_SHA'
A few things to note in the above gitlab-ci.yaml job.
Key points:
- Image: We use Dependabot’s release image to run the
changelog.sh
script. - changelog.sh: This script fetches changelog information from the GitLab API, stores it in
RELEASE_NOTES_FILE
, and updates theCHANGELOG.md
file. It is based on the file changelog.sh from dependabot chart and it is shown below.
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/utils.sh"
changelog_endpoint="${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/repository/changelog"
header="PRIVATE-TOKEN: ${GITLAB_ACCESS_TOKEN}"
release_version=$(echo ${CI_COMMIT_TAG} | grep -oP 'v\K[0-9.]+')
data="version=${release_version}&trailer=changelog"
info "Fetching release notes"
curl -s --header "${header}" "${changelog_endpoint}?${data}" | jq -r ".notes" > ${RELEASE_NOTES_FILE}
info "Updating CHANGELOG.md"
curl -X POST --header "${header}" --data "${data}" "${changelog_endpoint}"
You’ll need a GitLab access token with API scope stored as a CI/CD variable (e.g., GITLAB_ACCESS_TOKEN
), and permission for the user associated with the token to push to the protected master branch. So, in Settings -> Repository
expand the Protected branches
options and in Allowed to push and merge
add the the user which the token you created.
When the CHANGELOG.md is updated in the master branch, the commit message is in the format Add changelog for version X
, where X
is the value of the version
argument. If you don’t want to run circular dependency in the master branch you have to add to the rule to not run the job when the file is commited, as we did above.
Optional: Adding a Check Pipeline for Commit Trailers
Since adding commit trailers is manual, it’s easy to forget. To prevent missing trailers, you can add a pipeline that checks if the first commit in a merge request contains a trailer. Here’s the check_commit_trailer.sh
script:
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/utils.sh"
info "Fetching the merge base..."
# Fetch the merge base between the target branch and the MR branch
# shellcheck disable=SC2086
git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME":$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
MERGE_BASE=$(git merge-base "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" "$CI_COMMIT_SHA")
info "Listing commits from the merge base to the latest commit..."
# List all commits from the merge base to the current commit (in MR)
COMMITS=$(git rev-list "$MERGE_BASE".."$CI_COMMIT_SHA")
info "Identifying the first commit in the merge request..."
# Get the first commit (the oldest) from the commit list
FIRST_COMMIT=$(echo "$COMMITS" | tail -n 1)
info "First commit is $FIRST_COMMIT"
# Extract the commit message of the first commit
FIRST_COMMIT_MESSAGE=$(git log -1 --pretty=%B "$FIRST_COMMIT")
info "First commit message is $FIRST_COMMIT_MESSAGE"
PATTERN="changelog: (breaking|security|feature|improvement|fix|dependency|ci|chore|doc|deploy|other)"
info "Checking if the first commit contains the 'changelog:' trailer..."
# Check if the trailer 'Changelog:' exists in the first commit message
if echo "$FIRST_COMMIT_MESSAGE" | grep -i -qE "$PATTERN"; then
success "Changelog trailer found in the first commit.";
else
warn "Changelog trailer not found in the first commit.";
warn "please rebase or amend your commit to add a changelog in the format ${PATTERN}";
exit 1;
fi
And the corresponding GitLab CI job:
check-first-commit-trailer:
stage: check
image: registry.gitlab.com/dependabot-gitlab/ci-images/release-cli:0.18
allow_failure: true # allow the pipeline to fail as it is just a warning
variables:
GIT_SUBMODULE_STRATEGY: recursive
GIT_SUBMODULE_UPDATE_FLAGS: --remote --force
script:
- ./gitlab-ci-templates/scripts/check_commit_trailer.sh
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
In this case as it is just a warning I added allow_failure: true
so it will allow everything to proceed.
Updating Dependabot Configuration
You can add the following to dependabot.yaml
to ensure all dependency merge requests created by Dependabot include a trailer:
commit-message:
trailers:
- changelog: dependency
Like the following.
version: 2
registries:
Artifactory:
type: maven-repository
url: ...
username: ...
password: "..."
updates:
- package-ecosystem: gradle
directory: /
schedule:
interval: daily
time: "02:30"
timezone: "America/Vancouver"
open-pull-requests-limit: 100
commit-message:
trailers:
- changelog: dependency
ignore:
- dependency-name: "org.springframework*"
update-types: [ "version-update:semver-major" ]
Additional GitLab Project Configuration
To ensure everything works correctly, you’ll need to adjust some settings in your GitLab project configuration. For these changes, you’ll either need maintainer access or assistance from someone who has it.
Modify the Squash Commit Message Template
By default, the commit message for squashed commits uses the merge request (MR) title, which prevents the required trailer from being included in the release notes. To fix this, navigate to Settings -> Merge requests
and locate the Squash commit message
template section. Change the template from %{title}
to %{first_multiline_commit}
. This way, the full message of the first commit, including the trailer, will be used in the squashed commit. Remember to save the changes.
By making these improvements, we’ve streamlined the process of generating release notes and changelogs for our BOM libraries while also ensuring consistency and automation in our GitLab pipelines.
Cheers.