Publish a release¶
Which component you need depends on what you're releasing. Read Explanation: release automation for the shared reasoning; this page is the setup steps per ecosystem.
Prerequisite: a tag-safe workflow: block¶
Every publish job below runs on the tag pipeline cut when your release
tag lands. If your repo has a workflow: block that suppresses duplicate
branch pipelines, its never rule must be guarded so it does not also
swallow the tag pipeline. Use the canonical GitLab form:
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
when: never # dedupe branch+MR — $CI_COMMIT_BRANCH is empty on tags
- if: '$CI_COMMIT_TAG'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "schedule"'
Do not key the dedupe rule on $CI_PIPELINE_SOURCE == \"push\"
A tag push is also $CI_PIPELINE_SOURCE == "push". A rule like
$CI_OPEN_MERGE_REQUESTS && $CI_PIPELINE_SOURCE == "push" → never
fires before the $CI_COMMIT_TAG rule whenever the tagged commit is
associated with an open MR (which the releaser-pleaser flow makes
likely), suppressing the entire tag pipeline — GitLab reports "the
resulting pipeline would have been empty" and your release never
publishes. The $CI_COMMIT_BRANCH guard above is empty on tag
pipelines, so it can only ever dedupe branch pushes. See the
tag-pipeline-workflow-guard spec.
Conventional-Commits projects → colophon¶
The estate's release tool. It keeps a Release MR open on the default branch, and merging that MR cuts the tag.
include:
- component: gitlab.com/phpboyscout/cicd/colophon@main
inputs:
stage: release
branch: main
# Only for a project whose releases carry binaries:
assets: true
asset_base: "https://pkg.phpboyscout.uk/<project>/$CI_COMMIT_TAG"
- component: gitlab.com/phpboyscout/cicd/goreleaser@main
- Use Conventional Commits on every merge. colophon derives the version
and the changelog from them; release-note prose goes in a
Release-Note:trailer on the commit, before it merges. Nothing written in the Release MR description survives. - Merge the Release MR when you want the release. Merging tags; with
assets: truethe tag pipeline builds and thencolophon-releasecreates the release carrying the artefacts.
Every input, the token and the swap from releaser-pleaser are on the
colophon reference page.
When colophon cannot cut the release¶
Push an annotated vX.Y.Z tag by hand. Every artefact job keys off the tag,
not off which tool made it, and for an assets: true project
colophon-release creates the release from the tag on its own. A project
without assets needs one command afterwards. The steps are in colophon's
runbook,
Recover with a hand-cut tag.
Not yet migrated → releaser-pleaser¶
Some repositories still release with releaser-pleaser. Its procedure has
nothing in common with colophon's, so check which a repository uses before
following either: grep its .gitlab-ci.yml for colophon or
releaser-pleaser.
include:
- component: gitlab.com/phpboyscout/cicd/[email protected]
inputs:
token: $RELEASER_PLEASER_TOKEN
branch: main
stage: release
- component: gitlab.com/phpboyscout/cicd/[email protected]
- Create the token.
$RELEASER_PLEASER_TOKENneedsapi+write_repositoryscope — a project or group access token, not$CI_JOB_TOKEN(a job-token-pushed tag doesn't fire downstream tag pipelines, sogoreleaserwould never run). Store it as a project CI/CD variable, Masked + Protected. - Use Conventional Commits on every merge to your default branch.
releaser-pleaserderives the next version and changelog from them. - Merge normally. On each push to
main,releaser-pleaseropens or updates a Release MR with the computed version bump and generated changelog. - Review and merge the Release MR. Merging it cuts the
vX.Y.Ztag — that's what triggersgoreleaserto build and publish the release binaries. - Check the tag is on your default branch — see below.
Check the tag landed on the default branch¶
Fast-forward + automatic rebase can tag a commit that is not on main
If your project uses merge_method: ff with GitLab 19.2's
automatic rebase before merge,
and anything merged to the default branch while the Release MR was
open, the tag can be cut at the Release MR's pre-rebase head — a
commit that is not an ancestor of your default branch. Everything
that landed in between is then missing from the tagged tree.
Automatic rebase is what allows a Release MR that is behind its target to be merged at all; GitLab rebases it during the merge and never writes that rebase back to the MR. Without the setting, GitLab blocks the merge until someone rebases explicitly — which does update the MR — so a project without it is not exposed.
Tracked as cicd#7.
The component now checks this for you
From releaser-pleaser v0.34.1 the releaser-pleaser:verify job
makes the Release MR squash on merge — which records a
squash_commit_sha, a commit GitLab creates on top of the current
target head and so always on the branch — and asserts the tag is on
the release branch after every release, failing the job if it is
not. No project-wide squash setting is needed, and a Release MR is a
single commit so squashing it changes nothing. The manual check
below is still what to run on a release cut before you picked the
new component version up.
Only commits that do not change the changelog can be lost this
way (docs, chore, chore(deps), ci, style, test) — a
feat/fix forces releaser-pleaser to refresh the Release MR onto
the current branch, which pulls everything before it in. So a code
change cannot be dropped by this mechanism. See
spec 0061.
After each release:
git fetch --tags
git merge-base --is-ancestor "$TAG" origin/main && echo ok || echo "TAG NOT ON MAIN"
A non-zero exit means the release is missing commits. To see exactly what:
Worth doing even though it usually passes, because the failure is silent and the window is ordinary practice rather than an edge case: a Release MR sits open, a normal MR lands, and the rebase happens on merge.
If the tag is wrong, how much it matters depends on what was missed:
- Documentation only — the docs site deploys from the default branch as well, so the published site is already correct. Note it and move on; the next release picks the commits up.
- Code — for a Go module this is not repairable in place. Once
proxy.golang.orghas served a version it is immutable, so the fix is to release again immediately from a default branch that has everything, and to treat the bad version as skipped.
Rust crates → release-plz¶
include:
- component: gitlab.com/phpboyscout/cicd/[email protected]
- Create two tokens:
$RELEASE_PLZ_TOKEN(api+write_repository, same constraint as above — not$CI_JOB_TOKEN) and$CARGO_REGISTRY_TOKEN(your crates.io API token, forcargo publish). Both Masked + Protected CI/CD variables. - Use Conventional Commits. Same as above —
release-plzderives versions and per-crate changelogs from them. - Merge normally.
release-plz:propens/updates the Release MR on every push to the default branch. - Review and merge the Release MR.
release-plz:releasethen publishes every crate (in dependency order) and cuts the tag + GitLab release.
Terraform / OpenTofu modules → tofu-module-publish¶
include:
- component: gitlab.com/phpboyscout/cicd/[email protected]
inputs:
module_name: bootstrap
module_system: aws
This one doesn't drive a Release MR at all — it publishes whatever tag you
cut, by hand or via another tool, to the GitLab Terraform Module Registry.
No token setup needed for the common case: the default token
($CI_JOB_TOKEN) has write access to your own project's package registry,
even on GitLab Free.
Once published, consumers reference your module by registry address
instead of a git:: tag ref:
Using more than one at a time¶
A repo can combine these where it makes sense — e.g. a project publishing
both a Go binary (colophon + goreleaser) and a Terraform module
(tofu-module-publish) from the same tag pipeline runs all three
components together; they don't conflict.
See also¶
- Reference:
colophon,releaser-pleaser,goreleaser,release-plz,tofu-module-publish - Explanation: release automation
- Explanation: the token-input convention