From 1e2b9a07cb924816f6dafb865c9fc52a1a9d97cb Mon Sep 17 00:00:00 2001 From: kkdev92 Date: Mon, 28 Sep 2026 10:03:33 +0900 Subject: [PATCH] ci: compare only the newest changelog heading with PROJECT_VER The version check reads every `## X.Y.Z` heading in CHANGELOG.md and requires each one to equal PROJECT_VER. With a single release that holds. The next release adds its heading above 0.1.0, the older heading then fails the check, and the invariants job fails on the very change that prepares the release. The check's own message calls the copy "the newest heading", which is what it was meant to compare. Only the first match in the changelog is now treated as a copy of the version. The other copies, the README status line and the device.describe example, are still checked in full. Verified on a copy of the tree set up as the next release would be: PROJECT_VER, the status line and the example at 0.2.0, and a 0.2.0 heading above 0.1.0. The previous script exits 1 with "the newest heading says 0.1.0", and this one exits 0. With the 0.2.0 heading left out, this one still exits 1. On the tree as it is, both exit 0. Co-Authored-By: Claude Opus 5.5 --- tools/check-invariants.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tools/check-invariants.py b/tools/check-invariants.py index acf3045..751cd2d 100644 --- a/tools/check-invariants.py +++ b/tools/check-invariants.py @@ -203,14 +203,17 @@ def check_the_version_agrees(version): A copy that disagrees sends someone looking for a difference between their device and the documentation that is not there. """ - # Each copy, with the pattern that should carry the version. + # Each copy, with the pattern that should carry the version, and whether + # only the first match is a copy. The changelog keeps every earlier release + # below the newest one, and those headings are history, not copies: only the + # newest heading has to name the version being built. copies = [ - ("README.md", r'^>\s*\*\*Status:\*\*\s*(\S+)', "the status line"), - ("CHANGELOG.md", r'^##\s*\[?(\d+\.\d+\.\d+)\]?', "the newest heading"), + ("README.md", r'^>\s*\*\*Status:\*\*\s*(\S+)', "the status line", False), + ("CHANGELOG.md", r'^##\s*\[?(\d+\.\d+\.\d+)\]?', "the newest heading", True), ("docs/api/device-interface.md", r'"version":\s*"([^"]+)"', - "the device.describe example"), + "the device.describe example", False), ] - for relative, pattern, what in copies: + for relative, pattern, what, first_only in copies: path = os.path.join(ROOT, relative) with open(path, encoding="utf-8") as handle: text = handle.read() @@ -219,6 +222,8 @@ def check_the_version_agrees(version): fail(0, path, "%s does not state a version, so it cannot be checked " "against PROJECT_VER (%s)." % (what, version)) continue + if first_only: + seen = seen[:1] for other in sorted(set(seen)): if other != version: fail(0, path, "%s says %s; PROJECT_VER is %s, and PROJECT_VER is "