Files
pikasTech-HWLAB/scripts/src/deploy-desired-state-plan.mjs
T

1038 lines
40 KiB
JavaScript

import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import {
DEV_CODE_AGENT_PROVIDER_CONTRACT,
inspectCodeAgentProviderManifestRefs
} from "../../internal/cloud/code-agent-contract.ts";
import { DEV_DB_ENV_CONTRACT } from "../../internal/cloud/db-contract.ts";
import { tempReportPath } from "./report-paths.mjs";
import { readStructuredFile } from "./structured-config.mjs";
const execFileAsync = promisify(execFile);
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const deployPath = "deploy/deploy.yaml";
const catalogPath = "deploy/artifact-catalog.dev.json";
const workloadsPath = "deploy/k8s/base/workloads.yaml";
const artifactReportPath = tempReportPath("dev-artifacts.json");
const mutableTags = new Set(["latest", "dev", "main", "master", "prod", "production"]);
const mirrorEnvNames = [
"HWLAB_COMMIT_ID",
"HWLAB_IMAGE",
"HWLAB_IMAGE_TAG",
"HWLAB_SKILLS_COMMIT_ID",
"HWLAB_IMAGE_DIGEST"
];
const forbiddenDeployArtifactFields = new Set(["commitId", "image", "imageTag", "digest", "sourceCommitId", "repositoryDigest", "publishState"]);
const forbiddenDeployArtifactEnv = new Set(mirrorEnvNames.concat(["HWLAB_REVISION", "HWLAB_BUILD_CREATED_AT", "HWLAB_BUILD_SOURCE"]));
const commitMirrorEnvNames = new Set(["HWLAB_COMMIT_ID", "HWLAB_SKILLS_COMMIT_ID"]);
const imageMirrorEnvNames = new Set(["HWLAB_IMAGE"]);
const tagMirrorEnvNames = new Set(["HWLAB_IMAGE_TAG"]);
const digestMirrorEnvNames = new Set(["HWLAB_IMAGE_DIGEST"]);
const tagPattern = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
const commitPattern = /^[a-f0-9]{7,40}$/;
function parseArgs(argv) {
const args = {
check: false,
plan: false,
pretty: false,
targetRef: null,
targetTag: null,
promotionCommit: null,
help: false
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--check") {
args.check = true;
} else if (arg === "--plan") {
args.plan = true;
} else if (arg === "--pretty") {
args.pretty = true;
} else if (arg === "--target-ref") {
args.targetRef = requireValue(argv, index, arg);
index += 1;
} else if (arg === "--target-tag") {
args.targetTag = requireValue(argv, index, arg);
index += 1;
} else if (arg === "--promotion-commit") {
args.promotionCommit = requireValue(argv, index, arg);
index += 1;
} else if (arg === "--help" || arg === "-h") {
args.help = true;
} else {
throw new Error(`unknown argument: ${arg}`);
}
}
return args;
}
function requireValue(argv, index, arg) {
const value = argv[index + 1];
if (!value || value.startsWith("--")) {
throw new Error(`${arg} requires a value`);
}
return value;
}
function usage() {
return [
"usage: node scripts/deploy-desired-state-plan.mjs [--plan] [--check] [--pretty] [--target-ref REF] [--target-tag TAG] [--promotion-commit SHA]",
"",
"Read-only DEV desired-state commit/image planner.",
"",
"Options:",
" --plan explicitly request the default read-only JSON plan",
" --check exit non-zero when desired-state sources are invalid, drifted, or do not match an explicit target",
" --pretty print indented JSON",
" --target-ref REF resolve REF with git and review promotion to its short commit tag",
" --target-tag TAG review promotion to an explicit image tag without proving registry existence",
" --promotion-commit SHA",
" require every authoritative desired-state commit/image/tag field to match this promotion commit",
"",
"This command reads repository files only. It does not build, pull, push, kubectl apply, restart services, or touch PROD."
].join("\n");
}
async function readJson(repoRoot, relativePath) {
return readStructuredFile(repoRoot, relativePath);
}
async function readOptionalJson(repoRoot, relativePath) {
try {
return await readJson(repoRoot, relativePath);
} catch {
return null;
}
}
function parseImageRef(image) {
if (typeof image !== "string" || image.length === 0) {
return { valid: false, repository: null, tag: null, reason: "image must be a non-empty string" };
}
if (image.includes("@")) {
return { valid: false, repository: null, tag: null, reason: "digest-qualified image refs are not desired-state tags" };
}
const slashIndex = image.lastIndexOf("/");
const colonIndex = image.lastIndexOf(":");
if (colonIndex <= slashIndex) {
return { valid: false, repository: image, tag: null, reason: "image ref is missing a tag" };
}
const repository = image.slice(0, colonIndex);
const tag = image.slice(colonIndex + 1);
if (!repository || !tag) {
return { valid: false, repository, tag, reason: "image repository and tag must be non-empty" };
}
return { valid: true, repository, tag, reason: null };
}
function replaceImageTag(image, targetTag) {
const parsed = parseImageRef(image);
return parsed.valid ? `${parsed.repository}:${targetTag}` : null;
}
function shortCommit(value) {
return typeof value === "string" && value.length >= 7 ? value.slice(0, 7) : value;
}
function commitEquivalent(actual, expected) {
if (typeof actual !== "string" || typeof expected !== "string") return false;
return actual === expected || actual === expected.slice(0, 7) || expected === actual.slice(0, 7);
}
function listItems(document) {
if (!document) return [];
return document.kind === "List" ? document.items ?? [] : [document];
}
function serviceIdForWorkload(item, container) {
return (
item?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
item?.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
container?.name ||
item?.metadata?.name
);
}
function workloadContainers(item) {
return item?.spec?.template?.spec?.containers ?? [];
}
function envListToObject(envList) {
const env = {};
if (!Array.isArray(envList)) return env;
for (const entry of envList) {
if (!entry?.name) continue;
if (Object.hasOwn(entry, "value")) {
env[entry.name] = entry.value;
} else if (entry.valueFrom) {
env[entry.name] = { valueFrom: entry.valueFrom };
}
}
return env;
}
function addDiagnostic(ctx, diagnostic) {
ctx.diagnostics.push({
severity: "blocker",
...diagnostic
});
}
function addMismatch(ctx, code, sourcePath, message, expected, actual, extra = {}) {
addDiagnostic(ctx, {
code,
path: sourcePath,
message,
expected,
actual,
...extra
});
}
function recordObservation(ctx, observation) {
ctx.observations.push(observation);
}
function expectedEnvValue(name, service, desiredCommitId) {
if (name === "HWLAB_COMMIT_ID" || name === "HWLAB_SKILLS_COMMIT_ID") return service?.commitId ?? service?.imageTag ?? desiredCommitId;
if (name === "HWLAB_IMAGE") return service?.image ?? null;
if (name === "HWLAB_IMAGE_TAG") return service?.imageTag ?? null;
if (name === "HWLAB_IMAGE_DIGEST") return service?.digest ?? "not_published";
return null;
}
function observationKindForEnv(name) {
if (commitMirrorEnvNames.has(name)) return "commit";
if (imageMirrorEnvNames.has(name)) return "image";
if (tagMirrorEnvNames.has(name)) return "tag";
if (digestMirrorEnvNames.has(name)) return "digest";
return "unknown";
}
function valueMatchesExpected(kind, actual, expected) {
if (expected === null || expected === undefined) return true;
if (kind === "commit") return commitEquivalent(actual, expected);
return Object.is(actual, expected);
}
function inspectEnvMirrors(ctx, { source, basePath, serviceId, env, service, desiredCommitId, requiredMirrorNames = [] }) {
const present = {};
const missing = [];
const diagnostics = [];
const required = new Set(requiredMirrorNames);
for (const name of mirrorEnvNames) {
if (!Object.hasOwn(env ?? {}, name)) {
missing.push(name);
if (required.has(name)) {
const kind = observationKindForEnv(name);
const expected = expectedEnvValue(name, service, desiredCommitId);
const diagnostic = {
code: "missing_mirror",
path: `${basePath}.${name}`,
message: `${source} ${serviceId} must include ${name} so DEV applies own runtime ${kind} identity`,
expected,
actual: null
};
diagnostics.push(diagnostic);
addDiagnostic(ctx, diagnostic);
}
continue;
}
const actual = env[name];
const kind = observationKindForEnv(name);
const expected = expectedEnvValue(name, service, desiredCommitId);
const record = {
name,
value: actual,
expected,
status: valueMatchesExpected(kind, actual, expected) ? "match" : "drift"
};
present[name] = record;
recordObservation(ctx, {
source,
serviceId,
path: `${basePath}.${name}`,
field: name,
kind,
value: actual,
expected
});
if (record.status === "drift") {
const message = `${source} ${serviceId} ${name} mirror does not match desired ${kind}`;
const diagnostic = {
code: "mirror_mismatch",
path: `${basePath}.${name}`,
message,
expected,
actual
};
diagnostics.push(diagnostic);
addDiagnostic(ctx, diagnostic);
}
}
return { present, missing, diagnostics };
}
function inspectCodeAgentProviderDesiredState(ctx, deployService, workloadRecords) {
const workloadRecord = workloadRecords.find((workload) => workload.containerName === "hwlab-cloud-api") ?? workloadRecords[0] ?? null;
const inspection = inspectCodeAgentProviderManifestRefs({
deployEnv: deployService?.env ?? {},
workloadEnv: workloadRecord?.env ?? {}
});
if (!inspection.ready) {
addDiagnostic(ctx, {
code: "code_agent_provider_contract_mismatch",
path: "deploy.codeAgentProvider",
message: "hwlab-cloud-api desired state must preserve Code Agent provider Secret ref and DEV egress proxy env",
expected: {
provider: DEV_CODE_AGENT_PROVIDER_CONTRACT.codeAgentProvider,
secretRef: `${DEV_CODE_AGENT_PROVIDER_CONTRACT.secretRefs[0].secretName}/${DEV_CODE_AGENT_PROVIDER_CONTRACT.secretRefs[0].secretKey}`,
egressTarget: DEV_CODE_AGENT_PROVIDER_CONTRACT.egress.target
},
actual: {
missingDeployEnv: inspection.missingDeployEnv,
missingWorkloadEnv: inspection.missingWorkloadEnv,
deployMismatches: inspection.deployMismatches.map((entry) => entry.name),
workloadMismatches: inspection.workloadMismatches.map((entry) => entry.name),
missingSecretRefs: inspection.missingSecretRefs,
missingEgressContract: inspection.missingEgressContract,
missingCodexHomeContract: inspection.missingCodexHomeContract,
missingWorkspaceContract: inspection.missingWorkspaceContract,
missingNoProxyContract: inspection.missingNoProxyContract,
missingForwarderContract: inspection.missingForwarderContract
},
secretMaterialRead: false,
valuesRedacted: true
});
}
recordObservation(ctx, {
source: "deploy",
serviceId: "hwlab-cloud-api",
path: `${deployPath}.services.hwlab-cloud-api.env.OPENAI_API_KEY`,
field: "OPENAI_API_KEY",
kind: "secret-ref",
value: inspection.secretRef.present ? "secretRef:declared" : "missing_or_mismatched",
expected: "secretRef:hwlab-code-agent-provider/openai-api-key"
});
recordObservation(ctx, {
source: "workload",
serviceId: "hwlab-cloud-api",
path: `${workloadsPath}.hwlab-cloud-api.env.HWLAB_CODE_AGENT_OPENAI_BASE_URL`,
field: "HWLAB_CODE_AGENT_OPENAI_BASE_URL",
kind: "codex-api-forwarder",
value: inspection.egress.workloadMatchesDevProxy ? "pod-local-codex-api-forwarder" : "missing_or_mismatched",
expected: DEV_CODE_AGENT_PROVIDER_CONTRACT.egress.target
});
return inspection;
}
function dbSecretRefPlaceholder() {
const ref = DEV_DB_ENV_CONTRACT.secretRefs[0];
return `secretRef:${ref.secretName}/${ref.secretKey}`;
}
function secretRefMatches(entry, ref) {
return (
entry?.valueFrom?.secretKeyRef?.name === ref.secretName &&
entry?.valueFrom?.secretKeyRef?.key === ref.secretKey
);
}
function inspectCloudApiDbDesiredState(ctx, deployService, workloadRecords) {
const workloadRecord = workloadRecords.find((workload) => workload.containerName === "hwlab-cloud-api") ?? workloadRecords[0] ?? null;
const deployEnv = deployService?.env ?? {};
const workloadEnv = workloadRecord?.env ?? {};
const secretRef = DEV_DB_ENV_CONTRACT.secretRefs[0];
const expectedSecretRef = dbSecretRefPlaceholder();
const expectedSslMode = DEV_DB_ENV_CONTRACT.nonSecretDefaults.HWLAB_CLOUD_DB_SSL_MODE;
const deploySecretMatches = deployEnv.HWLAB_CLOUD_DB_URL === expectedSecretRef;
const workloadSecretMatches = secretRefMatches(workloadEnv.HWLAB_CLOUD_DB_URL, secretRef);
const deploySslMatches = deployEnv.HWLAB_CLOUD_DB_SSL_MODE === expectedSslMode;
const workloadSslMatches = workloadEnv.HWLAB_CLOUD_DB_SSL_MODE === expectedSslMode;
const ready = deploySecretMatches && workloadSecretMatches && deploySslMatches && workloadSslMatches;
if (!ready) {
addDiagnostic(ctx, {
code: "cloud_api_db_contract_mismatch",
path: "deploy.cloudApiDb",
message: "hwlab-cloud-api desired state must preserve DEV DB Secret ref and non-SSL runtime mode",
expected: {
secretRef: `${secretRef.secretName}/${secretRef.secretKey}`,
sslMode: expectedSslMode
},
actual: {
deploySecretRef: deploySecretMatches ? "secretRef:declared" : "missing_or_mismatched",
workloadSecretRef: workloadSecretMatches ? "secretRef:declared" : "missing_or_mismatched",
deploySslMode: deployEnv.HWLAB_CLOUD_DB_SSL_MODE ?? null,
workloadSslMode: workloadEnv.HWLAB_CLOUD_DB_SSL_MODE ?? null
},
secretMaterialRead: false,
valuesRedacted: true
});
}
recordObservation(ctx, {
source: "deploy",
serviceId: "hwlab-cloud-api",
path: `${deployPath}.services.hwlab-cloud-api.env.HWLAB_CLOUD_DB_SSL_MODE`,
field: "HWLAB_CLOUD_DB_SSL_MODE",
kind: "runtime-config",
value: deployEnv.HWLAB_CLOUD_DB_SSL_MODE ?? null,
expected: expectedSslMode
});
recordObservation(ctx, {
source: "workload",
serviceId: "hwlab-cloud-api",
path: `${workloadsPath}.hwlab-cloud-api.env.HWLAB_CLOUD_DB_SSL_MODE`,
field: "HWLAB_CLOUD_DB_SSL_MODE",
kind: "runtime-config",
value: workloadEnv.HWLAB_CLOUD_DB_SSL_MODE ?? null,
expected: expectedSslMode
});
return {
contractVersion: DEV_DB_ENV_CONTRACT.contractVersion,
environment: DEV_DB_ENV_CONTRACT.environment,
status: ready ? "pass" : "blocked",
ready,
secretRef: {
env: secretRef.env,
secretName: secretRef.secretName,
secretKey: secretRef.secretKey,
deployPlaceholderPresent: deploySecretMatches,
workloadSecretRefPresent: workloadSecretMatches,
present: deploySecretMatches && workloadSecretMatches,
redacted: true
},
sslMode: {
env: "HWLAB_CLOUD_DB_SSL_MODE",
value: deployEnv.HWLAB_CLOUD_DB_SSL_MODE ?? null,
workloadValue: workloadEnv.HWLAB_CLOUD_DB_SSL_MODE ?? null,
expected: expectedSslMode,
deployMatches: deploySslMatches,
workloadMatches: workloadSslMatches,
secret: false
},
secretMaterialRead: false,
valuesRedacted: true,
liveDbEvidence: false,
fixtureEvidence: false
};
}
function indexWorkloads(workloads) {
const byService = new Map();
for (const item of listItems(workloads)) {
for (const [containerIndex, container] of workloadContainers(item).entries()) {
const serviceId = serviceIdForWorkload(item, container);
if (!serviceId) continue;
const record = {
kind: item?.kind ?? "unknown",
name: item?.metadata?.name ?? "unknown",
namespace: item?.metadata?.namespace ?? null,
containerIndex,
containerName: container?.name ?? "unknown",
image: container?.image ?? null,
env: {
...envListToObject(container?.env),
__volumeMounts: Array.isArray(container?.volumeMounts) ? container.volumeMounts : [],
__volumes: Array.isArray(item?.spec?.template?.spec?.volumes) ? item.spec.template.spec.volumes : [],
__podContainers: Array.isArray(item?.spec?.template?.spec?.containers) ? item.spec.template.spec.containers : [],
__initContainers: Array.isArray(item?.spec?.template?.spec?.initContainers) ? item.spec.template.spec.initContainers : [],
__deploymentStrategy: item?.spec?.strategy ?? null
},
path: `${workloadsPath}.${item?.kind ?? "Workload"}.${item?.metadata?.name ?? serviceId}.containers[${containerIndex}]`
};
const list = byService.get(serviceId) ?? [];
list.push(record);
byService.set(serviceId, list);
}
}
return byService;
}
function serviceIdsFrom(...sources) {
const ids = [];
for (const source of sources) {
for (const id of source) {
if (typeof id === "string" && id.length > 0 && !ids.includes(id)) ids.push(id);
}
}
return ids;
}
function assertTargetTag(ctx, tag, pathName) {
if (!tagPattern.test(tag)) {
addMismatch(ctx, "invalid_target_tag", pathName, "target tag is not a valid Docker tag", "Docker tag syntax", tag);
}
if (mutableTags.has(tag)) {
addMismatch(ctx, "mutable_target_tag", pathName, `target tag ${tag} is mutable and forbidden for desired-state pinning`, "immutable tag", tag);
}
}
async function resolveTarget(repoRoot, { targetRef, targetTag, promotionCommit, requireTargetConvergence = false }, ctx) {
if (!targetRef && !targetTag && !promotionCommit) return null;
let commitId = null;
let shortCommitId = null;
let acceptedCurrentMain = null;
if (targetRef) {
try {
const result = await execFileAsync("git", ["rev-parse", "--verify", `${targetRef}^{commit}`], { cwd: repoRoot });
commitId = result.stdout.trim();
shortCommitId = commitId.slice(0, 7);
} catch (error) {
addDiagnostic(ctx, {
code: "target_ref_unresolved",
path: "--target-ref",
message: `target ref ${targetRef} could not be resolved with git`,
expected: "local git ref",
actual: error instanceof Error ? error.message : String(error)
});
}
}
if (promotionCommit) {
if (!commitPattern.test(String(promotionCommit))) {
addMismatch(ctx, "invalid_promotion_commit", "--promotion-commit", "promotion commit must be a short or full lowercase Git SHA", "7-40 lowercase hex", promotionCommit);
} else {
const promotionShortCommitId = promotionCommit.slice(0, 7);
if (commitId && !commitEquivalent(promotionCommit, commitId)) {
addMismatch(ctx, "promotion_commit_ref_mismatch", "--promotion-commit", "promotion commit must match the resolved target ref", commitId, promotionCommit);
}
commitId = commitId ?? promotionCommit;
shortCommitId = shortCommitId ?? promotionShortCommitId;
}
}
const tag = targetTag ?? shortCommitId;
if (tag) assertTargetTag(ctx, tag, "--target-tag");
if (promotionCommit && commitPattern.test(String(promotionCommit)) && targetTag && targetTag !== promotionCommit.slice(0, 7)) {
addMismatch(ctx, "promotion_tag_mismatch", "--target-tag", "promotion target tag must be the short promotion commit", promotionCommit.slice(0, 7), targetTag);
}
const requireConvergence = Boolean(promotionCommit || requireTargetConvergence);
if (targetRef && commitId && requireConvergence && !promotionCommit && !targetTag) {
acceptedCurrentMain = await resolveAcceptedCurrentMain(repoRoot, commitId);
}
const comparisonCommitId = acceptedCurrentMain?.commitId ?? commitId;
const comparisonTag = acceptedCurrentMain?.tag ?? tag;
return {
targetRef,
promotionCommit,
commitId,
shortCommitId,
tag,
tagSource: targetTag ? "target-tag" : promotionCommit ? "promotion-commit-short" : "target-ref-short-commit",
acceptedCurrentMain,
comparisonCommitId,
comparisonTag,
requireConvergence,
convergenceRequirement: promotionCommit ? "promotion-commit" : requireTargetConvergence ? "target-check" : "plan-only"
};
}
async function resolveAcceptedCurrentMain(repoRoot, targetCommitId) {
try {
const [{ stdout: headStdout }, { stdout: parentsStdout }] = await Promise.all([
execFileAsync("git", ["rev-parse", "--verify", "HEAD^{commit}"], { cwd: repoRoot }),
execFileAsync("git", ["rev-list", "--parents", "-n", "1", targetCommitId], { cwd: repoRoot })
]);
const headCommitId = headStdout.trim();
if (headCommitId !== targetCommitId) return null;
const [commitId, firstParentId, secondParentId] = parentsStdout.trim().split(/\s+/u);
if (commitId !== targetCommitId || !firstParentId || !secondParentId) return null;
return {
mode: "target-ref-head-merge-first-parent",
reason: "A merged main commit cannot contain desired-state files pinned to its own content hash; compare against the accepted first-parent main commit.",
commitId: firstParentId,
shortCommitId: firstParentId.slice(0, 7),
tag: firstParentId.slice(0, 7)
};
} catch {
return null;
}
}
function targetLabel(target) {
if (target.acceptedCurrentMain) return `${target.targetRef} accepted first parent (${target.acceptedCurrentMain.tag})`;
if (target.targetRef && target.tag) return `${target.targetRef} (${target.tag})`;
if (target.promotionCommit) return target.promotionCommit;
if (target.tag) return `tag ${target.tag}`;
return "target";
}
function observationMatchesTarget(observation, target) {
if (!target) return null;
if (observation.kind === "commit") {
return target.comparisonCommitId ? commitEquivalent(observation.value, target.comparisonCommitId) : null;
}
if (observation.kind === "tag") {
return target.comparisonTag ? observation.value === target.comparisonTag : null;
}
if (observation.kind === "image") {
return target.comparisonTag ? parseImageRef(observation.value).tag === target.comparisonTag : null;
}
return null;
}
function buildTargetConvergence(ctx, target) {
if (!target) return null;
if (target.targetRef && !target.commitId) {
return {
state: "target_unresolved",
comparableFields: 0,
matchingTargetFields: 0,
pendingTargetFields: 0,
matchingPaths: [],
pendingPaths: []
};
}
const comparable = ctx.observations
.map((observation) => ({
...observation,
matchesTarget: observationMatchesTarget(observation, target)
}))
.filter((observation) => observation.matchesTarget !== null);
const matching = comparable.filter((observation) => observation.matchesTarget === true);
const pending = comparable.filter((observation) => observation.matchesTarget === false);
const partial = matching.length > 0 && pending.length > 0;
const promotionRequired = target.requireConvergence === true;
if (partial) {
addDiagnostic(ctx, {
code: "partial_target_drift",
path: "desired-state",
message: "desired-state fields partially match the target while other authoritative fields still point elsewhere",
expected: target.tag,
actual: {
matching: matching.length,
pending: pending.length
}
});
} else if (promotionRequired && pending.length > 0) {
const promotionCommitRequired = Boolean(target.promotionCommit);
addDiagnostic(ctx, {
code: promotionCommitRequired ? "promotion_commit_mismatch" : "target_desired_state_mismatch",
path: "desired-state",
message: promotionCommitRequired
? "desired-state fields do not match the required promotion commit/tag"
: `stale artifact catalog is not pinned to ${targetLabel(target)}; explicit target checks require current-main/commit-pinned catalog identity`,
expected: {
commitId: target.comparisonCommitId,
imageTag: target.comparisonTag
},
actual: {
pending: pending.length,
samplePendingPaths: pending.slice(0, 8).map((item) => item.path)
}
});
}
const state = partial
? "partial_drift"
: pending.length === 0
? target.acceptedCurrentMain
? "accepted_main_promoted"
: "already_promoted"
: promotionRequired
? target.promotionCommit
? "promotion_mismatch"
: "target_mismatch"
: "promotion_pending";
return {
state,
comparableFields: comparable.length,
matchingTargetFields: matching.length,
pendingTargetFields: pending.length,
matchingPaths: matching.map((item) => item.path),
pendingPaths: pending.map((item) => item.path)
};
}
function reportHints(report, desiredCommitId) {
if (!report) {
return {
path: artifactReportPath,
present: false,
authoritative: false,
note: "optional report snapshot is absent; desired-state review still uses deploy files only"
};
}
const artifactPublish = report.artifactPublish ?? {};
const reportCommitId = report.commitId ?? null;
const artifactSourceCommitId = artifactPublish.sourceCommitId ?? null;
return {
path: artifactReportPath,
present: true,
authoritative: false,
note: "temporary publish JSON is contextual evidence only; deploy desired-state remains authoritative in deploy/",
commitId: reportCommitId,
matchesDesiredState: reportCommitId ? commitEquivalent(reportCommitId, desiredCommitId) : null,
artifactPublish: {
status: artifactPublish.status ?? null,
sourceCommitId: artifactSourceCommitId,
sourceMatchesDesiredState: artifactSourceCommitId ? commitEquivalent(artifactSourceCommitId, desiredCommitId) : null,
publishedCount: artifactPublish.publishedCount ?? null,
serviceCount: artifactPublish.serviceCount ?? null,
registryVerified: artifactPublish.registryVerified ?? null,
registryCapabilitiesClassification: artifactPublish.registryCapabilities?.classification ?? null
}
};
}
function servicePromotion(service, target) {
const targetTag = target?.comparisonTag ?? target?.tag;
if (!targetTag) return null;
return {
targetCommitId: target.comparisonCommitId ?? target.commitId,
targetImageTag: targetTag,
deployImage: null,
catalogImage: service.catalog?.image ? replaceImageTag(service.catalog.image, targetTag) : null,
workloadImages: (service.workloads ?? []).map((workload) => ({
workload: workload.name,
container: workload.container,
image: workload.image ? replaceImageTag(workload.image, targetTag) : null
}))
};
}
function targetCommands(target) {
if (!target?.targetRef && !target?.promotionCommit) return [];
const commands = [];
if (target.promotionCommit) {
commands.push(`node scripts/deploy-desired-state-plan.mjs --promotion-commit ${target.promotionCommit} --check --pretty`);
}
if (target.targetRef) {
commands.push(
`node scripts/deploy-desired-state-plan.mjs --target-ref ${target.targetRef} --pretty`,
`node scripts/refresh-artifact-catalog.mjs --target-ref ${target.targetRef} --blocked --no-write`,
`node scripts/refresh-artifact-catalog.mjs --target-ref ${target.targetRef} --publish-report ${artifactReportPath} --no-write`
);
}
return commands;
}
export async function buildDesiredStatePlan(options = {}) {
const repoRoot = options.repoRoot ?? defaultRepoRoot;
const ctx = { diagnostics: [], observations: [] };
const [deploy, catalog, workloads, artifactReport] = await Promise.all([
readJson(repoRoot, deployPath),
readJson(repoRoot, catalogPath),
readJson(repoRoot, workloadsPath),
readOptionalJson(repoRoot, artifactReportPath)
]);
const target = await resolveTarget(repoRoot, {
targetRef: options.targetRef ?? null,
targetTag: options.targetTag ?? null,
promotionCommit: options.promotionCommit ?? null,
requireTargetConvergence: options.requireTargetConvergence === true
}, ctx);
const deployServices = deploy.services ?? [];
const catalogServices = catalog.services ?? [];
const deployByService = new Map(deployServices.map((service) => [service.serviceId, service]));
const catalogByService = new Map(catalogServices.map((service) => [service.serviceId, service]));
const workloadByService = indexWorkloads(workloads);
const desiredCommitId = catalog.commitId;
const desiredImageTag = shortCommit(desiredCommitId);
recordObservation(ctx, {
source: "catalog",
path: `${catalogPath}.commitId`,
field: "commitId",
kind: "commit",
value: catalog.commitId,
expected: desiredCommitId
});
if (Object.hasOwn(deploy, "commitId")) {
addMismatch(ctx, "generated_artifact_identity", `${deployPath}.commitId`, "deploy.yaml is human-authored config; generated commit identity must stay in artifact catalog", "absent", deploy.commitId);
}
if (!commitPattern.test(String(catalog.commitId ?? ""))) {
addMismatch(ctx, "invalid_commit", `${catalogPath}.commitId`, "artifact catalog commitId must be a short or full lowercase Git SHA", "7-40 lowercase hex", catalog.commitId);
}
const serviceIds = serviceIdsFrom(
deployServices.map((service) => service.serviceId),
catalogServices.map((service) => service.serviceId),
[...workloadByService.keys()]
);
const services = [];
let codeAgentProvider = null;
let cloudApiDb = null;
for (const serviceId of serviceIds) {
const deployService = deployByService.get(serviceId);
const catalogService = catalogByService.get(serviceId);
const workloadsForService = workloadByService.get(serviceId) ?? [];
const service = {
serviceId,
deploy: null,
catalog: null,
workloads: [],
promotion: null
};
if (!deployService) {
addMismatch(ctx, "missing_service", `${deployPath}.services.${serviceId}`, `${serviceId} missing from deploy manifest`, "service entry", null, { serviceId });
}
if (!catalogService) {
addMismatch(ctx, "missing_service", `${catalogPath}.services.${serviceId}`, `${serviceId} missing from artifact catalog`, "service entry", null, { serviceId });
}
if (!workloadsForService.length) {
addMismatch(ctx, "missing_workload", `${workloadsPath}.${serviceId}`, `${serviceId} missing from k8s workloads`, "workload container", null, { serviceId });
}
const catalogImage = parseImageRef(catalogService?.image);
const serviceImageTag = catalogService?.imageTag ?? catalogImage.tag ?? desiredImageTag;
const serviceCommitId = catalogService?.commitId ?? serviceImageTag;
if (deployService) {
const deployPathBase = `${deployPath}.services.${serviceId}`;
for (const field of Object.keys(deployService)) {
if (forbiddenDeployArtifactFields.has(field)) {
addMismatch(ctx, "generated_artifact_identity", `${deployPathBase}.${field}`, `${serviceId} deploy service field is generated artifact identity and must stay in artifact catalog`, "absent", deployService[field], { serviceId });
}
}
for (const envName of Object.keys(deployService.env ?? {})) {
if (forbiddenDeployArtifactEnv.has(envName)) {
addMismatch(ctx, "generated_artifact_identity", `${deployPathBase}.env.${envName}`, `${serviceId} deploy env is generated artifact identity and must be rendered from artifact catalog`, "absent", deployService.env[envName], { serviceId });
}
}
service.deploy = {
artifactIdentity: "human-authored-config",
envMirrors: inspectEnvMirrors(ctx, {
source: "deploy",
basePath: `${deployPathBase}.env`,
serviceId,
env: deployService.env ?? {},
service: null,
desiredCommitId: null,
requiredMirrorNames: []
})
};
}
if (catalogService) {
const catalogPathBase = `${catalogPath}.services.${serviceId}`;
recordObservation(ctx, {
source: "catalog",
serviceId,
path: `${catalogPathBase}.commitId`,
field: "commitId",
kind: "commit",
value: catalogService.commitId,
expected: serviceCommitId
});
recordObservation(ctx, {
source: "catalog",
serviceId,
path: `${catalogPathBase}.image`,
field: "image",
kind: "image",
value: catalogService.image,
expected: catalogService.image
});
recordObservation(ctx, {
source: "catalog",
serviceId,
path: `${catalogPathBase}.imageTag`,
field: "imageTag",
kind: "tag",
value: catalogService.imageTag,
expected: serviceImageTag
});
if (!commitPattern.test(String(catalogService.commitId ?? ""))) {
addMismatch(ctx, "invalid_commit", `${catalogPathBase}.commitId`, `${serviceId} catalog service commitId must be a short or full lowercase Git SHA`, "7-40 lowercase hex", catalogService.commitId, { serviceId });
} else if (!commitEquivalent(catalogService.commitId, serviceImageTag)) {
addMismatch(ctx, "commit_mismatch", `${catalogPathBase}.commitId`, `${serviceId} catalog service commitId must match service artifact tag`, serviceImageTag, catalogService.commitId, { serviceId });
}
if (!catalogImage.valid) {
addMismatch(ctx, "invalid_image", `${catalogPathBase}.image`, catalogImage.reason, "tagged image ref", catalogService.image, { serviceId });
}
if (catalogService.imageTag !== serviceImageTag) {
addMismatch(ctx, "image_tag_mismatch", `${catalogPathBase}.imageTag`, `${serviceId} catalog imageTag must match service artifact tag`, serviceImageTag, catalogService.imageTag, { serviceId });
}
if (catalogImage.valid && catalogImage.tag !== catalogService.imageTag) {
addMismatch(ctx, "image_tag_mismatch", `${catalogPathBase}.image`, `${serviceId} catalog image tag must match catalog imageTag`, catalogService.imageTag, catalogImage.tag, { serviceId });
}
service.catalog = {
commitId: catalogService.commitId,
image: catalogService.image,
imageTag: catalogService.imageTag,
digest: catalogService.digest,
publishState: catalogService.publishState,
artifactRequired: catalogService.artifactRequired
};
}
for (const workload of workloadsForService) {
const workloadImage = parseImageRef(workload.image);
recordObservation(ctx, {
source: "workload",
serviceId,
path: `${workload.path}.image`,
field: "image",
kind: "image",
value: workload.image,
expected: catalogService?.image ?? null
});
if (!workloadImage.valid) {
addMismatch(ctx, "invalid_image", `${workload.path}.image`, workloadImage.reason, "tagged image ref", workload.image, { serviceId });
}
service.workloads.push({
kind: workload.kind,
name: workload.name,
namespace: workload.namespace,
container: workload.containerName,
image: workload.image,
imageTag: workloadImage.tag,
envMirrors: inspectEnvMirrors(ctx, {
source: "workload",
basePath: `${workload.path}.env`,
serviceId,
env: workload.env,
service: null,
desiredCommitId: null,
requiredMirrorNames: []
})
});
}
service.promotion = servicePromotion(service, target);
services.push(service);
if (serviceId === "hwlab-cloud-api") {
cloudApiDb = inspectCloudApiDbDesiredState(ctx, deployService, workloadsForService);
codeAgentProvider = inspectCodeAgentProviderDesiredState(ctx, deployService, workloadsForService);
}
}
const targetConvergence = buildTargetConvergence(ctx, target);
const blockers = ctx.diagnostics.filter((diagnostic) => diagnostic.severity === "blocker");
const status = blockers.length > 0
? "blocked"
: targetConvergence?.state === "promotion_pending"
? "planned"
: "pass";
const presentMirrorCount = services.reduce((count, service) => {
const deployCount = service.deploy ? Object.keys(service.deploy.envMirrors.present).length : 0;
const workloadCount = service.workloads.reduce((inner, workload) => inner + Object.keys(workload.envMirrors.present).length, 0);
return count + deployCount + workloadCount;
}, 0);
return {
kind: "hwlab-deploy-desired-state-plan",
mode: "read-only-plan",
status,
source: {
deploy: deployPath,
artifactCatalog: catalogPath,
workloads: workloadsPath,
optionalReport: artifactReportPath
},
safety: {
readOnly: true,
sourceOrDryRunSupportOnly: true,
kubectlApply: false,
registryPull: false,
registryPush: false,
imageBuild: false,
serviceRestart: false,
prod: false,
devLiveClaim: false
},
checkSemantics: "--check exits non-zero for missing/invalid desired-state sources, generated artifact identity leaking into deploy.yaml, invalid target tags, partial target drift, or catalog identity mismatch with an explicit --target-ref, --target-tag, or --promotion-commit. When the checked target ref is the current checked-out merge commit, the comparable current-main target is its accepted first parent because a commit cannot embed its own content hash. Without --check, a uniform older catalog under --target-ref/--target-tag is a read-only promotion plan.",
summary: {
desiredCommitId,
desiredImageTag,
artifactState: catalog.artifactState,
ciPublished: catalog.publish?.ciPublished === true,
registryVerified: catalog.publish?.registryVerified === true,
services: services.length,
workloadContainers: [...workloadByService.values()].reduce((sum, entries) => sum + entries.length, 0),
presentMirrorCount,
diagnostics: ctx.diagnostics.length,
blockers: blockers.length,
targetConvergence: targetConvergence?.state ?? "not_requested"
},
target: target ? {
...target,
convergence: targetConvergence,
commands: targetCommands(target)
} : null,
promotionBoundary: {
writes: [],
authoritativeDesiredState: [deployPath, catalogPath, workloadsPath],
humanAuthoredTruth: [deployPath, workloadsPath],
artifactIdentityTruth: [catalogPath],
nonAuthoritativeEvidence: [artifactReportPath],
note: "This planner is read-only. deploy.yaml is human-authored config; artifact-catalog carries generated image identity and is written by G14 Tekton promotion only to G14-gitops. It does not prove image existence, registry reachability, a real DEV apply, or M3 DEV-LIVE hardware-loop evidence."
},
cloudApiDb: cloudApiDb ?? {
status: "blocked",
ready: false,
secretMaterialRead: false,
valuesRedacted: true,
liveDbEvidence: false
},
codeAgentProvider: codeAgentProvider ?? {
status: "blocked",
ready: false,
missingDeployEnv: ["hwlab-cloud-api"],
missingWorkloadEnv: ["hwlab-cloud-api"],
secretMaterialRead: false,
valuesRedacted: true
},
reportHints: reportHints(artifactReport, desiredCommitId),
services,
diagnostics: ctx.diagnostics
};
}
export async function runDeployDesiredStatePlanCli(argv = process.argv.slice(2), options = {}) {
const args = parseArgs(argv);
if (args.help) {
process.stdout.write(`${usage()}\n`);
return;
}
const plan = await buildDesiredStatePlan({
repoRoot: options.repoRoot,
targetRef: args.targetRef,
targetTag: args.targetTag,
promotionCommit: args.promotionCommit,
requireTargetConvergence: args.check && Boolean(args.targetRef || args.targetTag)
});
process.stdout.write(`${JSON.stringify(plan, null, args.pretty ? 2 : 0)}\n`);
if (args.check && plan.status === "blocked") {
process.exitCode = 1;
}
}
export function writeDeployDesiredStatePlanError(error) {
process.stdout.write(`${JSON.stringify({
kind: "hwlab-deploy-desired-state-plan",
mode: "read-only-plan",
status: "error",
error: error instanceof Error ? error.message : String(error),
safety: {
readOnly: true,
kubectlApply: false,
registryPull: false,
registryPush: false,
imageBuild: false,
serviceRestart: false,
prod: false,
devLiveClaim: false
}
})}\n`);
process.exitCode = 1;
}