Archilyzer · Source

archilyzer

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

commit a899a505ed1839480c53d8d04c28648fdccded20
parent b7f718a268278aa1a525cc5dbe136201e4cee501
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Tue,  6 Oct 2026 12:18:43 -0400

publish: the deploy stage's request checks are resolveDeployRequest (asked before a job exists by the surfaces); a stage ends "Done"

Steps 1–3 of runDeployStage (the request, the target's own refusals, a build
to ship) move unchanged into an exported resolver that returns the sentence
and its exit code; the stage refuses with it as before. The editor's publish
actions and the ops route ask the same function before enqueueing, so
"no build of X in <dir> — archilyzer publish build X" is one sentence in both
places. The success line reads "Done" / "Done (no-op)": a console's reader
waits for it.

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

Diffstat:
Mcommon/publish/deployStage.ts | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mcommon/publish/stageRun.ts | 4+++-
2 files changed, 63 insertions(+), 24 deletions(-)

diff --git a/common/publish/deployStage.ts b/common/publish/deployStage.ts @@ -90,6 +90,7 @@ import { HUB_TARGET, deployRecordFor, readBuiltStamp, + type BuiltStamp, readDeployedFile, recordDeploy, targetDir, @@ -292,40 +293,56 @@ async function copyToLocal(outDir: string, dest: string): Promise<number> { * slot already holds this build); THROWS a DeployStageError on every refusal * and failure, leaving `deployed.json` untouched. */ -export async function runDeployStage( - ctx: DeployStageContext, - req: DeployStageRequest, -): Promise<DeployStageOutcome> { - const { paths, signal } = ctx; - const env = ctx.env ?? process.env; - const now = ctx.now ?? (() => new Date()); - const log = (line: string) => ctx.onLog(line.endsWith("\n") ? line : `${line}\n`); - const refuse = (why: string, exitCode: 1 | 2 | 3 = 1): never => { - const line = /^\[deploy\] REFUSED/.test(why) ? why : `[deploy] REFUSED — ${why}`; - log(line); - throw new DeployStageError(line, exitCode); - }; +/** What a deploy request resolves to, once its own refusals are answered. */ +export type ResolvedDeploy = { + target: string; + toLocal: boolean; + branch: string | undefined; + recordKind: DeployRecord["kind"]; + site: Site | null; + project: string; + publicUrl: string | undefined; + cwd: string; + stampDir: string; + outDir: string; + built: BuiltStamp; +}; +/** The sentence a deploy request is refused with, and its exit code. */ +export type DeployRequestProblem = { problem: string; exitCode: 2 | 3 }; + +/** + * Steps 1–3 of the deploy stage (the request, the target's own refusals, a + * build to ship), answered without touching anything. The stage body asks it + * first; the editor's actions and the ops route ask it BEFORE a job exists, so + * a refusal is the same sentence, word for word, either way. A site id that is + * not one throws (getSite), as the stage always has. + */ +export async function resolveDeployRequest( + paths: Paths, + req: DeployStageRequest, +): Promise<ResolvedDeploy | DeployRequestProblem> { // --- 1. the request (a refusal here is a usage error, exit 2) --- const target = req.target.trim(); + const fail = (problem: string, exitCode: 2 | 3): DeployRequestProblem => ({ problem, exitCode }); // The hub's and the homepage's targets are fixed, and a site's is never one // of them: a mismatch would read and WRITE another target's stamps (a // homepage deploy recorded in a site's production slot). const fixed = req.kind === "deploy-hub" ? HUB_TARGET : req.kind === "deploy-homepage" ? HOMEPAGE_TARGET : null; if (fixed !== null && target !== fixed) { - refuse(`${req.kind} deploys "${fixed}", not "${target}".`, 2); + return fail(`${req.kind} deploys "${fixed}", not "${target}".`, 2); } if (fixed === null && (target === HUB_TARGET || target === HOMEPAGE_TARGET)) { - refuse(`"${target}" is not a site — deploy it with ${target === HUB_TARGET ? "deploy-hub" : "deploy-homepage"}.`, 2); + return fail(`"${target}" is not a site — deploy it with ${target === HUB_TARGET ? "deploy-hub" : "deploy-homepage"}.`, 2); } const toLocal = req.to === "local"; if (req.preview !== undefined) { const problem = previewBranchProblem(req.preview); - if (problem) refuse(problem, 2); + if (problem) return fail(problem, 2); } const branch = req.preview?.trim() || undefined; - if (toLocal && branch) refuse("a local deploy has no preview branch — deploy locally or as a preview, not both.", 2); - if (toLocal && req.kind === "deploy-hub") refuse("the hub has no local target — deploy it to Cloudflare Pages.", 2); + if (toLocal && branch) return fail("a local deploy has no preview branch — deploy locally or as a preview, not both.", 2); + if (toLocal && req.kind === "deploy-hub") return fail("the hub has no local target — deploy it to Cloudflare Pages.", 2); const recordKind: DeployRecord["kind"] = toLocal ? "local" : branch ? "preview" : "production"; // --- 2. the target's own refusals --- @@ -336,14 +353,14 @@ export async function runDeployStage( if (req.kind === "deploy-site") { site = getSite(target, paths); const privateProblem = siteDeployProblem(site); - if (privateProblem) refuse(`${privateProblem}.`, 3); + if (privateProblem) return fail(`${privateProblem}.`, 3); project = site.cloudflareProject?.trim() ?? ""; - if (!project && !toLocal) refuse(`Site "${target}" has no Cloudflare Pages project configured.`, 3); + if (!project && !toLocal) return fail(`Site "${target}" has no Cloudflare Pages project configured.`, 3); publicUrl = site.siteUrl?.trim() || undefined; } else if (req.kind === "deploy-hub") { const hub = getHomepageConfig(paths); const problem = hubProjectProblem(hub.cloudflareProject); - if (problem) refuse(problem, 3); + if (problem) return fail(problem, 3); project = hub.cloudflareProject!.trim(); publicUrl = hub.siteUrl; } else { @@ -358,12 +375,32 @@ export async function runDeployStage( const built = await readBuiltStamp(paths, target); const cliTarget = req.kind === "deploy-hub" ? "hub" : req.kind === "deploy-homepage" ? "homepage" : target; if (!built) { - refuse( + return fail( `no build of ${target} in ${stampDir} — archilyzer publish ${req.kind === "deploy-site" ? `build ${cliTarget}` : cliTarget}`, 3, ); } - const b = built!; + return { target, toLocal, branch, recordKind, site, project, publicUrl, cwd, stampDir, outDir, built }; +} + +export async function runDeployStage( + ctx: DeployStageContext, + req: DeployStageRequest, +): Promise<DeployStageOutcome> { + const { paths, signal } = ctx; + const env = ctx.env ?? process.env; + const now = ctx.now ?? (() => new Date()); + const log = (line: string) => ctx.onLog(line.endsWith("\n") ? line : `${line}\n`); + const refuse = (why: string, exitCode: 1 | 2 | 3 = 1): never => { + const line = /^\[deploy\] REFUSED/.test(why) ? why : `[deploy] REFUSED — ${why}`; + log(line); + throw new DeployStageError(line, exitCode); + }; + + const resolved = await resolveDeployRequest(paths, req); + if ("problem" in resolved) refuse(resolved.problem, resolved.exitCode); + const { target, toLocal, branch, recordKind, site, project, publicUrl, cwd, outDir } = resolved as ResolvedDeploy; + const b = (resolved as ResolvedDeploy).built; // (A run's `builtAfter` is the stage's needs() — stages.ts needsDeploy — asked // before this body runs: it knows a no-op build's `checkedAt`.) const slot = deployRecordFor(await readDeployedFile(paths, target), recordKind, branch); diff --git a/common/publish/stageRun.ts b/common/publish/stageRun.ts @@ -110,7 +110,9 @@ export async function runStage( try { const outcome = await STAGES[req.kind].run({ paths, onLog, signal }, req); if (signal.aborted) throw new StageCancelled(); - onLog(`[stage] ${name}: ${outcome.status === "noop" ? "no-op" : "done"} — ${outcome.summary}\n`); + // "Done" is what a console's reader waits for (the e2e buildIndex helper + // reads "Build index output" for it, as it did before the stages). + onLog(`[stage] ${name}: ${outcome.status === "noop" ? "Done (no-op)" : "Done"} — ${outcome.summary}\n`); return { code: STAGE_EXIT.ok, outcome, message: null }; } catch (err) { if (err instanceof StageCancelled || signal.aborted) {