test: guard artifact runtime readiness

This commit is contained in:
Code Queue Review
2026-05-23 02:31:56 +00:00
parent 0a7311c3c2
commit e3c2537091
7 changed files with 1192 additions and 2 deletions
@@ -0,0 +1,890 @@
import { execFileSync } from "node:child_process";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { activeReportLifecycle } from "../../internal/dev-report-lifecycle.mjs";
import { DEV_ENDPOINT, DEV_FRONTEND_ENDPOINT } from "../../internal/protocol/index.mjs";
import { buildDesiredStatePlan } from "./deploy-desired-state-plan.mjs";
const defaultRepoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const defaultArtifactReport = "reports/dev-gate/dev-artifacts.json";
const defaultArtifactCatalog = "deploy/artifact-catalog.dev.json";
const defaultRuntimeReport = "reports/dev-gate/dev-cloud-workbench-live.json";
const defaultOutputReport = "reports/dev-gate/dev-artifact-runtime-readiness.json";
const defaultTargetRef = "origin/main";
const guardCommand = `node scripts/artifact-runtime-readiness-guard.mjs --target-ref ${defaultTargetRef} --check --no-report`;
const expectedBlockedGuardCommand = `${guardCommand} --expect-blocked`;
const digestPattern = /^sha256:[a-f0-9]{64}$/u;
const commitPattern = /^[a-f0-9]{7,40}$/u;
export function normalizeCommit(value) {
if (typeof value !== "string") return "unknown";
const trimmed = value.trim().toLowerCase();
return commitPattern.test(trimmed) ? trimmed : "unknown";
}
export function shortCommit(value) {
const commit = normalizeCommit(value);
return commit === "unknown" ? "unknown" : commit.slice(0, 7);
}
export function commitsMatch(left, right) {
const a = normalizeCommit(left);
const b = normalizeCommit(right);
return a !== "unknown" && b !== "unknown" && (a === b || a.startsWith(b) || b.startsWith(a));
}
export function evaluateArtifactRuntimeReadiness({
artifactReport,
artifactCatalog,
desiredStatePlan,
runtimeReport,
liveRuntime,
targetRef,
targetCommitId
}) {
const target = {
ref: targetRef,
commitId: normalizeCommit(targetCommitId),
shortCommitId: shortCommit(targetCommitId)
};
const artifact = summarizeArtifactReport(artifactReport);
const catalog = summarizeArtifactCatalog(artifactCatalog);
const desiredState = summarizeDesiredStatePlan(desiredStatePlan, target);
const runtime = summarizeRuntimeSources(runtimeReport, liveRuntime);
const apiRuntime = runtime.api.effective;
const cloudWebRuntime = runtime.cloudWeb.effective;
const checks = [];
addCheck(checks, {
id: "target-ref-resolved",
pass: target.commitId !== "unknown",
type: "contract_blocker",
summary: `Target ${target.ref} resolves to ${target.shortCommitId}.`,
blockedSummary: `Target ${target.ref} could not be resolved to a Git commit.`,
nextTask: "Fetch origin/main and rerun this guard before preparing any DEV apply."
});
addCheck(checks, {
id: "artifact-report-published",
pass: artifact.publishVerified,
type: "runtime_blocker",
summary: `Artifact report status=${artifact.status}; required published=${artifact.publishedRequiredCount}/${artifact.requiredServiceCount}; sha256=${artifact.digestCounts.sha256}; invalid=${artifact.digestCounts.invalid}.`,
nextTask: "Complete the DEV artifact publish workflow and keep real sha256 digests for every required service."
});
addCheck(checks, {
id: "artifact-report-target-match",
pass: commitsMatch(artifact.sourceCommitId, target.commitId),
type: "observability_blocker",
summary: `Artifact report source=${artifact.shortCommitId}; target=${target.shortCommitId}.`,
nextTask: "Publish artifacts for latest origin/main or keep rollout readiness blocked."
});
addCheck(checks, {
id: "artifact-catalog-published",
pass: catalog.publishVerified,
type: "runtime_blocker",
summary: `Artifact catalog state=${catalog.artifactState}; ciPublished=${catalog.ciPublished}; registryVerified=${catalog.registryVerified}; sha256=${catalog.digestCounts.sha256}; not_published=${catalog.digestCounts.notPublished}; invalid=${catalog.digestCounts.invalid}.`,
nextTask: "Refresh deploy/artifact-catalog.dev.json only from a successful DEV artifact publish report."
});
addCheck(checks, {
id: "artifact-catalog-target-match",
pass: commitsMatch(catalog.commitId, target.commitId),
type: "observability_blocker",
summary: `Artifact catalog commit=${catalog.shortCommitId}; target=${target.shortCommitId}.`,
nextTask: "Refresh desired-state and artifact catalog identity to latest origin/main before apply."
});
addCheck(checks, {
id: "artifact-report-catalog-match",
pass: commitsMatch(artifact.sourceCommitId, catalog.commitId),
type: "observability_blocker",
summary: `Artifact report source=${artifact.shortCommitId}; catalog=${catalog.shortCommitId}.`,
nextTask: "Keep reports/dev-gate/dev-artifacts.json and deploy/artifact-catalog.dev.json aligned to the same artifact source."
});
addCheck(checks, {
id: "desired-state-internal-valid",
pass: desiredState.status !== "blocked" && desiredState.status !== "missing",
type: "contract_blocker",
summary: `Desired-state planner status=${desiredState.status}; convergence=${desiredState.targetConvergence.state}.`,
nextTask: "Repair deploy/deploy.json, deploy/artifact-catalog.dev.json, and deploy/k8s/base/workloads.yaml before any apply."
});
addCheck(checks, {
id: "desired-state-target-match",
pass: desiredState.matchesTarget,
type: "observability_blocker",
summary: `Desired-state deploy=${desiredState.deployCommitId}; target=${target.shortCommitId}; convergence=${desiredState.targetConvergence.state}; pending=${desiredState.targetConvergence.pendingTargetFields}.`,
nextTask: `Run node scripts/deploy-desired-state-plan.mjs --promotion-commit ${target.shortCommitId} --check after refreshing desired-state for latest origin/main.`
});
addCheck(checks, {
id: "rollout-read-access",
pass: runtime.live.mode === "live-read-only-http",
type: "observability_blocker",
summary: `Runtime rollout-read mode=${runtime.live.mode}.`,
nextTask: "Run this guard with read-only DEV endpoint access; SOURCE/local/static checks cannot claim latest-main deployment."
});
addCheck(checks, {
id: "api-runtime-identity-observed",
pass: apiRuntime.observed,
type: "observability_blocker",
summary: `API /health/live identity status=${apiRuntime.status}; source=${apiRuntime.evidenceSource}; commit=${apiRuntime.shortCommitId}.`,
nextTask: "Restore read-only access to Cloud API /health/live before treating DEV runtime identity as observed."
});
addCheck(checks, {
id: "api-runtime-target-match",
pass: apiRuntime.observed && commitsMatch(apiRuntime.commitId, target.commitId),
type: "runtime_blocker",
summary: `API /health/live commit=${apiRuntime.shortCommitId}; target=${target.shortCommitId}.`,
nextTask: "After publish and desired-state refresh, run the authorized DEV rollout path and re-observe /health/live."
});
addCheck(checks, {
id: "api-runtime-artifact-match",
pass: apiRuntime.observed && commitsMatch(apiRuntime.commitId, catalog.commitId) && commitsMatch(apiRuntime.commitId, artifact.sourceCommitId),
type: "runtime_blocker",
summary: `API /health/live commit=${apiRuntime.shortCommitId}; catalog=${catalog.shortCommitId}; artifact=${artifact.shortCommitId}.`,
nextTask: "Do not treat a healthy API route as current unless runtime, artifact report, and catalog identity all match."
});
addCheck(checks, {
id: "cloud-web-runtime-identity-observed",
pass: cloudWebRuntime.observed,
type: "observability_blocker",
summary: `Cloud Web :16666 /health/live identity status=${cloudWebRuntime.status}; source=${cloudWebRuntime.evidenceSource}; commit=${cloudWebRuntime.shortCommitId}.`,
nextTask: "Observe Cloud Web /health/live on :16666; HTML and static asset checks alone do not prove the deployed artifact revision."
});
addCheck(checks, {
id: "cloud-web-runtime-target-match",
pass: cloudWebRuntime.observed && commitsMatch(cloudWebRuntime.commitId, target.commitId),
type: "runtime_blocker",
summary: `Cloud Web :16666 revision=${cloudWebRuntime.shortCommitId}; target=${target.shortCommitId}.`,
nextTask: "Roll out the latest Cloud Web artifact through the authorized DEV path, then re-check :16666 /health/live."
});
addCheck(checks, {
id: "cloud-web-runtime-artifact-match",
pass: cloudWebRuntime.observed && commitsMatch(cloudWebRuntime.commitId, catalog.commitId) && commitsMatch(cloudWebRuntime.commitId, artifact.sourceCommitId),
type: "runtime_blocker",
summary: `Cloud Web revision=${cloudWebRuntime.shortCommitId}; catalog=${catalog.shortCommitId}; artifact=${artifact.shortCommitId}.`,
nextTask: "Block rollout claims until the Cloud Web served runtime identity matches the artifact catalog and publish report."
});
addCheck(checks, {
id: "cloud-web-published-build-freshness",
pass: artifact.cloudWeb.published && digestPattern.test(artifact.cloudWeb.digest) && artifact.cloudWeb.distFreshnessStatus === "pass",
type: "runtime_blocker",
summary: `Cloud Web artifact status=${artifact.cloudWeb.status}; imageTag=${artifact.cloudWeb.imageTag}; digest=${artifact.cloudWeb.digestStatus}; distFreshness=${artifact.cloudWeb.distFreshnessStatus}.`,
nextTask: "Run the Cloud Web build before publish and keep dist freshness evidence in the artifact publish report."
});
addCheck(checks, {
id: "source-local-static-not-authoritative",
pass: true,
type: "safety_blocker",
summary: "SOURCE, LOCAL, STATIC, and report snapshots remain non-authoritative until runtime and artifact identity checks above pass.",
nextTask: "Use this guard before apply and after any authorized rollout; never infer deployed latest-main from local source checks."
});
const blockers = checks.filter((item) => item.status === "blocked");
return {
target,
artifact,
catalog,
desiredState,
runtime,
ready: blockers.length === 0,
canInferFromSourceLocalStatic: false,
checks,
blockers
};
}
export function buildArtifactRuntimeReadinessReport({
artifactReport,
artifactCatalog,
desiredStatePlan,
runtimeReport,
liveRuntime,
targetRef,
targetCommitId,
generatedFromCommit = "unknown"
}) {
const readiness = evaluateArtifactRuntimeReadiness({
artifactReport,
artifactCatalog,
desiredStatePlan,
runtimeReport,
liveRuntime,
targetRef,
targetCommitId
});
const status = readiness.ready ? "pass" : "blocked";
return {
$schema: "https://hwlab.pikastech.local/schemas/dev-gate-report.schema.json",
$id: "https://hwlab.pikastech.local/reports/dev-gate/dev-artifact-runtime-readiness.json",
reportVersion: "v1",
status,
issue: "pikasTech/HWLAB#164",
supports: [
"pikasTech/HWLAB#7",
"pikasTech/HWLAB#78",
"pikasTech/HWLAB#131"
],
taskId: "dev-artifact-runtime-readiness",
commitId: shortCommit(generatedFromCommit),
acceptanceLevel: "dev_artifact_runtime_readiness",
devOnly: true,
prodDisabled: true,
generatedAt: new Date().toISOString(),
evidenceLevel: status === "pass" ? "DEV-LIVE-RUNTIME-IDENTITY-NON-M3-M4-M5" : "BLOCKED",
devLive: status === "pass",
reportLifecycle: activeReportLifecycle("Current latest-main DEV artifact/runtime readiness guard; SOURCE and static checks cannot imply deployed 16666 readiness."),
sourceContract: {
status: "pass",
documents: [
"docs/dev-artifact-publish.md",
"docs/dev-deploy-apply.md",
"docs/reference/deployment-publish.md"
],
summary: "Target commit, artifact publish/catalog identity, desired-state convergence, API runtime identity, and Cloud Web served runtime identity are separate gates."
},
validationCommands: [
"node --check scripts/artifact-runtime-readiness-guard.mjs",
"node --check scripts/src/artifact-runtime-readiness-guard.mjs",
"node --test scripts/artifact-runtime-readiness-guard.test.mjs",
guardCommand,
expectedBlockedGuardCommand
],
localSmoke: {
status,
commands: [expectedBlockedGuardCommand],
evidence: [
defaultArtifactReport,
defaultArtifactCatalog,
defaultRuntimeReport,
`${DEV_FRONTEND_ENDPOINT}/health/live`,
`${DEV_ENDPOINT}/health/live`
],
summary: "The guard is read-only and blocks unless artifact, desired-state, API runtime, and Cloud Web runtime identities all align."
},
dryRun: {
status: "not_applicable",
commands: [guardCommand],
evidence: ["No deploy, publish, apply, restart, k8s mutation, PROD access, or secret read is performed."],
summary: "This is a readiness guard, not a deployment dry-run or rollout substitute."
},
devPreconditions: {
status,
requirements: [
"Latest origin/main must resolve to a concrete target commit.",
"DEV artifact publish report and artifact catalog must match the target and contain real registry digests.",
"Desired-state deploy, catalog, workload image, and mirror env fields must converge to the target commit before apply.",
"Cloud API /health/live must report the target runtime identity.",
"Cloud Web :16666 /health/live must report the target served artifact identity.",
"M3/M4/M5 acceptance remains out of scope for this guard."
],
commands: [guardCommand],
summary: readiness.ready
? "Latest-main artifact, desired-state, API runtime, and Cloud Web runtime identity are aligned."
: "Latest-main DEV rollout readiness is blocked; see actionable blockers."
},
artifactRuntimeReadiness: {
version: "v1",
artifactReportPath: defaultArtifactReport,
artifactCatalogPath: defaultArtifactCatalog,
runtimeReportPath: defaultRuntimeReport,
...readiness
},
blockedReasons: readiness.blockers.map((item) => ({
scope: item.id,
type: item.type,
summary: item.summary,
nextTask: item.nextTask
})),
blockers: readiness.blockers.map((item) => ({
type: item.type,
scope: item.id,
status: "open",
summary: item.summary,
nextTask: item.nextTask
})),
safety: {
readOnly: true,
sourceOnlyWhenBlocked: true,
publicHttpGetOnly: readiness.runtime.live.mode === "live-read-only-http",
kubectlApply: false,
k8sMutation: false,
registryPush: false,
imageBuild: false,
serviceRestart: false,
prodTouched: false,
secretsRead: false,
canInferReadinessFromSourceLocalStatic: false,
statement: "No SOURCE, LOCAL, STATIC, or report-only check may claim deployed latest-main without matching artifact, desired-state, API runtime, and Cloud Web runtime identity."
}
};
}
export async function observeLiveRuntimeIdentities({ live = true, timeoutMs = 5000 } = {}) {
const endpoints = {
api: new URL("/health/live", DEV_ENDPOINT).toString(),
cloudWeb: new URL("/health/live", DEV_FRONTEND_ENDPOINT).toString()
};
if (!live) {
return {
mode: "disabled",
endpoints,
api: notObservedRuntimeIdentity(endpoints.api, "live observation disabled by --no-live", "live-http"),
cloudWeb: notObservedRuntimeIdentity(endpoints.cloudWeb, "live observation disabled by --no-live", "live-http")
};
}
const [api, cloudWeb] = await Promise.all([
observeHealthLive(endpoints.api, { evidenceSource: "live-http", timeoutMs }),
observeHealthLive(endpoints.cloudWeb, { evidenceSource: "live-http", timeoutMs })
]);
return {
mode: "live-read-only-http",
endpoints,
api,
cloudWeb
};
}
export async function runArtifactRuntimeReadinessGuardCli(argv = process.argv.slice(2), options = {}) {
const repoRoot = options.repoRoot ?? defaultRepoRoot;
const args = parseArgs(argv);
if (args.help) {
process.stdout.write(`${usage()}\n`);
return 0;
}
const [artifactReport, artifactCatalog, runtimeReport, liveRuntime] = await Promise.all([
readOptionalJson(repoRoot, args.artifactReport),
readOptionalJson(repoRoot, args.artifactCatalog),
readOptionalJson(repoRoot, args.runtimeReport),
observeLiveRuntimeIdentities({ live: args.live, timeoutMs: args.timeoutMs })
]);
const targetCommitId = resolveTargetCommit(repoRoot, args.targetRef);
const desiredStatePlan = await buildDesiredStatePlanOrBlocked(repoRoot, args.targetRef);
const report = buildArtifactRuntimeReadinessReport({
artifactReport,
artifactCatalog,
desiredStatePlan,
runtimeReport,
liveRuntime,
targetRef: args.targetRef,
targetCommitId,
generatedFromCommit: resolveTargetCommit(repoRoot, "HEAD")
});
if (args.writeReport) {
const outputPath = path.join(repoRoot, args.report);
await mkdir(path.dirname(outputPath), { recursive: true });
await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
}
const output = args.check ? summaryForCheck(report) : report;
process.stdout.write(`${JSON.stringify(output, null, args.pretty || args.check ? 2 : 0)}\n`);
if (args.expectBlocked) {
return report.status === "blocked" ? 0 : 2;
}
if (args.check && report.status !== "pass") {
return 2;
}
return 0;
}
export function formatArtifactRuntimeReadinessFailure(error) {
return {
kind: "hwlab-artifact-runtime-readiness",
status: "error",
error: error instanceof Error ? error.message : String(error),
safety: {
readOnly: true,
kubectlApply: false,
k8sMutation: false,
registryPush: false,
imageBuild: false,
serviceRestart: false,
prodTouched: false,
secretsRead: false
}
};
}
async function buildDesiredStatePlanOrBlocked(repoRoot, targetRef) {
try {
return await buildDesiredStatePlan({ repoRoot, targetRef });
} catch (error) {
return {
kind: "hwlab-deploy-desired-state-plan",
status: "blocked",
summary: {
desiredCommitId: "unknown",
diagnostics: 1,
blockers: 1
},
target: {
targetRef,
convergence: {
state: "planner_error",
comparableFields: 0,
matchingTargetFields: 0,
pendingTargetFields: 0
}
},
diagnostics: [{
severity: "blocker",
code: "planner_error",
message: error instanceof Error ? error.message : String(error)
}]
};
}
}
function summarizeArtifactReport(report) {
const publish = report?.artifactPublish;
if (!publish || typeof publish !== "object") {
return missingArtifactReport();
}
const services = Array.isArray(publish.services) ? publish.services : [];
const required = services.filter((service) => service?.artifactRequired === true);
const cloudWeb = services.find((service) => service?.serviceId === "hwlab-cloud-web") ?? null;
const digestCounts = countDigests(required);
const publishedRequiredCount = required.filter((service) => service?.status === "published").length;
const requiredServiceCount = Number.isInteger(publish.requiredServiceCount)
? publish.requiredServiceCount
: required.length;
const allRequiredServicesPublished = requiredServiceCount > 0 &&
publishedRequiredCount === requiredServiceCount &&
digestCounts.sha256 === requiredServiceCount;
return {
status: sanitizeString(publish.status),
sourceCommitId: normalizeCommit(publish.sourceCommitId ?? report.commitId),
shortCommitId: shortCommit(publish.sourceCommitId ?? report.commitId),
registryPrefix: sanitizeString(publish.registryPrefix),
serviceCount: Number.isInteger(publish.serviceCount) ? publish.serviceCount : services.length,
requiredServiceCount,
publishedRequiredCount,
allRequiredServicesPublished,
publishVerified: publish.status === "published" && allRequiredServicesPublished,
digestCounts,
cloudWeb: summarizeArtifactService(cloudWeb)
};
}
function summarizeArtifactCatalog(catalog) {
if (!catalog || typeof catalog !== "object") {
return missingArtifactCatalog();
}
const services = Array.isArray(catalog.services) ? catalog.services : [];
const required = services.filter((service) => service?.artifactRequired === true);
const digestCounts = countDigests(required);
const publishedRequiredCount = required.filter((service) => service?.publishState === "published").length;
const allRequiredServicesPublished = required.length > 0 &&
publishedRequiredCount === required.length &&
digestCounts.sha256 === required.length;
return {
artifactState: sanitizeString(catalog.artifactState),
commitId: normalizeCommit(catalog.commitId),
shortCommitId: shortCommit(catalog.commitId),
ciPublished: catalog.publish?.ciPublished === true,
registryVerified: catalog.publish?.registryVerified === true,
serviceCount: services.length,
requiredServiceCount: required.length,
publishedRequiredCount,
digestCounts,
publishVerified: catalog.artifactState === "published" &&
catalog.publish?.ciPublished === true &&
catalog.publish?.registryVerified === true &&
allRequiredServicesPublished
};
}
function summarizeDesiredStatePlan(plan, target) {
if (!plan || typeof plan !== "object") {
return {
status: "missing",
deployCommitId: "unknown",
shortCommitId: "unknown",
targetConvergence: emptyConvergence("missing"),
matchesTarget: false
};
}
const deployCommitId = normalizeCommit(plan.summary?.desiredCommitId);
const targetConvergence = plan.target?.convergence ?? emptyConvergence("not_requested");
const pendingTargetFields = Number.isInteger(targetConvergence.pendingTargetFields)
? targetConvergence.pendingTargetFields
: 0;
return {
status: sanitizeString(plan.status),
deployCommitId: shortCommit(deployCommitId),
fullDeployCommitId: deployCommitId,
targetConvergence: {
state: sanitizeString(targetConvergence.state),
comparableFields: Number.isInteger(targetConvergence.comparableFields) ? targetConvergence.comparableFields : 0,
matchingTargetFields: Number.isInteger(targetConvergence.matchingTargetFields) ? targetConvergence.matchingTargetFields : 0,
pendingTargetFields
},
matchesTarget: commitsMatch(deployCommitId, target.commitId) && pendingTargetFields === 0
};
}
function summarizeRuntimeSources(runtimeReport, liveRuntime) {
const reportApi = normalizeRuntimeIdentity(runtimeReport?.runtimeIdentity, "report-snapshot");
const live = liveRuntime ?? {
mode: "missing",
endpoints: {},
api: null,
cloudWeb: null
};
return {
live: {
mode: sanitizeString(live.mode),
endpoints: live.endpoints ?? {}
},
reportSnapshot: {
api: reportApi,
status: sanitizeString(runtimeReport?.status)
},
api: {
live: normalizeRuntimeIdentity(live.api, "live-http"),
reportSnapshot: reportApi,
effective: normalizeRuntimeIdentity(live.api, "live-http")
},
cloudWeb: {
live: normalizeRuntimeIdentity(live.cloudWeb, "live-http"),
reportSnapshot: normalizeRuntimeIdentity(runtimeReport?.cloudWebRuntimeIdentity, "report-snapshot"),
effective: normalizeRuntimeIdentity(live.cloudWeb, "live-http")
}
};
}
function summarizeArtifactService(service) {
if (!service) {
return {
status: "missing",
published: false,
imageTag: "unknown",
digest: "unknown",
digestStatus: "missing",
distFreshnessStatus: "missing"
};
}
const digest = sanitizeString(service.digest);
return {
status: sanitizeString(service.status),
published: service.status === "published",
imageTag: sanitizeString(service.imageTag),
digest,
digestStatus: digestPattern.test(digest) ? "sha256" : digest === "not_published" ? "not_published" : "invalid",
distFreshnessStatus: service.distFreshness?.status ? sanitizeString(service.distFreshness.status) : "missing"
};
}
function countDigests(services) {
const counts = {
sha256: 0,
notPublished: 0,
invalid: 0
};
for (const service of services) {
const digest = service?.digest;
if (digestPattern.test(String(digest ?? ""))) {
counts.sha256 += 1;
} else if (digest === "not_published" || digest == null) {
counts.notPublished += 1;
} else {
counts.invalid += 1;
}
}
return counts;
}
async function observeHealthLive(endpoint, { evidenceSource, timeoutMs }) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(endpoint, {
method: "GET",
headers: { accept: "application/json" },
signal: controller.signal
});
const text = await response.text();
const body = parseJson(text);
if (!response.ok || !body) {
return notObservedRuntimeIdentity(
endpoint,
body ? `HTTP ${response.status}` : `HTTP ${response.status}; response was not JSON`,
evidenceSource,
response.status
);
}
return observedRuntimeIdentity(endpoint, body, evidenceSource, response.status);
} catch (error) {
return notObservedRuntimeIdentity(
endpoint,
error instanceof Error ? error.message : String(error),
evidenceSource
);
} finally {
clearTimeout(timeout);
}
}
function observedRuntimeIdentity(endpoint, body, evidenceSource, httpStatus) {
const commitId = body.commit?.id ?? body.commitId ?? body.revision ?? body.runtime?.commitId;
const imageTag = body.image?.tag ?? body.imageTag ?? body.runtime?.imageTag;
const imageDigest = body.image?.digest ?? body.imageDigest ?? body.runtime?.imageDigest;
return {
status: "observed",
observed: true,
source: "health-live",
evidenceSource,
endpoint,
serviceId: sanitizeString(body.serviceId),
environment: sanitizeString(body.environment),
healthStatus: sanitizeString(body.status),
artifactKind: sanitizeString(body.artifactKind),
commitId: normalizeCommit(commitId),
shortCommitId: shortCommit(commitId),
commitSource: sanitizeString(body.commit?.source ?? body.commitSource ?? (body.revision ? "runtime-env" : "unknown")),
imageTag: sanitizeString(imageTag),
imageDigest: sanitizeString(imageDigest),
observedAt: sanitizeTimestamp(body.observedAt),
httpStatus,
reason: null
};
}
function notObservedRuntimeIdentity(endpoint, reason, evidenceSource, httpStatus = null) {
return {
status: "not_observed",
observed: false,
source: "health-live",
evidenceSource,
endpoint,
serviceId: "not_observed",
environment: "not_observed",
healthStatus: "not_observed",
artifactKind: "not_observed",
commitId: "unknown",
shortCommitId: "unknown",
commitSource: "not_observed",
imageTag: "not_observed",
imageDigest: "not_observed",
observedAt: new Date().toISOString(),
httpStatus,
reason: oneLine(reason)
};
}
function normalizeRuntimeIdentity(identity, evidenceSource) {
if (!identity || identity.status !== "observed") {
return notObservedRuntimeIdentity(identity?.endpoint ?? "unknown", identity?.reason ?? "runtime identity was not observed", evidenceSource);
}
return {
...identity,
observed: true,
evidenceSource: identity.evidenceSource ?? evidenceSource,
commitId: normalizeCommit(identity.commitId ?? identity.revision),
shortCommitId: shortCommit(identity.commitId ?? identity.revision),
serviceId: sanitizeString(identity.serviceId),
environment: sanitizeString(identity.environment),
healthStatus: sanitizeString(identity.healthStatus ?? identity.status)
};
}
function missingArtifactReport() {
return {
status: "missing",
sourceCommitId: "unknown",
shortCommitId: "unknown",
registryPrefix: "unknown",
serviceCount: 0,
requiredServiceCount: 0,
publishedRequiredCount: 0,
allRequiredServicesPublished: false,
publishVerified: false,
digestCounts: { sha256: 0, notPublished: 0, invalid: 0 },
cloudWeb: summarizeArtifactService(null)
};
}
function missingArtifactCatalog() {
return {
artifactState: "missing",
commitId: "unknown",
shortCommitId: "unknown",
ciPublished: false,
registryVerified: false,
serviceCount: 0,
requiredServiceCount: 0,
publishedRequiredCount: 0,
digestCounts: { sha256: 0, notPublished: 0, invalid: 0 },
publishVerified: false
};
}
function emptyConvergence(state) {
return {
state,
comparableFields: 0,
matchingTargetFields: 0,
pendingTargetFields: 0
};
}
function addCheck(checks, { id, pass, type, summary, blockedSummary = null, nextTask }) {
checks.push({
id,
status: pass ? "pass" : "blocked",
type,
summary: pass ? summary : (blockedSummary ?? summary),
nextTask
});
}
async function readJson(repoRoot, relativePath) {
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
return JSON.parse(raw);
}
async function readOptionalJson(repoRoot, relativePath) {
try {
return await readJson(repoRoot, relativePath);
} catch {
return null;
}
}
function resolveTargetCommit(repoRoot, ref) {
try {
return execFileSync("git", ["rev-parse", "--verify", `${ref}^{commit}`], {
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
timeout: 5000
}).trim();
} catch {
return "unknown";
}
}
function parseArgs(argv) {
const args = {
targetRef: defaultTargetRef,
artifactReport: defaultArtifactReport,
artifactCatalog: defaultArtifactCatalog,
runtimeReport: defaultRuntimeReport,
report: defaultOutputReport,
timeoutMs: 5000,
live: true,
writeReport: false,
check: false,
expectBlocked: false,
pretty: false,
help: false
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--target-ref") {
args.targetRef = requireValue(argv, index, arg);
index += 1;
} else if (arg === "--artifact-report") {
args.artifactReport = requireValue(argv, index, arg);
index += 1;
} else if (arg === "--artifact-catalog") {
args.artifactCatalog = requireValue(argv, index, arg);
index += 1;
} else if (arg === "--runtime-report") {
args.runtimeReport = requireValue(argv, index, arg);
index += 1;
} else if (arg === "--report") {
args.report = requireValue(argv, index, arg);
index += 1;
} else if (arg === "--timeout-ms") {
args.timeoutMs = Number.parseInt(requireValue(argv, index, arg), 10);
index += 1;
} else if (arg === "--write-report") {
args.writeReport = true;
} else if (arg === "--no-report") {
args.writeReport = false;
} else if (arg === "--no-live") {
args.live = false;
} else if (arg === "--check") {
args.check = true;
} else if (arg === "--expect-blocked") {
args.expectBlocked = true;
} else if (arg === "--pretty") {
args.pretty = true;
} else if (arg === "--help" || arg === "-h") {
args.help = true;
} else {
throw new Error(`unknown argument: ${arg}`);
}
}
if (!Number.isInteger(args.timeoutMs) || args.timeoutMs <= 0) {
throw new Error("--timeout-ms must be a positive integer");
}
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: ${guardCommand} [--write-report] [--no-live] [--expect-blocked]`,
"",
"Read-only latest-main DEV artifact/runtime readiness guard.",
"",
"The command compares target main, artifact publish/catalog identity, desired-state convergence,",
"Cloud API /health/live identity, and Cloud Web :16666 /health/live identity.",
"It does not deploy, publish, apply, restart services, mutate k8s, touch PROD, or read secrets."
].join("\n");
}
function summaryForCheck(report) {
return {
status: report.status,
target: report.artifactRuntimeReadiness.target.shortCommitId,
desiredState: {
deployCommitId: report.artifactRuntimeReadiness.desiredState.deployCommitId,
convergence: report.artifactRuntimeReadiness.desiredState.targetConvergence.state
},
artifact: {
sourceCommitId: report.artifactRuntimeReadiness.artifact.shortCommitId,
catalogCommitId: report.artifactRuntimeReadiness.catalog.shortCommitId,
reportPublished: report.artifactRuntimeReadiness.artifact.publishVerified,
catalogPublished: report.artifactRuntimeReadiness.catalog.publishVerified
},
runtime: {
api: report.artifactRuntimeReadiness.runtime.api.effective.shortCommitId,
cloudWeb16666: report.artifactRuntimeReadiness.runtime.cloudWeb.effective.shortCommitId
},
blockedReasons: report.blockedReasons
};
}
function sanitizeString(value) {
return typeof value === "string" && value.trim().length > 0 ? oneLine(value) : "unknown";
}
function sanitizeTimestamp(value) {
return typeof value === "string" && !Number.isNaN(Date.parse(value)) ? value : new Date().toISOString();
}
function parseJson(text) {
try {
return JSON.parse(text);
} catch {
return null;
}
}
function oneLine(value) {
return String(value ?? "unknown").replace(/\s+/gu, " ").trim().slice(0, 240) || "unknown";
}