fix(release): a dateless changelog header silently skipped the release
Demo images / Build & push demo images (push) Failing after 38s

create-release aborted at [4/8] with "web/dist/neode-ui does not contain
v1.8.4-alpha — the frontend build no-opped or its output is stale". The
build had not no-opped: it was fresh, and simply had no 1.8.4 string to
embed.

sync-whats-new.py only matches '## vX.Y.Z (YYYY-MM-DD)'. The entry read
'## v1.8.4-alpha (draft — date set at cut)', so the version was invisible
to it: the gate's whats-new-sync stage reported "87 versions, all present"
while the release being cut had no What's New block. That modal is the
only place a version string appears in the frontend, so the bundle carried
none and the freshness check — correctly — refused it, while naming the
wrong cause. Step [5/8] only greps for '^## v1.8.4-alpha (' so it passed
the draft too.

Three changes: date the v1.8.4-alpha entry, insert the modal block it was
owed, and make the sync tool refuse any version header without a real date
instead of skipping it. Skipping is what let a wrong "all present" through.

Verified: the draft header now fails the check with an explicit message,
the dated one passes (88 versions, up from 87), and a rebuilt bundle
contains 1.8.4-alpha where it did not before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-20 03:01:39 -04:00
co-authored by Claude Opus 5
parent 60c1db98bb
commit 3d7de3e902
3 changed files with 51 additions and 1 deletions
+34
View File
@@ -28,6 +28,15 @@ MONTHS = ["", "January", "February", "March", "April", "May", "June", "July",
HEADER_RE = re.compile(r"^## (v\d+\.\d+\.\d+\S*) \((\d{4})-(\d{2})-(\d{2})\)")
# A header that names a version but carries no YYYY-MM-DD date. Such a line
# does not match HEADER_RE, so the version is INVISIBLE here — the tool then
# reports "all present" while the very release being cut has no modal block.
# That is what happened to v1.8.4-alpha on 2026-08-20: the changelog said
# "(draft — date set at cut)", this check passed, and create-release then
# aborted at the frontend step because the built bundle contained no version
# string at all (the What's New modal is the only place one appears).
UNDATED_RE = re.compile(r"^## (v\d+\.\d+\.\d+\S*) \((?!\d{4}-\d{2}-\d{2}\))")
def parse_changelog():
"""Return [(version, 'Month D, YYYY', [bullet, ...]), ...] newest-first."""
@@ -48,6 +57,16 @@ def parse_changelog():
return entries
def undated_versions():
"""[(version, whole line), ...] for version headers with no real date."""
found = []
for line in CHANGELOG.read_text().splitlines():
m = UNDATED_RE.match(line)
if m:
found.append((m.group(1), line.strip()))
return found
def existing_versions():
text = MODAL.read_text()
return set(re.findall(r"<!-- (v\d+\.\d+\.\d+\S*) -->", text))
@@ -78,6 +97,21 @@ def render_block(entry):
def main():
check = "--check" in sys.argv
# Refuse to reason about a changelog whose newest entry has no date:
# silently skipping it is what makes this check pass while the release
# is in fact missing its modal block.
undated = undated_versions()
if undated:
for ver, line in undated:
print(f"FAIL: CHANGELOG entry for {ver} carries no release date:",
file=sys.stderr)
print(f" {line}", file=sys.stderr)
print("A dated '## vX.Y.Z (YYYY-MM-DD)' header is required before the "
"modal can be synced — without it the version is invisible to "
"this tool and never reaches the built frontend.", file=sys.stderr)
return 1
entries = parse_changelog()
have = existing_versions()
missing = [e for e in entries if e["ver"] not in have]