/*
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);
}
}