Free GitLab CI/CD componentGitLab Catalog listing coming soon

Release notes from your changelog, in every tag pipeline.

Push a tag, and this component writes the GitLab release from the matching CHANGELOG.md section, breaking changes first. No changelog? It writes the notes from your commits since the last tag, one line per merge request.

Add to .gitlab-ci.yml · the whole setup

.gitlab-ci.yml
stages:
  - test
  - deploy

include:
  - component: $CI_SERVER_FQDN/kalmwerk/release-notes/release-notes@1.0.1
    inputs:
      stage: deploy
GitLab Catalog listing coming soon. This include works once kalmwerk/release-notes is published there. Until then, use the download: the same component, included from your own copy.

SHA-256cd53121df45c05ef000590ef9e092b174b43b65225db961106c91de7d874b7e2

  • Only CI_JOB_TOKEN, no API key
  • No calls outside your GitLab
  • MIT licensed

You write · CHANGELOG.md

CHANGELOG.md
## [3.0.0] - 2026-11-03

Two breaking changes: `connect()` is async, and `Client.raw()` is gone.

### Added
- `Client.batch()` sends up to 100 queries in one round trip (!401).

### Changed
- **Breaking:** `connect()` is now async. Replace `db = connect(url)` with `db = await connect(url)`.
- Error messages include the line and column (!398).

### Deprecated
- `Client.legacy_query()`: use `Client.query()`. It will be removed in 4.0.0.

### Removed
- **Breaking:** `Client.raw()`, deprecated since 2.4.0. Use `Client.query(raw=True)`.

### Fixed
- Timeouts apply to DNS lookups (!405).

The component writes · GitLab release

v3.0.0 v3.0.0Released by the release-notes:publish job

Two breaking changes: connect() is async, and Client.raw() is gone.

⚠️ Breaking changes

  • connect() is now async. Replace db = connect(url) with db = await connect(url).
  • Removed Client.raw(), deprecated since 2.4.0. Use Client.query(raw=True).

Added

  • Client.batch() sends up to 100 queries in one round trip (!401).

Changed

  • Error messages include the line and column (!398).

Deprecated

  • Client.legacy_query(): use Client.query(). It will be removed in 4.0.0.

Fixed

  • Timeouts apply to DNS lookups (!405).

Full changelog: https://gitlab.com/acme/db/-/compare/v2.4.1...v3.0.0

A fictional project. The release text above is produced by the component's own code from the section on the left.

Use it today

Keep your own copy.

The download is the whole component project: the template with its built script, the TypeScript source, tests, README, changelog and MIT license. Check it, then pick one way in.

In your repository

One template file, include: local

  1. Download the zip (118 KB) and check its SHA-256 (below).
  2. Copy release-notes/templates/release-notes.yml to .gitlab/ci/release-notes.yml and commit it.
  3. Include it with the same inputs:
.gitlab-ci.yml
include:
  - local: .gitlab/ci/release-notes.yml
    inputs:
      stage: deploy

As a component project

Your group, your instance

  1. Push the release-notes/ folder to a GitLab project, for example your-group/release-notes. Its pipeline runs the component's tests.
  2. Tag it 1.0.1. That works on self-managed GitLab too.
  3. Include it from any project on that instance:
.gitlab-ci.yml
include:
  - component: $CI_SERVER_FQDN/your-group/release-notes/release-notes@1.0.1
    inputs:
      stage: deploy
Check the download · SHA-256
echo "cd53121df45c05ef000590ef9e092b174b43b65225db961106c91de7d874b7e2  logpulse-gitlab-release-notes.zip" | sha256sum -c -

On macOS, use shasum -a 256 -c - instead of sha256sum -c -.

Set up

One include: in .gitlab-ci.yml.

Pick a setup, add it, and the next tag gets its notes.

  1. 1Add the includeEach example uses $CI_SERVER_FQDN/kalmwerk/release-notes/release-notes@1.0.1, or your own copy.
  2. 2Check the stageBoth jobs run in the stage input (deploy by default), which must be in your stages.
  3. 3Push a tagThe notes job finds the notes; the publish job creates or updates the release with release-cli.
.gitlab-ci.yml
stages:
  - test
  - deploy

include:
  - component: $CI_SERVER_FQDN/kalmwerk/release-notes/release-notes@1.0.1
    inputs:
      stage: deploy

Push a tag like v1.2.0: the matching CHANGELOG.md section becomes the release, which is created if there isn't one yet. The stage must be in your stages.

How it works

Your changelog first. Your commits if there isn't one.

  1. Tag → versionv1.2.0, 1.2.0, sdk-v1.2.0 and @acme/sdk@1.2.0 all work. Tags without a version can pass one in.
  2. The matching CHANGELOG.md sectionKeep a Changelog 2.0.0's summary paragraph and in-place Breaking: markers included. Breaking changes go first with their migration steps, then each change once under its type.
  3. Or the commits since the previous tagThe previous tag is the highest earlier version with the same prefix; a stable release skips pre-release tags. The job fetches the full history for this.
  4. Then the release and the artifactsThe publish job creates or updates the release. notes.md, release.json and dotenv variables stay for later jobs.
How commits are sorted
CommitGoes under
feat:, or “Add…”, “Support…”Added
fix:, or “Fix…”, “Resolve…”Fixed
perf:, refactor:, or “Update…”, “Improve…”Changed
“Deprecate…” · “Remove…”, “Drop…”Deprecated · Removed
feat!: or a BREAKING CHANGE: footerBreaking changes, footer as migration steps
chore:, ci:, test:, release commits, plain mergesLeft out

A merge request counts once: its merge commit becomes the merge request title with its !reference, and the commits on its branch aren't listed again. Squashed and fast-forwarded commits are each listed once. Change what's left out with exclude-types.

Want to see what it will find in your changelog? Paste it into the free converter: same parser, results in your browser.

Reference

Inputs and outputs

Every input is optional; GitLab checks the types and allowed values before the pipeline starts. Later jobs read the outputs through needs:.

spec:inputs

stage
Stage for both jobs. It must exist in your pipeline's stages.Default: deploy
job-name
Name of the notes job; the publish job is <job-name>:publish. Use a different name to include the component more than once, for example per package.Default: release-notes
changelog-path
Path to the changelog, relative to the repository root.Default: CHANGELOG.md
tag
Tag to write notes for. Empty uses the pipeline's tag ($CI_COMMIT_TAG), or on other pipelines the newest version in the changelog.Default: ""
version
Version to look up in the changelog, for tags that don't contain one. Empty uses the version in the tag (v1.2.0 is 1.2.0).Default: ""
format
clean lists breaking changes first with their migration steps, then each change once under its type. original keeps the changelog section as written.Default: clean · one of clean, original
release-title
Title of a new release; {tag} and {version} are filled in. Empty uses the tag (plus the changelog heading's own title, if it has one). An existing release keeps its title unless this is set.Default: ""
source
auto uses the changelog section and falls back to commits. changelog fails without a section. commits always uses the commits since the previous tag.Default: auto · one of auto, changelog, commits
previous-tag
Where the commit range and the compare link start. Empty uses the highest earlier version with the same tag prefix (a stable release skips pre-releases).Default: ""
exclude-types
Conventional Commits types left out of notes from commits, comma separated. Breaking commits are always kept. none keeps everything.Default: chore,ci,build,test,tests,style
max-commits
Most commits listed in notes from commits (1 to 1000). A merge request counts as one.Default: 250 · number
body-mode
What happens to text already in the release: replace it, append or prepend the notes, or keep it (only fill an empty description). Re-runs only ever replace their own block.Default: replace · one of replace, append, prepend, keep
compare-link
End the notes with a Full changelog link from the previous tag to this one.Default: true · boolean
update-release
Write the notes into the GitLab release. false skips the publish job; the notes stay in the artifacts (notes.md, release.json and the dotenv variables).Default: true · boolean
create-release
Create the release when the tag exists but has no release yet. The component never creates tags.Default: true · boolean
rules
When both jobs run. The default runs them in tag pipelines.Default: [{if: $CI_COMMIT_TAG}] · array
node-image
Image for the notes job. It needs Node.js 20 or later, and git for notes from commits.Default: node:24
publish-image
Image for the publish job. GitLab's release-cli image has both release-cli and glab.Default: registry.gitlab.com/gitlab-org/release-cli:v0.24.0
release-tool
Tool that writes the release: release-cli, or glab (GitLab's newer CLI; release-cli is deprecated since GitLab 18.0 and planned for removal in 20.0).Default: release-cli · one of release-cli, glab

Artifacts · .release-notes/<job-name>/

notes.md
The release notes as Markdown.
release.json
The release in LogPulse's machine-readable shape: version, date, breaking changes with migration steps, deprecations, changes.
release.env
The dotenv report with the variables below.

Dotenv variables

RELEASE_NOTES_TAG
The tag the notes are for.
RELEASE_NOTES_VERSION
The version, without a leading v.
RELEASE_NOTES_PREVIOUS_TAG
Where the commit range and compare link start.
RELEASE_NOTES_SOURCE
changelog or commits.
RELEASE_NOTES_RELEASE_ACTION
create, update, unchanged or skipped.
RELEASE_NOTES_RELEASE_URL
The GitLab release's page.
RELEASE_NOTES_HAS_BREAKING_CHANGES
true or false.
RELEASE_NOTES_BREAKING_CHANGE_COUNT
Number of breaking changes.
RELEASE_NOTES_DEPRECATION_COUNT
Number of deprecations.
RELEASE_NOTES_FILE
Path to notes.md.
RELEASE_NOTES_JSON_FILE
Path to release.json.

Private by design

Runs in your pipeline. Talks only to your GitLab.

Only CI_JOB_TOKEN

No project token, personal token or secret. The job token reads the release and tags, and release-cli writes the release with it.

Nothing leaves your runner

No LogPulse account, analytics or telemetry. The script travels inside the template, so nothing is downloaded when the job runs.

Never touches your tags

It creates or edits the release for a tag that exists. With update-release: false there's no publish job at all.

Questions

Before you add it