/* RFC-0961: release.boot-smoke — boot built worker in miniflare/workerd and execute representative requests to catch runtime crashes before deploy. Does not replace post-deploy health checks (RFC-0930 leitstand.verify) — this is pre-deploy, local, offline. Does not simulate service worker bindings (Durable Objects, Queues, AI, Hyperdrive) — site workers only. Does not provide binding semantics fidelity — in-memory equivalents only; real semantics verified post-deploy. RFC-0979: add --wrangler-config and --languages flags to runBootSmokeCommand, implement wrangler config fallback resolution, write boot-smoke.json to dist directory. ADR-0067: extract buildMiniflareOptions from runBootSmoke for unit testability of Miniflare options construction without a real runtime. ADR-0068: remove unused resolvedWorkerPath field from MiniflareOptionsResult interface and buildMiniflareOptions return value — the field was never consumed by the caller. RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt RFC-0961, RFC-0978 HARDCODED_VALUES_AUDIT (RFC-0980): - SUPPORTED_BINDING_TYPES set (kv_namespaces, r2_buckets, d1_databases, vectorize, vars, secrets, assets) (a) safe default — Cloudflare Workers runtime binding types, not site-specific - compatibilityDate fallback "2024-01-01" (a) safe default — wrangler config usually provides compatibility_date; fallback only used when missing - workerMain fallback "./dist/_worker.js/index.js" (a) safe default — Astro Cloudflare adapter output (server/entry.mjs) is the primary path; fallback for non-Astro workers - Fallback languages ["de", "uk"] in detectBootSmokeLanguages (b) workshop-specific — ensures boot-smoke runs representative requests when dist/client/ structure is unexpected - timeoutMs default 60_000 (a) safe default — 60s timeout for boot-smoke requests; configurable via input.timeoutMs - HTTP status codes (200, 404) (a) safe default — HTTP protocol constants - "/favicon.ico" probe path (a) safe default — conventional favicon path for static asset smoke test - "/__boot-smoke-404-probe__" path (a) safe default — synthetic probe path for intentional 404 test - "http://localhost" URL (a) safe default — miniflare internal URL for request dispatch - "ASSETS" binding name fallback (a) safe default — Cloudflare convention for static assets binding - "__secret_placeholder_${key}__" format (a) safe default — synthetic placeholder for secret bindings during smoke test - "entry.mjs" filename (a) safe default — Astro Cloudflare adapter output filename - "server" directory name (a) safe default — Astro Cloudflare adapter output directory - "wrangler.json" / "wrangler.jsonc" filenames (a) safe default — Cloudflare wrangler config filenames - Excluded wrangler config keys (name, main, compatibility_date, compatibility_flags, observability, conditions) (a) safe default — wrangler config metadata keys, not binding declarations */ // CI GUARD: boot-smoke tests run in .github/workflows/ci.yml // Any change to Miniflare options, language detection, or module enumeration // MUST be accompanied by a test update. See ADR-0067 for testing strategy. import path from "node:path"; import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; // ─── Public contracts ──────────────────────────────────────────────── export interface BootSmokeRequestSpec { path: string; expectStatus: number[]; kind: "page" | "asset" | "api" | "not-found" | "negotiation"; } export interface BootSmokeRequestResult { path: string; status: number | null; ok: boolean; error: string | null; } export interface BootSmokeResult { booted: boolean; bootError: string | null; requests: BootSmokeRequestResult[]; egressViolations: string[]; missingBindings: string[]; } // ─── Local types for dist route discovery ──────────────────────────── interface DistRouteEntry { path?: string; pattern?: string; kind?: string; } // ─── Language detection (RFC-0978) ─────────────────────────────────── export async function detectBootSmokeLanguages( distDir: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const clientDistDir = path.join(distDir, "client"); const languages: string[] = []; if (await io.exists(clientDistDir)) { for (const entry of await io.readdir(clientDistDir)) { if ( entry.isDirectory && !entry.name.startsWith("_") && entry.name !== "api" && /^[a-z]{2}$/.test(entry.name) ) { languages.push(entry.name); } } } if (languages.length === 0) { languages.push("de", "uk"); } return languages; } // ─── Route planner ─────────────────────────────────────────────────── export async function planBootSmokeRequests(input: { distDir: string; languages: string[]; io?: WorkspaceIO; }): Promise { const io = input.io ?? createDefaultIO().io; const requests: BootSmokeRequestSpec[] = []; // 1. Root route requests.push({ path: "/", expectStatus: [200], kind: "page" }); // 2. One deep content route per language for (const lang of input.languages) { requests.push({ path: `/${lang}/`, expectStatus: [200, 307, 308], kind: "page", }); } // 3. Route-template kinds from surface.generated.json const surfacePath = path.join(input.distDir, "surface.generated.json"); if (await io.exists(surfacePath)) { try { const raw = await io.readFile(surfacePath); const routes = JSON.parse(raw) as DistRouteEntry[]; const seenKinds = new Set(); for (const route of routes) { const routePath = route.path ?? route.pattern; const kind = route.kind ?? "page"; if (!routePath) continue; if (kind === "page" && !seenKinds.has(kind)) { seenKinds.add(kind); if (routePath !== "/" && !requests.some((r) => r.path === routePath)) { requests.push({ path: routePath, expectStatus: [200], kind: "page" }); } } } } catch { // surface.generated.json not parseable — root + language routes are sufficient } } // 4. One API route when dist/client/api/ exists const apiDir = path.join(input.distDir, "client", "api"); if (await io.exists(apiDir)) { requests.push({ path: "/api/", expectStatus: [200, 404], kind: "api" }); } // 5. One static asset const clientDir = path.join(input.distDir, "client"); if (await io.exists(clientDir)) { requests.push({ path: "/favicon.ico", expectStatus: [200, 404], kind: "asset", }); } // 6. Intentional 404 requests.push({ path: "/__boot-smoke-404-probe__", expectStatus: [404], kind: "not-found", }); return requests; } // ─── Binding simulator ─────────────────────────────────────────────── const SUPPORTED_BINDING_TYPES = new Set([ "kv_namespaces", "r2_buckets", "d1_databases", "vectorize", "vars", "secrets", "assets", ]); interface SimulatedBindings { bindings: Record; missingBindings: string[]; } export function simulateBindings(wranglerConfig: Record): SimulatedBindings { const bindings: Record = {}; const missingBindings: string[] = []; // KV namespaces → in-memory Map const kvNamespaces = (wranglerConfig["kv_namespaces"] ?? []) as Array<{ binding: string; }>; for (const ns of kvNamespaces) { bindings[ns.binding] = new Map(); } // R2 buckets → in-memory Map const r2Buckets = (wranglerConfig["r2_buckets"] ?? []) as Array<{ binding: string; }>; for (const bucket of r2Buckets) { bindings[bucket.binding] = new Map(); } // D1 databases → in-memory SQLite (miniflare handles natively, we declare it) const d1Databases = (wranglerConfig["d1_databases"] ?? []) as Array<{ binding: string; }>; for (const db of d1Databases) { bindings[db.binding] = { __type: "d1" }; } // Vectorize indexes → JSON-compatible placeholder (miniflare bindings only // accept JSON values; the worker checks for binding existence at runtime) const vectorize = (wranglerConfig["vectorize"] ?? []) as Array<{ binding: string; }>; for (const idx of vectorize) { bindings[idx.binding] = { __type: "vectorize" }; } // Vars / env → placeholder strings const vars = (wranglerConfig["vars"] ?? {}) as Record; for (const [key, value] of Object.entries(vars)) { bindings[key] = value; } // Secrets → placeholder strings const secrets = (wranglerConfig["secrets"] ?? {}) as Record; for (const [key, value] of Object.entries(secrets)) { bindings[key] = value ?? `__secret_placeholder_${key}__`; } // ASSETS → served from dist/client if (wranglerConfig["assets"] && typeof wranglerConfig["assets"] === "object") { bindings["ASSETS"] = { __type: "assets" }; } // Detect unsupported binding types for (const key of Object.keys(wranglerConfig)) { if ( !SUPPORTED_BINDING_TYPES.has(key) && typeof wranglerConfig[key] === "object" && wranglerConfig[key] !== null && ![ "name", "main", "compatibility_date", "compatibility_flags", "observability", "conditions", ].includes(key) ) { // Unknown binding-type key that looks like a binding declaration const value = wranglerConfig[key]; if ( Array.isArray(value) && value.length > 0 && typeof value[0] === "object" && "binding" in value[0] ) { missingBindings.push(key); } } } return { bindings, missingBindings }; } // ─── Egress interceptor ────────────────────────────────────────────── function createEgressInterceptor(): { fetch: (input: string | URL | Request, init?: RequestInit) => Promise; violations: string[]; } { const violations: string[] = []; const interceptedFetch = async (input: string | URL | Request): Promise => { const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; // Allow localhost (miniflare internal) if (url.startsWith("http://localhost") || url.startsWith("http://127.0.0.1")) { return new Response("ok", { status: 200 }); } // Block all external egress violations.push(url); throw new Error(`[boot-smoke] egress blocked: ${url}`); }; return { fetch: interceptedFetch, violations }; } // ─── Miniflare options builder (ADR-0067) ──────────────────────────── export interface MiniflareOptionsInput { distDir: string; wranglerConfig: Record; bindings: Record; egressFetch: typeof fetch; io?: WorkspaceIO; } export interface MiniflareOptionsResult { options: Record; } export async function buildMiniflareOptions( input: MiniflareOptionsInput, ): Promise { const io = input.io ?? createDefaultIO().io; const workerMain = (input.wranglerConfig["main"] as string) ?? "./dist/_worker.js/index.js"; const workerPath = path.resolve(input.distDir, workerMain.replace(/^\.\/dist\//, "")); const astroEntry = path.resolve(input.distDir, "server", "entry.mjs"); const resolvedWorkerPath = (await io.exists(workerPath)) ? workerPath : (await io.exists(astroEntry)) ? astroEntry : workerPath; if (!(await io.exists(resolvedWorkerPath))) { return null; } void workerMain; const entryDir = path.dirname(resolvedWorkerPath); const moduleEntries: Array<{ type: string; path: string }> = [ { type: "ESModule", path: resolvedWorkerPath }, ]; const walkDir = async (dir: string): Promise => { const entries = await io.readdir(dir); for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory) { await walkDir(fullPath); } else if (entry.name.endsWith(".mjs") && fullPath !== resolvedWorkerPath) { moduleEntries.push({ type: "ESModule", path: fullPath }); } } }; await walkDir(entryDir); const mfOptions: Record = { modules: moduleEntries, modulesRoot: entryDir, compatibilityDate: (input.wranglerConfig["compatibility_date"] as string) ?? "2024-01-01", compatibilityFlags: (input.wranglerConfig["compatibility_flags"] as string[]) ?? [], bindings: input.bindings, fetch: input.egressFetch, }; const assetsConfig = input.wranglerConfig["assets"] as { directory?: string; binding?: string; run_worker_first?: boolean } | undefined; if (assetsConfig?.directory) { const assetsDir = path.resolve(input.distDir, assetsConfig.directory); if (await io.exists(assetsDir)) { mfOptions["assets"] = { directory: assetsDir, binding: assetsConfig.binding ?? "ASSETS", ...(assetsConfig.run_worker_first !== undefined ? { invoke_user_worker_ahead_of_assets: assetsConfig.run_worker_first } : {}), }; } } return { options: mfOptions }; } // ─── Boot-smoke runner ─────────────────────────────────────────────── export async function runBootSmoke(input: { distDir: string; wranglerConfigPath: string; requests: BootSmokeRequestSpec[]; timeoutMs?: number; io?: WorkspaceIO; }): Promise { const io = input.io ?? createDefaultIO().io; const timeoutMs = input.timeoutMs ?? 60_000; const egressInterceptor = createEgressInterceptor(); // Parse wrangler config let wranglerConfig: Record; try { const raw = await io.readFile(input.wranglerConfigPath); // wrangler.jsonc — strip JSONC comments const stripped = raw.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); wranglerConfig = JSON.parse(stripped) as Record; } catch { return { booted: false, bootError: `Failed to parse wrangler config at ${input.wranglerConfigPath}`, requests: [], egressViolations: [], missingBindings: [], }; } // Simulate bindings const { bindings, missingBindings } = simulateBindings(wranglerConfig); // If there are missing bindings, fail-closed if (missingBindings.length > 0) { return { booted: false, bootError: `Unsupported binding types: ${missingBindings.join(", ")}. Extend the simulator or remove the binding from wrangler.jsonc.`, requests: [], egressViolations: [], missingBindings, }; } // Build Miniflare options (ADR-0067: extracted for unit testability) const mfOptionsResult = await buildMiniflareOptions({ distDir: input.distDir, wranglerConfig, bindings, egressFetch: egressInterceptor.fetch as unknown as typeof fetch, }); if (!mfOptionsResult) { return { booted: false, bootError: `Worker entry point not found in ${input.distDir}`, requests: [], egressViolations: egressInterceptor.violations, missingBindings: [], }; } const { options: mfOptions } = mfOptionsResult; // Boot miniflare let mf: InstanceType | undefined; let booted = false; let bootError: string | null = null; try { const { Miniflare } = await import("miniflare"); mf = new Miniflare(mfOptions as ConstructorParameters[0]); // Test boot by dispatching a single request try { const response = await mf.dispatchFetch("http://localhost/"); booted = true; // Consume the response to free resources await response.text(); } catch (err) { booted = false; bootError = err instanceof Error ? err.message : String(err); } // If boot failed, return early if (!booted || !mf) { return { booted: false, bootError, requests: [], egressViolations: egressInterceptor.violations, missingBindings: [], }; } // Execute representative requests with timeout const requestResults: BootSmokeRequestResult[] = []; let timer: ReturnType | undefined; const timeoutPromise = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error("boot-smoke timeout")), timeoutMs); }); try { for (const spec of input.requests) { try { const result = await Promise.race([ mf.dispatchFetch(`http://localhost${spec.path}`), timeoutPromise, ]); const status = result.status; const ok = spec.expectStatus.includes(status); requestResults.push({ path: spec.path, status, ok, error: ok ? null : `Expected ${spec.expectStatus.join("|")}, got ${status}`, }); // Consume body await result.text(); } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); requestResults.push({ path: spec.path, status: null, ok: false, error: errMsg, }); } } } finally { if (timer) clearTimeout(timer); } return { booted: true, bootError: null, requests: requestResults, egressViolations: egressInterceptor.violations, missingBindings: [], }; } catch (err) { return { booted: false, bootError: err instanceof Error ? err.message : String(err), requests: [], egressViolations: egressInterceptor.violations, missingBindings, }; } finally { if (mf) { try { await mf.dispose(); } catch { // Best-effort cleanup } } } } // ─── Command handler ───────────────────────────────────────────────── export interface WranglerResolution { wranglerConfigPath: string; bootSmokeDistDir: string; } export async function resolveWranglerConfig( distDir: string, wranglerConfigFlag: string | undefined, workspaceRoot: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { if (wranglerConfigFlag) { return { wranglerConfigPath: path.resolve(wranglerConfigFlag), bootSmokeDistDir: distDir, }; } const astroWranglerPath = path.join(distDir, "server", "wrangler.json"); if (await io.exists(astroWranglerPath)) { return { wranglerConfigPath: astroWranglerPath, bootSmokeDistDir: path.join(distDir, "server"), }; } const fallbackPath = path.join(distDir, "..", "wrangler.jsonc"); if (await io.exists(fallbackPath)) { return { wranglerConfigPath: fallbackPath, bootSmokeDistDir: distDir, }; } const workpiecePath = path.join(workspaceRoot, "missions", "workpiece", "wrangler.jsonc"); if (await io.exists(workpiecePath)) { return { wranglerConfigPath: workpiecePath, bootSmokeDistDir: distDir, }; } return null; } export async function resolveLanguages( distDir: string, languagesFlag: string | undefined, ): Promise { if (languagesFlag) { return languagesFlag .split(",") .map((l) => l.trim()) .filter(Boolean); } return detectBootSmokeLanguages(distDir); } export interface BootSmokeCommandData { booted: boolean; requests: number; failed: number; egressViolations: string[]; missingBindings: string[]; bootError: string | null; } function flagString(input: KernelCommandInput, key: string): string | undefined { const v = input.flags[key]; return typeof v === "string" ? v : undefined; } export async function runBootSmokeCommand( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const siteId = flagString(input, "site"); const distFlag = flagString(input, "dist"); const wranglerConfigFlag = flagString(input, "wrangler-config"); const languagesFlag = flagString(input, "languages"); const diagnose = Boolean(input.flags["diagnose"]); if (!siteId) { return { exitCode: 1, summary: "[release.boot-smoke] --site is required", nextSteps: [{ action: "[release.boot-smoke] --site is required", kind: "required" as const }], data: { booted: false, requests: 0, failed: 0, egressViolations: [], missingBindings: [], bootError: "--site is required", }, }; } // Resolve dist directory let distDir: string; if (distFlag) { distDir = path.resolve(distFlag); } else { // Default: workpiece dist/ (open mission) or releases//dist/ const workpieceDist = path.join(workspaceRoot, "missions", "workpiece", "dist"); if (await context.io.exists(workpieceDist)) { distDir = workpieceDist; } else { distDir = path.join(workspaceRoot, "releases", siteId, "dist"); } } if (!(await context.io.exists(distDir))) { return { exitCode: 1, summary: `[release.boot-smoke] dist directory not found: ${distDir}`, nextSteps: [ { action: `[release.boot-smoke] dist directory not found: ${distDir}`, kind: "required" as const, }, ], data: { booted: false, requests: 0, failed: 0, egressViolations: [], missingBindings: [], bootError: `dist directory not found: ${distDir}`, }, }; } // Resolve wrangler config (RFC-0979): --wrangler-config flag → Astro-generated → fallback const wranglerResolution = await resolveWranglerConfig( distDir, wranglerConfigFlag, workspaceRoot, context.io, ); if (!wranglerResolution) { return { exitCode: 1, summary: `[release.boot-smoke] wrangler config not found`, nextSteps: [ { action: `[release.boot-smoke] wrangler config not found`, kind: "required" as const }, ], data: { booted: false, requests: 0, failed: 0, egressViolations: [], missingBindings: [], bootError: `wrangler config not found — tried ${path.join(distDir, "server", "wrangler.json")}, ${path.join(distDir, "..", "wrangler.jsonc")}, ${path.join(workspaceRoot, "missions", "workpiece", "wrangler.jsonc")}`, }, }; } const { wranglerConfigPath, bootSmokeDistDir } = wranglerResolution; // Plan requests — use --languages flag or auto-detect from dist/client/ (RFC-0978) const languages = await resolveLanguages(distDir, languagesFlag); const requests = await planBootSmokeRequests({ distDir, languages, io: context.io }); if (diagnose) { logger.info(`[release.boot-smoke] diagnose mode — planned ${requests.length} requests:`); for (const req of requests) { logger.info(` ${req.kind}: ${req.path} (expect ${req.expectStatus.join("|")})`); } } // Run boot-smoke const result = await runBootSmoke({ distDir: bootSmokeDistDir, wranglerConfigPath, requests, io: context.io, }); // Write boot-smoke.json to dist directory (RFC-0979) try { await context.io.writeFile( path.join(distDir, "boot-smoke.json"), JSON.stringify(result, null, 2) + "\n", ); } catch { // Best-effort — don't fail the command if evidence write fails } const failed = result.requests.filter((r) => !r.ok).length; const hasFailures = !result.booted || failed > 0 || result.egressViolations.length > 0 || result.missingBindings.length > 0; const data: BootSmokeCommandData = { booted: result.booted, requests: result.requests.length, failed, egressViolations: result.egressViolations, missingBindings: result.missingBindings, bootError: result.bootError, }; if (hasFailures) { const parts: string[] = []; if (!result.booted) parts.push("boot failed"); if (failed > 0) parts.push(`${failed}/${result.requests.length} requests failed`); if (result.egressViolations.length > 0) parts.push(`${result.egressViolations.length} egress violations`); if (result.missingBindings.length > 0) parts.push(`${result.missingBindings.length} missing bindings`); return { data, exitCode: 1, summary: `[release.boot-smoke] ${parts.join(", ")}`, nextSteps: [ { action: result.bootError ? `Fix boot error: ${result.bootError.slice(0, 200)}` : "Review failing requests and fix the worker code", kind: "required" as const, }, ], }; } return { data, exitCode: 0, summary: `[release.boot-smoke] worker booted, ${result.requests.length}/${result.requests.length} representative requests ok, egress clean`, }; }