test: add code queue cicd dry-run contract

This commit is contained in:
Codex
2026-05-21 05:13:29 +00:00
parent 71918843ae
commit 62a2eaad68
6 changed files with 286 additions and 11 deletions
@@ -0,0 +1,126 @@
import { spawnSync } from "node:child_process";
type JsonRecord = Record<string, unknown>;
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
}
function asRecord(value: unknown, label: string): JsonRecord {
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, { value });
return value as JsonRecord;
}
function asStringArray(value: unknown, label: string): string[] {
assertCondition(Array.isArray(value) && value.every((item) => typeof item === "string"), `${label} must be a string array`, value);
return value as string[];
}
function runCli(args: string[], expectStatus: number): JsonRecord {
const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
cwd: process.cwd(),
encoding: "utf8",
maxBuffer: 8 * 1024 * 1024,
});
assertCondition(result.status === expectStatus, `cli status mismatch for ${args.join(" ")}`, {
status: result.status,
stdout: result.stdout.slice(-3000),
stderr: result.stderr.slice(-3000),
});
return asRecord(JSON.parse(result.stdout) as unknown, "cli envelope");
}
function firstService(envelope: JsonRecord, label: string): JsonRecord {
const data = asRecord(envelope.data, `${label} data`);
const services = Array.isArray(data.services) ? data.services : [];
assertCondition(services.length === 1, `${label} should return exactly one service`, data);
return asRecord(services[0], `${label} service`);
}
function includes(value: unknown, expected: string): boolean {
return Array.isArray(value) && value.some((item) => item === expected);
}
const commit = "0123456789abcdef0123456789abcdef01234567";
const devPlan = firstService(runCli(["deploy", "plan", "--env", "dev", "--service", "code-queue"], 0), "dev plan");
const devArtifact = asRecord(devPlan.artifactConsumer, "dev artifactConsumer");
const devTarget = asRecord(devPlan.target, "dev target");
const devBoundary = asRecord(devPlan.boundary, "dev boundary");
const devCd = asRecord(devBoundary.cdConsumer, "dev cdConsumer");
assertCondition(devArtifact.consumerKind === "d601-k3s-managed", "dev Code Queue must be a k3s-managed artifact consumer", devArtifact);
assertCondition(devArtifact.noRuntimeSourceBuild === true, "dev Code Queue must not build source on the runtime target", devArtifact);
assertCondition(devTarget.namespace === "unidesk-dev", "dev Code Queue target namespace must be unidesk-dev", devTarget);
assertCondition(devTarget.deployment === "code-queue-scheduler-dev", "dev Code Queue should target the dev scheduler deployment", devTarget);
assertCondition(devCd.prodMutationAllowed === false, "dev Code Queue boundary must prohibit production mutation", devCd);
assertCondition(String(devCd.manualAuthorizationPoint ?? "").includes("DEV apply"), "dev Code Queue boundary should expose the DEV manual authorization point", devCd);
assertCondition(asRecord(devBoundary.ciProducer, "dev ciProducer").allowed === true, "Code Queue CI producer should be allowed for image publication", devBoundary);
assertCondition(includes(devTarget.forbiddenActions, "docker build"), "dev target must forbid runtime docker build", devTarget);
assertCondition(includes(devTarget.forbiddenActions, "NodePort"), "dev target must forbid NodePort", devTarget);
const prodPlan = firstService(runCli(["deploy", "plan", "--env", "prod", "--service", "code-queue"], 1), "prod plan");
const prodArtifact = asRecord(prodPlan.artifactConsumer, "prod artifactConsumer");
const prodTarget = asRecord(prodPlan.target, "prod target");
const prodBoundary = asRecord(prodPlan.boundary, "prod boundary");
const prodCd = asRecord(prodBoundary.cdConsumer, "prod cdConsumer");
const prodLiveApply = asRecord(prodPlan.liveApply, "prod liveApply");
const prodUnsupported = asRecord(prodPlan.unsupported, "prod unsupported");
const prodForbidden = asStringArray(prodTarget.forbiddenActions, "prod forbiddenActions");
assertCondition(prodPlan.deploymentPath === "unsupported", "prod Code Queue deployment path must be unsupported", prodPlan);
assertCondition(prodArtifact.consumerKind === "unsupported", "prod Code Queue artifact consumer must be unsupported", prodArtifact);
assertCondition(prodArtifact.registryImage === null, "prod Code Queue plan must not advertise a production registry image target", prodArtifact);
assertCondition(prodArtifact.noRuntimeSourceBuild === true, "prod Code Queue plan must still block runtime source builds", prodArtifact);
assertCondition(prodTarget.runtimeHost === null, "prod Code Queue plan must not expose a runtime host target", prodTarget);
assertCondition(prodTarget.deployCommandShape === "none", "prod Code Queue plan must not expose a deploy command shape", prodTarget);
assertCondition(prodLiveApply.allowed === false, "prod Code Queue live apply must be blocked", prodLiveApply);
assertCondition(String(prodLiveApply.reason ?? "").includes("production CD is intentionally unsupported"), "prod blocked reason should name the intentional CD gap", prodLiveApply);
assertCondition(String(prodUnsupported.reason ?? "").includes("prod artifact deploy"), "prod unsupported reason should mention prod artifact deploy", prodUnsupported);
assertCondition(prodCd.prodMutationAllowed === false, "prod Code Queue boundary must prohibit production mutation", prodCd);
assertCondition(prodCd.liveApplyCommandShape === null, "prod Code Queue boundary must not advertise a live apply command", prodCd);
assertCondition(String(prodCd.manualAuthorizationPoint ?? "").includes("future supervisor-approved"), "prod boundary should require a future supervisor-approved design", prodCd);
assertCondition(prodForbidden.includes("production namespace mutation"), "prod forbidden actions must include production namespace mutation", prodTarget);
assertCondition(prodForbidden.includes("interrupt running Code Queue tasks"), "prod forbidden actions must include task interrupt", prodTarget);
assertCondition(prodForbidden.includes("cancel running Code Queue tasks"), "prod forbidden actions must include task cancel", prodTarget);
assertCondition(JSON.stringify(prodTarget).includes("code-queue-read"), "prod excluded targets should name production Code Queue deployments", prodTarget);
const devArtifactDryRun = asRecord(runCli([
"artifact-registry",
"deploy-service",
"--env",
"dev",
"--service",
"code-queue",
"--commit",
commit,
"--dry-run",
], 0).data, "dev artifact-registry dry-run");
const artifactTarget = asRecord(devArtifactDryRun.target, "artifact dry-run target");
assertCondition(devArtifactDryRun.mutation === false, "artifact-registry dev dry-run must be non-mutating", devArtifactDryRun);
assertCondition(artifactTarget.namespace === "unidesk-dev", "artifact-registry dev dry-run must target unidesk-dev", artifactTarget);
const prodArtifactDryRun = asRecord(runCli([
"artifact-registry",
"deploy-service",
"--env",
"prod",
"--service",
"code-queue",
"--commit",
commit,
"--dry-run",
], 1).data, "prod artifact-registry dry-run");
assertCondition(prodArtifactDryRun.error === "unsupported-environment", "artifact-registry prod code-queue should be unsupported", prodArtifactDryRun);
assertCondition(Array.isArray(prodArtifactDryRun.supportedEnvironments) && prodArtifactDryRun.supportedEnvironments.length === 1 && prodArtifactDryRun.supportedEnvironments[0] === "dev", "artifact-registry prod code-queue should expose only dev as supported", prodArtifactDryRun);
process.stdout.write(`${JSON.stringify({
ok: true,
checks: [
"deploy plan exposes Code Queue CI producer and dev CD consumer boundary",
"dev Code Queue dry-run targets only unidesk-dev and forbids runtime builds/public ports",
"prod Code Queue plan is unsupported and exposes no runtime deploy target",
"prod Code Queue boundary forbids self-deploy, prod mutation, interrupt and cancel actions",
"artifact-registry dev dry-run is non-mutating while prod remains unsupported",
],
}, null, 2)}\n`);
+124 -11
View File
@@ -1002,6 +1002,105 @@ function artifactConsumerPlanValidation(service: UniDeskMicroserviceConfig, envi
return ["unsupported service remains blocked before source materialization or runtime mutation"];
}
function unsupportedEnvironmentPlanReason(serviceId: string, environment: DeployEnvironment): string {
if (environment === "prod" && serviceId === "code-queue") {
return "Code Queue production CD is intentionally unsupported in this phase: only CI image publication and dev artifact-consumer validation are implemented; prod artifact deploy, rollout and manifest mutation are blocked.";
}
return environment === "prod"
? "No standardized prod D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed."
: "No standardized dev deployment or artifact validation path is implemented for this service.";
}
function unsupportedPlanTarget(serviceId: string, environment: DeployEnvironment, reason: string): Record<string, unknown> {
const forbiddenActions = [
"docker build",
"docker compose build",
"docker compose up --build",
"maintenance-channel source deploy",
"dirty worktree source deploy",
];
if (serviceId === "code-queue") {
forbiddenActions.push(
"codex deploy",
"server rebuild code-queue",
"production namespace mutation",
"production manifest mutation",
"interrupt running Code Queue tasks",
"cancel running Code Queue tasks",
);
}
return {
consumerKind: "unsupported",
runtimeHost: null,
deploymentMode: "none",
noRuntimeSourceBuild: true,
dryRunOnly: true,
blockedReason: reason,
deployCommandShape: "none",
forbiddenActions,
...(serviceId === "code-queue"
? {
excludedTargets: [
{
namespace: "unidesk",
deployments: ["code-queue", "code-queue-read", "code-queue-write", "d601-provider-egress-proxy", "d601-tcp-egress-gateway"],
reason: `${environment} dry-run must not mutate production Code Queue execution-plane objects.`,
},
{
operations: ["interrupt", "cancel", "restart scheduler", "restart runner"],
reason: "Code Queue cannot self-deploy or alter active task control state from this runner path.",
},
],
}
: {}),
};
}
function codeQueueCiCdBoundary(serviceId: string, environment: DeployEnvironment): Record<string, unknown> | undefined {
if (serviceId !== "code-queue") return undefined;
return {
serviceId,
selfDeployBlocked: true,
ciProducer: {
command: "bun scripts/cli.ts ci publish-user-service --service code-queue --commit <full-sha>",
allowed: true,
outputImage: "127.0.0.1:5000/unidesk/code-queue:<full-sha>",
responsibility: "build a commit-pinned Code Queue image from pushed Git source, push it to the D601 registry, and report digest/label evidence",
forbiddenActions: ["deploy apply", "kubectl apply", "rollout restart", "interrupt", "cancel"],
},
cdConsumer: environment === "dev"
? {
environment: "dev",
dryRunCommand: "bun scripts/cli.ts deploy apply --env dev --service code-queue --commit <full-sha> --dry-run",
liveApplyCommandShape: "bun scripts/cli.ts deploy apply --env dev --service code-queue --commit <full-sha> --run-now",
allowedTarget: "unidesk-dev Code Queue scheduler/read/write/provider-egress-proxy only",
manualAuthorizationPoint: "operator reviews CI artifact summary and dev dry-run plan, then explicitly authorizes DEV apply outside this Code Queue task",
prodMutationAllowed: false,
}
: {
environment: "prod",
dryRunCommand: "bun scripts/cli.ts deploy plan --env prod --service code-queue",
liveApplyCommandShape: null,
allowedTarget: null,
manualAuthorizationPoint: "PROD requires a future supervisor-approved Code Queue CD design; current runner output is plan/unsupported only",
prodMutationAllowed: false,
},
manualAuthorization: {
dev: "DEV live apply is a human operator decision after dry-run evidence; this task may not execute it.",
prod: "PROD live apply is not implemented and must remain unsupported, even with manual intent, until a separate reviewed CD consumer exists.",
},
forbiddenActions: [
"codex deploy",
"server rebuild code-queue",
"deploy apply --env prod --service code-queue",
"artifact-registry deploy-service --env prod --service code-queue",
"kubectl apply to namespace unidesk",
"rollout restart production Code Queue scheduler/read/write",
"interrupt or cancel active Code Queue tasks",
],
};
}
function environmentPlanServiceConfig(config: UniDeskConfig | null, service: DeployManifestService, environment: DeployEnvironment): UniDeskMicroserviceConfig | null {
if (environment === "dev") {
const devService = devK3sDeployService(service.id);
@@ -2845,7 +2944,12 @@ function environmentDryRunPlan(
const unsupported = (environment === "prod" && !prodArtifactConsumerServiceIds.has(service.id))
|| (environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null;
const planKind = serviceConfig === null ? "unsupported" : artifactConsumerPlanKind(serviceConfig, environment);
const planTarget = serviceConfig === null ? null : artifactConsumerPlanTarget(serviceConfig, environment);
const unsupportedReason = unsupported ? unsupportedEnvironmentPlanReason(service.id, environment) : null;
const effectiveTarget = unsupported
? unsupportedPlanTarget(service.id, environment, unsupportedReason ?? "unsupported")
: planTarget;
return {
id: service.id,
repo: service.repo,
@@ -2863,24 +2967,33 @@ function environmentDryRunPlan(
dryRunOnly: true,
blockedReason: "service is missing from config.json and no synthetic deploy service is available for this environment",
} : {
consumerKind: artifactConsumerPlanKind(serviceConfig, environment),
registryImage: `127.0.0.1:5000/unidesk/${service.id}:${service.commitId}`,
noRuntimeSourceBuild: artifactConsumerPlanKind(serviceConfig, environment) !== "d601-dev-target-side-build",
dryRunOnly: (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)) || dryRunBlockedReason !== null,
blockedReason: dryRunBlockedReason ?? (environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null),
consumerKind: unsupported ? "unsupported" : planKind,
registryImage: unsupported ? null : `127.0.0.1:5000/unidesk/${service.id}:${service.commitId}`,
noRuntimeSourceBuild: unsupported || planKind !== "d601-dev-target-side-build",
dryRunOnly: unsupported || (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)) || dryRunBlockedReason !== null,
blockedReason: unsupportedReason ?? dryRunBlockedReason ?? (environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null),
},
target: planTarget,
validation: serviceConfig === null ? undefined : artifactConsumerPlanValidation(serviceConfig, environment),
target: effectiveTarget,
validation: unsupported
? ["unsupported service remains blocked before source materialization, registry pull, manifest update, rollout, or runtime mutation"]
: serviceConfig === null
? undefined
: artifactConsumerPlanValidation(serviceConfig, environment),
boundary: codeQueueCiCdBoundary(service.id, environment),
unsupported: unsupported
? {
ok: false,
supported: false,
reason: environment === "prod"
? "No standardized prod D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed."
: "No standardized dev deployment or artifact validation path is implemented for this service.",
reason: unsupportedReason,
}
: undefined,
liveApply: dryRunBlockedReason !== null
liveApply: unsupported
? {
allowed: false,
reason: unsupportedReason,
dryRunOnly: true,
}
: dryRunBlockedReason !== null
? {
allowed: false,
reason: dryRunBlockedReason,