releaser-pleaser can tag a commit that is not on the default branch (fast-forward merge)

On a project using the fast-forward merge method, releaser-pleaser can cut the version tag at a commit that is not on the default branch — silently omitting anything merged to main while the Release MR was open.

Observed twice on phpboyscout/go/config, both times losing documentation only. Nothing about the mechanism limits it to documentation.

Evidence

config v0.12.1, tagged 2026-07-31:

Release MR !94 (merged) head (sha) 1c17efd18edcf465c3355653b2cd171d67f1bf2e
Tag v0.12.1 points at 1c17efd18edcf465c3355653b2cd171d67f1bf2e
main after merge dd4b82efa9e9c24f192da30a970376e53333b27b
merge_commit_sha null (fast-forward, no merge commit)
git merge-base --is-ancestor v0.12.1 origin/main   →  false

The tag equals the MR's own head SHA exactly. main carries a different commit with the same title, whose parent is the commit that merged while the Release MR was open.

Checking the previous four tags on the same project:

Tag Commit On main?
v0.12.1 1c17efd1 no
v0.12.0 ec600bfe yes
v0.11.0 7319bc20 no
v0.10.0 06702601 yes

Two of four. git log --all --grep="chore(main): release v0.11.0" returns three distinct commits, which is the rebase churn showing through.

What appears to be happening

Stated as inference — the SHAs and the merge_commit_sha: null are measured, the mechanism is deduced from them:

  1. The Release MR is opened from main at commit A.
  2. Other work merges; main moves to B.
  3. The Release MR is merged. Fast-forward merge cannot replay a stale branch as-is, so the source branch is rebased onto B, producing B' — which is what main becomes.
  4. The MR record still reports its pre-rebase head, and the tag is created there.

So the tag names a commit whose parent is A, not B. Everything between A and B is missing from the tagged tree.

Why it matters more than it looks

  • The whole phpboyscout/go group uses merge_method: ff by convention, so every project in it has this shape.
  • A Release MR is routinely open for a while — long enough for an ordinary MR to land. That is the trigger, and it is normal working practice rather than an edge case.
  • Both observed losses were documentation, which is luck. In v0.12.1 the missing commit added a correctness warning to two adapter pages; in v0.11.0 it flipped three spec statuses. Had either been a code fix, the tagged module would have silently shipped without it — and a Go module version cannot be re-cut once proxy.golang.org has served it.

Suggested directions

Not prescribing an implementation — whoever owns this will know whether it belongs in the component or upstream in apricote/releaser-pleaser:

  1. Tag the default branch's post-merge HEAD rather than the Release MR's recorded head. That is the commit the release actually corresponds to.
  2. Fail loudly instead. Assert git merge-base --is-ancestor <tag> <default-branch> after tagging and fail the job when it does not hold. Cheap, and it is exactly the assertion that failed here — a release that cannot be cut correctly is better than one cut wrongly and unnoticed.
  3. Refuse to merge a stale Release MR, so the rebase never happens: have the component re-open or refresh the Release MR when the default branch moves.

(2) is worth having regardless of whether (1) or (3) lands, because it turns a silent wrong release into a visible failed job.

Detection today

Anyone releasing from a fast-forward project can check with:

git fetch --tags && git merge-base --is-ancestor <tag> origin/main

Non-zero means the tag is not on the default branch and the release is missing commits.