import { NextResponse } from "next/server"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import { planPublishRun } from "yt-dlp-transcript-common/publish/publishPlan"; import { readPublishStatus } from "yt-dlp-transcript-common/publish/publishState"; import { ALL_TARGET, HOMEPAGE_TARGET, HUB_TARGET, INDEX_TARGET } from "yt-dlp-transcript-common/publish/stamps"; import { indexPlan } from "../../../sites/lib/publishCore"; import { enqueueAsks, enqueuePlan, publishResponse } from "../_publish"; import { OpsInputError, oneOf, ops, opsAuth, optBool, optPreviewBranch, reqSiteIds, type OpsBody, } from "../_lib"; export const dynamic = "force-dynamic"; const VERBS = ["index", "build", "deploy", "hub", "homepage", "now", "stale"] as const; type Verb = (typeof VERBS)[number]; // POST { verb: index | build | deploy | hub | homepage | now | stale, // siteId | siteIds?, preview?, to?: "pages" | "local", force?, // runner?: "local" | "docker", deploy?, skipArchives?, // allowMissingMedia? } // // index the index update (a no-op when nothing changed) // build each named site's build, forced — the index first when stale; // `runner: "docker"` builds every site in containers (host only); // `allowMissingMedia: true` lets a report citation with no // prepared media through compose (local builds only) // deploy each named site's built bundle: production, `preview`, or // `to: "local"` (ARCHILYZER_SITE_OUT); `force` redeploys a // bundle already shipped there // hub the hub's build; `deploy: true` then deploys it (production, or // `preview`) // homepage the same for the homepage (`to: "local"` deploys locally) // now Publish now: what the lane would run — the stale index, then // each policy target (site.json publish.auto, settings.publish) // stale Build all stale: every stale site, whatever its policy // // → { ok, runId, jobs: [{ target, kind, jobId, previewUrl?, existing? }], // skipped: [{ kind, target, reason }], refused: [{ target, kind?, error }] } // plus `jobId` when exactly one job. Every request refused → 400. // // GET → the publish status (`readPublishStatus`): the index, the lane, a row // per target with its chips, and the plan Publish now would run. const KEYS = [ "verb", "siteId", "siteIds", "preview", "to", "force", "runner", "deploy", "skipArchives", "allowMissingMedia", ]; function only(body: OpsBody, verb: Verb, allowed: string[]): void { const extra = Object.keys(body).filter((k) => k !== "verb" && !allowed.includes(k)); if (extra.length) { throw new OpsInputError( `verb "${verb}" takes ${allowed.length ? allowed.join(", ") : "no other keys"} — not ${extra.join(", ")}`, ); } } export async function POST(request: Request) { return ops(request, KEYS, async (body) => { const verb = oneOf(body, "verb", VERBS); const force = optBool(body, "force"); const skipArchives = optBool(body, "skipArchives"); switch (verb) { case "index": only(body, verb, []); return publishResponse(await enqueuePlan(indexPlan())); case "now": case "stale": { only(body, verb, []); const status = await readPublishStatus(getPaths()); const plan = verb === "now" ? planPublishRun(status, { deploys: "policy" }) : planPublishRun(status, { builds: "stale", deploys: "none" }); return publishResponse(await enqueuePlan(plan)); } case "build": { only(body, verb, ["siteId", "siteIds", "force", "runner", "skipArchives", "allowMissingMedia"]); const runner = body.runner === undefined ? "local" : oneOf(body, "runner", ["local", "docker"] as const); const allowMissingMedia = optBool(body, "allowMissingMedia"); if (runner === "docker") { if (allowMissingMedia !== undefined) { throw new OpsInputError('"allowMissingMedia" is a local build\'s — the docker runner does not take it'); } if (body.siteId !== undefined || body.siteIds !== undefined) { throw new OpsInputError('"runner": "docker" builds every site — send no "siteId"/"siteIds"'); } // As every build: the index update first when it is not fresh, and // the containers wait on disk for it (indexAfter). const runStart = Date.now(); const status = await readPublishStatus(getPaths()); const withIndex = !status.index.fresh; return publishResponse( await enqueuePlan({ runStart, steps: [ ...(withIndex ? [{ kind: "update-index" as const, target: INDEX_TARGET, reason: "the index is not fresh" }] : []), { kind: "build-site", target: ALL_TARGET, runner: "docker", reason: "asked", ...(force !== false ? { force: true } : {}), ...(skipArchives ? { skipArchives: true } : {}), ...(withIndex ? { indexAfter: runStart } : {}), }, ], skipped: [], }), ); } const ids = reqSiteIds(body); return publishResponse( await enqueueAsks( ids.map((target) => ({ target, build: { force: force !== false, skipArchives, ...(allowMissingMedia ? { allowMissingMedia: true } : {}) }, })), ), ); } case "deploy": { only(body, verb, ["siteId", "siteIds", "preview", "to", "force"]); const preview = optPreviewBranch(body); const to = body.to === undefined ? "pages" : oneOf(body, "to", ["pages", "local"] as const); if (to === "local" && preview !== undefined) { throw new OpsInputError("a local deploy has no preview branch — deploy locally or as a preview, not both."); } const where = to === "local" ? "local" : preview ? "preview" : "production"; return publishResponse( await enqueueAsks( reqSiteIds(body).map((target) => ({ target, deploy: { where, ...(preview ? { preview } : {}), force: force === true }, })), ), ); } case "hub": case "homepage": { only(body, verb, ["deploy", "preview", "to", "force", "skipArchives"]); const target = verb === "hub" ? HUB_TARGET : HOMEPAGE_TARGET; const deploy = optBool(body, "deploy") === true; const preview = optPreviewBranch(body); const to = body.to === undefined ? "pages" : oneOf(body, "to", ["pages", "local"] as const); if (!deploy && (preview !== undefined || body.to !== undefined)) { throw new OpsInputError('"preview" and "to" need "deploy": true — a build alone deploys nothing'); } if (to === "local" && preview !== undefined) { throw new OpsInputError("a local deploy has no preview branch — deploy locally or as a preview, not both."); } const where = to === "local" ? "local" : preview ? "preview" : "production"; return publishResponse( await enqueueAsks([ { target, build: { force: force !== false, skipArchives }, ...(deploy ? { deploy: { where, ...(preview ? { preview } : {}), force: true } } : {}), }, ]), ); } } }); } export async function GET(request: Request) { const denied = opsAuth(request); if (denied) return denied; return NextResponse.json({ ok: true, ...(await readPublishStatus(getPaths())) }); }