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.

Release notes from Dependabot Gitlab

Release notes from Dependabot Gitlab

Additionally, it automatically updates the CHANGELOG.md file, as shown below:

Changelog from Dependabot Gitlab

Changelog from Dependabot Gitlab

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 the CHANGELOG.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.