commit fa5e654fb90ee44ad213c11827a4017d6a50ead1
parent e897dff42c538f11c469bc0169a65935d78c0a04
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sun, 24 May 2026 02:26:05 -0400
Update release cut
Diffstat:
5 files changed, 96 insertions(+), 5 deletions(-)
diff --git a/common/lib/changelog.ts b/common/lib/changelog.ts
@@ -82,6 +82,6 @@ export function cutRelease(
const headingEnd = headingStart + match[0].length;
const before = source.slice(0, headingStart);
const after = source.slice(headingEnd);
- const replacement = `## [Unreleased]\n\n## [${version}] - ${dateISO}`;
+ const replacement = `## [${version}] - ${dateISO}`;
return `${before}${replacement}${after}`;
}
diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md
@@ -1,6 +1,7 @@
# Changelog
## [Unreleased]
+- **Cutting a release now clears `## [Unreleased]` entirely** instead of leaving an empty heading sitting above the new dated section. The `/changelog` and `/deploy` pages stop showing a blank Unreleased block after a release; the heading reappears the next time somebody hand-adds a pending bullet to the file.
## [0.2.0] - 2026-05-22
- **`/actionable` page** aggregating pending work across all channels — three sections (channels with undownloaded videos, channels with downloaded videos awaiting transcription, and channels with stale or missing reports), each row linking to the channel with inline "Download missing" / "Transcribe pending" / "Refresh report" buttons. An "Update all reports" button queues a refresh per channel. A "Needs attention" card on the dashboard links here whenever either of the first two lists has anything in it.
diff --git a/editor/app/deploy/page.tsx b/editor/app/deploy/page.tsx
@@ -71,8 +71,7 @@ export default function DeployPage() {
<h2 className="text-lg font-semibold">Cut release</h2>
<p className="text-sm text-zinc-500">
Turns the current <code>## [Unreleased]</code> heading into a
- dated semver heading and inserts a fresh empty{" "}
- <code>## [Unreleased]</code> above it.
+ dated semver heading.
</p>
</div>
<CutReleaseForm
diff --git a/editor/e2e/cut-release.spec.ts b/editor/e2e/cut-release.spec.ts
@@ -0,0 +1,93 @@
+import { readFile, writeFile } from "node:fs/promises";
+import { test, expect } from "@playwright/test";
+import { resolvePath } from "./helpers";
+
+const CHANGELOG_PATH = resolvePath("CHANGELOG.md");
+
+const WITH_UNRELEASED =
+ "# Changelog\n" +
+ "\n" +
+ "## [Unreleased]\n" +
+ "- test bullet for cut-release spec\n" +
+ "\n" +
+ "## [9.9.9] - 2024-01-01\n" +
+ "- old released bullet\n";
+
+const WITHOUT_UNRELEASED =
+ "# Changelog\n" +
+ "\n" +
+ "## [9.9.9] - 2024-01-01\n" +
+ "- old released bullet\n";
+
+function todayISO(): string {
+ const now = new Date();
+ const y = now.getFullYear().toString().padStart(4, "0");
+ const m = (now.getMonth() + 1).toString().padStart(2, "0");
+ const d = now.getDate().toString().padStart(2, "0");
+ return `${y}-${m}-${d}`;
+}
+
+let original: string;
+
+test.beforeEach(async () => {
+ original = await readFile(CHANGELOG_PATH, "utf8");
+});
+
+test.afterEach(async () => {
+ await writeFile(CHANGELOG_PATH, original);
+});
+
+test("cutting a release removes the Unreleased heading from the file", async ({
+ page,
+}) => {
+ await writeFile(CHANGELOG_PATH, WITH_UNRELEASED);
+
+ await page.goto("/changelog");
+ await page.locator("#version-editor").fill("9.9.10");
+ await page.getByRole("button", { name: /^cut release$/i }).click();
+
+ await expect(
+ page.getByRole("status").filter({ hasText: "Cut release 9.9.10." }),
+ ).toBeVisible();
+
+ const after = await readFile(CHANGELOG_PATH, "utf8");
+ expect(after).toContain(`## [9.9.10] - ${todayISO()}`);
+ expect(after).not.toMatch(/^##\s+\[Unreleased\]/im);
+ expect(after).toContain("## [9.9.9] - 2024-01-01");
+ expect(after).toContain("- test bullet for cut-release spec");
+});
+
+test("form disables after a successful cut on reload", async ({ page }) => {
+ await writeFile(CHANGELOG_PATH, WITH_UNRELEASED);
+
+ await page.goto("/changelog");
+ await page.locator("#version-editor").fill("9.9.10");
+ await page.getByRole("button", { name: /^cut release$/i }).click();
+ await expect(
+ page.getByRole("status").filter({ hasText: "Cut release 9.9.10." }),
+ ).toBeVisible();
+
+ await page.goto("/changelog");
+ await expect(page.locator("#version-editor")).toBeDisabled();
+ await expect(
+ page.getByRole("button", { name: /^cut release$/i }),
+ ).toBeDisabled();
+ await expect(
+ page.getByText("Nothing pending under [Unreleased] yet."),
+ ).toBeVisible();
+});
+
+test("form is disabled when the file has no Unreleased heading", async ({
+ page,
+}) => {
+ await writeFile(CHANGELOG_PATH, WITHOUT_UNRELEASED);
+
+ await page.goto("/changelog");
+ await expect(page.locator("#version-editor")).toBeDisabled();
+ await expect(
+ page.getByRole("button", { name: /^cut release$/i }),
+ ).toBeDisabled();
+ await expect(
+ page.getByText("Nothing pending under [Unreleased] yet."),
+ ).toBeVisible();
+});
diff --git a/export/CHANGELOG.md b/export/CHANGELOG.md
@@ -1,7 +1,5 @@
# Changelog
-## [Unreleased]
-
## [0.2.0] - 2026-05-22
- **Composable layered search.** The single search bar is now a query builder: combine any number of layers with AND / OR / NOT and arbitrary nesting, each layer scoped to transcripts, live chat, or title/channel. Matches from every layer surface in the result list with a per-layer colour swatch. Editing one layer doesn't re-run the others, so deep nests stay fast. Simple one-keyword search still looks like a single input — the builder collapses to compact mode when there's only one layer. Legacy `?q=&m=&re=` share-links auto-migrate to a one-layer tree.
- **Query-builder controls.** A "Reset layers" button collapses the builder back to one empty transcripts leaf. An "Unwrap" button on every non-root group flattens its children into the parent; if the group had NOT set, the promoted children inherit a flipped NOT so the meaning of the subtree doesn't silently change. The "slow / medium / fast" cost badges next to each scope dropdown have been removed — they read as a warning on the primary mode (transcripts) without giving you anything to do about it.