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
stages:
- test
- deploy
include:
- component: $CI_SERVER_FQDN/kalmwerk/release-notes/release-notes@1.0.1
inputs:
stage: deploySHA-256cd53121df45c05ef000590ef9e092b174b43b65225db961106c91de7d874b7e2
- Only CI_JOB_TOKEN, no API key
- No calls outside your GitLab
- MIT licensed
You write · 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
Two breaking changes: connect() is async, and Client.raw() is gone.
⚠️ Breaking changes
connect()is now async. Replacedb = connect(url)withdb = await connect(url).- Removed
Client.raw(), deprecated since 2.4.0. UseClient.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(): useClient.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
- Download the zip (118 KB) and check its SHA-256 (below).
- Copy
release-notes/templates/release-notes.ymlto.gitlab/ci/release-notes.ymland commit it. - Include it with the same inputs:
include:
- local: .gitlab/ci/release-notes.yml
inputs:
stage: deployAs a component project
Your group, your instance
- Push the
release-notes/folder to a GitLab project, for exampleyour-group/release-notes. Its pipeline runs the component's tests. - Tag it
1.0.1. That works on self-managed GitLab too. - Include it from any project on that instance:
include:
- component: $CI_SERVER_FQDN/your-group/release-notes/release-notes@1.0.1
inputs:
stage: deployecho "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.
- 1Add the includeEach example uses
$CI_SERVER_FQDN/kalmwerk/release-notes/release-notes@1.0.1, or your own copy. - 2Check the stageBoth jobs run in the stage input (deploy by default), which must be in your stages.
- 3Push a tagThe notes job finds the notes; the publish job creates or updates the release with release-cli.
How it works
Your changelog first. Your commits if there isn't one.
- 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.
- 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.
- 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.
- Then the release and the artifactsThe publish job creates or updates the release. notes.md, release.json and dotenv variables stay for later jobs.
| Commit | Goes 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: footer | Breaking changes, footer as migration steps |
| chore:, ci:, test:, release commits, plain merges | Left 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