"use server"; // The publish surfaces' server actions (release 18 S4): every button on the // /sites Publish panel, a site's Publish tab and the Pool's Build index, and // the publish lane's runner controls on /operations/publish. // // Each returns the shape JobLane and StreamActionLog already take — `{ok, // jobId, stream, done}` or `{ok: false, error, info?}` — plus `runId` and, // for a preview, `previewUrl`. A run of several stages is followed in ONE // console (publishRunStream.ts): the index update when a build needs it, the // build, its deploy. The jobs are the `publish` queue's (publish/ // publishStages.ts) and run one at a time; their order is on disk. // // What these do NOT do: build or deploy in the editor's process. Every stage // is a child (`archilyzer stage …`), so a heavy build cannot starve the editor. import { revalidatePath } from "next/cache"; import { getRegistry } from "yt-dlp-transcript-common/jobs/registry"; import { getPaths } from "yt-dlp-transcript-common/lib/paths"; import type { StreamActionResult } from "yt-dlp-transcript-common/jobs/streamCommand"; import { planPublishRun, type PublishPlan } from "yt-dlp-transcript-common/publish/publishPlan"; import { drainPublishRunner, startPublishRunner, startPublishRunnerBlockedReason, stopPublishRunner, } from "yt-dlp-transcript-common/publish/publishRunner"; import { readPublishStatus } from "yt-dlp-transcript-common/publish/publishState"; import { PUBLISH_QUEUE } from "yt-dlp-transcript-common/publish/publishStages"; import { enqueueRun, indexPlan, refusal, skippedLines, wantedPlan, type DeployWhere, type TargetAsk, } from "./publishCore"; import { followRun } from "./publishRunStream"; export type PublishActionResult = | (Extract & { runId: string; previewUrl?: string }) | { ok: false; error: string; info?: boolean }; function revalidatePublish(): void { revalidatePath("/sites"); revalidatePath("/sites/[siteId]/publish", "page"); revalidatePath("/operations/publish"); } // Enqueue `plan` and hand back one console over it. async function runPlan(plan: PublishPlan, empty: string): Promise { const paths = getPaths(); if (plan.steps.length === 0) { const why = skippedLines(plan.skipped); return { ok: false, info: true, error: why.length ? `${empty} ${why.join("; ")}` : empty }; } const run = await enqueueRun(paths, plan, { keepStreams: true }); revalidatePublish(); if (run.parts.length === 0) { return { ok: false, error: run.refused.map((r) => r.error).join("; ") || "Nothing was enqueued.", }; } const preface = [ `[publish] run ${run.runId}: ${run.jobs.length} stage${run.jobs.length === 1 ? "" : "s"}`, ...skippedLines(run.skipped), ...run.refused.map((r) => `[publish] refused ${r.kind} ${r.target}: ${r.error}`), ]; const previewUrl = run.jobs.find((j) => j.previewUrl)?.previewUrl; return { ...followRun(run.parts, preface), runId: run.runId, ...(previewUrl ? { previewUrl } : {}) }; } async function runAsks(asks: TargetAsk[]): Promise { const paths = getPaths(); for (const ask of asks) { const why = await refusal(paths, ask); if (why) return { ok: false, error: why }; } const status = await readPublishStatus(paths); return runPlan(wantedPlan(status, asks, { withIndex: true, paths }), "Nothing to run."); } /** Publish now: what the lane would run — the stale index, then each policy target. */ export async function publishNowAction(): Promise { const status = await readPublishStatus(getPaths()); return runPlan(planPublishRun(status, { deploys: "policy" }), "Nothing to publish: everything is current."); } /** Build all stale: every stale site whatever its policy, no deploys. */ export async function buildAllStaleAction(): Promise { const status = await readPublishStatus(getPaths()); return runPlan(planPublishRun(status, { builds: "stale", deploys: "none" }), "Nothing to build: every site is current."); } /** The Pool's Build index: the index update (a no-op when nothing changed). */ export async function updateIndexAction(): Promise { return runPlan(indexPlan(), "Nothing to run."); } /** Build one target (a site id, "_hub", "_homepage") — forced; the index first when stale. */ export async function buildTargetAction( target: string, opts: { skipArchives?: boolean } = {}, ): Promise { return runAsks([{ target, build: { force: true, skipArchives: opts.skipArchives } }]); } /** Deploy what is built: production, a preview branch, or the local service. */ export async function deployTargetAction( target: string, opts: { where: DeployWhere; preview?: string }, ): Promise { return runAsks([{ target, deploy: { where: opts.where, preview: opts.preview, force: true } }]); } /** Build, then deploy that build (the deploy waits on disk for it). */ export async function buildAndDeployTargetAction( target: string, opts: { where: DeployWhere; preview?: string; skipArchives?: boolean }, ): Promise { return runAsks([ { target, build: { force: true, skipArchives: opts.skipArchives }, deploy: { where: opts.where, preview: opts.preview, force: true }, }, ]); } /** * A publish console's Cancel: the RUN, not the one job the console holds. Every * queued or running stage carrying that job's run id is cancelled, newest * first (so a queued stage is gone before the slot ahead of it frees and * nothing is promoted). A job that is not a publish stage, or carries no run * id, is cancelled alone. Another run's jobs — another tab's, the lane's — carry * another id and are never touched. */ export async function cancelPublishRunAction(jobId: string): Promise<{ ok: boolean }> { const registry = getRegistry(); const record = registry.get(jobId); const runId = record?.spec?.params?.runId; let ok = false; if (typeof runId === "string" && record?.queueKey === PUBLISH_QUEUE) { const live = registry .list() .filter( (r) => r.queueKey === PUBLISH_QUEUE && (r.status === "queued" || r.status === "running") && r.spec?.params?.runId === runId, ) .sort((a, b) => b.queuedAt - a.queuedAt); for (const r of live) ok = registry.cancel(r.id) || ok; } else { ok = registry.cancel(jobId); } revalidatePath("/jobs"); revalidatePublish(); return { ok }; } // --- the publish lane's runner (/operations/publish) ------------------------- export type LaneControlResult = { ok: boolean; error?: string }; export async function startPublishLaneAction(): Promise { const blocked = startPublishRunnerBlockedReason(); if (blocked) return { ok: false, error: blocked }; const id = await startPublishRunner(getPaths()); revalidatePublish(); return id ? { ok: true } : { ok: false, error: "The publish lane's runner did not start." }; } export async function stopPublishLaneAction(): Promise { stopPublishRunner(); revalidatePublish(); return { ok: true }; } export async function drainPublishLaneAction(): Promise { drainPublishRunner(); revalidatePublish(); return { ok: true }; }