Skip to content

fix(release): pin the compare base of a versioned CHANGELOG section - #171

Closed
waterbro-8 wants to merge 1 commit into
mainfrom
fix/validate-release-compare-base
Closed

waterbro-8 wants to merge 1 commit into
mainfrom
fix/validate-release-compare-base

Conversation

@waterbro-8

Copy link
Copy Markdown
Collaborator

Refs #168

Summary

scripts/validate_release_version.sh pins where a release section's comparison link ends but not where it starts: the accepted form is a glob with the base segment eaten by a wildcard (:99).

"[${version}]: https://github.com/bytefolk/mem/compare/"*"...v${version}"

So a [0.1.2] section linked as compare/v0.1.0...v0.1.2 — a base off by one release, silently widening or narrowing which commits the release contains — passes the gate that exists to keep release surfaces consistent. v0.0.1, v0.1.0 and a self-compare v0.1.2 are all accepted today. The adjacent die at :100 says must terminate at v${version}, so the implementation matches the property it declares; that property is just not the one that matters.

Reproduction

On 8a92baa680b (this PR changes no data, only the checker):

sed -i 's#compare/v0.1.1...v0.1.2#compare/v0.1.0...v0.1.2#' CHANGELOG.md
bash scripts/validate_release_version.sh 0.1.2   # -> PASS, rc=0

Change

compare/ no longer takes a wildcard base. The base is derived from the CHANGELOG itself: the assertion directly above already pins ${version} as the first versioned heading, so the second versioned heading is the release this one follows. The releases/tag form stays exact-character, and if no previous versioned heading can be derived the gate fails closed instead of guessing.

One file, scripts/validate_release_version.sh, +13 / −5. No version strings, no surfaces, no workflow, no release action touched.

Bidirectional mutation evidence

Same tree, same set of corruptions, pristine checker and patched checker side by side (8 cases):

Case Unpatched Patched Expected
clean (base v0.1.1) PASS PASS PASS
base -> v0.0.1 PASS (the gap) FAIL FAIL
base -> v0.1.0 (off-by-one) PASS (the gap) FAIL FAIL
base -> v0.1.2 (self-compare) PASS (the gap) FAIL FAIL
endpoint -> v0.9.9 FAIL FAIL FAIL (property preserved)
link written as releases/tag form PASS PASS PASS (alternative still accepted)
main validating its own 0.1.1 PASS PASS PASS (no regression)
main with base -> v0.0.1 PASS (the gap) FAIL FAIL

All 4 gap cases flip to red and the other 4 are untouched; 0 mismatches against expectation.

Closing checks: bash -n clean; scripts/test_release_guards.sh and scripts/test_release_helpers_compat.sh both rc=0 with the patch applied; the gate still passes main's own 0.1.1. No test anywhere in the repo asserts the old error string (grep -rn "must terminate at" . → only the checker itself), so replacing that message breaks no existing assertion.

Evidence level

  • E1 (executed here, Linux): the 8-case matrix above, the two guard suites, and the main@7a194f1eba4 baseline.
  • E2 (measured on a different tree, same checker): the corruption also passes on the chore(release): sync every version surface to 0.1.2 #168 branch head, which is where it was found.
  • Not covered: nothing in this PR is platform-specific, and no CI job consumes this script's output beyond test_release_guards.sh.

Why it is its own PR

The gap is in the release checker, not in the CI legs #169 touches, and #169 does not modify this file. Shipping it separately keeps both reviewable. It is also independent of the #162 / #168 merge order — it does not change any release surface, only what a future release section is allowed to claim about its own range.

Explicit non-goals

@waterbro-8

Copy link
Copy Markdown
Collaborator Author

Closing as a duplicate of #170, which is the same fix and was opened at 2026-09-06T10:39:54Z — three and a half hours before I pushed this branch.

My process error: I checked that the gap was real and that my patch closed it, but I did not list open PRs before opening one. pulls?state=open was one call away.

I then measured both implementations rather than guessing which one to keep. Three checkers side by side on the same trees, mutating only CHANGELOG.md — pristine 7a194f1eba4, sun-970/mem@03c57963592 (blob 937ac84009801f335819b2940b75895e3a0532fc), and this branch's script:

Case Expected Unfixed #170 this branch
merged #162×#168 tree, clean base v0.1.1 PASS PASS PASS PASS
base → v0.0.1 FAIL PASS (gap) FAIL FAIL
base → v0.1.0 (off-by-one) FAIL PASS (gap) FAIL FAIL
base → v0.1.2 (self-compare) FAIL PASS (gap) FAIL FAIL
endpoint → v0.9.9 FAIL FAIL FAIL FAIL
link in releases/tag form PASS PASS PASS PASS
main validating its own 0.1.1 PASS PASS PASS PASS
main, base → v0.0.1 FAIL PASS (gap) FAIL FAIL

On all eight rows the two implementations agree. So there is nothing here worth merging on top of #170, and reviewers should look at #170 only.

One divergence exists and I left it out of the table because it favours #170: my derivation matches headings starting with a digit, theirs reuses the file's own definition at :77 (/^## \[/ && $0 != "## [Unreleased]"). On a synthetic ## [Foo] heading inserted above the previous release, mine accepts and theirs rejects. Reusing the existing predicate is the better anchor, so if anything is worth carrying over it is a test, not this implementation.

Full differential, the one PR-body correction I found in #170 (its releases/tag "glob character class" bullet describes an effect that does not exist — a quoted pattern in [[ ]] is literal), and the fact that #170's three runs are parked at action_required, are in #170's comment thread.

No tag, no publish, no version bump, no merge, nothing closed but this PR, which is mine to close. The branch fix/validate-release-compare-base is left in place untouched in case anyone wants to diff it.

@waterbro-8 waterbro-8 closed this Sep 6, 2026
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