Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit ad1dd112f7360625366e6fe3586d1583bb426a02
parent bee55a498b270eef10993bbd138af6558d2f22e9
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 26 Sep 2026 19:07:13 -0400

editor: POST /api/ops/cut-release and `pnpm ops cut-release` — the remote path to the same writer (workspace editor|export|all, version X.Y.Z|next|next-minor, commit, date)

Synchronous, on ops(): a bad workspace / version / date is an OpsInputError
400 before anything is read; a cut answers 200 { ok, version, results } with
each heading; a refusal is a 400 carrying the writer's sentence plus what was
already cut (`all` stops at the first failure). Revalidates the changelog
pages on any cut. ACTIONS + usage gain cut-release (the body, that `all` cuts
both with one version, and that only a release-10+ editor has the route);
archilyzer-ops.test.mjs +1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Aeditor/app/api/ops/cut-release/route.ts | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/archilyzer-ops.mjs | 15+++++++++++++++
Mscripts/archilyzer-ops.test.mjs | 17+++++++++++++++++
3 files changed, 99 insertions(+), 0 deletions(-)

diff --git a/editor/app/api/ops/cut-release/route.ts b/editor/app/api/ops/cut-release/route.ts @@ -0,0 +1,67 @@ +import { NextResponse } from "next/server"; +import { + cutReleases, + dateProblem, + RELEASE_TARGETS, + versionSpecProblem, +} from "yt-dlp-transcript-common/controller/cutRelease"; +import { revalidateAfterReleaseCut } from "../../../sites/lib/revalidateAfterReleaseCut"; +import { + OpsInputError, + oneOf, + ops, + opsFail, + optBool, + optString, + reqString, +} from "../_lib"; + +export const dynamic = "force-dynamic"; + +// POST { workspace: "editor" | "export" | "all", +// version: "X.Y.Z" | "next" | "next-minor", +// commit?: boolean (default false), +// date?: "YYYY-MM-DD" (default today, server local time) } +// +// SYNCHRONOUS: a cut is one heading edit per changelog and at most two +// path-limited commits, not a job. The writer is the one the Cut release form +// and `archilyzer release cut` call (common/controller/cutRelease.ts), so every +// refusal is its sentence. `all` cuts editor then export with ONE version, a +// commit each, and stops at the first failure. +// +// 200 { ok: true, version, results } when every changelog was cut. A refusal +// is a 400 { ok: false, error, version, results, notAttempted }: `results` +// still says what WAS cut before it (`all` may have cut and committed editor). +export async function POST(request: Request) { + return ops(request, ["workspace", "version", "commit", "date"], async (body) => { + const workspace = oneOf(body, "workspace", RELEASE_TARGETS); + const version = reqString(body, "version"); + const versionProblem = versionSpecProblem(version); + if (versionProblem) throw new OpsInputError(versionProblem); + const date = optString(body, "date"); + const badDate = date === undefined ? null : dateProblem(date); + if (badDate) throw new OpsInputError(badDate); + const commit = optBool(body, "commit") ?? false; + + const outcome = await cutReleases({ workspace, version, commit, date }); + if (outcome.results.some((r) => r.ok)) revalidateAfterReleaseCut(); + if (outcome.ok) { + return NextResponse.json({ + ok: true, + version: outcome.version, + results: outcome.results, + }); + } + const failure = outcome.results.find((r) => !r.ok); + const reason = failure && !failure.ok ? failure.error : "cut failed"; + return opsFail( + workspace === "all" && failure ? `${failure.workspace}: ${reason}` : reason, + 400, + { + version: outcome.version, + results: outcome.results, + notAttempted: outcome.notAttempted, + }, + ); + }); +} diff --git a/scripts/archilyzer-ops.mjs b/scripts/archilyzer-ops.mjs @@ -41,6 +41,7 @@ // pnpm ops tags --json '{"op":"define","tag":{"id":"eva-collab","label":"Collab"}}' // pnpm ops tag-videos --file ids.json // pnpm ops get tags eva-collab +// pnpm ops cut-release --json '{"workspace":"all","version":"next","commit":true}' // // --file reads the BODY from a JSON file, which is how a big one gets sent: a // four-thousand-id tag-videos body is written by a script, not typed by a model @@ -117,6 +118,11 @@ const ACTIONS = [ // Set the per-video do-not-clean marker on every video of a channel whose // title/description matches a download-filter pattern ({slug, match}). "keep-videos", + // Cut a changelog's [Unreleased] into a dated release heading (release 10 + // slice P). Synchronous. The same writer as `archilyzer release cut`, which + // needs no editor at all — this route exists only on an editor built from + // release 10 or later. + "cut-release", ]; // The provenance a tag write from this CLI carries. Everything else ignores it. @@ -303,6 +309,15 @@ export function usage() { ' after, and deploy-hub ships the one already built. Both deploy to the', " Pages project set on /sites under Hub, and take \"preview\" too.", "", + 'cut-release turns a changelog\'s [Unreleased] into "## [<version>] - <date>":', + ' {"workspace": "editor" | "export" | "all",', + ' "version": "X.Y.Z" | "next" | "next-minor",', + ' "commit": true (default false), "date": "YYYY-MM-DD" (default today)}.', + ' "all" cuts both with ONE version and commits each ("Release <workspace>', + ' <version>"); it stops at the first failure. Only an editor built from', + " release 10 or later has the route (an older one answers 404); with no", + " editor running, `archilyzer release cut` does the same locally.", + "", "Env: ARCHILYZER_EDITOR_URL (default http://localhost:3001), WORKER_TOKEN,", " ARCHILYZER_AGENT (provenance of a tag write; default \"cli\")", ].join("\n"); diff --git a/scripts/archilyzer-ops.test.mjs b/scripts/archilyzer-ops.test.mjs @@ -42,6 +42,23 @@ test("an unknown action is refused by name, with the list", () => { assert.match(p.error, /metadata-scan/); }); +test("cut-release posts its body to /api/ops/cut-release and is a known action", () => { + const p = parseArgs([ + "cut-release", + "--json", + '{"workspace":"all","version":"next","commit":true}', + ]); + assert.equal(p.method, "POST"); + assert.equal(p.path, "/api/ops/cut-release"); + assert.deepEqual(p.body, { workspace: "all", version: "next", commit: true }); + // It is on the list an unknown action is refused with, and usage says what + // its body takes and where the route exists. + assert.match(parseArgs(["cut-releases"]).error, /known: .*\bcut-release\b/); + assert.match(usage(), /cut-release turns a changelog's \[Unreleased\]/); + assert.match(usage(), /"all" cuts both with ONE version/); + assert.match(usage(), /release 10 or later/); +}); + test("malformed --json is refused before any request", () => { assert.match(parseArgs(["sync", "--json", "{"]).error, /not valid JSON/); assert.match(parseArgs(["sync", "--json", "[1]"]).error, /must be a JSON object/);