// The ops API's way into the publish stages (release 18 S4): `POST // /api/ops/publish` and the eight routes it replaced, which stay as aliases // with their old bodies and answers (build-index, build-site, build-deploy, // deploy-site, build-hub, deploy-hub, build-homepage, deploy-homepage). // // Adapters only (this directory's rule, _lib.ts): every refusal is // publishCore's — the deploy stage's own sentence, asked before a job exists — // and every job is `enqueuePublishRun`'s, under one run id. import { NextResponse } from "next/server"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import type { PublishPlan } from "yt-dlp-transcript-common/publish/publishPlan"; import type { PublishRunResult } from "yt-dlp-transcript-common/publish/publishStages"; import { enqueueRun, publishStatus, refusal, stageKindsOf, wantedPlan, type TargetAsk, } from "../../sites/lib/publishCore"; import { opsFail } from "./_lib"; export type OpsRefusal = { target: string; kind?: string; error: string }; export type OpsRun = PublishRunResult & { preRefused: OpsRefusal[] }; /** Enqueue a plan as it stands (Publish now, Build all stale, the index). */ export async function enqueuePlan(plan: PublishPlan): Promise { const run = await enqueueRun(getPaths(), plan); return { ...run, preRefused: [] }; } /** * Enqueue what the asks name: each is refused before any job exists when the * deploy stage would refuse it (the others still run); the index update goes * first when a build is asked for and the index is not fresh. */ export async function enqueueAsks(asks: TargetAsk[]): Promise { const paths = getPaths(); const ok: TargetAsk[] = []; const preRefused: OpsRefusal[] = []; for (const ask of asks) { // One target's throw (an unreadable site.json) is that target's refusal, // never the whole request's 500: the others still run. let why: string | null; try { why = await refusal(paths, ask); } catch (e) { why = (e as Error).message; } if (why) { const kinds = stageKindsOf(ask.target); preRefused.push({ target: ask.target, kind: ask.deploy ? kinds.deploy : kinds.build, error: why }); } else ok.push(ask); } if (ok.length === 0) return { runId: "", jobs: [], skipped: [], refused: [], preRefused }; const status = await publishStatus(paths); const run = await enqueueRun(paths, wantedPlan(status, ok, { withIndex: true, paths })); return { ...run, preRefused }; } /** Every refusal of a run, before a job or by the queue. */ export function refusalsOf(run: OpsRun): OpsRefusal[] { return [...run.preRefused, ...run.refused]; } /** * The publish route's answer: `{ok, runId, jobs, skipped, refused}` and a * top-level `jobId` when exactly one job (so `--wait` follows it). Asking for * jobs and getting none because every one was refused is a 400 naming the * reasons; a plan with nothing to run is an `ok` with no jobs. */ export function publishResponse(run: OpsRun): NextResponse { const refused = refusalsOf(run); if (run.jobs.length === 0 && refused.length > 0) { return opsFail( refused.length === 1 ? refused[0].error : refused.map((r) => `${r.target}: ${r.error}`).join("; "), ); } return NextResponse.json({ ok: true, runId: run.runId, jobs: run.jobs, skipped: run.skipped, refused, ...(run.jobs.length === 1 ? { jobId: run.jobs[0].jobId } : {}), ...(run.jobs.length === 1 && run.jobs[0].previewUrl ? { previewUrl: run.jobs[0].previewUrl } : {}), }); } /** The last job of a run for `target` (its deploy when it has one), or undefined. */ export function lastJobOf(run: OpsRun, target: string) { return [...run.jobs].reverse().find((j) => j.target === target); }