Cutting a release
Maintainers only. Everything here happens on main; the pipeline does the rest.
The only thing that triggers a release is a tag. Pushing to
maindeploys to production; pushing a tag builds and publishes a release. The two never happen in the same pipeline —deploy_productionchecks$CI_COMMIT_BRANCH, which is empty in a tag pipeline.
Before you start
| Working tree | clean, on main, up to date |
Last pipeline on main |
green |
| Packages registry | enabled (Settings → General → Visibility → Packages) |
1. Pick the version
Semantic versioning, read from the operator's point of view — the question is always what does this force them to do?
| When | Example | |
|---|---|---|
| Major | The update needs a manual step, or something they relied on is gone | a removed .env key, a migration that rewrites data, a changed default that acts on existing records |
| Minor | New behaviour, update is update.sh and nothing else |
a new settings area, a new command |
| Patch | Fixes and wording, no new behaviour | a broken release note, a wrong label |
2. Close the CHANGELOG section
This is the step that is easy to forget and the one the pipeline refuses to let you skip
(see below). In CHANGELOG.md:
- Rename
## [Unreleased]to## [X.Y.Z] — YYYY-MM-DD. - Add a fresh, empty
## [Unreleased]above it. - Update the link definitions at the bottom:
[Unreleased]: https://gitlab.com/mankeldor/shortlink/-/compare/vX.Y.Z...main
[X.Y.Z]: https://gitlab.com/mankeldor/shortlink/-/compare/vPREVIOUS...vX.Y.Z
The release notes are generated from exactly this section, so it is what the world reads. Write it for someone who runs the software, not for someone who wrote it.
2b. Update the version markers in the docs
Two files name a version in prose, and both go stale silently — nothing breaks, they just start lying:
| File | What to change |
|---|---|
docs/ADMIN-HANDBUCH.md |
the Stand: **X.Y.Z** line near the top |
INSTALL.md |
VERSION=vX.Y.Z in the manual download block |
Deliberately only these two. The READMEs say that the project is released and point at this file for the number, so they need no edit — and the setup script resolves the latest release on its own, so it names no version either. Every place that would have to be touched per release is a place that will eventually be forgotten.
2c. Dry-run before tagging
Two minutes, and the last moment at which a mistake is free.
Generate the release note — it is what the world reads:
awk '/^ \{$/{f=1} f{print} /^ \} > dist\/release-notes.md$/{exit}' \
.gitlab-ci.yml | sed 's/^ //' > /tmp/blk.sh
mkdir -p /tmp/dr/dist && cp CHANGELOG.md /tmp/dr/
echo "0 x" > /tmp/dr/dist/shortlink-vX.Y.Z.tar.gz.sha256
( cd /tmp/dr && sh -c 'NAME=shortlink-vX.Y.Z; CI_API_V4_URL=https://gitlab.com/api/v4;
CI_PROJECT_ID=123; CI_COMMIT_TAG=vX.Y.Z; . /tmp/blk.sh' )
# The block above ENDS at `} > dist/release-notes.md`. In the job the CHANGELOG section
# is appended after it, with `>>` — so append it here as well, or the dry run shows a
# note without the one part you came to read.
( cd /tmp/dr && { echo "## Changes"; echo; awk -v v=X.Y.Z '
$0 ~ "^## \\[" v "\\]" { inside = 1; next }
inside && /^## / { exit }
inside { print }
' CHANGELOG.md; } >> dist/release-notes.md )
The cp CHANGELOG.md is not decoration: the generated block and the extraction both read
that file, and they run in /tmp/dr.
Then read it. Does the "Updating" section mention the thing this version introduces? Are
the backticks still there — or did a shell swallow them? Is the ## Changes section
actually there, and does it start at the right version?
Pull the package self-check forward — the same two loops the job runs, but now:
rsync -a --dry-run --exclude-from=.rsync-exclude --exclude='/dist/' \
--out-format='%n' ./ /tmp/pkg/ > /tmp/liste.txt
# then check the "must" and "must not" lists from .gitlab-ci.yml against /tmp/liste.txt
And check the extraction for the PREVIOUS version too. An edit to CHANGELOG.md can
shift it; that the new section extracts cleanly does not mean the old one still does.
3. Commit, push, wait
git add -A
git commit -m "Release X.Y.Z"
git push origin main
Wait for the pipeline on main to go green. It runs the tests, builds the frontend and
deploys to production — so this push is already the moment the change goes live. The
tag afterwards only packages what is by then already running.
4. Tag and push
git tag -a vX.Y.Z -m "Shortlink X.Y.Z"
git push origin vX.Y.Z
The v prefix is expected: the archive is named after the tag, and the version check
strips it when comparing.
5. What the pipeline does now
| Stage | |
|---|---|
test |
test suite |
build |
frontend, public/build/ as an artifact |
release → package_release |
checks the CHANGELOG section, builds the archive, verifies its contents, writes VERSION, uploads archive and SHA256 to the package registry |
release → release |
creates the GitLab release with the generated notes and links both assets |
deploy_production does not run.
6. Check afterwards
- The release page lists the changes — not just "See CHANGELOG.md in the archive". If it does say that, step 2 was skipped or the version does not match the heading.
- Both asset links resolve.
- Optional but cheap:
curl -sL <archive-url> -o /tmp/pkg.tar.gz
curl -sL <sha256-url> | sha256sum -c -
The VERSION file
VERSION sits in .gitignore and must not be committed. It is a build artefact:
package_releasewrites the tag into it, so the archive knows what it is.deploy_productionwrites the tag or, on a branch deployment,main@<short-sha>.
update.sh and Settings → System read it to report "2.0.1 → 2.0.2". Committed, it would
go stale and tell every git clone a version that is not true — and a wrong answer here
is worse than none, because the "unknown" case is handled everywhere.
If you ever created one locally to look at the interface, leave it. Git ignores it, the package overwrites it.
When something goes wrong
The pipeline stops with "CHANGELOG.md has no section". Nothing is published yet — that check runs before anything is built. Take the tag back, fix, tag again:
git tag -d vX.Y.Z
git push --delete origin vX.Y.Z
# fix CHANGELOG.md, commit, push
git tag -a vX.Y.Z -m "Shortlink X.Y.Z" && git push origin vX.Y.Z
The tag already exists. GitLab refuses to create a second release for it. Either delete release and tag (Deployments → Releases, then the commands above), or go one patch version up. Going up is cleaner — a tag that already left your machine may already be somewhere else.
The package upload fails with 403. The package registry is switched off for the project: Settings → General → Visibility → Packages.
The release notes are missing words that were written in backticks. That was fixed in
2.0.1 and should not come back: GitLab passes release:description through a shell, where
a backtick is command substitution. The notes are therefore generated as a file and
referenced by path. If someone ever puts literal text back into description:, this
returns.
Publishing nothing but a fix (patch release)
Same steps, in fewer words: [Unreleased] → [X.Y.Z+1], commit, push, wait for green,
tag, push. The archive and the update path are identical — there is no separate "small
update" package, and that is deliberate: two formats would mean two ways for an update to
go wrong.
Quelle dieser Seite:
RELEASE.md
im Repository. Sie wird beim Bauen daraus erzeugt — Änderungen gehören in die
Markdown-Datei, nicht in diese Seite.