Skip to main content

Glab Release Notes

This is a GitLab-oriented take on latest-changes: a small, dependency-free CLI that runs in your GitLab CI and keeps docs/release-notes.md up to date as merge requests get merged.

Unlike tools that scrape git log or conventional-commit messages, it reads the data from the merge request itself:

  • a label decides which section the entry lands in,
  • the last non-empty > … quote in the MR description is the entry text,
  • a ticket key in the source-branch name (e.g. PROJ-123) becomes an optional Jira link.

So the person writing the MR writes the one sentence that matters, and CI takes care of the formatting, links, author, and ordering.

How it works

A release notes file is plain Markdown with an ## Latest Changes block at the top and versioned blocks below it:

# Release notes

## Latest Changes

### Features

### Fixes

## 0.8.1

### Fixes {#fix_0.8.1}

* 🐛 Fixed a crash on empty input ([#35](https://gitlab.example/your/project/-/merge_requests/35)) by [@alice](https://gitlab.example/alice)

After a merge request is merged, the append command finds the ## Latest Changes block and adds a new list item to the section chosen by the MR's label:

Latest Changes

Features

The entry is built from:

  • the label → section and emoji (see Using labels),
  • the last non-empty > … quote in the MR description → the entry text,
  • the ticket key parsed from the source branch name → a Jira link (only when a --jira-base is given),
  • the MR author and URL → trailing by [@author](…).

The entry is prepended to the top of the section so the newest change reads first. Because the notes are just a file in your repo, you can later tweak, reorder, or extend any entry by hand.

Using labels

A merge request is routed to a section by its labels. The built-in default mapping is:

Label Section Emoji
feature Features ✨
bug Fixes 🐛
docs Documentation 📝
ci CI 👷

Labels that never become an entry:

  • release, refactor, test, chore, style

Rules:

  • A merge request needs a section label or a skip label. If both are present, the skip label wins.
  • With several section labels, the first one in the configured order wins.
  • A section label requires a > … quote in the description; a skip label does not.

Features and Fixes always exist (even when empty). The other sections appear only once they have at least one entry.

Getting started

  1. Install the package into the project that will run it (Python 3.12+):

    uv add glab-release-notes
    
  2. Add the notes file (if you don't have one) at the default path, docs/release-notes.md:

    # Release notes
    
    ## Latest Changes
    
    ### Features
    
    ### Fixes
    
  3. Add a merge-request template that asks authors to set a label and write a one-line quote. A minimal Default.md:

    /label ~feature
    
    ## Release notes
    
    One line in a quote. No emojis and no links — CI adds those after the merge.
    
    >
    
  4. Add the CI jobs (see CI wiring).

That's enough for validate and append to work. promote is what your release job calls.

Commands

glab-release-notes [--config PATH] [--file PATH] <command>
  • validate — a gate for the MR pipeline. Fails the pipeline when the MR has neither a section label nor a skip label, or has a section label but no > … quote.
  • append — after a merge, appends the entry to the ## Latest Changes block and pushes a chore(notes): add entry for MR !<iid> commit. It retries on a push race and is idempotent: an already-recorded MR is skipped.
  • promote — moves the non-empty sections of ## Latest Changes under a new ## <version> heading with anchors, then empties the block back to the always-kept sections. --version is required; --no-push rewrites the file only so your release process can own the commit.

CI wiring

The tool talks to the GitLab API using CI variables, so it is bound to whichever project's pipeline runs it:

  • CI_API_V4_URL, CI_PROJECT_ID, GITLAB_API_TOKEN — API endpoint and auth;
  • CI_MERGE_REQUEST_IID — used by validate;
  • CI_COMMIT_SHA — used by append to find the MR of the current commit.

A typical .gitlab-ci.yml (the image needs uv on PATH):

validate-release-notes:
  stage: linter
  script:
    - uv run glab-release-notes validate
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

append-release-notes:
  stage: linter
  variables:
    GIT_DEPTH: 0
  before_script:
    - git config user.email "glab-release-notes@${CI_SERVER_HOST}"
    - git config user.name "glab-release-notes"
    - git remote set-url origin "https://oauth2:${GITLAB_API_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
  script:
    - uv run glab-release-notes append --jira-base https://jira.example/browse
  rules:
    - if: $CI_COMMIT_TAG || $CI_PIPELINE_SOURCE == "merge_request_event"
      when: never
    - if: '$CI_COMMIT_MESSAGE =~ /^chore\(notes\)/'
      when: never
    - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"

The chore(notes) rule prevents the job from re-running on its own commits. GIT_DEPTH: 0 gives git pull --rebase a history when two pushes race. If the branch is protected, the token still has to be allowed to push (see Pushing to a protected branch).

Promoting on release

Call promote in the release job, immediately before the release commit. With --no-push the command only rewrites the file, and the release commit includes that change:

    - uv run glab-release-notes promote --version "${version}" --no-push

Existing MRs — running manually

append resolves the MR from CI_COMMIT_SHA by default. To record a specific merge request by hand — for example one that was merged before you added this — run append on the target branch with the MR_IID variable set:

MR_IID=123 uv run glab-release-notes append --jira-base https://jira.example/browse

This is also the escape hatch for any MR the automatic run skipped.

Configuration

The wording of the release notes — the file, the Latest Changes header, the sections, the labels, and the skip labels — is configurable, in the same spirit as latest-changes. Put a release-notes.toml in the repo root and it is picked up automatically; pass --config PATH to point at a different file. Without a config file, the built-in defaults from Using labels apply.

Precedence: CLI flags (--file) > config file > built-in defaults.

# release-notes.toml
file = "docs/release-notes.md"        # the notes file (CLI --file wins)
unreleased_header = "## Latest Changes"
section_prefix = "### "
skip_labels = ["release", "refactor", "test", "chore", "style"]
always_keep = ["Features", "Fixes"]

[[section]]
label = "feature"
header = "Features"
emoji = "✨"
anchor_prefix = "feat"

[[section]]
label = "bug"
header = "Fixes"
emoji = "🐛"
anchor_prefix = "fix"

[[section]]
label = "docs"
header = "Documentation"
emoji = "📝"
anchor_prefix = "docs"

[[section]]
label = "ci"
header = "CI"
emoji = "👷"
anchor_prefix = "ci"

Keys:

  • file — the notes file to update. Defaults to docs/release-notes.md.
  • unreleased_header — the header of the accumulating block. Defaults to ## Latest Changes.
  • section_prefix — the prefix of the section headers inside the block. Defaults to ### .
  • skip_labels — labels that never become an entry. A skip label also wins over a section label.
  • always_keep — section headers that are always rendered, even when empty. Defaults to ["Features", "Fixes"]; use [] to render a section only once it has an entry (the latest-changes behavior).
  • [[section]] — one table per section:
    • label (required) — the GitLab label that routes an MR to this section.
    • header (required) — the section header text.
    • emoji (optional) — the emoji prefixed to entries in this section. Omit it for a plain entry.
    • anchor_prefix (optional) — the anchor prefix used when promoting, e.g. {#feat_1.0.0}. Defaults to the label.

The order of [[section]] tables matters in both directions: it is the display order of the sections, and the first matching label wins when an MR carries several section labels.

The entry line format itself is fixed: * <emoji> <text> (links) by <author>. It is not a template.

Pushing to a protected branch

append and promote (without --no-push) git commit and git push the updated notes. If your target branch is protected against direct pushes, the CI job needs credentials that can push to it.

In GitLab that usually means one of:

  • the job runs as a user/role allowed to push to the branch, or
  • the before_script above uses GITLAB_API_TOKEN as the git password. The token needs API access and permission to push.

The commits are attributed to the CI identity, not to any individual author.

Development

uv sync
uv run ruff check .
uv run ruff format --check .
uv run mypy glab_release_notes tests
uv run pytest

Releases are managed with commitizen: uv run cz bump updates the version and CHANGELOG.md and tags the commit. A GitHub Release on that tag publishes the package to PyPI via trusted publishing (.github/workflows/publish.yaml).

License

This project is licensed under the terms of the MIT license.

Metadata

Release files for glab-release-notes 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for glab-release-notes 0.1.2
File Size Uploaded Provenance
glab_release_notes-0.1.2.tar.gz 16.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for glab-release-notes 0.1.2
File Interpreter ABI Platform
glab_release_notes-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 38.0 kB

Release files / glab_release_notes-0.1.2.tar.gz

Download URL glab_release_notes-0.1.2.tar.gz
Size 16.5 kB
Tags Source
SHA-256 checksum
How to use checksums
ca380de4fe8d868c829079b8844f873848b6fbcf9069c28f027350338cb0700f
BLAKE2b-256 checksum
How to use checksums
ac508e344684029f34652b6e7c8d13d0d228ac9ac100fb0e1284a2d96aad5a57
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Release files / glab_release_notes-0.1.2-py3-none-any.whl

Download URL glab_release_notes-0.1.2-py3-none-any.whl
Size 21.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c6eadfcdd81dff7dcb533581c7618877b04c8276a4eb27c685d229c33ffef80a
BLAKE2b-256 checksum
How to use checksums
f8425039b92b3b94676717a3f6e73cbc6898eebab89a3043c179e171fed7a8d0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

Security for glab-release-notes 0.1.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for glab_release_notes-0.1.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 24, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page