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.
Go / Conventional-Commits projects → 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 (releaser-pleaser + 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:
releaser-pleaser,goreleaser,release-plz,tofu-module-publish - Explanation: release automation
- Explanation: the token-input convention