/* RFC-0806: leitstand.service.promote command handler — promotes a service from dev to production (wrangler.jsonc) with pre-deploy gates, subdomain validation, lock, wrangler deploy, health check, and prod state recording. Do not deploy to dev — use leitstand.service.dev-deploy for that. Do not implement build verification — services have no build step. RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared RFC-1188: Web Bot Auth outbound identity: wba-sign primitives, directory emit+sweep, AGS-WBA-01, leitstand signed fetches RFC-1188: fo-review fix — signing failures surface through logger.warn via onSigningFailure RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt RFC-1254: drop unused io-migration leftovers RFC-0806, RFC-0829 */ import path from "node:path"; import type { KernelCommandInput, KernelCommandResult } from "@warpgogol/werkstatt-engine/kernel"; import { readServicesRegistry, findServiceEntry } from "../sternsystem/registry-io.ts"; import { resolveOutboundAgentClient } from "./wba-signer.ts"; import { flagString, flagBoolean, generateOperationId, runWranglerDeploy, extractWorkersDevUrl, runHealthCheck, parseEnvFile, runPreDeployGates, runBuildCheck, runSmokeCheck, acquireServiceLock, releaseServiceLock, recordProdDeployState, updateWorkersDevUrl, type ServicePromoteData, type KernelRuntimeContext, } from "./service-deploy-helpers.ts"; export async function runLeitstandServicePromote( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot, logger } = context; const serviceId = flagString(input, "service"); const skipHealthCheck = flagBoolean(input, "skip-health-check"); if (!serviceId) { throw new Error("[leitstand.service.promote] --service is required"); } const operationId = generateOperationId(); const startedAt = new Date().toISOString(); const registry = await readServicesRegistry(workspaceRoot); const serviceEntry = findServiceEntry(registry, serviceId); if (!serviceEntry) { throw new Error( `[leitstand.service.promote] service '${serviceId}' not found in services/registry.yaml`, ); } const serviceDir = path.join(workspaceRoot, "services", serviceId); if (!(await context.io.exists(serviceDir))) { throw new Error(`[leitstand.service.promote] services/${serviceId}/ does not exist`); } const wranglerPath = path.join(serviceDir, "wrangler.jsonc"); if (!(await context.io.exists(wranglerPath))) { throw new Error( `[leitstand.service.promote] services/${serviceId}/wrangler.jsonc not found — not a Cloudflare Worker service`, ); } await acquireServiceLock(workspaceRoot, serviceId, operationId, "leitstand.service.promote"); try { // 1. Pre-deploy gates: service.naming.validate, service.registry.validate, services.check.run, build:check, deploy.preflight const gates = [ { commandName: "service.naming.validate", argv: [] }, { commandName: "service.registry.validate", argv: [] }, { commandName: "services.check.run", argv: [] }, { commandName: "deploy.preflight", argv: ["--service", serviceId] }, ]; const gateResults = await runPreDeployGates(workspaceRoot, gates, logger); // 1b. build:check gate (not a kernel command — runs pnpm script directly) const buildCheckResult = await runBuildCheck(serviceDir, logger); gateResults.push(buildCheckResult); const failedGate = gateResults.find((g) => !g.passed); if (failedGate) { const failedData: ServicePromoteData = { command: "leitstand.service.promote", serviceId, workerName: serviceEntry.workerName, deployState: "failed", workersDevUrl: serviceEntry.workersDevUrl ?? "", healthState: "unknown", preDeployGates: gateResults, testEvidenceVerified: false, startedAt, completedAt: new Date().toISOString(), operationId, }; return { data: failedData, exitCode: 1, summary: `[leitstand.service.promote] ${serviceId}: pre-deploy gate '${failedGate.command}' failed — ${failedGate.summary}`, nextSteps: [ { action: `Fix the failing gate '${failedGate.command}' for ${serviceId}, then re-run leitstand.service.promote`, kind: "required", }, ], }; } // 2. Subdomain validation (RFC-0752) — best-effort, skip if command not available if (serviceEntry.subdomains && serviceEntry.subdomains.length > 0) { logger.info(`[leitstand.service.promote] running subdomain.validate for ${serviceId}…`); try { const { executeKernelCommand } = await import("@warpgogol/werkstatt-engine/kernel"); const subdomainResult = (await executeKernelCommand({ workspaceRoot, commandName: "subdomain.validate", argv: ["--service", serviceId], })) as { exitCode: number; summary?: string }; if (subdomainResult.exitCode !== 0) { logger.info( `[leitstand.service.promote] subdomain.validate reported missing subdomains — attempting subdomain.register…`, ); const registerResult = (await executeKernelCommand({ workspaceRoot, commandName: "subdomain.register", argv: ["--service", serviceId], })) as { exitCode: number; summary?: string }; if (registerResult.exitCode !== 0) { const failedData: ServicePromoteData = { command: "leitstand.service.promote", serviceId, workerName: serviceEntry.workerName, deployState: "failed", workersDevUrl: serviceEntry.workersDevUrl ?? "", healthState: "unknown", preDeployGates: gateResults, testEvidenceVerified: false, startedAt, completedAt: new Date().toISOString(), operationId, }; return { data: failedData, exitCode: 1, summary: `[leitstand.service.promote] ${serviceId}: subdomain.register failed — ${registerResult.summary ?? "unknown error"}`, nextSteps: [ { action: `Fix the subdomain registration for ${serviceId}, then re-run leitstand.service.promote`, kind: "required", }, ], }; } } } catch { logger.warn( `[leitstand.service.promote] subdomain.validate not available — skipping subdomain validation`, ); } } // RFC-0829: Test evidence gate — verify L1 (unit), L2 (integration), L5 (smoke) evidence { let commitSha = ""; try { const headResult = await context.io.exec("git", ["rev-parse", "HEAD"], { cwd: workspaceRoot, }); if (headResult.exitCode !== 0) throw new Error(headResult.stderr.trim()); commitSha = headResult.stdout.trim(); } catch (err) { const errMsg = err instanceof Error ? err.message : String(err); const { GRACE_PERIOD_END } = await import("./test-evidence-gate.ts"); if (new Date().toISOString() < GRACE_PERIOD_END) { logger.warn( `[leitstand.service.promote] could not resolve git HEAD — skipping test evidence gate: ${errMsg}`, ); } else { throw new Error( `[leitstand.service.promote] could not resolve git HEAD — test evidence gate cannot be verified: ${errMsg}`, ); } } if (commitSha) { const { runTestEvidenceGate } = await import("./test-evidence-gate.ts"); await runTestEvidenceGate({ workspaceRoot, commandName: "leitstand.service.promote", target: serviceId, levels: ["L1", "L2", "L5"], commitSha, service: serviceId, logger, }); } } // 3. Read .env for secrets const envPath = path.join(serviceDir, ".env"); const deployEnv = await parseEnvFile(envPath); // 4. Wrangler deploy with production config logger.info( `[leitstand.service.promote] deploying ${serviceEntry.workerName} to production via wrangler…`, ); const wranglerResult = await runWranglerDeploy(serviceDir, "wrangler.jsonc", deployEnv); if (wranglerResult.exitCode !== 0) { await recordProdDeployState(workspaceRoot, serviceId, { at: new Date().toISOString(), state: "failed", operationId, }); const failedData: ServicePromoteData = { command: "leitstand.service.promote", serviceId, workerName: serviceEntry.workerName, deployState: "failed", workersDevUrl: extractWorkersDevUrl(wranglerResult.stdout) ?? serviceEntry.workersDevUrl ?? "", healthState: "unknown", preDeployGates: gateResults, testEvidenceVerified: true, startedAt, completedAt: new Date().toISOString(), operationId, }; return { data: failedData, exitCode: 1, summary: `[leitstand.service.promote] ${serviceId}: wrangler deploy failed — ${wranglerResult.stderr.slice(-200)}`, nextSteps: [ { action: `Check the wrangler deploy error for ${serviceId}, fix the issue, then re-run leitstand.service.promote`, kind: "required", }, ], }; } // 5. Resolve workersDevUrl const deployedUrl = extractWorkersDevUrl(wranglerResult.stdout) ?? serviceEntry.workersDevUrl ?? ""; // 6. Health check let healthState: "healthy" | "unhealthy" | "unknown" = "unknown"; if (!skipHealthCheck && serviceEntry.publicEndpoints && deployedUrl) { logger.info(`[leitstand.service.promote] running health check on ${deployedUrl}…`); // RFC-1188: platform-originated agent-directed probe — sign when the // identity resolves (WBA_IDENTITY_SITE), degrade to unsigned otherwise. const outbound = await resolveOutboundAgentClient(workspaceRoot, undefined, undefined, { onSigningFailure: (e) => logger.warn( `[leitstand.service.promote] outbound signing failed, request sent unsigned: ${e instanceof Error ? e.message : String(e)}`, ), }); healthState = await runHealthCheck( deployedUrl, serviceEntry.healthCheckPath, outbound.client.fetch, ); } else if (!serviceEntry.publicEndpoints) { logger.info( `[leitstand.service.promote] skipping health check — service has no public endpoints`, ); } // 7. Smoke check (RFC-0825) — fatal on promote let smokeResult: ServicePromoteData["smokeResult"]; if (deployedUrl) { smokeResult = await runSmokeCheck(workspaceRoot, serviceId, deployedUrl, logger); } if (smokeResult?.status === "fail") { await recordProdDeployState(workspaceRoot, serviceId, { at: new Date().toISOString(), state: "failed", operationId, }); const failedData: ServicePromoteData = { command: "leitstand.service.promote", serviceId, workerName: serviceEntry.workerName, deployState: "failed", workersDevUrl: deployedUrl, healthState, smokeResult, preDeployGates: gateResults, testEvidenceVerified: true, startedAt, completedAt: new Date().toISOString(), operationId, }; return { data: failedData, exitCode: 1, summary: `[leitstand.service.promote] ${serviceId}: smoke tests failed — promotion blocked`, nextSteps: [ { action: `Fix the failing smoke tests for ${serviceId}, then re-run leitstand.service.promote`, kind: "required", }, ], }; } // 8. Record prod deploy state await recordProdDeployState(workspaceRoot, serviceId, { at: new Date().toISOString(), state: "succeeded", operationId, }); // 9. Update workersDevUrl in registry if resolved if (deployedUrl && deployedUrl !== serviceEntry.workersDevUrl) { await updateWorkersDevUrl(workspaceRoot, serviceId, deployedUrl); } const completedAt = new Date().toISOString(); const data: ServicePromoteData = { command: "leitstand.service.promote", serviceId, workerName: serviceEntry.workerName, deployState: "succeeded", workersDevUrl: deployedUrl, healthState, smokeResult, preDeployGates: gateResults, testEvidenceVerified: true, startedAt, completedAt, operationId, }; return { data, exitCode: 0, summary: `[leitstand.service.promote] ${serviceId}: promoted to production (${healthState})`, nextSteps: [ { action: `Verify service health: pnpm exec werkstatt run leitstand.health --service ${serviceId}`, kind: "optional", }, ], }; } finally { await releaseServiceLock(workspaceRoot, serviceId); } }