from merge request titles and labels, as the last step of every deploy pipeline
Manually maintained release notes almost always go stale faster than they get written, because nobody finds time to properly fill them in after a hectic deploy. With a Conventional Commits convention, clear merge request labels, and the GitLab Releases API, release notes can instead be generated automatically from exactly the information that already accrues with every merge request anyway. This article walks through the full path, from the commit convention to integration as the final step of the deploy pipeline.
Table of Contents
- 1. Why manual release notes are a maintenance problem
- 2. Conventional Commits as the structural foundation
- 3. Merge request titles and labels as a data source
- 4. How the GitLab Releases API is structured
- 5. A pipeline job that generates release notes from merge requests
- 6. Categorizing by label in the release description
- 7. Integration into the deploy pipeline as the final step
- 8. Limits of the automation: what it does not replace
- 9. Checklist for getting started with automated release notes
- 10. Summary
- 11. FAQ
1. Why manual release notes are a maintenance problem
Maintaining release notes by hand in practice almost always means someone hastily writes up a list of the most important changes shortly before or after a deploy, mostly from memory and without systematically cross-checking against what actually landed in merge requests since the last release. Smaller but genuinely customer-relevant changes regularly get left out this way, simply because they were forgotten.
The actual problem here is not laziness, but the fact that the information about what changed already exists in full: in merge request titles, the labels applied, and the commit history since the last tag. This data merely needs to be read out in a structured way and summarized automatically, instead of being reconstructed a second time by hand.
2. Conventional Commits as the structural foundation
The Conventional Commits convention requires every commit message to start with a prefix such as feat:, fix:, docs:, or chore:, followed by a short description of the change, plus optionally a ! after the type or a BREAKING CHANGE: line in the body for incompatible changes. This simple, machine-readable structure makes it possible to automatically sort commits into categories such as new features, bug fixes, and breaking changes.
For the convention to actually be followed, a commit message linter as a dedicated CI job pays off, checking every merge request for the correct prefix and failing when a commit message does not match the expected pattern. Without such enforcement, the convention tends to erode within a few weeks, since individual commits get committed without a prefix and the automatic categorization then develops gaps.
# Examples of Conventional Commits compliant messages
feat(checkout): add support for saved payment methods
fix(catalog): correct price rounding for bundle products
feat(api)!: remove deprecated v1 product endpoint
BREAKING CHANGE: Clients must migrate to the v2 product endpoint.
3. Merge request titles and labels as a data source
Beyond the commit history, GitLab itself already provides a structured data source: every merge request has a title, one or more labels, and a target milestone. A sensible label convention, for instance type::feature, type::fix, type::breaking, and type::internal, makes categorization for release notes independent of whether every individual commit inside the merge request follows the Conventional Commits convention.
The merge request title itself should be deliberately phrased in customer-understandable terms rather than technical implementation detail, since it often gets carried over directly, or only lightly adjusted, into the final release notes. A title like New payment method: invoice purchase for B2B customers fits release notes considerably better than refactor PaymentMethodProvider interface, even though both phrasings would be equally valid for the code review itself.
4. How the GitLab Releases API is structured
The GitLab Releases API allows creating a new release, complete with tag name, description, and optional release assets such as compiled artifacts, through a POST request to /projects/:id/releases. The description itself is passed as Markdown text, which lets headings, bullet lists, and links to the respective merge requests render directly in GitLab's release view.
In practice, combining this with GitLab's own release-cli pays off, which exists as a prebuilt CI/CD component for exactly this purpose and wraps the API call, so the own pipeline only has to hand over the generated Markdown description as a file, instead of building the raw API request by hand.
curl --request POST \
--header "PRIVATE-TOKEN: $CI_JOB_TOKEN" \
--header "Content-Type: application/json" \
--data "{
\"tag_name\": \"v2.14.0\",
\"name\": \"Release v2.14.0\",
\"description\": \"$(cat release-notes.md)\"
}" \
"https://gitlab.mironsoft.de/api/v4/projects/$CI_PROJECT_ID/releases"
5. A pipeline job that generates release notes from merge requests
A dedicated job shortly before the actual deploy fetches, via the GitLab API, every merge request merged into the target branch since the last tag, filtered by merge date and target branch. For every merge request found, the title, labels, and merge request URL are read out and assembled into a structured Markdown file, sorted by label category.
This script can be implemented in Python or directly in Bash with curl and jq, though Python is usually the more maintainable choice for more complex filtering logic. It is important that the job stays idempotent, meaning it produces the same output for the same tag on a repeated run, instead of returning different results on every re-trigger.
generate_release_notes:
stage: release
image: python:3.12-slim
script:
- pip install --quiet python-gitlab
- python scripts/generate_release_notes.py
--project-id "$CI_PROJECT_ID"
--since-tag "$(git describe --tags --abbrev=0 HEAD^)"
--output release-notes.md
artifacts:
paths:
- release-notes.md
rules:
- if: '$CI_COMMIT_TAG'
6. Categorizing by label in the release description
The generated Markdown file is sensibly organized into fixed sections such as ## New Features, ## Bug Fixes, and ## Breaking Changes, with every merge request assigned to the matching category based on its type:: label. Merge requests without a matching label end up in a separate Other Changes category, instead of silently disappearing from the release notes, which simultaneously serves as a signal to improve label discipline within the team.
Breaking changes deserve a visually highlighted position of their own right at the top of the release notes, because they need the most attention from customers and other teams. A type::breaking label should therefore always take precedence over other labels on the same merge request in the pipeline logic, even when type::feature is also set.
7. Integration into the deploy pipeline as the final step
The release notes job should deliberately run as the last step of the deploy pipeline, after the actual deploy has completed successfully, triggered through a rules: condition that only fires on a set Git tag. This results in a release entry only for versions that were actually rolled out to production, not for every arbitrary merge into the main branch.
This ordering additionally ensures the release notes never document a failed deploy: if the actual deploy job fails, the downstream release notes job never runs at all, thanks to a needs: dependency, which avoids a misleading release announcement for a version that in reality never went live.
publish_release:
stage: release
needs:
- job: deploy_production
artifacts: false
- job: generate_release_notes
script:
- >
release-cli create --name "Release $CI_COMMIT_TAG"
--tag-name "$CI_COMMIT_TAG"
--description "$(cat release-notes.md)"
rules:
- if: '$CI_COMMIT_TAG'
8. Limits of the automation: what it does not replace
Automatically generated release notes do not replace manual curation once a release spans several related but technically separate merge requests that read better as a single point rather than several separate entries. A short manual editing step before actual publication remains sensible here, even though most of the work is handled automatically.
Breaking changes, despite automatic highlighting, should also never rely solely on the automated description, but should additionally be documented in a separate migration guide describing concrete steps for affected users. The automation delivers a reliable rough draft, while the editorial fine-tuning of more complex releases remains a human task.
9. Checklist for getting started with automated release notes
Getting started works best with a clear label convention across the team, a commit message linter as a CI job, and a single pipeline job that queries the GitLab API and produces a Markdown file. Only after that does the integration into the Releases API follow as the final step of the deploy pipeline.
The table below compares the three central data sources for automated release notes and shows which combination delivers the most reliable results for which use case.
| Data source | Structure | Customer clarity | Recommendation |
|---|---|---|---|
| Raw commit history | Unstructured without convention | Low | Do not use directly for release notes |
| Conventional Commits | Prefix-based, machine-readable | Medium | Suitable for technical changelogs |
| Merge request titles | One title per business-level change | High, with careful phrasing | Best basis for release notes |
| Merge request labels | Explicit categorization | High, with a convention | For sorting by category |
Mironsoft
CI/CD pipelines, zero-downtime deployments and release automation
Deployments that run without downtime and without the nail-biting?
We review existing GitLab pipelines for fragile deployment steps and missing safeguards, then build a release process with zero-downtime deployments, automated checks and a rollback you can actually trust in an emergency.
Pipeline Review
Checking an existing .gitlab-ci.yml for fragility, missing stages and security gaps.
Zero-Downtime Deployment
Building symlink releases, health checks and rollback strategies for Magento stores.
CI/CD Automation
Connecting tests, security scans and deployments into one reliable pipeline.
10. Summary
Automated Release Notes: The Essentials at a Glance
Core idea
Generate release notes from information that already accrues in merge request titles, labels, and commits, instead of writing it twice by hand.
Structural basis
Conventional Commits for machine-readable commits, complemented by a type:: label convention for merge requests.
Technical path
A pipeline job reads merge requests since the last tag via the GitLab API and turns them into a Markdown description for the Releases API.
Limit of automation
Complex, related releases and breaking-change migration guides still need manual editorial follow-up.