relnote docs · CHANGELOG.md · Next version · Compare · gh relnote
Release notes in GitLab CI, Jenkins or any CI
relnote ships as one file: a Python zipapp attached to every GitHub Release. Download it in a job, run it, and hand the notes to whatever creates your release. No pip, no clone, no Node. It needs Python 3.11+ and git, which most CI images already have.
One file, no install
curl -fsSL -o relnote https://github.com/loki-inu/relnote/releases/latest/download/relnote
chmod +x relnote
./relnote --version # relnote 0.3.0
./relnote --no-bots --output notes.md --quiet
Each release has three assets:
| Asset | What it is |
|---|---|
relnote | Executable zipapp with a #!/usr/bin/env python3 shebang. Only the relnote package and the licence are inside; it does not read anything from a source tree. |
relnote.pyz | The same bytes. Use python relnote.pyz where the shebang is not honoured (Windows). |
SHA256SUMS | Checksums of both files, in sha256sum -c format. |
In CI, pin a version instead of latest and check the download:
base=https://github.com/loki-inu/relnote/releases/download/v0.3.0
curl -fsSL -o relnote "$base/relnote"
curl -fsSL -o SHA256SUMS "$base/SHA256SUMS"
grep ' relnote$' SHA256SUMS | sha256sum -c -
chmod +x relnote
Full history, or the notes are wrong
relnote finds the previous tag with git describe and lists the commits after it. A shallow clone usually does not contain that tag, so relnote falls back to "everything it can see": the notes cover only the fetched commits, the footer reads Range: (start) → HEAD, and --next-version prints v0.1.0. Before running it, make sure the clone is complete and has tags:
- GitLab CI: set
GIT_DEPTH: 0on the job (or globally). GitLab's docs: a depth of0disables the shallow clone and fetches all branches and tags. New projects default to a depth of 20. - Anything else: run the two lines below in the job before relnote. They are safe on a clone that is already complete.
if [ "$(git rev-parse --is-shallow-repository)" = true ]; then git fetch --unshallow; fi
git fetch --tags --force
GitLab CI: release on tag push
Two jobs. The first runs in any image with Python 3.11+ and git (python:3.12 has both; the -slim and -alpine variants do not include git) and writes notes.md as an artifact. The second runs in GitLab's CLI image and uses the release keyword, whose description may be a path to a file relative to $CI_PROJECT_DIR.
stages: [notes, release]
release_notes:
stage: notes
image: python:3.12
variables:
GIT_DEPTH: 0 # full history: relnote needs the previous tag
rules:
- if: $CI_COMMIT_TAG
script:
- curl -fsSL -o relnote https://github.com/loki-inu/relnote/releases/download/v0.3.0/relnote
- chmod +x relnote
- ./relnote --version
- ./relnote --no-bots --output notes.md --quiet
artifacts:
paths: [notes.md]
release:
stage: release
image: registry.gitlab.com/gitlab-org/cli:latest
needs:
- job: release_notes
artifacts: true
rules:
- if: $CI_COMMIT_TAG
script:
- echo "Releasing $CI_COMMIT_TAG" # the release keyword requires a script
release:
tag_name: $CI_COMMIT_TAG
name: $CI_COMMIT_TAG
description: notes.md # path relative to $CI_PROJECT_DIR
Because HEAD is the tag being released, relnote starts the range at the tag before it, so the notes describe exactly this release. Prefer a command? The same image has glab, and the job token works once GLAB_ENABLE_CI_AUTOLOGIN is set:
release:
stage: release
image: registry.gitlab.com/gitlab-org/cli:latest
needs: [release_notes]
rules:
- if: $CI_COMMIT_TAG
variables:
GLAB_ENABLE_CI_AUTOLOGIN: "true"
script:
- glab release create "$CI_COMMIT_TAG" --name "$CI_COMMIT_TAG" --notes-file notes.md
release-cli (registry.gitlab.com/gitlab-org/release-cli) is deprecated since GitLab 18.0; the release keyword now runs glab release create and falls back to release-cli only where glab is missing. On older GitLab, the first example works unchanged with image: registry.gitlab.com/gitlab-org/release-cli:latest.
GitLab CI: let relnote pick the next tag
For a manual (or scheduled) release from the default branch, --next-version decides the tag: a breaking change bumps major, a feat bumps minor, anything else bumps patch (details). GitLab does not expand variables exported in script inside the same job's release block, so the tag goes through a dotenv artifact to a second job. With ref set, release:tag_name creates the tag that does not exist yet.
stages: [prepare, release]
prepare_release:
stage: prepare
image: python:3.12
variables:
GIT_DEPTH: 0
rules:
- if: $CI_COMMIT_TAG
when: never # not in tag pipelines
- if: $CI_PIPELINE_SOURCE == "schedule" # e.g. a weekly release
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual # or a button on main
allow_failure: false # pipeline waits for it
script:
- curl -fsSL -o relnote https://github.com/loki-inu/relnote/releases/download/v0.3.0/relnote
- chmod +x relnote
- echo "TAG=$(./relnote --no-bots --next-version)" >> variables.env
- ./relnote --no-bots --output notes.md --quiet
- cat variables.env
artifacts:
paths: [notes.md]
reports:
dotenv: variables.env
release:
stage: release
image: registry.gitlab.com/gitlab-org/cli:latest
needs:
- job: prepare_release
artifacts: true
rules:
- if: $CI_COMMIT_TAG
when: never
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- echo "Releasing $TAG"
release:
tag_name: $TAG # e.g. v1.4.0, from --next-version
ref: $CI_COMMIT_SHA # create the tag at this commit
name: $TAG
description: notes.md
Pick one of the two setups. Creating the tag starts a tag pipeline, and a second release job there would fail because the release already exists. If there is nothing to release since the last tag, relnote exits 1 in prepare_release and the pipeline stops before anything is tagged.
Monorepos
--path keeps only commits touching a directory and --tag-prefix makes tags like api/v1.2.0 the ones that count. One job per package, matched on the tag prefix:
api_notes:
stage: notes
image: python:3.12
variables:
GIT_DEPTH: 0
rules:
- if: $CI_COMMIT_TAG =~ /^api\/v/
script:
- curl -fsSL -o relnote https://github.com/loki-inu/relnote/releases/download/v0.3.0/relnote
- chmod +x relnote
- ./relnote --path packages/api --tag-prefix api/v --no-bots --output notes.md --quiet
artifacts:
paths: [notes.md]
With --next-version, the prefix is kept: ./relnote --path packages/api --tag-prefix api/v --next-version prints api/v1.3.0.
Jenkins, Buildkite, CircleCI, or a plain shell
The same four lines work in any step that has sh, curl, git, and python3 (3.11+). Put them in a Jenkinsfile sh block, a Buildkite command step, or a CircleCI run:
set -eu
if [ "$(git rev-parse --is-shallow-repository)" = true ]; then git fetch --unshallow; fi
git fetch --tags --force
curl -fsSL -o relnote https://github.com/loki-inu/relnote/releases/download/v0.3.0/relnote
chmod +x relnote
./relnote --no-bots --output notes.md --quiet
# then hand notes.md to whatever publishes the release, for example:
# gh release create "$TAG" --notes-file notes.md
# glab release create "$TAG" --notes-file notes.md
# relnote --changelog CHANGELOG.md --changelog-title "$TAG" -q
Pass --since v1.2.0 --until v1.3.0 for an explicit range, or --repo /path if the checkout is not the working directory. On GitHub Actions use the action instead; it is the same code.
Flags you will want in CI
| Flag | Why |
|---|---|
--output notes.md --quiet | Write the file and keep the job log clean. Exit 1 if the parent directory is missing. |
--no-bots | Drop dependabot, renovate, and [bot] authors. |
--next-version | Print the next tag instead of notes. Exit 1 if there are no commits or the previous tag is not x.y.z. |
--path DIR, --tag-prefix PREFIX | Scope to one package in a monorepo. |
--changelog CHANGELOG.md --changelog-title TAG | Prepend the same notes to a Keep a Changelog file; a repeated title is refused, so re-runs cannot duplicate a section. |
Everything relnote reads is local git history. No token, no network call, no config file.