Skip to content

release-stamp

Comments the shipped version onto every issue a release closed, on the tag pipeline.

stages:
  - release

include:
  - component: gitlab.com/phpboyscout/cicd/[email protected]
    inputs:
      stage: release
      token: $RELEASE_STAMP_TOKEN

What problem this solves

org spec 0001 (D3) moved issue closure to merge. That is the right boundary — once a merge request lands, no engineering action remains — but it drops what the previous rule carried: which release actually shipped the work.

The cost scales with cadence. On a repo that releases daily the gap is a day; on a slower one it has run to a fortnight, during which the ticket reads done and the thing is not yet usable by anyone consuming a tag.

Neither release tool closes it. releaser-pleaser's forge interface has no comment method at all, and release-plz's forge client has no /comments endpoint. Both are feature requests upstream, not configuration.

The chain, though, is derivable entirely from Free-tier API endpoints:

compare(previous_tag, tag)  ->  commits
commits/<sha>/merge_requests  ->  merge requests
merge_requests/<iid>/closes_issues  ->  the issues the release shipped

What it writes

One comment per issue:

Shipped in v0.37.0.

A comment, not a label. A label would need a value per version, would need creating per release, and goes stale the moment anything moves — the reason org 0001 D10 rules it out. A comment carries the version in its own text and cannot rot.

Each comment carries a hidden <!-- release-stamp --> marker, which is how the job recognises its own work on a re-run.

It will not double-comment

A tag pipeline gets retried — by a person, or by the runner after an infrastructure failure. Without a guard, every retry adds another Shipped in vX.Y.Z. to every issue in the release.

Before commenting, the job reads the issue's notes and skips any already carrying this version's marker. Keyed on the version rather than the whole rendered comment, so changing template does not re-stamp history.

It will not fail your release

allow_failure defaults to true, matching discord-release. By the time this job runs the tag exists and the artefacts are published; a red release pipeline because an annotation did not post sends someone looking for a release problem that is not there.

It compensates by being loud — the log carries the resolved tag span, every issue stamped, and every one skipped with the reason:

release span: v0.36.0..v0.37.0
  18 commit(s)
  13 merge request(s): [206, 207, ..., 219]

4 issue(s) closed by v0.37.0:
  #6: stamped
  #14: stamped
  #15: already stamped for v0.37.0, skipping
  #16: stamped

stamped 3, skipped 1 already carrying v0.37.0

Set allow_failure: false if you would rather know loudly.

How the span is derived

The span is previous_tag..$CI_COMMIT_TAG, and getting previous wrong silently changes which issues get stamped — too few, or a re-stamp of an older release's.

The tags API's default ordering is by commit date, which is not release order: a patch cut from an older branch sorts after a later minor. The job asks for order_by=version and selects the tag immediately preceding the current one in that list.

Two cases are handled rather than crashing:

  • The first release has no predecessor. The span is the whole history, and the job says so.
  • A tag not in the list means the job is running on something that is not a release tag. It refuses rather than guessing a span.

Cross-project issues

closes_issues can return issues in other projects. Stamping those writes into a repository whose release this is not, on the strength of a cross-reference — so the job filters to the running project (by numeric $CI_PROJECT_ID, which is what that endpoint returns) and logs what it skipped, rather than silently handling it either way.

Adopting on an existing backlog

Set dry_run: true for one release to see exactly which issues would be stamped, then turn it off.

    inputs:
      dry_run: true

Retrofitting past releases is deliberately out of scope. The engine could do it, but a bulk edit of historical issues should be a deliberate act, not a side effect of adopting a component.

Jobs

Job When
release-stamp On tag pipelines ($CI_COMMIT_TAG). Override with if.

Inputs

Input Type Default Description
stage string release Stage to assign the job to. Declare it in your stages:.
image string …/dev-tools:v0.3.4 Runtime image. Needs only python3 — the engine is standard-library only, so there is no install step and no lockfile to rot.
token string $RELEASE_STAMP_TOKEN Token used to read the span and write a comment on each closed issue. Needs api scope. See token scope.
template string Shipped in {version}. The comment body. {version} is replaced with the tag.
dry_run boolean false Log the full plan and write nothing.
allow_failure boolean true Whether a failure here fails the release pipeline. See it will not fail your release.
if string $CI_COMMIT_TAG Gating expression. Tag pipelines are the only context where a release exists to stamp.

Token scope

The token default is $RELEASE_STAMP_TOKEN, not $CI_JOB_TOKEN — a deliberate deviation from the authoring rule that token inputs default to the job token.

A job token cannot post issue notes. Defaulting to it would fail every single run, so the input names a variable you supply instead; the job refuses to start when it resolves empty, rather than running and stamping nothing.

api scope is required — this is the one component in this repository that writes to issues. Scope it to the project it runs in, and make sure the variable is protected if your release tags are protected refs, or it will not be present on the tag pipeline.

Reusing an existing api-scoped release token is reasonable if you already have one; this repository points the input at its releaser-pleaser token rather than minting a second secret with the same power.

See also

  • releaser-pleaser — cuts the tag this job annotates.
  • discord-release — the other never-fail-the-release announcement job, and where the allow_failure default comes from.
  • spec 0073 — the decision record.