commit 4d523ddbd8b70965bd15143334a3fff5b0a8c57b
parent a587f4b3b7f0d8868bc24e1382df14858360ff37
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 23 Sep 2026 08:58:26 -0400
ops cli: deploy-site, and the preview alias printed where a person sees it
The alias is already in the response JSON, but JSON is what a pipe consumes and
the URL is what a person needs — and under --wait it scrolls off behind minutes
of build log. So it is reprinted last, on stderr, which keeps `| jq` seeing
nothing but the response. Usage says what a preview IS rather than only that a
key exists, because an operator reaching for one is about to decide whether to
touch production.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Diffstat:
2 files changed, 114 insertions(+), 6 deletions(-)
diff --git a/scripts/archilyzer-ops.mjs b/scripts/archilyzer-ops.mjs
@@ -33,6 +33,7 @@
// pnpm ops relocate --json '{"slugs":["x"],"locationId":"platter"}'
// pnpm ops build-site --json '{"siteId":"anilyzer"}' --wait
// pnpm ops build-deploy --json '{"siteIds":["anilyzer","jeralyzer"]}' --wait
+// pnpm ops deploy-site --json '{"siteId":"anilyzer","preview":"tags-exclude"}' --wait
// pnpm ops get channel the-quartering
// pnpm ops tags --json '{"op":"define","tag":{"id":"eva-collab","label":"Collab"}}'
// pnpm ops tag-videos --file ids.json
@@ -93,6 +94,10 @@ const ACTIONS = [
"build-index",
"build-deploy",
"build-site",
+ // Deploy the ALREADY-BUILT export/out — build-deploy's other half, and the
+ // one a preview is for: build once, look at the preview, then ship the same
+ // bundle to production without rebuilding it.
+ "deploy-site",
"relocate",
"relocate-back",
"evict-clips",
@@ -233,6 +238,32 @@ export function parseArgs(argv) {
};
}
+// The preview alias(es) a response carries, in job order and de-duplicated.
+//
+// WHY IT IS REPRINTED AT ALL. The alias is already in the JSON, but the JSON is
+// what a pipe consumes and the URL is what a person needs, and with --wait it
+// scrolls off behind minutes of build log. Printed on stderr, not stdout, so
+// `pnpm ops … | jq` still sees nothing but the response — the same split the
+// --wait log lines already use.
+export function previewUrlsIn(payload) {
+ const urls = [];
+ if (payload && Array.isArray(payload.jobs)) {
+ for (const job of payload.jobs) {
+ if (job && typeof job.previewUrl === "string") urls.push(job.previewUrl);
+ }
+ }
+ // Only as a fallback: the single-job case repeats jobs[0].previewUrl at the
+ // top level, and printing it twice would read as two previews.
+ if (urls.length === 0 && payload && typeof payload.previewUrl === "string") {
+ urls.push(payload.previewUrl);
+ }
+ return [...new Set(urls)];
+}
+
+function printPreviewUrls(payload) {
+ for (const url of previewUrlsIn(payload)) console.error(`preview: ${url}`);
+}
+
export function usage() {
return [
"Usage: pnpm ops <action> [--json '<body>' | --file <path>] [--wait]",
@@ -249,7 +280,14 @@ export function usage() {
"--wait-timeout <seconds> gives up and exits 1 instead of waiting forever.",
" Default: no timeout — the queue may legitimately hold a job for hours.",
"",
- 'build-site and build-deploy both take "siteId" (one) or "siteIds" (a list).',
+ 'build-site, build-deploy and deploy-site all take "siteId" (one) or',
+ ' "siteIds" (a list).',
+ "",
+ '"preview": "<branch>" on deploy-site or build-deploy makes it a Cloudflare',
+ " Pages PREVIEW instead of production: the same bundle goes to a branch",
+ " alias, https://<branch>.<project>.pages.dev, and the live site is left",
+ " alone. The alias is printed after the response. Lowercase letters,",
+ ' digits and dashes, up to 28 characters; "main" is refused.',
"",
"Env: ARCHILYZER_EDITOR_URL (default http://localhost:3001), WORKER_TOKEN,",
" ARCHILYZER_AGENT (provenance of a tag write; default \"cli\")",
@@ -459,7 +497,10 @@ async function main() {
}
console.log(JSON.stringify(payload, null, 2));
if (!res.ok || payload.ok === false) return 1;
- if (!parsed.wait) return 0;
+ if (!parsed.wait) {
+ printPreviewUrls(payload);
+ return 0;
+ }
// `jobIds` FIRST: a bulk fan-out (relocate, relocate-back) returns an array
// and also a single `jobId` when it started exactly one, so reading `jobId`
// first would follow one job out of five. Before `jobIds` existed those
@@ -474,6 +515,7 @@ async function main() {
: [];
if (jobIds.length === 0) {
// Not a job-starting action (or it queued nothing). --wait is satisfied.
+ printPreviewUrls(payload);
return 0;
}
let worst = 0;
@@ -484,6 +526,9 @@ async function main() {
console.error(`[${jobId}] ${status}`);
if (status !== "done") worst = 1;
}
+ // LAST, after the logs: with --wait the response scrolled off minutes ago,
+ // and the alias is the one thing the operator came for.
+ printPreviewUrls(payload);
return worst;
}
diff --git a/scripts/archilyzer-ops.test.mjs b/scripts/archilyzer-ops.test.mjs
@@ -5,7 +5,12 @@
// Run with: pnpm test:scripts
import assert from "node:assert/strict";
import test from "node:test";
-import { followJob, parseArgs, usage } from "./archilyzer-ops.mjs";
+import {
+ followJob,
+ parseArgs,
+ previewUrlsIn,
+ usage,
+} from "./archilyzer-ops.mjs";
test("no arguments prints usage", () => {
assert.equal(parseArgs([]).help, true);
@@ -214,9 +219,67 @@ test("--wait-timeout gives up on a job that never ends", async () => {
});
// The two build routes used to disagree about the spelling of their one
-// argument, so the usage text is where a reader finds out they no longer do.
-test("usage says both build routes take siteId or siteIds", () => {
- assert.match(usage(), /build-site and build-deploy both take "siteId".*"siteIds"/);
+// argument, so the usage text is where a reader finds out they no longer do —
+// and deploy-site, added later, is in the same sentence rather than a footnote.
+test("usage says all three site routes take siteId or siteIds", () => {
+ assert.match(
+ usage(),
+ /build-site, build-deploy and deploy-site all take "siteId"[\s\S]*"siteIds"/,
+ );
+});
+
+// A preview is the feature an operator reaches for BEFORE a production deploy,
+// so the usage text has to say what it is, not merely that a key exists.
+test("usage explains preview deploys and names deploy-site", () => {
+ const u = usage();
+ assert.match(u, /Actions:.*deploy-site/);
+ assert.match(u, /"preview": "<branch>"/);
+ assert.match(u, /https:\/\/<branch>\.<project>\.pages\.dev/);
+ assert.match(u, /"main" is refused/);
+});
+
+test("deploy-site is a POST to its own route", () => {
+ const parsed = parseArgs([
+ "deploy-site",
+ "--json",
+ '{"siteId":"anilyzer","preview":"tags-exclude"}',
+ ]);
+ assert.equal(parsed.method, "POST");
+ assert.equal(parsed.path, "/api/ops/deploy-site");
+ assert.deepEqual(parsed.body, { siteId: "anilyzer", preview: "tags-exclude" });
+});
+
+test("previewUrlsIn reads the alias off each job", () => {
+ assert.deepEqual(
+ previewUrlsIn({
+ ok: true,
+ jobs: [
+ { siteId: "a", jobId: "j1", previewUrl: "https://p.a.pages.dev" },
+ { siteId: "b", jobId: "j2", previewUrl: "https://p.b.pages.dev" },
+ ],
+ }),
+ ["https://p.a.pages.dev", "https://p.b.pages.dev"],
+ );
+});
+
+test("previewUrlsIn prints the single-job alias once, not twice", () => {
+ // The route repeats jobs[0].previewUrl at the top level so a one-site caller
+ // never indexes into `jobs`; printing both would read as two previews.
+ assert.deepEqual(
+ previewUrlsIn({
+ ok: true,
+ jobs: [{ siteId: "a", jobId: "j1", previewUrl: "https://p.a.pages.dev" }],
+ jobId: "j1",
+ previewUrl: "https://p.a.pages.dev",
+ }),
+ ["https://p.a.pages.dev"],
+ );
+});
+
+test("previewUrlsIn is empty for a production deploy", () => {
+ assert.deepEqual(previewUrlsIn({ ok: true, jobs: [{ siteId: "a", jobId: "j1" }] }), []);
+ assert.deepEqual(previewUrlsIn({ ok: true, jobId: "j1" }), []);
+ assert.deepEqual(previewUrlsIn(null), []);
});
test("a recovered log endpoint answering 'running' is not an outcome", async () => {