releaser-pleaser¶
Opens / updates the Release MR on the default branch, then — when the
Release MR merges — cuts the vX.Y.Z tag + GitLab release. A thin wrapper
over apricote/releaser-pleaser
with the phpboyscout conventions baked in.
Sibling of release-plz (Rust). Where
release-plz drives Cargo crates, releaser-pleaser drives any project
whose changelog + versioning follow Conventional Commits — the Go track
uses it. This very repo's own releases are driven by releaser-pleaser,
pinned to a released tag (not @$CI_COMMIT_SHA) so a WIP change to the
component can't break its own release machinery.
Why our wrapper instead of apricote's component directly
apricote's run component gates on $CI_COMMIT_BRANCH ==
"<branch>", which also matches a Renovate schedule running on the
default branch — so it fires on every scheduled pipeline. This wrapper
leads its rules: with the schedule-never guard, so a scheduled
pipeline runs renovate-self only. It also wires
the token via the standard $RELEASER_PLEASER_TOKEN input and echoes
spec.component for traceable CI logs.
Token requirement
token (default $RELEASER_PLEASER_TOKEN) must not be
$CI_JOB_TOKEN — a tag pushed with CI_JOB_TOKEN does not fire
downstream tag pipelines (GitLab loop-prevention), so the
goreleaser/release-asset job would never run.
Jobs¶
| Job | What it runs |
|---|---|
releaser-pleaser |
rp run --forge=gitlab --branch=$[[ inputs.branch ]] --extra-files="$[[ inputs.extra_files ]]" --updaters="$[[ inputs.updaters ]]". interruptible: true + resource_group: releaser-pleaser (idempotent, acts on shared global state — only one run at a time). No needs: — the job runs only after every job in earlier stages completes (the consumer's lint/test/security gates), so a release is never acted on ahead of its gates. |
releaser-pleaser:verify |
Sets squash: true on the open Release MR, then — on the pipeline that follows a Release MR merge — asserts the tag just cut is on the release branch. Runs on ci-base (the upstream image has no HTTP client at all) with needs: [releaser-pleaser] so it sees the tag. See the warning below. |
Inputs¶
| Input | Type | Default | Description |
|---|---|---|---|
image_version |
string | "v0.9.0" |
ghcr.io/apricote/releaser-pleaser image tag. Must be ≥ v0.9.0 — see the warning below. |
stage |
string | release |
GitLab CI stage. Consumers must declare it in stages:. |
branch |
string | "main" |
Release target branch — rp run --branch. |
token |
string | "$RELEASER_PLEASER_TOKEN" |
Token for the GitLab API + push + tag. Needs api + write_repository. |
if |
string | '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' |
Gating rules:if:. The schedule-never guard is always applied ahead of this. |
extra_files |
string | "" |
rp run --extra-files — additional files scanned for version references. Comma-separated. |
updaters |
string | "" |
rp run --updaters — which updaters run. Default updaters: changelog,generic. |
verify_image_version |
string | "v0.1.1" |
ci-base tag the releaser-pleaser:verify job runs in (it needs an HTTP client; the upstream releaser-pleaser image has none). |
ensure_release_mr_squash |
boolean | true |
Set squash: true on the open Release MR, so the merge records a squash_commit_sha. Set false to keep only the after-the-fact tag assertion. |
The releaser-pleaser job deliberately has no needs: input — it
follows stage ordering and runs after all earlier-stage jobs (the
consumer's gates). A consumer that genuinely needs DAG ordering can
override needs: on it in their own pipeline
(spec 0053).
needs: belongs on releaser-pleaser:verify instead, and is already set
there — putting it on the release job would override stage ordering and
run the release ahead of the consumer's gates.
Fast-forward + automatic rebase can tag a commit that is not on your default branch
On a project using merge_method: ff and GitLab 19.2's
automatic rebase before merge,
merging a Release MR that is behind the target branch makes
GitLab rebase it as part of the merge — but GitLab keeps the MR's
pre-rebase head, and that is where releaser-pleaser cuts the tag.
Everything that landed while the Release MR was open is then missing
from the tagged tree, and nothing goes red: the MR merges green, the
tag exists, the Release page appears.
Both settings are needed. Without automatic rebase, GitLab blocks the merge until someone rebases explicitly, and an explicit rebase does update the MR's recorded head — so a project that has not enabled it is not exposed. The phpboyscout fleet keeps automatic rebase on deliberately: it is what makes a high volume of Renovate MRs mergeable without hand-rebasing each one. The fix therefore works with the setting rather than asking you to turn it off.
Measured across phpboyscout/go on 2026-08-01: 9 of 231 recent tags
were not on their default branch, across 8 repos.
The Release MR only goes stale when releaser-pleaser declines to
refresh it, which it does whenever the landed commits do not change
the changelog — docs, chore, chore(deps), ci, style,
test. A feat/fix forces a refresh, so code changes cannot be
dropped this way; what is lost is documentation and CI config.
releaser-pleaser:verify closes this by routing around the broken
path rather than depending on it. Upstream picks the release commit
as MergeCommitSHA, else SquashCommitSHA, else the MR's recorded
head — so ensure_release_mr_squash makes the Release MR squash,
which records a squash_commit_sha. GitLab creates that commit on
top of the current target head, so it is on the branch by
construction, stale MR or not. The tag assertion then fails the job
(exit 2) if anything slips through.
This needs no project-wide squash setting — squash_option:
default_off is a default, not a prohibition, and a Release MR is a
single commit so squashing it changes nothing. Only
squash_option: never blocks it, and the job says so plainly.
See issue #7, spec 0061 and publishing a release.
Do not pin image_version below v0.9.0
Up to and including v0.8.0, releaser-pleaser's pending-release
lookup called the instance-wide GET /merge_requests endpoint
rather than the project-scoped one. It therefore returned merged
Release MRs from every project the token could see, and — with a
group-level RELEASER_PLEASER_TOKEN — a single merged-but-untagged
Release MR jammed the release job across the entire group, each
project trying to create another project's release against its own
repository (400 Target <sha> is invalid).
Fixed upstream in v0.9.0
(apricote/releaser-pleaser#374).
This is not something the component can work around: rp run's
--owner/--repo flags are overwritten from CI_PROJECT_PATH by
the image's own autodiscover(), and the unscoped lookup ignored the
project path in any case. The only fix is the image version.
If a release job ever reports a pending release whose commit belongs to another project, check this pin first.
Self-test coverage
The tests/releaser-pleaser/ self-test asserts the failure path
only — it passes a dummy token, rp gets a 401, and
allow_failure.exit_codes: [1] tolerates exactly that exit. Any
other exit (missing image, bad interpolation, missing rp binary)
still fails the pipeline. It cannot cover release behaviour itself:
that needs a live token, a merged MR carrying the pending label, and
mutation of a real project's releases. Correctness here is protected
by keeping image_version current (Renovate tracks it), not by the
test.
Usage¶
include:
- component: gitlab.com/phpboyscout/cicd/[email protected]
inputs:
token: $RELEASER_PLEASER_TOKEN
branch: main
stage: release
See also¶
goreleaser— consumes the tag this job cuts.renovate-self— the component this one's schedule guard protects against.- Explanation: release automation
- Spec:
0021-releaser-pleaser-component-v0.11 - Spec:
0041-releaser-pleaser-project-scoping— thev0.9.0minimum above. - Spec:
0053-releaser-pleaser-stage-ordering— why the job has noneeds:.