The release PR opened immediately after a release is cut restates every earlier release's changelog, on a green run, with no error anywhere.
Observed
Cutting v0.0.2 (run 30801524556) opened PR #409 proposing 0.0.3 with a changelog containing all of 0.0.1's and 0.0.2's entries — be8517f, 8990fec, 3407288 and the rest. The version number itself (0.0.3) was correct.
Cause
Version bookkeeping is tag-independent: manifest mode reads the current version from .release-please-manifest.json, a committed file. That is why draft-until-promoted is safe for it, and #351 says so.
The changelog boundary is not tag-independent. release-please resolves "commits since the last release" to a commit SHA via the git tag. release-please-config.json sets "draft": true, and release-please.yml publishes a release only after promote has verified and retagged its image — GitHub withholds the git tag for a draft.
So the single release-please invocation that cuts vX.Y.Z also computes the next release PR, at the one moment vX.Y.Z has no tag. The boundary is unresolvable, release-please falls back to its full 500-commit search depth, and the next PR restates everything it finds.
Why it went unnoticed
- Nothing fails. The run is green, the version is right, and only the changelog text is wrong.
- It self-heals on the next push to
main, which re-grooms the PR once the tag exists. The v0.0.1 cut hit this too and was corrected by the next merge before anyone looked.
- It is only visible if you read the pending release PR in the window between a release cut and the next merge.
Why it cannot be repaired by re-running
release-please.yml's grooming job is gated if: github.event_name == 'push', and the workflow_dispatch repair path requires a tag input and goes straight to promotion. There is no dispatch that re-grooms. Only a push to main fixes an affected PR.
Fix
Split the two passes around promotion, so the PR is computed after the tag exists:
release-please job — skip-github-pull-request: true, release-creation only.
promote — unchanged; publishing the release is what creates the tag.
- new
groom job — needs: [release-please, promote], skip-github-release: true, PR maintenance only.
Two guards on groom's condition are load-bearing and neither is the obvious default:
always() && !cancelled() — promote is skipped on an ordinary push, and a skipped dependency would otherwise skip grooming on every non-release merge.
needs.promote.result != 'failure' — a failed promotion leaves no tag, so grooming would overwrite a correct PR with the duplicated one. Leaving it untouched is the fail-closed choice.
groom mints its own App token and must keep the permission-* downscoping — omitting it mints the union of every grant the App holds rather than a narrow token, and the same App backs dependabot-lockfix.yml.
Acceptance
- Cutting a release opens a next-release PR whose changelog contains only commits after that release's tag.
- An ordinary (non-release) merge still grooms the PR.
- A failed promotion leaves the existing release PR unmodified.
Follow-up from #351.
The release PR opened immediately after a release is cut restates every earlier release's changelog, on a green run, with no error anywhere.
Observed
Cutting
v0.0.2(run 30801524556) opened PR #409 proposing0.0.3with a changelog containing all of 0.0.1's and 0.0.2's entries —be8517f,8990fec,3407288and the rest. The version number itself (0.0.3) was correct.Cause
Version bookkeeping is tag-independent: manifest mode reads the current version from
.release-please-manifest.json, a committed file. That is why draft-until-promoted is safe for it, and #351 says so.The changelog boundary is not tag-independent. release-please resolves "commits since the last release" to a commit SHA via the git tag.
release-please-config.jsonsets"draft": true, andrelease-please.ymlpublishes a release only afterpromotehas verified and retagged its image — GitHub withholds the git tag for a draft.So the single release-please invocation that cuts
vX.Y.Zalso computes the next release PR, at the one momentvX.Y.Zhas no tag. The boundary is unresolvable, release-please falls back to its full 500-commit search depth, and the next PR restates everything it finds.Why it went unnoticed
main, which re-grooms the PR once the tag exists. Thev0.0.1cut hit this too and was corrected by the next merge before anyone looked.Why it cannot be repaired by re-running
release-please.yml's grooming job is gatedif: github.event_name == 'push', and theworkflow_dispatchrepair path requires ataginput and goes straight to promotion. There is no dispatch that re-grooms. Only a push tomainfixes an affected PR.Fix
Split the two passes around promotion, so the PR is computed after the tag exists:
release-pleasejob —skip-github-pull-request: true, release-creation only.promote— unchanged; publishing the release is what creates the tag.groomjob —needs: [release-please, promote],skip-github-release: true, PR maintenance only.Two guards on
groom's condition are load-bearing and neither is the obvious default:always() && !cancelled()—promoteis skipped on an ordinary push, and a skipped dependency would otherwise skip grooming on every non-release merge.needs.promote.result != 'failure'— a failed promotion leaves no tag, so grooming would overwrite a correct PR with the duplicated one. Leaving it untouched is the fail-closed choice.groommints its own App token and must keep thepermission-*downscoping — omitting it mints the union of every grant the App holds rather than a narrow token, and the same App backsdependabot-lockfix.yml.Acceptance
Follow-up from #351.