From 26452917fa00516fb445e8e879cfc372e27a16c4 Mon Sep 17 00:00:00 2001 From: Shannon Atkinson Date: Sat, 15 Aug 2026 18:28:18 -0700 Subject: [PATCH] chore: record what shipped today, and stop a tag stranding it (#377) TWO SEPARATE FAILURES, and only one of them is #377. WHAT #377 DESCRIBES: entries land under [Unreleased], nothing folds them into the version being tagged, and because [Unreleased] sits ABOVE the version sections a tag cut now omits every line below it. It reopened twice on 2026-08-14; the second time it stranded all seven security fixes from #366 and the VOD editor from #365. GHSA-7jqx points at these notes, and the fix for its worst defect -- 0.7.0's seal-at-rest migration leaving plaintext keys in the WAL -- was in the section a tag would have skipped. The guard is now the FIRST step of the FIRST release job, before anything is built or pushed, because a release that has already uploaded assets and then discovers its notes are wrong is a release somebody has to go and delete. Proven both ways rather than asserted: exit 1 against the populated file in this commit, exit 0 against an empty [Unreleased]. "Nothing yet." is treated as the empty state it is. WHAT #377 CANNOT CATCH, and it is what actually happened today: entries that were never written. #397 recorded ONE of its seven changes and #400 recorded none, so [Unreleased] would have been empty and the guard would have passed while the website, the documentation rendering, the comparison pages, the SEO work, #387's first change and the port-allocator fix all went unrecorded. A guard trusted for more than it does is worse than none, so that limit is written at the step and the second check it implies -- on the pull request, not the tag -- is not smuggled in here. I also read this wrong before writing it. [Unreleased] looked empty because the main working tree is parked on a stale branch; on origin/main it held the tour entry. The gap was narrower than I first said and is now recorded accurately. WHAT WAS MISSING, now written: the 6-to-35 page site including 23 rendered documents; llms.txt, security.txt, the icon, HowTo data and git-derived sitemap lastmod; the _headers bug that shipped four of six pages with no revalidation; 114 inert Copy buttons; eleven copy defects across pages naming other people's products; the contiguous-port-window fix; and #387's first change, whose one intended behaviour difference -- GET /system reading the store rather than the engine snapshot -- is called out because "no behaviour change" with an exception is not no behaviour change. Claude-Session: https://claude.ai/code/session_01HeLrWaDmsNeeNSbHQfEofX --- .github/workflows/release.yml | 40 ++++++++++++++++++++ CHANGELOG.md | 69 +++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 65225519..d51ae24e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -257,6 +257,46 @@ jobs: timeout-minutes: 25 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + # FIRST, BEFORE ANYTHING IS BUILT OR PUSHED. A release that has already + # uploaded assets and then discovers its notes are wrong is a release + # somebody has to go and delete. + # + # WHAT THIS CATCHES. [Unreleased] sits ABOVE the version sections, so under + # Keep a Changelog its contents are NEWER than the release below them. + # Every PR correctly appends there and nothing folds it forward, so the gap + # reopens after each merge -- twice on 2026-08-14 alone. The second time it + # stranded all seven security fixes from #366 and the VOD editor from #365: + # nine entries a tag would have omitted. + # + # That is not cosmetic. GHSA-7jqx points at these release notes, and the + # fix for its worst defect -- 0.7.0's seal-at-rest migration leaving + # plaintext keys in the WAL -- was in the section a tag would have skipped. + # An operator following the advisory to the notes would not have found it. + # + # WHAT IT DOES NOT CATCH, stated because the gap is real and a guard that + # is trusted for more than it does is worse than none. This fires when + # entries were written and not folded. It cannot fire when they were never + # written at all -- a merge that records nothing leaves [Unreleased] empty + # and passes here. #400 recorded nothing and would have sailed through. + # That second failure needs a different check, on the pull request rather + # than the tag. + - name: "[Unreleased] must be empty before a tag" + run: | + set -euo pipefail + body=$(awk '/^## \[Unreleased\]/{f=1;next} /^## \[/{f=0} f' CHANGELOG.md \ + | sed '/^[[:space:]]*$/d') + # "Nothing yet." is the deliberate empty state, not an entry. + real=$(printf '%s\n' "$body" | sed '/^Nothing yet\.$/d' | sed '/^[[:space:]]*$/d') + if [ -n "$real" ]; then + echo "::error::CHANGELOG.md still has entries under [Unreleased]. Fold them" + echo "::error::into the version being tagged before releasing -- they sit ABOVE" + echo "::error::the version sections, so a tag cut now omits every line below." + printf '%s\n' "$real" | head -40 + exit 1 + fi + echo "[Unreleased] is empty; nothing would be stranded by this tag." + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7 with: go-version-file: go.mod diff --git a/CHANGELOG.md b/CHANGELOG.md index e950c74b..5af30d4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,75 @@ its first tagged release. ## [Unreleased] ### Added +- **polyemesis.com publishes the documentation.** The site went from 6 pages to + 35: the 23 user-facing documents in `docs/` are rendered at `/docs/` + rather than linked to GitHub, four comparison pages sit under `/vs/`, and + `/free-restream-service` and `/how-to-multistream-from-obs` answer the two + queries with the most measured search demand this project can address. + + The documents were invisible to search before this — 63,000 words reachable + only on github.com, so the authority accrued to Microsoft's domain and the + pages had no title, description or internal linking under our control. + + Publishing is an **allowlist**, not a glob with exclusions. `docs/` also holds + `RESEARCH-COMPETITIVE.md` and `COPY-CONSTRAINTS.md`, and a glob would make the + next internal note public by default. A build check fails when a file in that + directory appears in neither list, so adding a document is a decision somebody + writes down. + +- **`llms.txt`, `/.well-known/security.txt`, an apple-touch-icon, `HowTo` + structured data on the install steps, and `lastmod` in the sitemap.** The + `lastmod` dates come from `git log -1` per page rather than the clock: a + build-time stamp marks every page as modified on every deploy, which is a lie + a crawler discounts, costing the signal it was meant to give. + + `security.txt` carries the mandatory RFC 9116 `Expires` field, and the build + now fails when it is past and warns 30 days out. An expired security contact + reads as an abandoned one to the person deciding between reporting privately + and going public. + +### Fixed +- **Four of six pages were shipping with no cache revalidation.** + `web/public/_headers` scoped `no-cache` to `/` and `/*.html`, and its comment + claimed that covered the built pages. It did not: with + `build.format: "file"` and `trailingSlash: "never"` a page is BUILT as + `features.html` and SERVED as `/features`, and Cloudflare matches the request + path. The failure that block exists to prevent — "a deploy is invisible until + caches expire" — was live on `/features`, `/comparison`, `/docs` and + `/download`. A build check now derives the expectation from the built output. + +- **114 Copy buttons did nothing.** The click handler shipped inside + `CodeBlock.astro`, which the rendered documentation pages never mount. Styled, + focusable, labelled and inert. + +- **Eleven copy defects**, found by three independent reviewers with different + lenses. The ones that mattered were claims about other people's products: + a sentence whose "either" retroactively negated the clause before it, turning + a correct statement into the overclaim it was written to avoid; a wrong + restream.io plan limit that contradicted a sibling page; four capability cells + asserted "No" for competitors with nothing behind them; and a card presenting a + DESCRIPTION of a function in the quoted-source treatment that gives its + neighbours their authority. + + Also removed: a per-destination CPU figure that appears nowhere but in prose, + and a claim that the second Twitch audio track works, which + `docs/AUDIO-ROUTING.md` marks EXPERIMENTAL because no broadcast has ever been + published through a key Enhanced Broadcasting minted. + +- **An engine test asked for three contiguous UDP ports and checked one.** + `testenv.FreeUDPPort` probes a port, releases it, and returns the number, so a + three-port allocator built on it had two numbers nobody had checked and one + that was already nobody's. It failed CI three times in one day on three + different ranges, each time blaming the code under test for a port it never + held. `testenv.FreeUDPWindow` reserves the window and holds it. + +### Changed +- **Install-wide state comes off the engine** (first of six changes for #387). + Tools, recording paths, the host sampler and the settings read no longer route + through whichever engine happens to be default. No behaviour change intended, + with one exception worth stating: `GET /system` now reads settings from the + store rather than the engine's snapshot, so it stops lagging a settings save. + - **An onboarding tour, offered once per install rather than once per browser.** A new operator finishes the signup screen and lands on an empty dashboard, and everything they need next is either in a terminal they have closed or in