Skip to content

fix(commit): verify branch base is current before writing CHANGELOG - #126

Merged
bborbe merged 1 commit into
masterfrom
fix/commit-base-freshness-guard
Aug 26, 2026
Merged

fix(commit): verify branch base is current before writing CHANGELOG#126
bborbe merged 1 commit into
masterfrom
fix/commit-base-freshness-guard

Conversation

@bborbe

@bborbe bborbe commented Aug 26, 2026

Copy link
Copy Markdown
Owner

The bug this prevents

On autoRelease: true repos, merging a PR triggers a release that renames ## Unreleased## vX.Y.Z. A branch created before that cut is instantly stale, and its changelog bullets merge into the released section — cleanly, with no conflict marker.

T0  merge PR-1  → watcher cuts vX.Y.0, renames Unreleased
T1  your local master is now stale
T2  branch PR-2 from it, add bullets under `## Unreleased`
T3  PR-2 merges → bullets land INSIDE the released vX.Y.0

No concurrency required — your own merge stales your own base, minutes apart.

Two consequences, both silent:

  1. The tag and the CHANGELOG disagree about what shipped.
  2. With no ## Unreleased block left, the watcher has nothing to cut — the second PR's work never gets released.

The fix

Step A.0 in Workflow A, before any entry is composed:

git fetch
git diff origin/master -- CHANGELOG.md | grep -E '^[-+]## '

Only +## Unreleased is acceptable. A -## vX.Y.Z line means refresh the base and re-apply.

Why here and not in a memory file

The guard was already documented in operator memory. It didn't help, because nothing ran it — it depended on remembering, at exactly the moment you're focused on writing a good changelog entry. This command is the artifact that writes CHANGELOG and pushes; it is the only thing guaranteed loaded at the moment the mistake happens.

Evidence

Observed 2026-08-26 on bborbe/nuke, three times in one afternoon by one author:

#86 folded into released v0.18.0 — needed repair PR #87
#88 caught by the guard (-## v0.19.0 / +## Unreleased)
#90 caught by the guard (-## v0.20.1 / +## Unreleased)

Dogfooded: this PR's own changelog entry was written after running Step A.0.

Scope: CHANGELOG.md + commands/commit.md only — the repo had unrelated dirty bench/reports/ files, excluded via explicit pathspec.

Adds Step A.0 to Workflow A, run before composing any changelog entry:

  git fetch
  git diff origin/master -- CHANGELOG.md | grep -E '^[-+]## '

Only '+## Unreleased' is acceptable. A '-## vX.Y.Z' line means a release was cut
after the branch point -- '## Unreleased' was renamed upstream, so the branch's
bullets merge INTO the released section. Git resolves that cleanly with no
conflict marker, so nothing warns anyone.

Two silent consequences: the tag and the CHANGELOG disagree about what shipped,
and with no '## Unreleased' block left the release watcher has nothing to cut, so
the branch's work never gets released at all.

On autoRelease repos the trigger is usually your OWN merge -- merging PR-1 cuts a
release that stales the base for PR-2 minutes later, no second contributor needed.
So the check belongs before every commit, not once per session.

The guard already existed in operator memory; nothing ran it. The command that
actually writes CHANGELOG now does. Observed 2026-08-26 on bborbe/nuke: missed
once and needed a repair PR to unfold a released section, then caught on the next
two commits the same afternoon.

Excludes unrelated dirty bench/reports/ files via explicit pathspec.

@ben-s-pull-request-reviewer ben-s-pull-request-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reviewed the diff and evaluated all three concerns. Here's my assessment:

Concern 1 (documentation-only guard): Not applicable to this PR. This PR is the documentation — it adds Step A.0 to commands/commit.md. The concern is a general adoption question, not a code defect in this change.

Concern 2 (git reset --hard discarding changes): Addressed. The text explicitly says # save your non-CHANGELOG edits first inline in the recovery block — the warning is present in the doc.

Concern 3 (grep silent pass on empty ## headings): Confirmed — this is a real false-negative. grep -E '^[-+]## ' returns exit code 1 (no matches) on an empty result, which in a script would be indistinguishable from "only +## Unreleased found." If a CHANGELOG has body text but no section headers, the check silently passes when it should alert the user. This is a real gap, but it's a pre-existing limitation of the check's design, not something introduced by this PR (which correctly documents the intended behavior).

The PR correctly:

  • Adds the ## Unreleased entry to CHANGELOG.md with a well-written explanation
  • Documents Step A.0 with precise recovery instructions
  • Warns about autoRelease: true per-commit necessity

No mechanical findings. Only the grep silent-pass concern (pre-existing, not introduced) warrants noting.

{
  "verdict": "approve",
  "summary": "PR correctly documents Step A.0 guard in commands/commit.md and adds the corresponding CHANGELOG entry. The grep silent-pass concern (grep returns exit code 1 with zero output when no ## headings exist) is a pre-existing design limitation, not introduced by this change. The documentation is clear and includes the safety warning for git reset --hard.",
  "comments": [
    {
      "file": "commands/commit.md",
      "line": 297,
      "severity": "minor",
      "message": "grep -E '^[-+]## ' silently passes (zero output, exit code 1) when CHANGELOG.md has no ## headings, giving a false-positive clean result. Consider adding: grep -qE '^[-+]## ' && git diff ... | grep -E '^[-+]## ' || { echo 'WARNING: no section headers found in CHANGELOG.md diff — verify ## Unreleased exists upstream'; }"
    }
  ],
  "concerns_addressed": [
    "correctness: Step A.0 guard is documentation-only — not an issue in this PR; the PR IS the documentation adding the guard",
    "correctness: git reset --hard warning — addressed; inline comment '# save your non-CHANGELOG edits first' is present",
    "correctness: grep silent pass on empty ## headings — confirmed pre-existing limitation, not introduced by this PR"
  ]
}

@bborbe
bborbe merged commit cf05223 into master Aug 26, 2026
1 check passed
@bborbe
bborbe deleted the fix/commit-base-freshness-guard branch August 26, 2026 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant