Generating GitLab Release Notes Automatically from Merge Requests
AI generated
CI/CD
.yml
GitLab · CI/CD · Releases
Generating release notes automatically
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.

16 min read Releases API · Automation GitLab CI/CD · Conventional Commits

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.

11. FAQ: Automated Release Notes: The Essentials at a Glance

1Do I strictly need Conventional Commits for automated release notes?
No, merge request titles and labels already suffice as a data source for most projects. Conventional Commits additionally help with technical changelogs but are not a hard requirement for the GitLab Releases API integration.
2How do I make sure merge requests are labeled consistently?
Most reliably through a required field in the merge request template combined with a CI rule that blocks a merge request without a type:: label, or at least flags it with a warning.
3Can I call the Releases API directly with curl without the release-cli?
Yes, a simple POST request to /projects/:id/releases with a tag name and description is enough. The release-cli merely wraps this call more conveniently and is maintained as a prebuilt CI/CD component.
4What happens to merge requests without a matching label?
They should be sorted into a separate category such as Other Changes instead of silently disappearing. That simultaneously makes visible where label discipline within the team still needs improvement.
5Why should the release notes job only run after a successful deploy?
So that a release entry never gets created for a version that was actually never rolled out to production. A needs: dependency on the deploy job reliably prevents that.
6How are breaking changes highlighted in automated release notes?
Through a dedicated type::breaking label, which the generation logic always prioritizes over other labels on the same merge request and places right at the top of the release notes.
7Does automation replace a manual migration guide for breaking changes?
No, a separate, manually maintained guide is still needed for concrete migration steps. The automated release notes merely provide a reliable overview of what actually changed.
8Can automated release notes be combined with multiple languages?
In principle yes, by consistently maintaining merge request titles bilingually or translating them afterward. For most internal projects, however, a single language in the technical release notes is sufficient.
9How do I handle several related merge requests for one feature?
Best with a short manual editing step before publication that consolidates several technically separate but content-related entries into a single, understandable point.
10Does the GitLab Releases API work the same for GitLab.com and self-hosted instances?
Yes, the API structure is identical, only the base URL differs between gitlab.com and a self-hosted GitLab instance with its own domain.