/*
RFC-0566 deploy.artifact.build — build immutable platform artifact from local git clone.
- Do not deploy or swap symlinks — that is deploy.atomic.swap's job.
- Do not verify artifacts — that is deploy.artifact.verify's job.
- RFC-0566: initial deploy.artifact.build handler.
- 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
- RFC-1254: drop unused io-migration leftovers
*/
import path from "node:path";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-engine/kernel";
import { signJsonPayloadAsync, requireEnv } from "@warpgogol/werkstatt-engine/integrity";
import type { ArtifactBuildResult, ArtifactManifest } from "./types.ts";
import {
artifactDir,
hashArtifactDir,
platformArtifactsBase,
writeManifest,
} from "./deploy-utils.ts";
import { type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
function _flagString(input: KernelCommandInput, key: string): string | undefined {
const v = input.flags[key];
return typeof v === "string" ? v : undefined;
}
function flagBool(input: KernelCommandInput, key: string): boolean {
const v = input.flags[key];
return v === true || v === "true";
}
async function getGitSha(workspaceRoot: string, io: WorkspaceIO): Promise {
try {
const output = (
await io.exec("git", ["rev-parse", "HEAD"], {
cwd: workspaceRoot,
timeoutMs: 5000,
})
).stdout.trim();
return output;
} catch {
return "unknown";
}
}
async function copyDistTrees(
workspaceRoot: string,
destDist: string,
io: WorkspaceIO,
): Promise {
const packagesDir = path.join(workspaceRoot, "packages");
if (!(await io.exists(packagesDir))) return;
await io.mkdir(destDist);
const entries = await io.readdir(packagesDir);
for (const entry of entries) {
if (!entry.isDirectory) continue;
const pkgDir = path.join(packagesDir, entry.name);
const distDir = path.join(pkgDir, "dist");
if (!(await io.exists(distDir))) continue;
const destPkgDir = path.join(destDist, entry.name);
await io.mkdir(destPkgDir);
const cpResult = await io.exec("cp", ["-r", distDir, destPkgDir]);
if (cpResult.exitCode !== 0) {
throw new Error(`cp failed for '${entry.name}': ${cpResult.stderr.slice(0, 200)}`);
}
}
}
export async function runDeployArtifactBuild(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot, logger } = context;
const skipBuild = flagBool(input, "skip-build");
const skipSign = flagBool(input, "skip-sign");
const baseDir = platformArtifactsBase(workspaceRoot);
await context.io.mkdir(baseDir);
if (!skipBuild) {
logger.info("[deploy.artifact.build] running turbo run build...");
try {
const buildResult = await context.io.exec("pnpm", ["exec", "turbo", "run", "build"], {
cwd: workspaceRoot,
timeoutMs: 300000,
});
if (buildResult.exitCode !== 0) {
throw new Error(
`turbo run build exited ${buildResult.exitCode}: ${buildResult.stderr.slice(-400)}`,
);
}
} catch (err) {
throw new Error(`[deploy.artifact.build] turbo run build failed: ${(err as Error).message}`);
}
}
const stagingDir = path.join(baseDir, `.staging-${process.pid}-${Date.now()}`);
const stagingDist = path.join(stagingDir, "dist");
await context.io.mkdir(stagingDist);
try {
await copyDistTrees(workspaceRoot, stagingDist, context.io);
const { treeHash, files, totalSize } = await hashArtifactDir(stagingDist);
const hash = treeHash;
const gitSha = await getGitSha(workspaceRoot, context.io);
const builtAt = new Date().toISOString();
const buildHost = await requireEnv("WORKSHOP_ID", workspaceRoot).catch(
() => process.env.HOSTNAME ?? "unknown",
);
let manifest: ArtifactManifest = {
hash,
files,
totalSize,
builtAt,
gitSha,
buildHost,
signature: "",
signatureAlgorithm: "Ed25519",
};
if (!skipSign) {
try {
const privateKeyPem = await requireEnv("SIGNING_PRIVATE_KEY", workspaceRoot);
const signed = await signJsonPayloadAsync(
privateKeyPem,
manifest as unknown as Record,
);
manifest = { ...manifest, signature: signed.signatureHex };
} catch (err) {
logger.info(`[deploy.artifact.build] signing skipped: ${(err as Error).message}`);
}
}
const finalDir = artifactDir(workspaceRoot, hash);
if (await context.io.exists(finalDir)) {
logger.info(`[deploy.artifact.build] artifact ${hash} already exists — reusing`);
return {
data: {
hash,
gitSha,
builtAt,
buildHost: manifest.buildHost,
fileCount: files.length,
totalSize,
artifactPath: finalDir,
},
exitCode: 0,
summary: `[deploy.artifact.build] artifact ${hash.slice(0, 16)}... (${files.length} files, ${totalSize} bytes)`,
nextSteps: [
{
action: `Verify the artifact: pnpm exec werkstatt run deploy.artifact.verify --hash ${hash}`,
kind: "optional",
},
],
};
}
await context.io.rename(stagingDir, finalDir);
await writeManifest(workspaceRoot, hash, manifest);
logger.success(
`[deploy.artifact.build] artifact ${hash} created (${files.length} files, ${totalSize} bytes)`,
);
return {
data: {
hash,
gitSha,
builtAt,
buildHost: manifest.buildHost,
fileCount: files.length,
totalSize,
artifactPath: finalDir,
},
exitCode: 0,
summary: `[deploy.artifact.build] artifact ${hash.slice(0, 16)}... (${files.length} files, ${totalSize} bytes)`,
nextSteps: [
{
action: `Verify the artifact: pnpm exec werkstatt run deploy.artifact.verify --hash ${hash}`,
kind: "optional",
},
],
};
} finally {
if (await context.io.exists(stagingDir)) {
await context.io.rm(stagingDir, { recursive: true }).catch(() => {});
}
}
}