From f9187272ec31f8b01e7b071d748b57b1c8e9d876 Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sun, 19 Jul 2026 13:50:03 +0000 Subject: [PATCH 1/2] fix: the release suite accepts the ceremony's own tree (#108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release.test.ts demanded the real changelog's literal Unreleased section extract non-empty containing '#96' — false by construction on the very tree the release PR produces, so the first real 'release: 0.1.0' PR turned CI red and the ceremony blocked itself. Fork rehearsals missed it: a tag push runs release.yml, never ci.yml. The guard now asserts the TOP section, whatever its name, extracts non-empty via the exact tool release.yml runs — verified on both legitimate tree states. Fixes #108 Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 12 ++++++++++++ test/release.test.ts | 22 +++++++++++++++++----- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3231cad..7927f39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ actually cutting it, and this file starts there. ### Fixed +- **The release suite accepts the ceremony's own tree** (#108) — + `test/release.test.ts` demanded the real `CHANGELOG.md`'s literal + `Unreleased` section extract non-empty and contain `#96`: false by + construction on the `release: X.Y.Z` tree the ceremony's own PR produces + (it stamps that heading into `## X.Y.Z — date`), so the first real + release PR turned CI red and the flow blocked itself — invisible to the + fork rehearsals, which tag a branch (`release.yml` runs; `ci.yml` never + does). The guard now asserts its actual purpose: whatever the TOP `## ` + section is — `Unreleased` between releases, the stamped version on and + right after one — the exact `release-notes.sh` the workflow runs + extracts it non-empty. rig's twin is heavy-duty/rig#44. + - **`apply` no longer demands a GitHub App for a manifest that declares no applications** (#103) — found live in the 2026-07-19 release drill, where a databases-only manifest (`applications: {}`) rendered its plan of two diff --git a/test/release.test.ts b/test/release.test.ts index 341a823..050dbb1 100644 --- a/test/release.test.ts +++ b/test/release.test.ts @@ -143,12 +143,24 @@ describe("release-notes.sh", () => { expect(r.output).toContain("no such file"); }); - // The REAL changelog: its Unreleased section must extract non-empty with - // the exact tool release.yml runs — the guard against header-format drift. - it("the real CHANGELOG.md's Unreleased section extracts", async () => { - const r = await notes("Unreleased", join(ROOT, "CHANGELOG.md")); + // The REAL changelog: the guard against header-format drift. The file has + // two legitimate states, and this test used to know only one (#108, found + // the day the first release PR turned CI red): BETWEEN releases the top + // section is `## Unreleased`; on a `release: X.Y.Z` tree — the ceremony's + // own PR stamps that heading into `## X.Y.Z — date` — and on main right + // after it, the top section IS the stamped release. Demanding the literal + // Unreleased (with an issue number inside it, rotting per release) made + // the release PR unshippable by construction, invisible to fork + // rehearsals (a tag push runs release.yml, never ci.yml). Whatever the + // top section is called, the exact tool release.yml runs must extract it + // non-empty. + it("the real CHANGELOG.md's top section extracts", async () => { + const changelog = readFileSync(join(ROOT, "CHANGELOG.md"), "utf8"); + const top = changelog.match(/^## (\S+)/m); + expect(top).not.toBeNull(); + const r = await notes(top![1], join(ROOT, "CHANGELOG.md")); expect(r.code).toBe(0); - expect(r.output).toContain("#96"); + expect(r.output.trim()).not.toBe(""); }); }); From d7d715ac55bb13da5f0db1e5f0960a0f46bcedfe Mon Sep 17 00:00:00 2001 From: dan-claude-bot Date: Sun, 19 Jul 2026 13:52:10 +0000 Subject: [PATCH 2/2] chore: narrow the top-heading match with a guard, not a non-null assertion biome (error-on-warnings in CI) rejects the ! assertion; an explicit throw narrows properly and says what broke if the file ever has no heading at all. Co-Authored-By: Claude Fable 5 --- test/release.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/release.test.ts b/test/release.test.ts index 050dbb1..3627cac 100644 --- a/test/release.test.ts +++ b/test/release.test.ts @@ -157,8 +157,8 @@ describe("release-notes.sh", () => { it("the real CHANGELOG.md's top section extracts", async () => { const changelog = readFileSync(join(ROOT, "CHANGELOG.md"), "utf8"); const top = changelog.match(/^## (\S+)/m); - expect(top).not.toBeNull(); - const r = await notes(top![1], join(ROOT, "CHANGELOG.md")); + if (!top) throw new Error("CHANGELOG.md has no ## section at all"); + const r = await notes(top[1], join(ROOT, "CHANGELOG.md")); expect(r.code).toBe(0); expect(r.output.trim()).not.toBe(""); });