/* RFC-0806: Shared types and helpers for the service deployment pipeline (dev-deploy, promote, rollback). Extracted from service-deploy.ts to avoid duplication across the three new command handlers. Do not implement command-specific logic — handlers own their step sequence. Do not export types that are only used by a single command handler. RFC-0806: initial extraction of shared service deploy helpers. RFC-1188: Web Bot Auth outbound identity: wba-sign primitives, directory emit+sweep, AGS-WBA-01, leitstand signed fetches RFC-1325: provisional workers.dev as first-class site host Single host predicate module in werkstatt-shared; provisional registration derives dev/alt/main workers.dev channels from the account subdomain; first dev deploy auto-protects provisional hosts; provisional main responses carry noindex; dns-aid skips provisional sites; site.domain.adopt cuts over to a custom domain. */ import type { KernelCommandInput, KernelRuntimeContext } from "@warpgogol/werkstatt-engine/kernel"; import type { ServiceEntry } from "@warpgogol/werkstatt-engine/schemas"; import type { SmokeRunResult } from "@warpgogol/werkstatt-engine/testing/smoke"; import type { IntegrationRunResult } from "@warpgogol/werkstatt-engine/testing/integration"; import { readServicesRegistry, writeServicesRegistry, findServiceEntry, } from "../sternsystem/registry-io.ts"; import { acquireLock, releaseLock, generateOperationId } from "../werkstatt/index.ts"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; export interface PreDeployGateResult { command: string; passed: boolean; summary: string; } export interface ServiceDevDeployData { command: "leitstand.service.dev-deploy"; serviceId: string; workerName: string; deployState: "succeeded" | "failed"; workersDevUrl: string; healthState: "healthy" | "unhealthy" | "unknown"; smokeResult?: SmokeRunResult; integrationResult?: IntegrationRunResult; preDeployGates: PreDeployGateResult[]; startedAt: string; completedAt: string; operationId: string; } export interface ServicePromoteData { command: "leitstand.service.promote"; serviceId: string; workerName: string; deployState: "succeeded" | "failed"; workersDevUrl: string; healthState: "healthy" | "unhealthy" | "unknown"; smokeResult?: SmokeRunResult; preDeployGates: PreDeployGateResult[]; testEvidenceVerified: boolean; startedAt: string; completedAt: string; operationId: string; } export interface ServiceRollbackData { command: "leitstand.service.rollback"; serviceId: string; workerName: string; rollbackState: "succeeded" | "failed"; startedAt: string; completedAt: string; operationId: string; } export function flagString(input: KernelCommandInput, key: string): string | undefined { const v = input.flags[key]; return typeof v === "string" ? v : undefined; } export function flagBoolean(input: KernelCommandInput, key: string): boolean { const v = input.flags[key]; return v === true || v === "true"; } export async function runWranglerDeploy( serviceDir: string, configName: string, env: Record, io: WorkspaceIO = createDefaultIO().io, ): Promise<{ exitCode: number; stdout: string; stderr: string }> { try { const result = await io.exec("npx", ["--yes", "wrangler", "deploy", "--config", configName], { cwd: serviceDir, env, }); return { exitCode: result.exitCode ?? 1, stdout: result.stdout, stderr: result.stderr }; } catch { return { exitCode: 1, stdout: "", stderr: "Failed to spawn wrangler" }; } } export async function runWranglerRollback( serviceDir: string, env: Record, versionId?: string, io: WorkspaceIO = createDefaultIO().io, ): Promise<{ exitCode: number; stdout: string; stderr: string }> { const args = ["--yes", "wrangler", "rollback"]; if (versionId) { args.push(versionId); } try { const result = await io.exec("npx", args, { cwd: serviceDir, env }); return { exitCode: result.exitCode ?? 1, stdout: result.stdout, stderr: result.stderr }; } catch { return { exitCode: 1, stdout: "", stderr: "Failed to spawn wrangler" }; } } export function extractWorkersDevUrl(stdout: string): string | undefined { const match = stdout.match(/https?:\/\/[^\s]+\.workers\.dev[^\s]*/); // site-host: wrangler stdout URL extraction, not site-host classification return match ? match[0] : undefined; } export function extractWorkerVersionId(stdout: string): string | undefined { const match = stdout.match(/Current Version ID:\s*([a-f0-9-]+)/i); return match ? match[1] : undefined; } export async function runHealthCheck( url: string, healthCheckPath: string | undefined, fetchImpl: typeof fetch = fetch, ): Promise<"healthy" | "unhealthy"> { const checkUrl = url + (healthCheckPath ?? "/health"); try { const response = await fetchImpl(checkUrl, { redirect: "follow" }); if (response.status < 500) { return "healthy"; } return "unhealthy"; } catch { return "unhealthy"; } } export async function parseEnvFile( envPath: string, io: WorkspaceIO = createDefaultIO().io, ): Promise> { const env: Record = {}; if (!(await io.exists(envPath))) return env; const content = await io.readFile(envPath); for (const line of content.split("\n")) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith("#")) continue; const eqIdx = trimmed.indexOf("="); if (eqIdx === -1) continue; const key = trimmed.slice(0, eqIdx).trim(); const value = trimmed .slice(eqIdx + 1) .trim() .replace(/^["']|["']$/g, ""); if (value !== "") env[key] = value; } return env; } export async function runBuildCheck( serviceDir: string, logger: { info: (msg: string) => void }, io: WorkspaceIO = createDefaultIO().io, ): Promise { logger.info("[pre-deploy] running build:check (tsc --noEmit)…"); const result = await io.exec("pnpm", ["run", "build:check"], { cwd: serviceDir }).then( (r: { exitCode: number | null; stdout: string; stderr: string }) => ({ exitCode: r.exitCode ?? 1, stdout: r.stdout, stderr: r.stderr, }), () => ({ exitCode: 1, stdout: "", stderr: "Failed to spawn build:check" }), ); return { command: "build:check", passed: result.exitCode === 0, summary: result.exitCode === 0 ? "tsc --noEmit: 0 errors" : result.stderr.slice(-200) || result.stdout.slice(-200) || "failed", }; } export interface PreDeployGateConfig { commandName: string; argv: string[]; } export async function runPreDeployGates( workspaceRoot: string, gates: PreDeployGateConfig[], logger: { info: (msg: string) => void }, ): Promise { const { executeKernelCommand } = await import("@warpgogol/werkstatt-engine/kernel"); const results: PreDeployGateResult[] = []; for (const gate of gates) { logger.info(`[pre-deploy] running ${gate.commandName}…`); const result = (await executeKernelCommand({ workspaceRoot, commandName: gate.commandName, argv: gate.argv, })) as { exitCode: number; summary?: string }; const passed = result.exitCode === 0; results.push({ command: gate.commandName, passed, summary: result.summary ?? (passed ? "pass" : "failed"), }); if (!passed) break; } return results; } export async function acquireServiceLock( workspaceRoot: string, serviceId: string, operationId: string, commandName: string, ): Promise { await acquireLock(workspaceRoot, `service:${serviceId}`, operationId, commandName, "agent"); } export async function releaseServiceLock(workspaceRoot: string, serviceId: string): Promise { await releaseLock(workspaceRoot, `service:${serviceId}`); } export async function recordDevDeployState( workspaceRoot: string, serviceId: string, state: { at: string; state: "succeeded" | "failed"; operationId: string }, ): Promise { const registry = await readServicesRegistry(workspaceRoot); const entry = registry.services.find((s: ServiceEntry) => s.id === serviceId); if (!entry) return; (entry as Record).lastDevDeployed = { at: state.at, state: state.state, operationId: state.operationId, }; await writeServicesRegistry(workspaceRoot, registry); } export async function recordProdDeployState( workspaceRoot: string, serviceId: string, state: { at: string; state: "succeeded" | "failed" | "rolled-back"; operationId: string }, ): Promise { const registry = await readServicesRegistry(workspaceRoot); const entry = registry.services.find((s: ServiceEntry) => s.id === serviceId); if (!entry) return; entry.lastDeployed = { at: state.at, state: state.state === "rolled-back" ? "failed" : state.state, operationId: state.operationId, }; await writeServicesRegistry(workspaceRoot, registry); } export async function updateWorkersDevUrl( workspaceRoot: string, serviceId: string, deployedUrl: string, ): Promise { if (!deployedUrl) return; const registry = await readServicesRegistry(workspaceRoot); const entry = findServiceEntry(registry, serviceId); if (entry && entry.workersDevUrl !== deployedUrl) { entry.workersDevUrl = deployedUrl; await writeServicesRegistry(workspaceRoot, registry); } } export { generateOperationId }; export type { KernelRuntimeContext }; export async function runSmokeCheck( workspaceRoot: string, serviceId: string, deployedUrl: string, logger: { info: (msg: string) => void; warn: (msg: string) => void }, ): Promise { const { executeKernelCommand } = await import("@warpgogol/werkstatt-engine/kernel"); logger.info(`[smoke] running service.smoke.run for ${serviceId} against ${deployedUrl}…`); try { const result = (await executeKernelCommand({ workspaceRoot, commandName: "service.smoke.run", argv: [`--service=${serviceId}`, `--url=${deployedUrl}`], })) as { exitCode?: number; data?: SmokeRunResult }; if (result.data) { return result.data; } logger.warn( `[smoke] service.smoke.run returned no data (exitCode=${result.exitCode ?? "unknown"})`, ); return undefined; } catch (err) { logger.warn( `[smoke] service.smoke.run failed: ${err instanceof Error ? err.message : String(err)}`, ); return undefined; } } export async function runIntegrationTests( workspaceRoot: string, serviceId: string, deployedUrl: string, logger: { info: (msg: string) => void; warn: (msg: string) => void }, ): Promise { const { executeKernelCommand } = await import("@warpgogol/werkstatt-engine/kernel"); logger.info( `[integration] running service.integration.run for ${serviceId} against ${deployedUrl}…`, ); try { const result = (await executeKernelCommand({ workspaceRoot, commandName: "service.integration.run", argv: [`--service=${serviceId}`, `--url=${deployedUrl}`], })) as { exitCode?: number; data?: IntegrationRunResult }; if (result.data) { return result.data; } logger.warn( `[integration] service.integration.run returned no data (exitCode=${result.exitCode ?? "unknown"})`, ); return undefined; } catch (err) { logger.warn( `[integration] service.integration.run failed: ${err instanceof Error ? err.message : String(err)}`, ); return undefined; } }