commit 59ae09eea6ee07aeaccfafbb57bddcf6c87e36a1
parent ad302e7b45bc7915afa7f469da5b87731c2094f4
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 26 Sep 2026 19:01:36 -0400
editor: the Cut release form's action is a FormData adapter over cutReleaseForWorkspace; /sites reads the export changelog the form cuts
Fields, labels, refusals and the returned state are unchanged; the three
revalidations move to revalidateAfterReleaseCut, which the ops route shares.
/sites read `<exportDir>/CHANGELOG.md` while the action wrote
`exportChangelogFile` — the same file unless EXPORT_CHANGELOG_FILE is set,
which the e2e server now does (the ops-route commit), so the page reads the writer's.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
3 files changed, 32 insertions(+), 89 deletions(-)
diff --git a/editor/app/sites/lib/cutReleaseAction.ts b/editor/app/sites/lib/cutReleaseAction.ts
@@ -1,35 +1,18 @@
"use server";
-import fs from "node:fs";
-import path from "node:path";
-import { revalidatePath } from "next/cache";
-import { getPaths } from "yt-dlp-transcript-common/lib/paths";
-import {
- cutRelease,
- CutReleaseError,
-} from "yt-dlp-transcript-common/lib/changelog";
-import { commitPath, listDirtyPaths } from "yt-dlp-transcript-common/lib/git";
-import { writeFileAtomic } from "yt-dlp-transcript-common/lib/jsonFile-server";
+import { cutReleaseForWorkspace } from "yt-dlp-transcript-common/controller/cutRelease";
+import { revalidateAfterReleaseCut } from "./revalidateAfterReleaseCut";
+
+// The "Cut release" form's server action: a FormData adapter over the one
+// writer, `cutReleaseForWorkspace` (common/controller/cutRelease.ts), which the
+// CLI (`archilyzer release cut`) and POST /api/ops/cut-release call too. The
+// dirty-tree guard, the cut, the atomic write, the commit and every refusal's
+// sentence live there.
export type CutReleaseState =
| { ok: true; version: string; committed: boolean }
| { ok: false; error: string };
-function changelogPathFor(workspace: "editor" | "export"): string {
- const paths = getPaths();
- return workspace === "export"
- ? paths.exportChangelogFile
- : paths.editorChangelogFile;
-}
-
-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}`;
-}
-
export async function cutReleaseAction(
_prev: CutReleaseState | undefined,
formData: FormData,
@@ -50,66 +33,12 @@ export async function cutReleaseAction(
if (!version) {
return { ok: false, error: "Version is required." };
}
- const paths = getPaths();
- const filePath = changelogPathFor(workspace);
- const relPath = path.relative(paths.monorepoRoot, filePath);
- // Before mutating anything, refuse to commit over an otherwise-dirty tree so
- // the release commit contains only the changelog. A dirty changelog itself is
- // fine (uncommitted [Unreleased] bullets get folded into the release commit).
- if (shouldCommit) {
- let dirty: string[];
- try {
- dirty = await listDirtyPaths(paths.monorepoRoot);
- } catch (err) {
- return {
- ok: false,
- error: `Could not check git status: ${(err as Error).message}`,
- };
- }
- const others = dirty.filter((p) => p !== relPath);
- if (others.length > 0) {
- return {
- ok: false,
- error: `Other uncommitted changes present (${others.join(
- ", ",
- )}). Commit or stash them before cutting a release.`,
- };
- }
- }
- let source: string;
- try {
- source = await fs.promises.readFile(filePath, "utf8");
- } catch (err) {
- return {
- ok: false,
- error: `Could not read ${filePath}: ${(err as Error).message}`,
- };
- }
- let next: string;
- try {
- next = cutRelease(source, version, todayISO());
- } catch (err) {
- if (err instanceof CutReleaseError) {
- return { ok: false, error: err.message };
- }
- throw err;
- }
- await writeFileAtomic(filePath, next);
- if (shouldCommit) {
- const result = await commitPath(
- paths.monorepoRoot,
- relPath,
- `Release ${workspace} ${version}`,
- );
- if (!result.ok) {
- return {
- ok: false,
- error: `Cut release ${version}, but the commit failed: ${result.error}`,
- };
- }
- }
- revalidatePath("/");
- revalidatePath("/changelog");
- revalidatePath("/sites");
- return { ok: true, version, committed: shouldCommit };
+ const result = await cutReleaseForWorkspace({
+ workspace,
+ version,
+ commit: shouldCommit,
+ });
+ if (!result.ok) return { ok: false, error: result.error };
+ revalidateAfterReleaseCut();
+ return { ok: true, version: result.version, committed: result.committed };
}
diff --git a/editor/app/sites/lib/revalidateAfterReleaseCut.ts b/editor/app/sites/lib/revalidateAfterReleaseCut.ts
@@ -0,0 +1,12 @@
+import { revalidatePath } from "next/cache";
+
+// The pages that render a changelog: the home page's "what's new", /changelog
+// (the editor's notes and its Cut release form) and /sites (the export's notes
+// and its form). Called after a cut by the form's server action and by
+// POST /api/ops/cut-release — both run inside their request, so this is the
+// plain `revalidatePath`, not the job-body `safeRevalidate`.
+export function revalidateAfterReleaseCut(): void {
+ revalidatePath("/");
+ revalidatePath("/changelog");
+ revalidatePath("/sites");
+}
diff --git a/editor/app/sites/page.tsx b/editor/app/sites/page.tsx
@@ -1,5 +1,4 @@
import { readFileSync } from "node:fs";
-import path from "node:path";
import type { Metadata } from "next";
import Link from "next/link";
import { Changelog } from "yt-dlp-transcript-common/components/Changelog";
@@ -44,9 +43,12 @@ const BUILD_KINDS = new Set([
"archive-combined-live-chat",
]);
+// The same file the Cut release form rewrites (`exportChangelogFile`, which
+// EXPORT_CHANGELOG_FILE overrides for the e2e server), or the page would show
+// one changelog while the form cut another.
function loadExportChangelog(): string | null {
try {
- return readFileSync(path.join(getPaths().exportDir, "CHANGELOG.md"), "utf8");
+ return readFileSync(getPaths().exportChangelogFile, "utf8");
} catch {
return null;
}