442 lines
19 KiB
JavaScript
442 lines
19 KiB
JavaScript
"use strict";
|
|
|
|
function record(value) {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
}
|
|
|
|
function stringOrNull(value) {
|
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
}
|
|
|
|
function stringArray(value) {
|
|
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
|
|
}
|
|
|
|
function digestFromImage(value) {
|
|
if (typeof value !== "string" || !value.includes("@")) return null;
|
|
return value.split("@").slice(1).join("@") || null;
|
|
}
|
|
|
|
function exactSkipMarker(records, event) {
|
|
return [...records].reverse().find((item) => item.event === event
|
|
&& item.status === "skipped"
|
|
&& item.reason === "no-build-no-rollout-plan") || null;
|
|
}
|
|
|
|
function extractPacSourceObservation(recordsInput) {
|
|
const records = Array.isArray(recordsInput) ? recordsInput.map(record) : [];
|
|
const plan = [...records].reverse().find((item) => item.event === "g14-ci-plan") || null;
|
|
const collectMarker = exactSkipMarker(records, "collect-artifacts");
|
|
const promoteMarker = exactSkipMarker(records, "gitops-promote");
|
|
if (plan === null && collectMarker === null && promoteMarker === null) return null;
|
|
|
|
const affected = stringArray(plan?.affectedServices);
|
|
const rollout = stringArray(plan?.rolloutServices);
|
|
const build = stringArray(plan?.buildServices);
|
|
const reused = stringArray(plan?.reusedServices);
|
|
const sourceCommit = stringOrNull(plan?.sourceCommitId);
|
|
const planShapeValid = affected !== null && rollout !== null && build !== null;
|
|
const sourceCommitValid = sourceCommit !== null && /^[0-9a-f]{40}$/iu.test(sourceCommit);
|
|
const noRuntimeChangePlan = planShapeValid
|
|
&& affected.length === 0
|
|
&& rollout.length === 0
|
|
&& build.length === 0;
|
|
const noRuntimeChangeMarkers = collectMarker !== null && promoteMarker !== null;
|
|
const skipMarkerSeen = collectMarker !== null || promoteMarker !== null;
|
|
const skipMarkerConflict = skipMarkerSeen && !noRuntimeChangePlan;
|
|
|
|
let mode = "inconsistent";
|
|
let valid = false;
|
|
let reason = "structured-plan-or-terminal-marker-is-incomplete";
|
|
if (planShapeValid && sourceCommitValid && noRuntimeChangePlan && noRuntimeChangeMarkers) {
|
|
mode = "no-runtime-change";
|
|
valid = true;
|
|
reason = "no-build-no-rollout-plan";
|
|
} else if (planShapeValid && sourceCommitValid && !noRuntimeChangePlan && !skipMarkerConflict) {
|
|
mode = "delivery";
|
|
valid = true;
|
|
reason = "artifact-or-runtime-delivery-required";
|
|
} else if (!sourceCommitValid) {
|
|
reason = "source-commit-invalid-or-missing";
|
|
} else if (!planShapeValid) {
|
|
reason = "delivery-plan-shape-invalid-or-missing";
|
|
} else if (skipMarkerConflict) {
|
|
reason = "no-runtime-change-marker-conflicts-with-delivery-plan";
|
|
}
|
|
|
|
return {
|
|
schemaVersion: "v1",
|
|
contract: "hwlab-g14-ci-plan",
|
|
mode,
|
|
valid,
|
|
reason,
|
|
sourceCommit,
|
|
plan: {
|
|
affectedServiceCount: affected?.length ?? null,
|
|
rolloutServiceCount: rollout?.length ?? null,
|
|
buildServiceCount: build?.length ?? null,
|
|
reusedServiceCount: reused?.length ?? null,
|
|
},
|
|
terminal: {
|
|
collectArtifacts: collectMarker === null ? "missing" : "no-build-no-rollout-plan",
|
|
gitopsPromote: promoteMarker === null ? "missing" : "no-build-no-rollout-plan",
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function evaluatePacStatus(inputValue) {
|
|
const input = record(inputValue);
|
|
const artifact = record(input.artifact);
|
|
const argo = record(input.argo);
|
|
const runtime = record(input.runtime);
|
|
const sourceObservation = record(artifact.sourceObservation);
|
|
const revisionRelation = record(argo.revisionRelation);
|
|
const gitopsRelation = stringOrNull(revisionRelation.relation) || "unknown";
|
|
const sourceCommit = stringOrNull(input.sourceCommit);
|
|
const registryDigest = stringOrNull(input.registryDigest);
|
|
const artifactDigest = stringOrNull(artifact.digest);
|
|
const expectedDigest = registryDigest || artifactDigest;
|
|
const runtimeImage = stringOrNull(runtime.image);
|
|
const runtimeDigest = stringOrNull(runtime.digest) || digestFromImage(runtimeImage);
|
|
const runtimeMatches = expectedDigest === null || runtimeDigest === expectedDigest;
|
|
const runtimeReady = Number.isInteger(runtime.replicas)
|
|
&& runtime.replicas > 0
|
|
&& runtime.readyReplicas === runtime.replicas;
|
|
const selectedArtifactDigests = [...new Set([
|
|
artifactDigest,
|
|
...(Array.isArray(artifact.digests) ? artifact.digests : []),
|
|
].filter((value) => typeof value === "string" && value.length > 0))];
|
|
const selectedArtifactMatchesRuntime = runtimeDigest !== null && selectedArtifactDigests.includes(runtimeDigest);
|
|
const registryMatchesArtifact = registryDigest === null || artifactDigest === null || registryDigest === artifactDigest;
|
|
const healthUrl = stringOrNull(input.healthUrl);
|
|
const healthStatus = stringOrNull(input.healthStatus);
|
|
const healthReady = healthUrl === null || healthStatus === "200";
|
|
const gitopsReady = stringOrNull(artifact.gitopsCommit) !== null && stringOrNull(argo.revision) !== null;
|
|
const gitopsRevisionAligned = gitopsRelation === "exact" || gitopsRelation === "descendant";
|
|
const deliveryDisabled = artifact.imageStatus === "disabled";
|
|
const deliverySkipped = artifact.imageStatus === "skipped";
|
|
const sourceObservationMode = stringOrNull(sourceObservation.mode);
|
|
const noRuntimeChange = sourceObservationMode === "no-runtime-change";
|
|
const sourceObservationInconsistent = sourceObservationMode === "inconsistent"
|
|
|| (sourceObservationMode !== null && sourceObservation.valid !== true);
|
|
const observationSourceCommit = stringOrNull(sourceObservation.sourceCommit);
|
|
const observationSourceMatches = sourceCommit !== null && observationSourceCommit === sourceCommit;
|
|
const retainedRevision = stringOrNull(argo.revision);
|
|
const retainedDigestReady = runtimeDigest !== null && /^sha256:[0-9a-f]{64}$/iu.test(runtimeDigest);
|
|
const retainedOutputConflict = noRuntimeChange
|
|
&& (stringOrNull(artifact.gitopsCommit) !== null
|
|
|| artifactDigest !== null
|
|
|| selectedArtifactDigests.length > 0);
|
|
|
|
let code = "pac-diagnostics-not-applicable";
|
|
let phase = "not-applicable";
|
|
let ok = true;
|
|
let hint = "consumer did not expose artifact or runtime alignment evidence";
|
|
if (deliveryDisabled) {
|
|
code = "pac-delivery-disabled";
|
|
phase = "disabled";
|
|
hint = "delivery is disabled by YAML; registry, GitOps and runtime artifacts are intentionally absent";
|
|
} else if (!sourceCommit) {
|
|
ok = false;
|
|
code = "pac-source-unknown";
|
|
phase = "source-unknown";
|
|
hint = "latest PaC PipelineRun did not expose a source commit";
|
|
} else if (sourceObservationInconsistent) {
|
|
ok = false;
|
|
code = "pac-source-observation-inconsistent";
|
|
phase = "source-observation-inconsistent";
|
|
hint = "structured delivery plan and no-runtime-change terminal markers are incomplete or contradictory";
|
|
} else if (sourceObservationMode !== null && !observationSourceMatches) {
|
|
ok = false;
|
|
code = "pac-source-observation-mismatch";
|
|
phase = "source-observation-mismatch";
|
|
hint = "the structured no-runtime-change observation does not belong to the selected PipelineRun source commit";
|
|
} else if (noRuntimeChange && retainedOutputConflict) {
|
|
ok = false;
|
|
code = "pac-retained-output-conflict";
|
|
phase = "source-only-output-conflict";
|
|
hint = "a no-runtime-change PipelineRun unexpectedly reported a new artifact or GitOps output";
|
|
} else if (noRuntimeChange && retainedRevision === null) {
|
|
ok = false;
|
|
code = "pac-retained-gitops-missing";
|
|
phase = "retained-gitops-missing";
|
|
hint = "the no-runtime-change observation is valid but the retained Argo GitOps revision is missing";
|
|
} else if (noRuntimeChange && (argo.sync !== "Synced" || argo.health !== "Healthy")) {
|
|
ok = false;
|
|
code = "pac-retained-argo-not-ready";
|
|
phase = "retained-argo-not-ready";
|
|
hint = "the no-runtime-change observation is valid but retained Argo is not Synced/Healthy";
|
|
} else if (noRuntimeChange && gitopsRelation !== "retained") {
|
|
ok = false;
|
|
code = "pac-retained-revision-unproven";
|
|
phase = "retained-revision-unproven";
|
|
hint = "the retained GitOps revision was not linked to the structured no-runtime-change observation";
|
|
} else if (noRuntimeChange && !runtimeReady) {
|
|
ok = false;
|
|
code = "pac-retained-runtime-not-ready";
|
|
phase = "retained-runtime-not-ready";
|
|
hint = "the retained runtime ready replicas do not match the desired replica count";
|
|
} else if (noRuntimeChange && !retainedDigestReady) {
|
|
ok = false;
|
|
code = "pac-retained-runtime-digest-missing";
|
|
phase = "retained-runtime-digest-missing";
|
|
hint = "the retained runtime is not pinned to a visible sha256 digest";
|
|
} else if (noRuntimeChange && !healthReady) {
|
|
ok = false;
|
|
code = "pac-retained-health-not-ready";
|
|
phase = "retained-health-not-ready";
|
|
hint = "the retained runtime health endpoint is not ready";
|
|
} else if (noRuntimeChange) {
|
|
code = "pac-ready-no-runtime-change";
|
|
phase = "ready-no-runtime-change";
|
|
hint = "the successful source observation requires no artifact, GitOps or runtime change; retained Argo and runtime provenance remain healthy";
|
|
} else if (stringOrNull(input.imageRepository) !== null && input.registryPresent !== true) {
|
|
ok = false;
|
|
code = "pac-registry-missing";
|
|
phase = "source-ready-registry-missing";
|
|
hint = "PaC source commit is known but the configured registry tag is missing";
|
|
} else if (!registryMatchesArtifact) {
|
|
ok = false;
|
|
code = "pac-artifact-registry-mismatch";
|
|
phase = "artifact-registry-mismatch";
|
|
hint = "PipelineRun artifact digest does not match the configured registry digest";
|
|
} else if (!gitopsReady) {
|
|
ok = false;
|
|
code = "pac-gitops-missing";
|
|
phase = "artifact-ready-gitops-missing";
|
|
hint = "PipelineRun completed but its GitOps commit is not visible";
|
|
} else if (argo.sync !== "Synced" || argo.health !== "Healthy") {
|
|
ok = false;
|
|
code = "pac-argo-not-ready";
|
|
phase = "gitops-ready-argo-pending";
|
|
hint = "GitOps exists but Argo is not Synced/Healthy";
|
|
} else if (!gitopsRevisionAligned) {
|
|
ok = false;
|
|
code = `pac-argo-revision-${gitopsRelation}`;
|
|
phase = `gitops-ready-argo-revision-${gitopsRelation}`;
|
|
hint = `Argo revision relation is ${gitopsRelation}; only exact or commit-graph-proven descendant is deployable`;
|
|
} else if (gitopsRelation === "descendant" && !selectedArtifactMatchesRuntime) {
|
|
ok = false;
|
|
code = "pac-descendant-artifact-runtime-mismatch";
|
|
phase = "argo-descendant-runtime-mismatch";
|
|
hint = "a GitOps descendant is ready only when the selected PipelineRun artifact digest exactly matches the runtime digest";
|
|
} else if (!runtimeMatches) {
|
|
ok = false;
|
|
code = "pac-runtime-not-aligned";
|
|
phase = "argo-ready-runtime-mismatch";
|
|
hint = "runtime image digest does not match the artifact digest produced by the selected PipelineRun";
|
|
} else if (!runtimeReady) {
|
|
ok = false;
|
|
code = "pac-runtime-not-ready";
|
|
phase = "runtime-replicas-not-ready";
|
|
hint = "runtime ready replicas do not match the desired replica count";
|
|
} else if (!healthReady) {
|
|
ok = false;
|
|
code = "pac-health-not-ready";
|
|
phase = "runtime-ready-health-pending";
|
|
hint = "runtime image is aligned but the configured health endpoint is not ready";
|
|
} else if (artifactDigest || selectedArtifactDigests.length > 0 || stringOrNull(input.imageRepository) !== null) {
|
|
code = deliverySkipped
|
|
? "pac-ready-runtime-unchanged"
|
|
: gitopsRelation === "descendant"
|
|
? "pac-ready-gitops-descendant"
|
|
: "pac-ready-gitops-exact";
|
|
phase = "ready";
|
|
hint = deliverySkipped
|
|
? "YAML-declared runtime inputs are unchanged; the existing image, GitOps manifest, runtime and health remain aligned"
|
|
: gitopsRelation === "descendant"
|
|
? "Argo is on a commit-graph-proven GitOps descendant and the selected artifact, runtime, Argo and health are aligned"
|
|
: "Argo is on the exact selected GitOps commit and the artifact, runtime, Argo and health are aligned";
|
|
}
|
|
|
|
return {
|
|
ok,
|
|
code,
|
|
phase,
|
|
hint,
|
|
sourceCommit,
|
|
runtimeSourceCommit: stringOrNull(input.runtimeSourceCommit),
|
|
imageRepository: stringOrNull(input.imageRepository),
|
|
registryProbeBase: stringOrNull(input.registryProbeBase),
|
|
imageTag: stringOrNull(input.imageTag),
|
|
registry: {
|
|
present: input.registryPresent === true,
|
|
digest: registryDigest,
|
|
url: stringOrNull(input.registryUrl),
|
|
},
|
|
selectedArtifactDigests,
|
|
gitops: {
|
|
branch: stringOrNull(input.gitopsBranch),
|
|
manifestPath: stringOrNull(input.gitopsManifestPath),
|
|
commit: noRuntimeChange ? retainedRevision : stringOrNull(artifact.gitopsCommit) || retainedRevision,
|
|
revisionRelation,
|
|
},
|
|
argo: {
|
|
sync: stringOrNull(argo.sync),
|
|
health: stringOrNull(argo.health),
|
|
revision: retainedRevision,
|
|
repoURL: stringOrNull(argo.repoURL),
|
|
targetRevision: stringOrNull(argo.targetRevision),
|
|
revisionRelation,
|
|
},
|
|
runtime: {
|
|
image: runtimeImage,
|
|
digest: runtimeDigest,
|
|
readyReplicas: runtime.readyReplicas ?? null,
|
|
replicas: runtime.replicas ?? null,
|
|
},
|
|
health: { url: healthUrl, status: healthStatus, ready: healthReady },
|
|
pipelineRun: stringOrNull(input.pipelineRun),
|
|
deliveryDisabled,
|
|
deliverySkipped,
|
|
deliveryMode: noRuntimeChange ? "no-runtime-change" : sourceObservationMode || "delivery",
|
|
sourceObservation: Object.keys(sourceObservation).length > 0 ? sourceObservation : null,
|
|
provenance: noRuntimeChange ? {
|
|
relation: "retained",
|
|
sourceCommit,
|
|
gitopsRevision: retainedRevision,
|
|
runtimeImage,
|
|
runtimeDigest,
|
|
newArtifact: false,
|
|
newGitopsRevision: false,
|
|
runtimeChanged: false,
|
|
valuesPrinted: false,
|
|
} : {
|
|
relation: gitopsRelation,
|
|
sourceCommit,
|
|
gitopsRevision: retainedRevision,
|
|
runtimeImage,
|
|
runtimeDigest,
|
|
valuesPrinted: false,
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
}
|
|
|
|
function fixtureInput(overrides = {}) {
|
|
const digest = `sha256:${"a".repeat(64)}`;
|
|
const revision = "b".repeat(40);
|
|
return {
|
|
sourceCommit: "c".repeat(40),
|
|
artifact: {},
|
|
argo: {
|
|
sync: "Synced",
|
|
health: "Healthy",
|
|
revision,
|
|
revisionRelation: { relation: "retained", observedRevision: revision },
|
|
},
|
|
runtime: { image: `registry/service@${digest}`, digest, replicas: 1, readyReplicas: 1 },
|
|
registryPresent: false,
|
|
healthUrl: null,
|
|
healthStatus: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function noRuntimeChangeObservation(sourceCommit = "c".repeat(40)) {
|
|
return extractPacSourceObservation([
|
|
{
|
|
event: "g14-ci-plan",
|
|
sourceCommitId: sourceCommit,
|
|
affectedServices: [],
|
|
rolloutServices: [],
|
|
buildServices: [],
|
|
reusedServices: ["service"],
|
|
},
|
|
{ event: "collect-artifacts", status: "skipped", reason: "no-build-no-rollout-plan" },
|
|
{ event: "gitops-promote", status: "skipped", reason: "no-build-no-rollout-plan" },
|
|
]);
|
|
}
|
|
|
|
function deliveryObservation(sourceCommit = "c".repeat(40)) {
|
|
return extractPacSourceObservation([{
|
|
event: "g14-ci-plan",
|
|
sourceCommitId: sourceCommit,
|
|
affectedServices: ["service"],
|
|
rolloutServices: ["service"],
|
|
buildServices: ["service"],
|
|
reusedServices: [],
|
|
}]);
|
|
}
|
|
|
|
function runPacStatusFixtureChecks() {
|
|
const digestA = `sha256:${"a".repeat(64)}`;
|
|
const digestD = `sha256:${"d".repeat(64)}`;
|
|
const revision = "b".repeat(40);
|
|
const retained = noRuntimeChangeObservation();
|
|
const delivery = deliveryObservation();
|
|
const exactArgo = { sync: "Synced", health: "Healthy", revision, revisionRelation: { relation: "exact", expectedRevision: revision, observedRevision: revision } };
|
|
const cases = [
|
|
{
|
|
id: "retained-ready",
|
|
expectedOk: true,
|
|
expectedCode: "pac-ready-no-runtime-change",
|
|
input: fixtureInput({ artifact: { imageStatus: "retained", sourceObservation: retained } }),
|
|
},
|
|
{
|
|
id: "retained-gitops-missing",
|
|
expectedOk: false,
|
|
expectedCode: "pac-retained-gitops-missing",
|
|
input: fixtureInput({ artifact: { imageStatus: "retained", sourceObservation: retained }, argo: { sync: "Synced", health: "Healthy", revision: null, revisionRelation: { relation: "unknown" } } }),
|
|
},
|
|
{
|
|
id: "delivery-ready-exact",
|
|
expectedOk: true,
|
|
expectedCode: "pac-ready-gitops-exact",
|
|
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery }, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
|
|
},
|
|
{
|
|
id: "delivery-gitops-missing",
|
|
expectedOk: false,
|
|
expectedCode: "pac-gitops-missing",
|
|
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, sourceObservation: delivery }, argo: { sync: "Synced", health: "Healthy", revision: null, revisionRelation: { relation: "unknown" } } }),
|
|
},
|
|
{
|
|
id: "delivery-runtime-mismatch",
|
|
expectedOk: false,
|
|
expectedCode: "pac-runtime-not-aligned",
|
|
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery }, argo: exactArgo, runtime: { image: `registry/service@${digestD}`, digest: digestD, replicas: 1, readyReplicas: 1 } }),
|
|
},
|
|
{
|
|
id: "incomplete-no-runtime-change-markers",
|
|
expectedOk: false,
|
|
expectedCode: "pac-source-observation-inconsistent",
|
|
input: fixtureInput({ artifact: { sourceObservation: extractPacSourceObservation([{ event: "g14-ci-plan", sourceCommitId: "c".repeat(40), affectedServices: [], rolloutServices: [], buildServices: [], reusedServices: [] }]) } }),
|
|
},
|
|
{
|
|
id: "retained-source-mismatch",
|
|
expectedOk: false,
|
|
expectedCode: "pac-source-observation-mismatch",
|
|
input: fixtureInput({ sourceCommit: "e".repeat(40), artifact: { imageStatus: "retained", sourceObservation: retained } }),
|
|
},
|
|
{
|
|
id: "delivery-one-sided-skip-conflict",
|
|
expectedOk: false,
|
|
expectedCode: "pac-source-observation-inconsistent",
|
|
input: fixtureInput({ artifact: { sourceObservation: extractPacSourceObservation([
|
|
{ event: "g14-ci-plan", sourceCommitId: "c".repeat(40), affectedServices: ["service"], rolloutServices: ["service"], buildServices: ["service"], reusedServices: [] },
|
|
{ event: "gitops-promote", status: "skipped", reason: "no-build-no-rollout-plan" },
|
|
]) } }),
|
|
},
|
|
];
|
|
const checks = cases.map((item) => {
|
|
const result = evaluatePacStatus(item.input);
|
|
return {
|
|
id: item.id,
|
|
ok: result.ok === item.expectedOk && result.code === item.expectedCode,
|
|
expectedOk: item.expectedOk,
|
|
actualOk: result.ok,
|
|
expectedCode: item.expectedCode,
|
|
actualCode: result.code,
|
|
};
|
|
});
|
|
return { ok: checks.every((item) => item.ok), checks, valuesPrinted: false };
|
|
}
|
|
|
|
module.exports = {
|
|
evaluatePacStatus,
|
|
extractPacSourceObservation,
|
|
runPacStatusFixtureChecks,
|
|
};
|