Skip to content

Release: the next release PR restates every earlier release's changelog when cut alongside a draft release #410

Description

@mforce

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions