Skip to content

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 settingsquash_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