release: the first release describes the product; it has nothing to diff against
Build images / sign-extension (push) Successful in 4s
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 5s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 8s
Build images / build-web (push) Successful in 7s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 36s
CI / integration (push) Successful in 1m42s

Step 7 needs a release that reads as "what is FabledCurator and how do I run
it". What the script would actually have published is "changes since
v26.06.04.0" over 533 commits, truncated to 200 — a release page whose first
screen is the internal build-out that milestone 328 exists to stop shipping,
addressed to a reader who has never seen this project.

Two causes, fixed separately.

**A pre-convention tag is history, not a predecessor.** The 28 `v26.*` tags
were kept when their releases were deleted, so `--match v*` walks ancestry
straight back to one of them. Reachable is not comparable: nobody has run
v26.06.04.0 and its release page no longer exists to compare against. The
match is now `v[0-9][0-9][0-9][0-9].*` — rule 148's shape, which is exactly
the set of tags naming a release a reader could have been running.

**With that narrowed, the first rule-148 tag reaches no predecessor**, and the
old fallback — diff against the whole history — is worse than the problem it
replaced. A release with no predecessor now renders the product overview and
no commit list at all.

The overview is READ OUT OF README.md between `<!-- overview:start -->` and
`<!-- overview:end -->`, not written into the script, for the same reason the
changelog is derived: two hand-maintained descriptions of one product drift
and nothing ever catches it. The release page and the repo front page are one
source. Missing markers are reported as a note and publish anyway, on
cross_checks()'s reasoning — the release is still the useful object.

Every later release goes back to being a changelog, which is what note #3127
§5 says a release is for. MAX_COMMITS still guards the case it now guards:
two real releases far enough apart that the list stops being readable.

Also corrected while marking up the README: "Importing — ingests an existing
library from disk" was still advertising the folder-import feature that
3590c47 documented as deliberately retired. Replaced with what FC actually
does with what arrives — content-hash dedup, sidecar metadata, provenance.

Tests: the two that asserted the old no-predecessor behaviour are rewritten
rather than left; synthetic repos now carry their own copy of the script,
since the overview resolves relative to `__file__` (correct in production,
where release.yml checks out the tag) and would otherwise have every fixture
silently quoting FabledCurator's real README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TTjbZZ6JirCMSaJzQV1RhA
This commit is contained in:
2026-09-02 12:28:48 -04:00
co-authored by Claude Opus 5
parent 3590c478f5
commit c0370069e0
3 changed files with 224 additions and 33 deletions
+101 -17
View File
@@ -24,12 +24,36 @@ SCRIPT = ROOT / "scripts" / "release_notes.py"
def notes(*args: str, cwd: Path | None = None) -> str:
"""Run the script the way release.yml does.
A synthetic repo runs its OWN copy of the script, because the overview is
read relative to `__file__` rather than to the cwd — which is right in
production (release.yml checks out the tag, so the script IS the tagged
tree's copy) and would otherwise make every synthetic repo silently quote
FabledCurator's real README.
"""
root = cwd or ROOT
script = root / "scripts" / "release_notes.py"
return subprocess.run(
["python3", str(SCRIPT), "--dry-run", *args],
capture_output=True, text=True, check=True, cwd=cwd or ROOT,
["python3", str(script if script.exists() else SCRIPT), "--dry-run", *args],
capture_output=True, text=True, check=True, cwd=root,
).stdout
OVERVIEW_TEXT = "A synthetic product, described once."
def install_script(repo: Path, *, overview: bool = True) -> None:
"""Give a synthetic repo the script and a README to quote."""
(repo / "scripts").mkdir(exist_ok=True)
(repo / "scripts" / "release_notes.py").write_text(SCRIPT.read_text())
(repo / "scripts" / "artifacts.sh").write_text("#!/bin/sh\nexit 1\n")
readme = "# Synthetic\n\n"
if overview:
readme += f"<!-- overview:start -->\n{OVERVIEW_TEXT}\n<!-- overview:end -->\n"
(repo / "README.md").write_text(readme)
def body_of(out: str) -> str:
assert "--- body ---" in out, f"no body was rendered:\n{out}"
return out.split("--- body ---", 1)[1]
@@ -56,9 +80,10 @@ def shaped_history(tmp_path: Path) -> Path:
repo = tmp_path / "shaped"
repo.mkdir()
git(repo, "init", "-q", "-b", "main")
install_script(repo)
for i, tag in enumerate(("v26.06.04.0", "v2026.08.28.2208", "v2026.08.29.1000")):
(repo / "f.txt").write_text(f"{i}\n")
git(repo, "add", "f.txt")
git(repo, "add", "-A")
git(repo, "commit", "-q", "-m", f"work landing in {tag}")
git(repo, "tag", tag)
# One more commit and a merge, so the merge-exclusion test has something to
@@ -109,12 +134,57 @@ def test_merges_are_excluded_so_the_list_is_the_work(shaped_history):
assert "Merge pull request #999" not in body
def test_the_first_release_still_renders_with_nothing_behind_it(shaped_history):
"""No previous tag is reachable from the oldest one. That is a real state,
not an error, and it must not take the release down with it."""
out = notes("v26.06.04.0", cwd=shaped_history)
def test_a_pre_convention_tag_is_history_not_a_predecessor(shaped_history):
"""The defect milestone 328 hit, and the reason the match glob narrowed.
The 28 `v26.*` tags are kept as history while their releases were deleted.
Ancestry alone happily names `v26.06.04.0` as the predecessor of the first
rule-148 tag — and then the body offers "changes since" a release that no
longer exists, over a span (533 commits in the real repo) that is the
internal build-out this milestone exists to stop publishing.
Reachable is not the same as comparable. Only a `vYYYY.` tag names a
release a reader could have been running.
"""
out = notes("v2026.08.28.2208", cwd=shaped_history)
assert "previous=<none>" in out
assert "## Changes" in body_of(out)
assert "v26.06.04.0" not in body_of(out)
def test_the_first_release_describes_the_product_instead_of_diffing(shaped_history):
"""No predecessor means nobody reading has run an earlier version, so a
changelog has no referent. The alternative the script used to take — diff
against the whole history, truncated — puts 200 lines of internal build-out
in front of someone meeting the project for the first time."""
body = body_of(notes("v2026.08.28.2208", cwd=shaped_history))
assert OVERVIEW_TEXT in body
assert "## Changes" not in body
assert not [ln for ln in body.split("\n") if ln.startswith("- work landing")]
def test_the_overview_is_readmes_words_not_a_second_copy(shaped_history):
"""Two hand-maintained descriptions of one product drift and nothing
catches it. The release page quotes README.md so there is one source."""
readme = (shaped_history / "README.md").read_text()
assert OVERVIEW_TEXT in readme
assert OVERVIEW_TEXT in body_of(notes("v2026.08.28.2208", cwd=shaped_history))
def test_a_missing_overview_block_is_reported_and_still_publishes(tmp_path):
"""Same reasoning as cross_checks(): the release is the useful object even
when part of the derivation could not run. Say what is missing, publish
anyway — do not leave the operator with a tag and no release."""
repo = tmp_path / "no-markers"
repo.mkdir()
git(repo, "init", "-q", "-b", "main")
install_script(repo, overview=False)
git(repo, "add", "-A")
git(repo, "commit", "-q", "-m", "first")
git(repo, "tag", "v2026.09.01.1200")
out = notes("v2026.09.01.1200", cwd=repo)
assert "No `<!-- overview:start -->` block found" in out
assert "No `<!-- overview:start -->` block found" in body_of(out)
def test_a_non_tag_ref_renders_but_refuses_to_claim_it_published():
@@ -135,15 +205,29 @@ def test_the_rollback_refs_name_all_three_images():
assert f"bvandeusen/{image}:c-" in body, f"{image} missing from the rollback refs"
def test_an_unbounded_span_is_truncated_and_says_so():
"""With no reachable previous tag the span is the whole history. Emitting
eleven hundred lines would bury the one line explaining why there are
eleven hundred of them, so the cap is part of the message, not a silent
slice."""
out = notes("HEAD")
if "previous=<none>" not in out:
pytest.skip("a previous tag is reachable from HEAD in this checkout")
def test_a_long_span_between_two_releases_is_truncated_and_says_so(tmp_path):
"""The cap is still reachable, just not by the route it used to be.
It no longer fires on "no predecessor" — that renders the overview now.
What it still guards is two real releases far enough apart that the list
stops being something anyone reads, which is the ordinary case for a
project that cuts a bookmark twice a year. The cap is part of the message,
not a silent slice.
"""
repo = tmp_path / "long"
repo.mkdir()
git(repo, "init", "-q", "-b", "main")
install_script(repo)
git(repo, "add", "-A")
git(repo, "commit", "-q", "-m", "scaffold")
git(repo, "tag", "v2026.01.01.0000")
for i in range(205):
git(repo, "commit", "-q", "--allow-empty", "-m", f"fix: change {i}")
git(repo, "tag", "v2026.07.01.0000")
out = notes("v2026.07.01.0000", cwd=repo)
assert "previous=v2026.01.01.0000" in out
body = body_of(out)
listed = [ln for ln in body.split("\n") if ln.startswith("- ")]
assert len(listed) <= 200
assert len(listed) == 200
assert "more than a changelog is for" in body