/*
RFC-0566 deploy.atomic.swap — atomic symlink swap to deploy a new platform artifact.
- Do not build artifacts — that is deploy.artifact.build's job.
- Do not implement two-phase commit — Phase 4 is deferred.
- RFC-0566: initial deploy.atomic.swap handler with current+previous symlink management.
- 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.
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
*/
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-engine/kernel";
import type { AtomicSwapResult } from "./types.ts";
import {
artifactDir,
atomicSymlinkSwap,
currentSymlinkPath,
previousSymlinkPath,
readSymlinkBasename,
} from "./deploy-utils.ts";
import { runDeployArtifactVerify } from "./artifact-verify.ts";
function flagString(input: KernelCommandInput, key: string): string | undefined {
const v = input.flags[key];
return typeof v === "string" ? v : undefined;
}
export async function runDeployAtomicSwap(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot, logger } = context;
const hash = flagString(input, "hash");
if (!hash) {
throw new Error("[deploy.atomic.swap] --hash is required");
}
const dir = artifactDir(workspaceRoot, hash);
if (!(await context.io.exists(dir))) {
throw new Error(`[deploy.atomic.swap] artifact ${hash} not found`);
}
const verifyResult = await runDeployArtifactVerify(
{ ...input, flags: { ...input.flags, hash } },
context,
);
if (!verifyResult.data?.verified) {
throw new Error(
`[deploy.atomic.swap] artifact ${hash} hash verification failed (hash-mismatch)`,
);
}
const currentPath = currentSymlinkPath(workspaceRoot);
const previousPath = previousSymlinkPath(workspaceRoot);
const oldCurrentHash = await readSymlinkBasename(currentPath, context.io);
const startTime = Date.now();
if (oldCurrentHash && oldCurrentHash !== hash) {
const oldArtifactDir = artifactDir(workspaceRoot, oldCurrentHash);
await atomicSymlinkSwap(previousPath, oldArtifactDir);
} else if (!oldCurrentHash) {
logger.info("[deploy.atomic.swap] first deploy — no previous symlink to create");
}
await atomicSymlinkSwap(currentPath, dir);
const swapTimeMs = Date.now() - startTime;
logger.success(
`[deploy.atomic.swap] swapped to ${hash} in ${swapTimeMs}ms` +
(oldCurrentHash ? ` (previous: ${oldCurrentHash})` : ""),
);
return {
data: {
swapped: true,
previousHash: oldCurrentHash,
newHash: hash,
swapTimeMs,
},
exitCode: 0,
summary: `[deploy.atomic.swap] swapped to ${hash.slice(0, 16)}... in ${swapTimeMs}ms`,
nextSteps: [
{
action: `Verify health: pnpm exec werkstatt run leitstand.health --site --channel main`,
kind: "optional",
},
],
};
}