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; }