/*
RFC-0379: cloudflare-workers adapter — wraps wrangler deploy with injectable CommandRunner, health verification via @warpgogol/fingerprint HTML normalization.
- Do not implement netlify or other adapters — only cloudflare-workers in this wave.
- Do not log, echo, or serialize secret values or resolved secrets-file contents.
- RFC-1091: extract shared health-check helpers to health-helpers.ts, import and re-export for backward compatibility.
- RFC-1097: step 6 — compass.migrate codemod run
Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order.
- RFC-1097: sweep — werkstatt-engine clean
Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics.
- REVIEW-CODE-2026-10-01-01: bootSmokeCapable() → true — worker-runtime adapter emits a miniflare-bootable bundle.
- RFC-1188: Web Bot Auth outbound identity: wba-sign primitives, directory emit+sweep, AGS-WBA-01, leitstand signed fetches
ADR-0027, RFC-0379, RFC-0587, RFC-0595, RFC-0623
*/
import path from "node:path";
import type { PropagationResult, HealthCheck } from "@warpgogol/werkstatt-engine/schemas";
import { hashHtml } from "@warpgogol/werkstatt-engine/fingerprint";
import type {
CommandRunner,
DeploymentAdapter,
PropagateInput,
RollbackInput,
RollbackResult,
HealthInput,
} from "../adapter.ts";
import { runWranglerRollback, extractWorkerVersionId } from "../service-deploy-helpers.ts";
import {
filterEnv,
sourceDotenv,
readBehaviorSnapshot,
verifyRedirectRoute,
selectProbeRoutes,
fetchWithRetry,
createDefaultCommandRunner,
} from "./health-helpers.ts";
// RFC-1091: re-export for backward compatibility — consumers that import from
// cloudflare-workers.ts continue to work after extraction to health-helpers.ts.
export {
filterEnv,
sourceDotenv,
readBehaviorSnapshot,
verifyRedirectRoute,
} from "./health-helpers.ts";
import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
const TRANSIENT_ERROR_PATTERNS: readonly RegExp[] = [
/\b502\b/,
/\b503\b/,
/\b504\b/,
/\b522\b/,
/Gateway Timeout/i,
/malformed response/i,
/Received a malformed response from the API/i,
];
function isTransientError(stderr: string): boolean {
return TRANSIENT_ERROR_PATTERNS.some((pattern) => pattern.test(stderr));
}
async function runWranglerDeployWithRetry(
runner: CommandRunner,
args: string[],
opts: { cwd: string; env: Record },
maxRetries: number = 2,
delaysMs: number[] = [30_000, 60_000],
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
const totalAttempts = maxRetries + 1;
let result = await runner("npx", args, opts);
for (let attempt = 1; attempt < totalAttempts; attempt++) {
if (result.exitCode === 0) return result;
if (!isTransientError(result.stderr)) {
console.error(`[cloudflare-workers] wrangler deploy failed (exit ${result.exitCode})`);
console.error(`[cloudflare-workers] stdout: ${result.stdout.slice(-500)}`);
console.error(`[cloudflare-workers] stderr: ${result.stderr.slice(-500)}`);
return result;
}
const delayMs = delaysMs[attempt - 1] ?? delaysMs[delaysMs.length - 1];
console.error(
`[cloudflare-workers] wrangler deploy failed (attempt ${attempt}/${totalAttempts}): transient Cloudflare API error`,
);
console.error(`[cloudflare-workers] Retrying in ${delayMs / 1000}s...`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
result = await runner("npx", args, opts);
}
if (result.exitCode !== 0) {
console.error(`[cloudflare-workers] wrangler deploy failed (exit ${result.exitCode})`);
console.error(`[cloudflare-workers] stdout: ${result.stdout.slice(-500)}`);
console.error(`[cloudflare-workers] stderr: ${result.stderr.slice(-500)}`);
}
return result;
}
function extractDeploymentUrl(stdout: string): string | undefined {
const match = stdout.match(/https?:\/\/[^\s]+/);
return match ? match[0] : undefined;
}
export function createCloudflareWorkersAdapter(exec?: CommandRunner): DeploymentAdapter {
const runner = exec ?? createDefaultCommandRunner();
return {
name: "cloudflare-workers",
async propagate(
input: PropagateInput,
io: WorkspaceIO = createDefaultIO().io,
): Promise {
const now = new Date().toISOString();
const secretsEnv = input.secretsFilePath ? await sourceDotenv(input.secretsFilePath) : {};
const env: Record = { ...filterEnv(process.env), ...secretsEnv };
if (input.nodeModulesBinPath) {
env.PATH = `${input.nodeModulesBinPath}:${process.env.PATH ?? ""}`;
}
const serverDir = path.join(input.distPath, "server");
const wranglerConfigDir = (await io.exists(path.join(serverDir, "wrangler.json")))
? serverDir
: input.distPath;
const wranglerArgs = [
"--yes",
"wrangler",
"deploy",
"--config",
"wrangler.json",
"--name",
input.workerName,
];
if (input.secretsFilePath) {
wranglerArgs.push("--secrets-file", path.resolve(input.secretsFilePath));
}
const result = await runWranglerDeployWithRetry(runner, wranglerArgs, {
cwd: wranglerConfigDir,
env,
});
if (result.exitCode !== 0) {
return {
systemId: input.systemId,
releaseId: input.releaseId,
state: "failed",
deploymentUrl: input.url,
startedAt: now,
completedAt: new Date().toISOString(),
healthChecks: [],
};
}
const deployedUrl = input.url || extractDeploymentUrl(result.stdout) || "";
const workerVersionId = extractWorkerVersionId(result.stdout);
return {
systemId: input.systemId,
releaseId: input.releaseId,
state: "succeeded",
deploymentUrl: deployedUrl,
workerVersionId,
startedAt: now,
completedAt: new Date().toISOString(),
healthChecks: [],
};
},
async rollback(input: RollbackInput): Promise {
const now = new Date().toISOString();
const env: Record = { ...filterEnv(process.env) };
const result = await runWranglerRollback(input.wranglerConfigDir, env, input.versionId);
const state = result.exitCode === 0 ? "succeeded" : "failed";
return {
systemId: input.systemId,
channel: input.channel,
state,
workerName: input.workerName,
rolledBackToVersionId: input.versionId,
startedAt: now,
completedAt: new Date().toISOString(),
stdout: result.stdout,
stderr: result.stderr,
};
},
getLimits() {
return { maxTotalSize: 20 * 1024 * 1024 * 1024, maxFileSize: 25 * 1024 * 1024 };
},
purgeCapable() {
return true;
},
bootSmokeCapable() {
return true;
},
async health(
input: HealthInput,
): Promise<{ state: "healthy" | "unhealthy" | "unknown"; checks: HealthCheck[] }> {
const maxProbes = 10;
const maxAttempts = 5;
const snapshot = await readBehaviorSnapshot(input.workspaceRoot, input.releaseId);
const routes = snapshot?.routes ?? [];
const probeRoutes = selectProbeRoutes(routes, maxProbes);
if (probeRoutes.length === 0) {
return {
state: "unknown",
checks: [
{
name: "probe-selection",
url: input.deploymentUrl,
status: 0,
passed: false,
detail: "No routes available in behavior snapshot for probing",
},
],
};
}
const checks: HealthCheck[] = [];
let allPassed = true;
let anyNetworkFailure = false;
let anyContentMismatch = false;
for (const route of probeRoutes) {
const url = `${input.deploymentUrl}${route.path === "/" ? "" : route.path}`;
if (route.contentHash === null) {
const response = await fetchWithRetry(
url,
maxAttempts,
"manual",
input.authHeaders ?? {},
input.agentFetch,
);
if (response === null) {
anyNetworkFailure = true;
allPassed = false;
checks.push({
name: `probe:${route.path}`,
url,
status: 0,
passed: false,
detail: "Network failure after retries",
});
continue;
}
const location = response.headers.get("location") ?? "";
const result = verifyRedirectRoute(response.status, location, route);
if (!result.passed) allPassed = false;
if (response.status !== 307 && response.status !== 308) anyContentMismatch = true;
checks.push({
name: `probe:${route.path}`,
url,
status: response.status,
passed: result.passed,
detail: result.detail,
});
continue;
}
const response = await fetchWithRetry(url, maxAttempts, "follow", input.authHeaders ?? {}, input.agentFetch);
if (response === null) {
anyNetworkFailure = true;
allPassed = false;
checks.push({
name: `probe:${route.path}`,
url,
status: 0,
passed: false,
detail: "Network failure after retries",
expectedHash: route.contentHash,
});
continue;
}
if (!response.ok) {
allPassed = false;
checks.push({
name: `probe:${route.path}`,
url,
status: response.status,
passed: false,
detail: `HTTP ${response.status}`,
expectedHash: route.contentHash,
});
continue;
}
if (route.contentHash) {
const actualHash = hashHtml(response.body);
const hashMatch = actualHash === route.contentHash;
if (!hashMatch) {
allPassed = false;
anyContentMismatch = true;
}
checks.push({
name: `probe:${route.path}`,
url,
status: response.status,
passed: hashMatch,
detail: hashMatch ? "Content hash matches" : "Content hash mismatch",
expectedHash: route.contentHash,
actualHash,
});
} else {
checks.push({
name: `probe:${route.path}`,
url,
status: response.status,
passed: true,
detail: "HTTP OK, no content hash in snapshot",
});
}
}
const state: "healthy" | "unhealthy" | "unknown" = anyContentMismatch
? "unhealthy"
: anyNetworkFailure
? "unknown"
: allPassed
? "healthy"
: "unhealthy";
return { state, checks };
},
};
}