83e5f52c98
Summary: - shared runtime durable readiness classifier for live-status and DEV postflight - structured postflight blockers propagated through dev-cd apply - contract coverage for ready queryResult, auth/schema/migration blockers, evidence persistence blockers, and bounded stdout Verification: - git diff --check - node --check changed scripts/tests/live-status modules - node --test scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-cd-apply.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs - node scripts/dev-runtime-postflight.mjs --check - node web/hwlab-cloud-web/scripts/check.mjs - npm run check No DEV apply/rollout/live health verification was run.
1575 lines
59 KiB
JavaScript
1575 lines
59 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { execFile } from "node:child_process";
|
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { promisify } from "node:util";
|
|
|
|
import {
|
|
buildLockAnnotations,
|
|
classifyDeployLock,
|
|
deployLockHeldFailure,
|
|
parseDeployLock,
|
|
runDevCdApply,
|
|
verifyDevLive
|
|
} from "./dev-cd-apply.mjs";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
const repoRoot = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../..");
|
|
|
|
function iso(offsetMs = 0) {
|
|
return new Date(Date.parse("2026-05-23T08:00:00.000Z") + offsetMs).toISOString();
|
|
}
|
|
|
|
function leaseFromLock(lock) {
|
|
return {
|
|
apiVersion: "coordination.k8s.io/v1",
|
|
kind: "Lease",
|
|
metadata: {
|
|
name: "hwlab-dev-cd-lock",
|
|
namespace: "hwlab-dev",
|
|
resourceVersion: "1",
|
|
annotations: buildLockAnnotations(lock)
|
|
},
|
|
spec: {
|
|
holderIdentity: `${lock.ownerTaskId}/${lock.transactionId}`,
|
|
leaseDurationSeconds: lock.ttlSeconds,
|
|
acquireTime: lock.startedAt,
|
|
renewTime: lock.updatedAt
|
|
}
|
|
};
|
|
}
|
|
|
|
function parsePatch(args) {
|
|
const index = args.indexOf("-p");
|
|
assert.notEqual(index, -1, "kubectl patch must include -p");
|
|
return JSON.parse(args[index + 1]);
|
|
}
|
|
|
|
async function makeRepo(options = {}) {
|
|
const commitId = options.commitId ?? "abc1234";
|
|
const repoRoot = await fsTempDir();
|
|
await mkdir(path.join(repoRoot, "deploy/k8s/base"), { recursive: true });
|
|
await mkdir(path.join(repoRoot, "reports/dev-gate"), { recursive: true });
|
|
await writeFile(
|
|
path.join(repoRoot, "deploy/deploy.json"),
|
|
`${JSON.stringify({
|
|
manifestVersion: "v1",
|
|
environment: "dev",
|
|
namespace: "hwlab-dev",
|
|
endpoint: "http://74.48.78.17:16667",
|
|
commitId,
|
|
services: [
|
|
{
|
|
serviceId: "hwlab-cloud-api",
|
|
image: `127.0.0.1:5000/hwlab/hwlab-cloud-api:${commitId.slice(0, 7)}`,
|
|
namespace: "hwlab-dev",
|
|
profile: "dev",
|
|
env: { HWLAB_COMMIT_ID: commitId.slice(0, 7) }
|
|
},
|
|
{
|
|
serviceId: "hwlab-cloud-web",
|
|
image: `127.0.0.1:5000/hwlab/hwlab-cloud-web:${commitId.slice(0, 7)}`,
|
|
namespace: "hwlab-dev",
|
|
profile: "dev",
|
|
env: { HWLAB_COMMIT_ID: commitId.slice(0, 7) }
|
|
}
|
|
]
|
|
}, null, 2)}\n`
|
|
);
|
|
await writeFile(
|
|
path.join(repoRoot, "deploy/artifact-catalog.dev.json"),
|
|
`${JSON.stringify({
|
|
catalogVersion: "v1",
|
|
kind: "hwlab-artifact-catalog",
|
|
environment: "dev",
|
|
profile: "dev",
|
|
namespace: "hwlab-dev",
|
|
endpoint: "http://74.48.78.17:16667",
|
|
commitId,
|
|
artifactState: "published",
|
|
publish: {
|
|
ciPublished: true,
|
|
registryVerified: true,
|
|
provenance: "reports/dev-gate/dev-artifacts.json"
|
|
},
|
|
services: [
|
|
{
|
|
serviceId: "hwlab-cloud-api",
|
|
commitId,
|
|
image: `127.0.0.1:5000/hwlab/hwlab-cloud-api:${commitId.slice(0, 7)}`,
|
|
imageTag: commitId.slice(0, 7),
|
|
digest: "sha256:b3713aa07164a9ff79a436bd583552427de42922835a5baff50484f53d98f296",
|
|
publishState: "published",
|
|
profile: "dev",
|
|
namespace: "hwlab-dev",
|
|
artifactRequired: true
|
|
},
|
|
{
|
|
serviceId: "hwlab-cloud-web",
|
|
commitId,
|
|
image: `127.0.0.1:5000/hwlab/hwlab-cloud-web:${commitId.slice(0, 7)}`,
|
|
imageTag: commitId.slice(0, 7),
|
|
digest: "sha256:3be80fcfa769d68b0a7ec44e9215b93546d01bf23411533c5cb4888438f2bd84",
|
|
publishState: "published",
|
|
profile: "dev",
|
|
namespace: "hwlab-dev",
|
|
artifactRequired: true
|
|
}
|
|
]
|
|
}, null, 2)}\n`
|
|
);
|
|
await writeFile(
|
|
path.join(repoRoot, "reports/dev-gate/dev-artifacts.json"),
|
|
`${JSON.stringify({
|
|
reportVersion: "v1",
|
|
taskId: "dev-artifact-publish",
|
|
status: "published",
|
|
commitId,
|
|
artifactPublish: {
|
|
status: "published",
|
|
mode: "publish",
|
|
sourceCommitId: commitId,
|
|
registryPrefix: "127.0.0.1:5000/hwlab",
|
|
serviceCount: 2,
|
|
requiredServiceCount: 2,
|
|
publishedCount: 2,
|
|
generatedAt: iso(),
|
|
services: [
|
|
{
|
|
serviceId: "hwlab-cloud-api",
|
|
status: "published",
|
|
artifactRequired: true,
|
|
image: `127.0.0.1:5000/hwlab/hwlab-cloud-api:${commitId.slice(0, 7)}`,
|
|
digest: "sha256:b3713aa07164a9ff79a436bd583552427de42922835a5baff50484f53d98f296"
|
|
},
|
|
{
|
|
serviceId: "hwlab-cloud-web",
|
|
status: "published",
|
|
artifactRequired: true,
|
|
image: `127.0.0.1:5000/hwlab/hwlab-cloud-web:${commitId.slice(0, 7)}`,
|
|
digest: "sha256:3be80fcfa769d68b0a7ec44e9215b93546d01bf23411533c5cb4888438f2bd84"
|
|
}
|
|
]
|
|
}
|
|
}, null, 2)}\n`
|
|
);
|
|
await writeFile(path.join(repoRoot, "deploy/k8s/base/workloads.yaml"), `# fixture ${commitId}\n`);
|
|
return repoRoot;
|
|
}
|
|
|
|
async function fsTempDir() {
|
|
return await import("node:fs/promises").then((fs) => fs.mkdtemp(path.join(os.tmpdir(), "hwlab-cd-")));
|
|
}
|
|
|
|
function makeHttpGetJson() {
|
|
return async (url) => {
|
|
if (url.includes("16666")) {
|
|
return {
|
|
statusCode: 200,
|
|
json: {
|
|
serviceId: "hwlab-cloud-web",
|
|
environment: "dev",
|
|
status: "ok",
|
|
revision: "abc1234",
|
|
image: { reference: "127.0.0.1:5000/hwlab/hwlab-cloud-web:abc1234", tag: "abc1234" },
|
|
observedAt: iso()
|
|
}
|
|
};
|
|
}
|
|
return {
|
|
statusCode: 200,
|
|
json: {
|
|
serviceId: "hwlab-cloud-api",
|
|
environment: "dev",
|
|
status: "degraded",
|
|
ready: false,
|
|
commit: { id: "abc1234", source: "runtime-env" },
|
|
image: { reference: "127.0.0.1:5000/hwlab/hwlab-cloud-api:abc1234", tag: "abc1234" },
|
|
blockerCodes: ["runtime_durable_adapter_auth_blocked"],
|
|
observedAt: iso()
|
|
}
|
|
};
|
|
};
|
|
}
|
|
|
|
const publishedCommit = "abc1234abc1234abc1234abc1234abc1234abc1";
|
|
const laterControlCommit = "def5678def5678def5678def5678def5678def5";
|
|
const artifactMergeCommit = "fedcba9fedcba9fedcba9fedcba9fedcba9fedc";
|
|
const artifactReportCommit = "a77efa1a77efa1a77efa1a77efa1a77efa1a77e";
|
|
|
|
function makeRunCommand({
|
|
heldLock = null,
|
|
commandLog = [],
|
|
targetCommitId = publishedCommit,
|
|
headCommitId = targetCommitId,
|
|
extraGitRefs = {},
|
|
commitParents = {},
|
|
latestArtifactMergeCommit = null,
|
|
changedPaths = [
|
|
"deploy/artifact-catalog.dev.json",
|
|
"deploy/deploy.json",
|
|
"deploy/k8s/base/workloads.yaml",
|
|
"reports/dev-gate/dev-artifacts.json"
|
|
],
|
|
desiredStatePromotionStatus = "pass",
|
|
runtimeJobLogSuffix = "",
|
|
runtimePostflightReport = null,
|
|
kubeContext = "d601",
|
|
kubeServer = "https://d601:6443",
|
|
kubeNodes = ["d601"],
|
|
bareKubeContext = null,
|
|
bareKubeServer = null,
|
|
bareKubeNodes = null,
|
|
bareHwlabDevNamespace = false,
|
|
secretRefs = {
|
|
"hwlab-cloud-api-dev-db": ["database-url"],
|
|
"hwlab-cloud-api-dev-db-admin": ["admin-url"],
|
|
"hwlab-code-agent-provider": ["openai-api-key"]
|
|
}
|
|
} = {}) {
|
|
let lease = heldLock ? leaseFromLock(heldLock) : null;
|
|
const assertLeaseMicroTime = (value) => {
|
|
assert.match(value, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}Z$/u);
|
|
};
|
|
return async (command, args, options = {}) => {
|
|
commandLog.push({ command, args, env: options.env ?? {}, input: options.input ?? "" });
|
|
if (command === "git" && args[0] === "rev-parse" && args.includes("origin/main^{commit}")) {
|
|
return { code: 0, stdout: `${targetCommitId}\n`, stderr: "" };
|
|
}
|
|
if (command === "git" && args[0] === "rev-parse" && args.includes("--short=7")) {
|
|
const ref = String(args.at(-1) ?? "");
|
|
const commitId = extraGitRefs[ref] ?? ref;
|
|
return { code: 0, stdout: `${commitId.slice(0, 7)}\n`, stderr: "" };
|
|
}
|
|
if (command === "git" && args[0] === "rev-parse" && args.includes("HEAD^{commit}")) {
|
|
return { code: 0, stdout: `${headCommitId}\n`, stderr: "" };
|
|
}
|
|
if (command === "git" && args[0] === "rev-parse" && args[1] === "--verify") {
|
|
const ref = String(args[2] ?? "").replace(/\^\{commit\}$/u, "");
|
|
const commitId = extraGitRefs[ref];
|
|
if (commitId) return { code: 0, stdout: `${commitId}\n`, stderr: "" };
|
|
return { code: 1, stdout: "", stderr: `fatal: ambiguous argument '${ref}'` };
|
|
}
|
|
if (command === "git" && args[0] === "log" && args.includes("--first-parent")) {
|
|
return latestArtifactMergeCommit
|
|
? { code: 0, stdout: `${latestArtifactMergeCommit}\n`, stderr: "" }
|
|
: { code: 1, stdout: "", stderr: "no matching commits" };
|
|
}
|
|
if (command === "git" && args[0] === "rev-list" && args.includes("--parents")) {
|
|
const ref = String(args.at(-1) ?? "");
|
|
const parents = commitParents[ref] ?? [];
|
|
return { code: 0, stdout: `${[ref, ...parents].join(" ")}\n`, stderr: "" };
|
|
}
|
|
if (command === "git" && args[0] === "diff-tree") {
|
|
return { code: 0, stdout: `${changedPaths.join("\n")}\n`, stderr: "" };
|
|
}
|
|
if (command === "which" && args[0] === "kubectl") {
|
|
return { code: 0, stdout: "/usr/local/bin/kubectl\n", stderr: "" };
|
|
}
|
|
if (command.includes("kubectl") && args[0] === "config" && args[1] === "current-context") {
|
|
const hasKubeconfig = Object.hasOwn(options.env ?? {}, "KUBECONFIG");
|
|
return { code: 0, stdout: `${hasKubeconfig ? kubeContext : (bareKubeContext ?? kubeContext)}\n`, stderr: "" };
|
|
}
|
|
if (command.includes("kubectl") && args[0] === "config" && args[1] === "view") {
|
|
const hasKubeconfig = Object.hasOwn(options.env ?? {}, "KUBECONFIG");
|
|
return { code: 0, stdout: `${hasKubeconfig ? kubeServer : (bareKubeServer ?? kubeServer)}\n`, stderr: "" };
|
|
}
|
|
if (command.includes("kubectl") && args[0] === "get" && args[1] === "nodes") {
|
|
const hasKubeconfig = Object.hasOwn(options.env ?? {}, "KUBECONFIG");
|
|
return { code: 0, stdout: `${(hasKubeconfig ? kubeNodes : (bareKubeNodes ?? kubeNodes)).join(" ")}\n`, stderr: "" };
|
|
}
|
|
if (command.includes("kubectl") && args[0] === "get" && args[1] === "namespace") {
|
|
return bareHwlabDevNamespace
|
|
? { code: 0, stdout: "namespace/hwlab-dev\n", stderr: "" }
|
|
: { code: 1, stdout: "", stderr: "Error from server (NotFound): namespaces \"hwlab-dev\" not found" };
|
|
}
|
|
if (command.includes("kubectl") && args.includes("get") && args.includes("secret")) {
|
|
const name = args[args.indexOf("secret") + 1];
|
|
if (!secretRefs[name]) {
|
|
return { code: 1, stdout: "", stderr: `Error from server (NotFound): secrets "${name}" not found` };
|
|
}
|
|
return { code: 0, stdout: `secret/${name}\n`, stderr: "" };
|
|
}
|
|
if (command.includes("kubectl") && args.includes("describe") && args.includes("secret")) {
|
|
const name = args[args.indexOf("secret") + 1];
|
|
const keys = secretRefs[name];
|
|
if (!keys) {
|
|
return { code: 1, stdout: "", stderr: `Error from server (NotFound): secrets "${name}" not found` };
|
|
}
|
|
return {
|
|
code: 0,
|
|
stdout: `Name: ${name}\nNamespace: hwlab-dev\nType: Opaque\n\nData\n====\n${keys.map((key) => `${key}: 16 bytes`).join("\n")}\n`,
|
|
stderr: ""
|
|
};
|
|
}
|
|
if (command.includes("kubectl") && args.includes("get") && args.includes("lease")) {
|
|
if (!lease) return { code: 1, stdout: "", stderr: "Error from server (NotFound): leases.coordination.k8s.io \"hwlab-dev-cd-lock\" not found" };
|
|
return { code: 0, stdout: `${JSON.stringify(lease)}\n`, stderr: "" };
|
|
}
|
|
if (command.includes("kubectl") && args.includes("create")) {
|
|
lease = JSON.parse(options.input);
|
|
assertLeaseMicroTime(lease.spec.acquireTime);
|
|
assertLeaseMicroTime(lease.spec.renewTime);
|
|
lease.metadata.resourceVersion = "2";
|
|
return { code: 0, stdout: `${JSON.stringify(lease)}\n`, stderr: "" };
|
|
}
|
|
if (command.includes("kubectl") && args.includes("replace")) {
|
|
const nextLease = JSON.parse(options.input);
|
|
assert.equal(nextLease.metadata?.resourceVersion, lease?.metadata?.resourceVersion);
|
|
if (nextLease.spec?.acquireTime) assertLeaseMicroTime(nextLease.spec.acquireTime);
|
|
if (nextLease.spec?.renewTime) assertLeaseMicroTime(nextLease.spec.renewTime);
|
|
lease = nextLease;
|
|
lease.metadata.resourceVersion = String(Number(lease.metadata.resourceVersion ?? "2") + 1);
|
|
return { code: 0, stdout: `${JSON.stringify(lease)}\n`, stderr: "" };
|
|
}
|
|
if (command.includes("kubectl") && args.includes("apply") && args.includes("-f") && args.includes("-")) {
|
|
const manifest = JSON.parse(options.input);
|
|
assert.equal(manifest.kind, "Job");
|
|
assert.equal(manifest.metadata.namespace, "hwlab-dev");
|
|
assert.ok(manifest.metadata.name.startsWith("hwlab-runtime-"));
|
|
assert.equal(manifest.spec.template.spec.containers[0].env.some((entry) => entry.name === "HWLAB_CLOUD_DB_URL" && entry.valueFrom?.secretKeyRef?.name === "hwlab-cloud-api-dev-db"), true);
|
|
assert.equal(manifest.spec.template.spec.containers[0].env.some((entry) => entry.name === "HWLAB_CLOUD_DB_ADMIN_URL" && entry.valueFrom?.secretKeyRef?.name === "hwlab-cloud-api-dev-db-admin"), true);
|
|
assert.equal(JSON.stringify(manifest).includes("database-url"), true);
|
|
assert.equal(JSON.stringify(manifest).includes("postgresql://"), false);
|
|
return { code: 0, stdout: JSON.stringify(manifest), stderr: "" };
|
|
}
|
|
if (command.includes("kubectl") && args.includes("wait") && args.some((arg) => String(arg).startsWith("job/hwlab-runtime-"))) {
|
|
return { code: 0, stdout: "job.batch/hwlab-runtime complete\n", stderr: "" };
|
|
}
|
|
if (command.includes("kubectl") && args.includes("logs") && args.some((arg) => String(arg).startsWith("job/hwlab-runtime-provision"))) {
|
|
return {
|
|
code: 0,
|
|
stdout: `${runtimeJobLogSuffix}${JSON.stringify({ conclusion: { status: "ready" }, safety: { secretValuesPrinted: false } }, null, 2)}\n`,
|
|
stderr: ""
|
|
};
|
|
}
|
|
if (command.includes("kubectl") && args.includes("logs") && args.some((arg) => String(arg).startsWith("job/hwlab-runtime-migrate"))) {
|
|
return {
|
|
code: 0,
|
|
stdout: `${runtimeJobLogSuffix}${JSON.stringify({ conclusion: { status: "ready" }, safety: { secretValuesPrinted: false } }, null, 2)}\n`,
|
|
stderr: ""
|
|
};
|
|
}
|
|
if (command.includes("kubectl") && args.includes("patch")) {
|
|
assert.ok(lease, "lease must exist before patch");
|
|
const patch = parsePatch(args);
|
|
lease.metadata.annotations = {
|
|
...lease.metadata.annotations,
|
|
...(patch.metadata?.annotations ?? {})
|
|
};
|
|
lease.spec = {
|
|
...lease.spec,
|
|
...(patch.spec ?? {})
|
|
};
|
|
lease.metadata.resourceVersion = String(Number(lease.metadata.resourceVersion ?? "2") + 1);
|
|
return { code: 0, stdout: `${JSON.stringify(lease)}\n`, stderr: "" };
|
|
}
|
|
if ((command === process.execPath || command.endsWith("/node")) && args.includes("scripts/deploy-desired-state-plan.mjs") && args.includes("--promotion-commit")) {
|
|
const promotionCommit = args[args.indexOf("--promotion-commit") + 1];
|
|
const status = desiredStatePromotionStatus;
|
|
return {
|
|
code: status === "pass" ? 0 : 1,
|
|
stdout: `${JSON.stringify({
|
|
status,
|
|
summary: {
|
|
desiredCommitId: promotionCommit.slice(0, 7),
|
|
artifactState: "published",
|
|
ciPublished: true,
|
|
registryVerified: true,
|
|
blockers: status === "pass" ? 0 : 1
|
|
},
|
|
target: {
|
|
convergence: {
|
|
state: status === "pass" ? "already_promoted" : "promotion_mismatch"
|
|
}
|
|
},
|
|
diagnostics: status === "pass" ? [] : [{ code: "promotion_commit_mismatch", path: "desired-state", message: "fixture mismatch" }]
|
|
})}\n`,
|
|
stderr: ""
|
|
};
|
|
}
|
|
if (command === process.execPath || command.endsWith("/node")) {
|
|
if (args.includes("scripts/dev-runtime-postflight.mjs")) {
|
|
const report = runtimePostflightReport ?? {
|
|
conclusion: { status: "pass" },
|
|
blockers: [],
|
|
safety: { secretValuesPrinted: false },
|
|
apiHealth: { runtimeDurableReadiness: { ready: true } },
|
|
apiV1: { runtimeDurableReadiness: { ready: true } },
|
|
m3: {
|
|
persistenceContract: { ready: true },
|
|
operationCount: 4
|
|
}
|
|
};
|
|
return {
|
|
code: report.conclusion?.status === "pass" ? 0 : 1,
|
|
stdout: `${JSON.stringify(report, null, 2)}\n`,
|
|
stderr: ""
|
|
};
|
|
}
|
|
return { code: 0, stdout: JSON.stringify({ ok: true, command: args.join(" ") }), stderr: "" };
|
|
}
|
|
return { code: 0, stdout: "", stderr: "" };
|
|
};
|
|
}
|
|
|
|
function assertNoReadOnlySideEffects(commandLog) {
|
|
const mutatingKubectlVerbs = new Set(["apply", "create", "replace", "patch", "delete", "rollout", "wait", "logs"]);
|
|
assert.equal(commandLog.some((entry) => entry.command === "ps"), false);
|
|
assert.equal(commandLog.some((entry) => entry.command === "kill"), false);
|
|
assert.equal(commandLog.some((entry) =>
|
|
entry.command.includes("kubectl") && entry.args.some((arg) => mutatingKubectlVerbs.has(arg))
|
|
), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-artifact-publish.mjs")), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs")), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/refresh-artifact-catalog.mjs")), false);
|
|
}
|
|
|
|
function assertNoWriteSideEffects(commandLog) {
|
|
const writeKubectlVerbs = new Set(["apply", "create", "replace", "patch", "delete", "rollout", "wait", "logs"]);
|
|
assert.equal(commandLog.some((entry) =>
|
|
entry.command.includes("kubectl") && entry.args.some((arg) => writeKubectlVerbs.has(arg))
|
|
), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-artifact-publish.mjs")), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs")), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/refresh-artifact-catalog.mjs")), false);
|
|
}
|
|
|
|
test("lock classifier reports held and stale states with retryAfterSeconds", () => {
|
|
const lock = {
|
|
ownerTaskId: "task-a",
|
|
transactionId: "tx-a",
|
|
promotionCommit: "abc1234",
|
|
deployJsonHash: "sha256:a",
|
|
phase: "applying",
|
|
startedAt: iso(0),
|
|
updatedAt: iso(0),
|
|
ttlSeconds: 300,
|
|
targetNamespace: "hwlab-dev"
|
|
};
|
|
const parsed = parseDeployLock(leaseFromLock(lock));
|
|
const held = classifyDeployLock(parsed, new Date(iso(120000)));
|
|
assert.equal(held.held, true);
|
|
assert.equal(held.stale, false);
|
|
assert.equal(held.retryAfterSeconds, 180);
|
|
|
|
const stale = classifyDeployLock(parsed, new Date(iso(301000)));
|
|
assert.equal(stale.held, false);
|
|
assert.equal(stale.stale, true);
|
|
assert.equal(stale.retryAfterSeconds, 0);
|
|
});
|
|
|
|
test("dev-cd status and dry-run are read-only compact JSON", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--dry-run",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml",
|
|
"--write-report"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({ commandLog }),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const status = JSON.parse(output);
|
|
assert.equal(code, 0);
|
|
assert.equal(status.mode, "dry-run");
|
|
assert.equal(status.mutationAttempted, false);
|
|
assert.equal(status.prodTouched, false);
|
|
assert.equal(status.lock.status, "absent");
|
|
assert.equal(status.deployJson.manifest, undefined);
|
|
assert.equal(status.deployJson.matchesTarget, true);
|
|
assert.equal(status.live.summary.checked, 2);
|
|
assertNoReadOnlySideEffects(commandLog);
|
|
await assert.rejects(
|
|
readFile(path.join(repoRoot, "reports/dev-gate/dev-cd-apply.json"), "utf8"),
|
|
{ code: "ENOENT" }
|
|
);
|
|
});
|
|
|
|
test("dev-cd default status is read-only compact JSON", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({ commandLog }),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const status = JSON.parse(output);
|
|
assert.equal(code, 0);
|
|
assert.equal(status.mode, "status");
|
|
assert.equal(status.mutationAttempted, false);
|
|
assert.equal(status.prodTouched, false);
|
|
assert.equal(status.target.shortCommitId, "abc1234");
|
|
assert.equal(status.deployJson.hash.startsWith("sha256:"), true);
|
|
assert.equal(status.deployJson.matchesTarget, true);
|
|
assert.equal(status.lock.status, "absent");
|
|
assertNoReadOnlySideEffects(commandLog);
|
|
});
|
|
|
|
test("dev-cd status accepts a published deploy-json promotion under a later control HEAD", async () => {
|
|
const repoRoot = await makeRepo({ commitId: "abc1234" });
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply(["--status", "--kubeconfig", "/etc/rancher/k3s/k3s.yaml"], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
targetCommitId: laterControlCommit,
|
|
headCommitId: laterControlCommit,
|
|
extraGitRefs: {
|
|
abc1234: publishedCommit
|
|
},
|
|
latestArtifactMergeCommit: artifactMergeCommit,
|
|
commitParents: {
|
|
[artifactMergeCommit]: [publishedCommit, artifactReportCommit],
|
|
[artifactReportCommit]: [publishedCommit]
|
|
}
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const status = JSON.parse(output);
|
|
assert.equal(code, 0);
|
|
assert.equal(status.status, "pass");
|
|
assert.equal(status.target.promotionCommit, publishedCommit);
|
|
assert.equal(status.target.shortCommitId, "abc1234");
|
|
assert.equal(status.target.promotionSource, "deploy-json");
|
|
assert.equal(status.target.publishRequired, false);
|
|
assert.equal(status.target.controlRef.shortCommitId, "def5678");
|
|
assert.equal(status.target.artifactBoundary.status, "pass");
|
|
assert.equal(status.target.artifactBoundary.artifactMergeCommit.shortCommitId, "fedcba9");
|
|
assert.equal(status.target.artifactBoundary.artifactMergeCommit.promotionParentMatches, true);
|
|
assert.equal(status.target.artifactBoundary.artifactCommit.shortCommitId, "a77efa1");
|
|
assert.equal(status.target.artifactBoundary.artifactCommit.promotionParentMatches, true);
|
|
assert.equal(status.artifactCatalog.commitId, "abc1234");
|
|
assert.equal(status.artifactCatalog.digestCounts.sha256, 2);
|
|
assert.equal(status.artifactReport.sourceCommitId, "abc1234");
|
|
assert.equal(status.liveDelta.status, "pass");
|
|
assert.equal(status.liveDelta.expectedCommit, "abc1234");
|
|
assert.equal(status.target.headMatchesTarget, true);
|
|
assert.equal(status.target.desiredStateCheck.status, "pass");
|
|
assert.equal(status.deployJson.matchesTarget, true);
|
|
assert.match(status.nextActions[0], /without republishing HEAD/u);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-artifact-publish.mjs")), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs")), false);
|
|
assert.equal(commandLog.some((entry) =>
|
|
entry.args.includes("scripts/deploy-desired-state-plan.mjs") &&
|
|
entry.args.includes("--promotion-commit") &&
|
|
entry.args.includes(publishedCommit)
|
|
), true);
|
|
assertNoReadOnlySideEffects(commandLog);
|
|
});
|
|
|
|
test("dev-cd status exposes moving origin/main when checkout is behind a deploy-json promotion control ref", async () => {
|
|
const repoRoot = await makeRepo({ commitId: "abc1234" });
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply(["--status", "--kubeconfig", "/etc/rancher/k3s/k3s.yaml"], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
targetCommitId: laterControlCommit,
|
|
headCommitId: publishedCommit,
|
|
extraGitRefs: {
|
|
abc1234: publishedCommit
|
|
},
|
|
latestArtifactMergeCommit: artifactMergeCommit,
|
|
commitParents: {
|
|
[artifactMergeCommit]: [publishedCommit, artifactReportCommit],
|
|
[artifactReportCommit]: [publishedCommit]
|
|
}
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const status = JSON.parse(output);
|
|
assert.equal(code, 0);
|
|
assert.equal(status.status, "degraded");
|
|
assert.equal(status.target.promotionCommit, publishedCommit);
|
|
assert.equal(status.target.controlRef.shortCommitId, "def5678");
|
|
assert.equal(status.target.headCommitId, publishedCommit);
|
|
assert.equal(status.target.headMatchesTarget, false);
|
|
assert.match(status.nextActions.join("\n"), /Checkout or fast-forward/u);
|
|
assertNoReadOnlySideEffects(commandLog);
|
|
});
|
|
|
|
test("apply stops before lock acquisition when checkout is behind deploy-json promotion control ref", async () => {
|
|
const repoRoot = await makeRepo({ commitId: "abc1234" });
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-moving-main",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
targetCommitId: laterControlCommit,
|
|
headCommitId: publishedCommit,
|
|
extraGitRefs: {
|
|
abc1234: publishedCommit
|
|
},
|
|
latestArtifactMergeCommit: artifactMergeCommit,
|
|
commitParents: {
|
|
[artifactMergeCommit]: [publishedCommit, artifactReportCommit],
|
|
[artifactReportCommit]: [publishedCommit]
|
|
}
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const summary = JSON.parse(output);
|
|
assert.equal(code, 2);
|
|
assert.equal(summary.status, "blocked");
|
|
assert.equal(summary.target.controlRef.shortCommitId, "def5678");
|
|
assert.equal(summary.blockers.some((blocker) => blocker.scope === "target-ref-head"), true);
|
|
assert.equal(commandLog.some((entry) => entry.command.includes("kubectl")), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-artifact-publish.mjs")), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs")), false);
|
|
});
|
|
|
|
test("dev-cd status degrades artifact merge provenance when catalog/report do not match deploy-json promotion", async () => {
|
|
const repoRoot = await makeRepo({ commitId: "abc1234" });
|
|
await writeFile(
|
|
path.join(repoRoot, "deploy/artifact-catalog.dev.json"),
|
|
`${JSON.stringify({
|
|
kind: "hwlab-artifact-catalog",
|
|
commitId: "9999999",
|
|
artifactState: "published",
|
|
publish: { ciPublished: true, registryVerified: true, provenance: "reports/dev-gate/dev-artifacts.json" },
|
|
services: []
|
|
}, null, 2)}\n`
|
|
);
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply(["--status", "--kubeconfig", "/etc/rancher/k3s/k3s.yaml"], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
targetCommitId: laterControlCommit,
|
|
headCommitId: laterControlCommit,
|
|
extraGitRefs: {
|
|
abc1234: publishedCommit
|
|
},
|
|
latestArtifactMergeCommit: artifactMergeCommit,
|
|
commitParents: {
|
|
[artifactMergeCommit]: [publishedCommit, artifactReportCommit],
|
|
[artifactReportCommit]: [publishedCommit]
|
|
}
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const status = JSON.parse(output);
|
|
assert.equal(code, 0);
|
|
assert.equal(status.status, "degraded");
|
|
assert.equal(status.target.promotionCommit, publishedCommit);
|
|
assert.equal(status.target.artifactBoundary.status, "degraded");
|
|
assert.equal(status.target.artifactBoundary.desiredState.deployCommitMatches, true);
|
|
assert.equal(status.target.artifactBoundary.desiredState.catalogCommitMatches, false);
|
|
assert.equal(status.target.artifactBoundary.desiredState.reportCommitMatches, true);
|
|
assertNoReadOnlySideEffects(commandLog);
|
|
});
|
|
|
|
test("apply stops before lock acquisition when artifact merge provenance mismatches deploy-json promotion", async () => {
|
|
const repoRoot = await makeRepo({ commitId: "abc1234" });
|
|
await writeFile(
|
|
path.join(repoRoot, "reports/dev-gate/dev-artifacts.json"),
|
|
`${JSON.stringify({
|
|
status: "published",
|
|
commitId: "9999999",
|
|
artifactPublish: {
|
|
status: "published",
|
|
sourceCommitId: "9999999",
|
|
serviceCount: 0,
|
|
requiredServiceCount: 0,
|
|
publishedCount: 0,
|
|
services: []
|
|
}
|
|
}, null, 2)}\n`
|
|
);
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-provenance",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
targetCommitId: laterControlCommit,
|
|
headCommitId: laterControlCommit,
|
|
extraGitRefs: {
|
|
abc1234: publishedCommit
|
|
},
|
|
latestArtifactMergeCommit: artifactMergeCommit,
|
|
commitParents: {
|
|
[artifactMergeCommit]: [publishedCommit, artifactReportCommit],
|
|
[artifactReportCommit]: [publishedCommit]
|
|
}
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const summary = JSON.parse(output);
|
|
assert.equal(code, 2);
|
|
assert.equal(summary.status, "blocked");
|
|
assert.equal(summary.blockers.some((blocker) => blocker.scope === "artifact-boundary"), true);
|
|
assert.equal(commandLog.some((entry) => entry.command.includes("kubectl")), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-artifact-publish.mjs")), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs")), false);
|
|
});
|
|
|
|
test("dev-cd status summarizes held, released, and stale locks without unsafe takeover hints", async () => {
|
|
const cases = [
|
|
{
|
|
name: "held",
|
|
phase: "applying",
|
|
now: iso(100000),
|
|
expectedStatus: "held",
|
|
expectedRetryAfterSeconds: 200
|
|
},
|
|
{
|
|
name: "released",
|
|
phase: "released",
|
|
releasedAt: iso(50000),
|
|
releaseStatus: "pass",
|
|
now: iso(100000),
|
|
expectedStatus: "released",
|
|
expectedRetryAfterSeconds: 0
|
|
},
|
|
{
|
|
name: "stale",
|
|
phase: "verifying",
|
|
now: iso(301000),
|
|
expectedStatus: "stale",
|
|
expectedRetryAfterSeconds: 0
|
|
}
|
|
];
|
|
|
|
for (const item of cases) {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply(["--status", "--kubeconfig", "/etc/rancher/k3s/k3s.yaml"], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
heldLock: {
|
|
promotionCommit: "abc1234",
|
|
deployJsonHash: "sha256:a",
|
|
ownerTaskId: `task-${item.name}`,
|
|
transactionId: `tx-${item.name}`,
|
|
phase: item.phase,
|
|
startedAt: iso(0),
|
|
updatedAt: iso(0),
|
|
ttlSeconds: 300,
|
|
liveBefore: { status: "pass" },
|
|
targetNamespace: "hwlab-dev",
|
|
targetRef: "origin/main",
|
|
lockBackend: "Lease",
|
|
...(item.releasedAt ? { releasedAt: item.releasedAt } : {}),
|
|
...(item.releaseStatus ? { releaseStatus: item.releaseStatus } : {})
|
|
}
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(item.now),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const status = JSON.parse(output);
|
|
assert.equal(code, 0);
|
|
assert.equal(status.lock.status, item.expectedStatus);
|
|
assert.equal(status.lock.lockName, "hwlab-dev-cd-lock");
|
|
assert.equal(status.lock.current.ownerTaskId, `task-${item.name}`);
|
|
assert.equal(status.lock.retryAfterSeconds, item.expectedRetryAfterSeconds);
|
|
assert.equal(JSON.stringify(status).includes("force-interrupt"), false);
|
|
assert.equal(JSON.stringify(status.nextActions).includes("force"), false);
|
|
if (item.expectedStatus === "stale") {
|
|
assert.match(status.lock.allowedLockAction.breakStale, /--break-stale-lock/u);
|
|
}
|
|
assertNoReadOnlySideEffects(commandLog);
|
|
}
|
|
});
|
|
|
|
test("dev-cd status reports unavailable kubectl with redacted reason", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const runCommand = async (command, args, options = {}) => {
|
|
commandLog.push({ command, args, env: options.env ?? {}, input: options.input ?? "" });
|
|
if (command === "git" && args[0] === "rev-parse" && args.includes("origin/main^{commit}")) {
|
|
return { code: 0, stdout: "abc1234abc1234abc1234abc1234abc1234abc1\n", stderr: "" };
|
|
}
|
|
if (command === "git" && args[0] === "rev-parse" && args.includes("--short=7")) {
|
|
return { code: 0, stdout: `${String(args.at(-1)).slice(0, 7)}\n`, stderr: "" };
|
|
}
|
|
if (command === "git" && args[0] === "rev-parse" && args.includes("HEAD^{commit}")) {
|
|
return { code: 0, stdout: "abc1234abc1234abc1234abc1234abc1234abc1\n", stderr: "" };
|
|
}
|
|
if (command === "git" && args[0] === "log" && args.includes("--first-parent")) {
|
|
return { code: 1, stdout: "", stderr: "no matching commits" };
|
|
}
|
|
if (command === "git" && args[0] === "rev-list" && args.includes("--parents")) {
|
|
const ref = String(args.at(-1) ?? "");
|
|
return { code: 0, stdout: `${ref}\n`, stderr: "" };
|
|
}
|
|
if (command === "git" && args[0] === "diff-tree") {
|
|
return { code: 0, stdout: "", stderr: "" };
|
|
}
|
|
if (command === "which" && args[0] === "kubectl") {
|
|
return { code: 1, stdout: "", stderr: "kubectl missing" };
|
|
}
|
|
if (command === "kubectl") {
|
|
return {
|
|
code: 1,
|
|
stdout: "",
|
|
stderr: "connection failed token=SECRET_TOKEN password=SECRET_PASSWORD postgresql://user:SECRET_DB@db"
|
|
};
|
|
}
|
|
return { code: 0, stdout: "", stderr: "" };
|
|
};
|
|
|
|
const code = await runDevCdApply(["--status"], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand,
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const status = JSON.parse(output);
|
|
const text = JSON.stringify(status);
|
|
assert.equal(code, 0);
|
|
assert.equal(status.status, "degraded");
|
|
assert.equal(status.lock.status, "unavailable");
|
|
assert.equal(status.lock.reason, "kubectl or DEV kubeconfig unavailable; command output is redacted");
|
|
assert.equal(text.includes("SECRET_TOKEN"), false);
|
|
assert.equal(text.includes("SECRET_PASSWORD"), false);
|
|
assert.equal(text.includes("SECRET_DB"), false);
|
|
assert.match(status.lock.redactedReason, /<redacted>/u);
|
|
assertNoReadOnlySideEffects(commandLog);
|
|
});
|
|
|
|
test("force-interrupt option is rejected before process discovery or lease mutation", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply(["--force-interrupt-lock"], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({ commandLog }),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const failure = JSON.parse(output);
|
|
assert.equal(code, 2);
|
|
assert.equal(failure.error, "unknown-argument");
|
|
assert.equal(failure.mutationAttempted, false);
|
|
assert.equal(failure.prodTouched, false);
|
|
assert.deepEqual(commandLog, []);
|
|
});
|
|
|
|
test("deploy-lock-held failure is structured before publish/apply", () => {
|
|
const failure = deployLockHeldFailure({
|
|
ownerTaskId: "task-a",
|
|
transactionId: "tx-a",
|
|
promotionCommit: "abc1234",
|
|
deployJsonHash: "sha256:a",
|
|
phase: "applying",
|
|
updatedAt: iso(0),
|
|
ttlSeconds: 300,
|
|
targetNamespace: "hwlab-dev",
|
|
lockName: "hwlab-dev-cd-lock",
|
|
lockBackend: "Lease"
|
|
}, new Date(iso(100000)));
|
|
|
|
assert.equal(failure.ok, false);
|
|
assert.equal(failure.error, "deploy-lock-held");
|
|
assert.equal(failure.holder, "task-a");
|
|
assert.equal(failure.promotionCommit, "abc1234");
|
|
assert.equal(failure.phase, "applying");
|
|
assert.equal(failure.retryAfterSeconds, 200);
|
|
assert.equal(failure.mutationAttempted, false);
|
|
});
|
|
|
|
test("second transaction exits deploy-lock-held without running side effects", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-b",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
heldLock: {
|
|
promotionCommit: "abc1234",
|
|
deployJsonHash: "sha256:a",
|
|
ownerTaskId: "task-a",
|
|
transactionId: "tx-a",
|
|
phase: "publishing",
|
|
startedAt: iso(0),
|
|
updatedAt: iso(0),
|
|
ttlSeconds: 300,
|
|
liveBefore: { status: "pass" },
|
|
targetNamespace: "hwlab-dev",
|
|
targetRef: "origin/main",
|
|
lockBackend: "Lease"
|
|
}
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const failure = JSON.parse(output);
|
|
assert.equal(code, 2);
|
|
assert.equal(failure.error, "deploy-lock-held");
|
|
assert.equal(failure.holder, "task-a");
|
|
assert.equal(failure.retryAfterSeconds, 200);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-artifact-publish.mjs")), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs")), false);
|
|
});
|
|
|
|
test("stale lock requires explicit break-stale-lock confirmation", async () => {
|
|
const repoRoot = await makeRepo();
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-b",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
heldLock: {
|
|
promotionCommit: "abc1234",
|
|
deployJsonHash: "sha256:a",
|
|
ownerTaskId: "task-a",
|
|
transactionId: "tx-a",
|
|
phase: "verifying",
|
|
startedAt: iso(0),
|
|
updatedAt: iso(0),
|
|
ttlSeconds: 300,
|
|
liveBefore: { status: "pass" },
|
|
targetNamespace: "hwlab-dev",
|
|
targetRef: "origin/main",
|
|
lockBackend: "Lease"
|
|
}
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(301000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const failure = JSON.parse(output);
|
|
assert.equal(code, 2);
|
|
assert.equal(failure.error, "deploy-lock-held");
|
|
assert.equal(failure.stale, true);
|
|
assert.equal(failure.requiresBreakStaleLock, true);
|
|
});
|
|
|
|
test("apply preflight passes with D601 native k3s and required SecretRef keys before mutation", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-preflight-pass",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml",
|
|
"--write-report",
|
|
"--full-output"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({ commandLog }),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const report = JSON.parse(output);
|
|
const preflight = report.devCdApply.preflight;
|
|
assert.equal(code, 0);
|
|
assert.equal(preflight.status, "pass");
|
|
assert.equal(preflight.kubeconfig.required, "/etc/rancher/k3s/k3s.yaml");
|
|
assert.equal(preflight.kubeconfig.enforcedEnv, true);
|
|
assert.deepEqual(preflight.controlPlane.forced.nodeNames, ["d601"]);
|
|
assert.deepEqual(
|
|
preflight.secretRefs.map((ref) => `${ref.secretName}/${ref.secretKey}:${ref.status}`),
|
|
[
|
|
"hwlab-cloud-api-dev-db/database-url:present",
|
|
"hwlab-cloud-api-dev-db-admin/admin-url:present",
|
|
"hwlab-code-agent-provider/openai-api-key:present"
|
|
]
|
|
);
|
|
assert.equal(preflight.safety.secretValuesRead, false);
|
|
assert.equal(preflight.safety.secretValuesPrinted, false);
|
|
|
|
const firstWriteIndex = commandLog.findIndex((entry) =>
|
|
entry.command.includes("kubectl") && ["create", "replace", "apply", "patch", "delete", "rollout", "wait", "logs"].some((verb) => entry.args.includes(verb))
|
|
);
|
|
const secretDescribeIndexes = commandLog
|
|
.map((entry, index) => ({ entry, index }))
|
|
.filter(({ entry }) => entry.command.includes("kubectl") && entry.args.includes("describe") && entry.args.includes("secret"))
|
|
.map(({ index }) => index);
|
|
assert.ok(secretDescribeIndexes.length > 0);
|
|
assert.ok(secretDescribeIndexes.every((index) => index < firstWriteIndex));
|
|
});
|
|
|
|
test("apply preflight blocks missing SecretRef before Lease or side-effect commands", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-missing-secret",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml",
|
|
"--write-report"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
secretRefs: {
|
|
"hwlab-cloud-api-dev-db": ["database-url"],
|
|
"hwlab-cloud-api-dev-db-admin": ["admin-url"]
|
|
}
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const summary = JSON.parse(output);
|
|
const writtenReport = JSON.parse(await readFile(path.join(repoRoot, "reports/dev-gate/dev-cd-apply.json"), "utf8"));
|
|
assert.equal(code, 2);
|
|
assert.equal(summary.status, "blocked");
|
|
assert.equal(summary.preflight.status, "blocked");
|
|
assert.equal(summary.preflight.blockerCount, 1);
|
|
assert.equal(summary.blockers.some((blocker) => blocker.scope === "secretref:hwlab-code-agent-provider/openai-api-key"), true);
|
|
assert.equal(writtenReport.devCdApply.preflight.safety.writeSideEffectsAttempted, false);
|
|
assert.equal(writtenReport.devCdApply.preflight.safety.secretValuesRead, false);
|
|
assert.equal(JSON.stringify(writtenReport).includes("SECRET"), false);
|
|
assertNoWriteSideEffects(commandLog);
|
|
});
|
|
|
|
test("apply preflight blocks non-D601 kubeconfig path before kubectl mutation", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-wrong-path",
|
|
"--kubeconfig",
|
|
"/tmp/kubeconfig"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({ commandLog }),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const summary = JSON.parse(output);
|
|
assert.equal(code, 2);
|
|
assert.equal(summary.status, "blocked");
|
|
assert.equal(summary.preflight.status, "blocked");
|
|
assert.equal(summary.blockers.some((blocker) => blocker.scope === "d601-kubeconfig-path"), true);
|
|
assertNoWriteSideEffects(commandLog);
|
|
});
|
|
|
|
test("apply preflight blocks docker-desktop and 127.0.0.1 control-plane signals before mutation", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-docker-desktop",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
kubeContext: "docker-desktop",
|
|
kubeServer: "https://127.0.0.1:11700",
|
|
kubeNodes: ["desktop-control-plane"]
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const summary = JSON.parse(output);
|
|
assert.equal(code, 2);
|
|
assert.equal(summary.status, "blocked");
|
|
assert.equal(summary.blockers.some((blocker) => blocker.scope === "d601-native-k3s-context"), true);
|
|
assert.equal(summary.blockers.some((blocker) => blocker.scope === "d601-native-k3s-server"), true);
|
|
assert.equal(summary.blockers.some((blocker) => blocker.scope === "d601-native-k3s-nodes"), true);
|
|
assertNoWriteSideEffects(commandLog);
|
|
});
|
|
|
|
test("apply preflight blocks second hwlab-dev control plane visible through bare kubectl", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-second-control-plane",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
bareKubeContext: "docker-desktop",
|
|
bareKubeServer: "https://127.0.0.1:11700",
|
|
bareKubeNodes: ["desktop-control-plane"],
|
|
bareHwlabDevNamespace: true
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const summary = JSON.parse(output);
|
|
assert.equal(code, 2);
|
|
assert.equal(summary.status, "blocked");
|
|
assert.equal(summary.blockers.some((blocker) => blocker.scope === "bare-kubectl-context"), true);
|
|
assert.equal(summary.blockers.some((blocker) => blocker.scope === "bare-kubectl-server"), true);
|
|
assert.equal(summary.blockers.some((blocker) => blocker.scope === "second-hwlab-dev-control-plane"), true);
|
|
assertNoWriteSideEffects(commandLog);
|
|
});
|
|
|
|
test("transaction runs phases, allows internal side-effect env, releases lock, and reports live verify", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--break-stale-lock",
|
|
"--owner-task-id",
|
|
"task-b",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml",
|
|
"--write-report",
|
|
"--full-output"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
heldLock: {
|
|
promotionCommit: "old1234",
|
|
deployJsonHash: "sha256:old",
|
|
ownerTaskId: "task-a",
|
|
transactionId: "tx-a",
|
|
phase: "verifying",
|
|
startedAt: iso(0),
|
|
updatedAt: iso(0),
|
|
ttlSeconds: 300,
|
|
liveBefore: { status: "pass" },
|
|
targetNamespace: "hwlab-dev",
|
|
targetRef: "origin/main",
|
|
lockBackend: "Lease"
|
|
}
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(301000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const report = JSON.parse(output);
|
|
assert.equal(code, 0);
|
|
assert.equal(report.status, "pass");
|
|
assert.deepEqual(
|
|
report.transaction.phases.map((phase) => phase.phase),
|
|
["publishing", "applying", "verifying", "released"]
|
|
);
|
|
assert.equal(report.devCdApply.lock.staleBreak.ownerTaskId, "task-a");
|
|
assert.equal(report.transaction.release.status, "released");
|
|
assert.equal(report.devCdApply.liveVerify.status, "pass");
|
|
assert.equal(report.devCdApply.liveVerify.summary.checked, 2);
|
|
assert.equal(report.devCdApply.liveVerify.endpoints.some((endpoint) => endpoint.id === "cloud-web-16666"), true);
|
|
assert.equal(report.devCdApply.liveVerify.endpoints.some((endpoint) => endpoint.id === "cloud-api-16667"), true);
|
|
assert.equal(report.devCdApply.reportPaths.transaction, "reports/dev-gate/dev-cd-apply.json");
|
|
assert.equal(report.devCdApply.reportPaths.runtimeProvisioning, "reports/dev-gate/dev-runtime-provisioning-report.json");
|
|
assert.equal(report.devCdApply.reportPaths.runtimeMigration, "reports/dev-gate/dev-runtime-migration-report.json");
|
|
assert.equal(report.devCdApply.reportPaths.runtimePostflight, "reports/dev-gate/dev-runtime-postflight-report.json");
|
|
|
|
const publishCall = commandLog.find((entry) => entry.args.includes("scripts/dev-artifact-publish.mjs"));
|
|
const applyCall = commandLog.find((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs"));
|
|
const provisioningCall = commandLog.find((entry) => entry.args.includes("scripts/dev-runtime-provisioning.mjs"));
|
|
const migrationCall = commandLog.find((entry) => entry.args.includes("scripts/dev-runtime-migration.mjs"));
|
|
const postflightCall = commandLog.find((entry) => entry.args.includes("scripts/dev-runtime-postflight.mjs"));
|
|
const refreshCall = commandLog.find((entry) => entry.args.includes("scripts/refresh-artifact-catalog.mjs"));
|
|
assert.ok(publishCall?.env.HWLAB_CD_TRANSACTION_ID);
|
|
assert.equal(applyCall?.env.HWLAB_CD_TRANSACTION_ID, publishCall.env.HWLAB_CD_TRANSACTION_ID);
|
|
const provisioningJobApply = commandLog.find((entry) =>
|
|
entry.command.includes("kubectl") &&
|
|
entry.args.includes("apply") &&
|
|
entry.input.includes("hwlab-runtime-provision")
|
|
);
|
|
const migrationJobApply = commandLog.find((entry) =>
|
|
entry.command.includes("kubectl") &&
|
|
entry.args.includes("apply") &&
|
|
entry.input.includes("hwlab-runtime-migrate")
|
|
);
|
|
assert.ok(provisioningJobApply);
|
|
assert.ok(migrationJobApply);
|
|
assert.equal(JSON.parse(provisioningJobApply.input).spec.template.spec.containers[0].args[0], "scripts/dev-runtime-provisioning.mjs");
|
|
assert.equal(JSON.parse(migrationJobApply.input).spec.template.spec.containers[0].args[0], "scripts/dev-runtime-migration.mjs");
|
|
assert.equal(postflightCall?.env.HWLAB_CD_TRANSACTION_ID, publishCall.env.HWLAB_CD_TRANSACTION_ID);
|
|
assert.deepEqual(postflightCall.args.slice(0, 4), ["scripts/dev-runtime-postflight.mjs", "--live", "--confirm-dev", "--confirmed-non-production"]);
|
|
assert.equal(refreshCall.args[refreshCall.args.indexOf("--target-ref") + 1], "abc1234abc1234abc1234abc1234abc1234abc1");
|
|
assert.ok(
|
|
commandLog.findIndex((entry) => entry.input.includes("hwlab-runtime-provision")) <
|
|
commandLog.findIndex((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs"))
|
|
);
|
|
assert.ok(
|
|
commandLog.findIndex((entry) => entry.input.includes("hwlab-runtime-migrate")) <
|
|
commandLog.findIndex((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs"))
|
|
);
|
|
|
|
const writtenReport = JSON.parse(await readFile(path.join(repoRoot, "reports/dev-gate/dev-cd-apply.json"), "utf8"));
|
|
assert.equal(writtenReport.devCdApply.liveVerify.summary.checked, 2);
|
|
});
|
|
|
|
test("transaction blocks with structured runtime postflight blocker when M3 evidence is not persisted", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const runtimePostflightReport = {
|
|
conclusion: {
|
|
status: "blocked",
|
|
blockerCount: 1
|
|
},
|
|
apiHealth: {
|
|
runtimeDurableReadiness: {
|
|
ready: true,
|
|
classification: "runtime_durable_ready"
|
|
}
|
|
},
|
|
apiV1: {
|
|
runtimeDurableReadiness: {
|
|
ready: true,
|
|
classification: "runtime_durable_ready"
|
|
}
|
|
},
|
|
m3: {
|
|
persistenceContract: {
|
|
ready: false,
|
|
sequence: [
|
|
{ id: "write-do1-true", persisted: true, idsPresent: true },
|
|
{ id: "read-di1-true", persisted: false, idsPresent: true }
|
|
]
|
|
},
|
|
operationCount: 4
|
|
},
|
|
blockers: [
|
|
{
|
|
type: "runtime_blocker",
|
|
scope: "m3-durable-postflight",
|
|
status: "open",
|
|
reason: "M3 DO1 true/false durable postflight did not prove persisted operation/audit/evidence for every step.",
|
|
impact: "A later rollout could appear ready while losing evidence persistence.",
|
|
safeNextAction: "Repair durable audit/evidence persistence, then rerun DEV runtime postflight.",
|
|
retryable: true
|
|
}
|
|
],
|
|
safety: {
|
|
secretValuesPrinted: false,
|
|
readsKubernetesSecrets: false
|
|
}
|
|
};
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-postflight-blocked",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml",
|
|
"--write-report"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({ commandLog, runtimePostflightReport }),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const summary = JSON.parse(output);
|
|
const report = JSON.parse(await readFile(path.join(repoRoot, "reports/dev-gate/dev-cd-apply.json"), "utf8"));
|
|
const blocker = report.blockers.find((item) => item.scope === "runtime-durable-postflight");
|
|
assert.equal(code, 2);
|
|
assert.equal(summary.status, "blocked");
|
|
assert.equal(summary.devCdApply, undefined);
|
|
assert.equal(output.length < 12000, true);
|
|
assert.equal(report.status, "blocked");
|
|
assert.equal(report.devCdApply.steps.at(-1).id, "runtime-durable-postflight");
|
|
assert.equal(report.devCdApply.steps.at(-1).status, "blocked");
|
|
assert.ok(blocker);
|
|
assert.equal(blocker.reason, runtimePostflightReport.blockers[0].reason);
|
|
assert.equal(blocker.impact, runtimePostflightReport.blockers[0].impact);
|
|
assert.equal(blocker.safeNextAction, runtimePostflightReport.blockers[0].safeNextAction);
|
|
assert.equal(blocker.retryable, true);
|
|
assert.equal(blocker.evidence.blockerScope, "m3-durable-postflight");
|
|
assert.equal(report.devCdApply.liveVerify.status, "not_run");
|
|
assert.equal(JSON.stringify(report).includes("postgresql://"), false);
|
|
assert.equal(JSON.stringify(report).includes("SECRET"), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-runtime-postflight.mjs")), true);
|
|
});
|
|
|
|
test("runtime maintenance job reports are parsed after redacting secret-like log output", async () => {
|
|
const repoRoot = await makeRepo();
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-redaction",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml",
|
|
"--write-report",
|
|
"--full-output"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
runtimeJobLogSuffix: "debug postgresql://hwlab:SECRET_DB@db.example:5432/hwlab password=SECRET_PASSWORD token=SECRET_TOKEN\n"
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const report = JSON.parse(output);
|
|
const serialized = JSON.stringify(report);
|
|
assert.equal(code, 0);
|
|
assert.equal(report.status, "pass");
|
|
assert.equal(serialized.includes("SECRET_DB"), false);
|
|
assert.equal(serialized.includes("SECRET_PASSWORD"), false);
|
|
assert.equal(serialized.includes("SECRET_TOKEN"), false);
|
|
assert.equal(report.devCdApply.steps.find((step) => step.id === "runtime-db-provisioning").stdoutJson.safety.secretValuesPrinted, false);
|
|
assert.equal(report.devCdApply.steps.find((step) => step.id === "runtime-db-migration").stdoutJson.safety.secretValuesPrinted, false);
|
|
});
|
|
|
|
test("transaction can apply an already published deploy-json promotion without republishing control HEAD", async () => {
|
|
const repoRoot = await makeRepo({ commitId: "abc1234" });
|
|
const commandLog = [];
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-promotion",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml",
|
|
"--write-report",
|
|
"--full-output"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand({
|
|
commandLog,
|
|
targetCommitId: laterControlCommit,
|
|
headCommitId: laterControlCommit,
|
|
extraGitRefs: {
|
|
abc1234: publishedCommit
|
|
},
|
|
latestArtifactMergeCommit: artifactMergeCommit,
|
|
commitParents: {
|
|
[artifactMergeCommit]: [publishedCommit, artifactReportCommit],
|
|
[artifactReportCommit]: [publishedCommit]
|
|
}
|
|
}),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const report = JSON.parse(output);
|
|
assert.equal(code, 0);
|
|
assert.equal(report.status, "pass");
|
|
assert.equal(report.devCdApply.target.promotionCommit, publishedCommit);
|
|
assert.equal(report.devCdApply.target.promotionSource, "deploy-json");
|
|
assert.equal(report.devCdApply.target.publishRequired, false);
|
|
assert.equal(report.devCdApply.target.artifactBoundary.status, "pass");
|
|
assert.equal(report.devCdApply.target.artifactBoundary.artifactMergeCommit.shortCommitId, "fedcba9");
|
|
assert.deepEqual(
|
|
report.devCdApply.steps.map((step) => step.id),
|
|
[
|
|
"desired-state-check-existing-promotion",
|
|
"artifact-catalog-validate",
|
|
"runtime-db-provisioning",
|
|
"runtime-db-migration",
|
|
"dev-deploy-apply",
|
|
"runtime-durable-postflight"
|
|
]
|
|
);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/dev-artifact-publish.mjs")), false);
|
|
assert.equal(commandLog.some((entry) => entry.args.includes("scripts/refresh-artifact-catalog.mjs")), false);
|
|
assert.equal(commandLog.some((entry) =>
|
|
entry.args.includes("scripts/deploy-desired-state-plan.mjs") &&
|
|
entry.args.includes("--promotion-commit") &&
|
|
entry.args.includes(publishedCommit)
|
|
), true);
|
|
const applyCall = commandLog.find((entry) => entry.args.includes("scripts/dev-deploy-apply.mjs"));
|
|
assert.ok(applyCall?.env.HWLAB_CD_TRANSACTION_ID);
|
|
assert.equal(applyCall.env.HWLAB_CD_TRANSACTION_OWNER, "task-promotion");
|
|
assert.equal(report.devCdApply.liveVerify.status, "pass");
|
|
assert.equal(report.devCdApply.liveVerify.summary.commitMatches, 2);
|
|
});
|
|
|
|
test("apply stdout is concise by default while write-report keeps the full report", async () => {
|
|
const repoRoot = await makeRepo();
|
|
let output = "";
|
|
const code = await runDevCdApply([
|
|
"--apply",
|
|
"--confirm-dev",
|
|
"--confirmed-non-production",
|
|
"--owner-task-id",
|
|
"task-c",
|
|
"--kubeconfig",
|
|
"/etc/rancher/k3s/k3s.yaml",
|
|
"--write-report"
|
|
], {
|
|
repoRoot,
|
|
env: {},
|
|
runCommand: makeRunCommand(),
|
|
httpGetJson: makeHttpGetJson(),
|
|
now: () => new Date(iso(100000)),
|
|
stdout: { write: (chunk) => { output += chunk; } }
|
|
});
|
|
|
|
const summary = JSON.parse(output);
|
|
assert.equal(code, 0);
|
|
assert.equal(summary.status, "pass");
|
|
assert.equal(summary.devCdApply, undefined);
|
|
assert.equal(summary.fullOutputHint.includes("--full-output"), true);
|
|
assert.equal(summary.lock.acquired, true);
|
|
assert.equal(summary.steps.some((step) => step.id === "dev-deploy-apply"), true);
|
|
|
|
const writtenReport = JSON.parse(await readFile(path.join(repoRoot, "reports/dev-gate/dev-cd-apply.json"), "utf8"));
|
|
assert.equal(writtenReport.devCdApply.deployJson.before.manifest.environment, "dev");
|
|
});
|
|
|
|
test("live verify summarizes 16666 and 16667 health identity", async () => {
|
|
const result = await verifyDevLive({
|
|
now: () => new Date(iso()),
|
|
httpGetJson: makeHttpGetJson()
|
|
}, { expectedCommit: "abc1234" });
|
|
|
|
assert.equal(result.status, "pass");
|
|
assert.equal(result.summary.checked, 2);
|
|
assert.deepEqual(result.endpoints.map((endpoint) => endpoint.url), [
|
|
"http://74.48.78.17:16666/health/live",
|
|
"http://74.48.78.17:16667/health/live"
|
|
]);
|
|
assert.equal(result.endpoints.every((endpoint) => endpoint.commitMatches), true);
|
|
});
|
|
|
|
test("legacy artifact publish CLI rejects side effects without transaction env", async () => {
|
|
await assert.rejects(
|
|
execFileAsync(process.execPath, ["scripts/dev-artifact-publish.mjs", "--publish", "--no-report"], {
|
|
cwd: repoRoot,
|
|
env: {
|
|
...process.env,
|
|
HWLAB_CD_TRANSACTION_ID: ""
|
|
}
|
|
}),
|
|
(error) => {
|
|
assert.equal(error.code, 2);
|
|
const payload = JSON.parse(error.stdout);
|
|
assert.equal(payload.error, "cd-transaction-required");
|
|
assert.equal(payload.script, "scripts/dev-artifact-publish.mjs");
|
|
assert.equal(payload.mutationAttempted, false);
|
|
return true;
|
|
}
|
|
);
|
|
});
|