From 23cfc5e9cbd6d29bf0ea6130dd9338e6cb897c77 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Jul 2026 19:07:41 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=20PaC=20=E7=9B=B8?= =?UTF-8?q?=E5=90=8C=20revision=20=E7=8A=B6=E6=80=81=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 --- scripts/native/cicd/pac-status-evaluator.cjs | 70 ++++++++++++++++++- .../src/platform-infra-pac-provenance.test.ts | 33 ++++++++- ...platform-infra-pipelines-as-code-remote.sh | 5 +- 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/scripts/native/cicd/pac-status-evaluator.cjs b/scripts/native/cicd/pac-status-evaluator.cjs index f1a0db17..030760dd 100644 --- a/scripts/native/cicd/pac-status-evaluator.cjs +++ b/scripts/native/cicd/pac-status-evaluator.cjs @@ -19,6 +19,53 @@ function digestFromImage(value) { return value.split("@").slice(1).join("@") || null; } +function normalizeGitopsRevisionRelation(value) { + const relation = record(value); + const expectedRevision = stringOrNull(relation.expectedRevision); + const observedRevision = stringOrNull(relation.observedRevision); + const validRevision = /^[0-9a-f]{7,64}$/iu; + if (expectedRevision === null + || observedRevision === null + || expectedRevision !== observedRevision + || !validRevision.test(expectedRevision)) { + return relation; + } + + const originalReason = stringOrNull(relation.reason); + const warnings = Array.isArray(relation.warnings) ? [...relation.warnings] : []; + if (originalReason === "owning-git-fetch-failed" + && !warnings.some((item) => record(item).code === "pac-owning-git-fetch-failed")) { + warnings.push({ + warning: true, + blocking: false, + code: "pac-owning-git-fetch-failed", + object: { kind: "gitops-revision", id: "owning-git" }, + configPath: "live:gitops-revision", + reason: originalReason, + mutation: false, + valuesPrinted: false, + }); + } + + const graphProven = relation.relation === "exact" && relation.proof === "owning-git-commit-graph"; + const directlyProven = relation.relation === "exact" && relation.proof === "direct-revision-equality"; + return { + ...relation, + relation: "exact", + expectedRevision, + observedRevision, + proof: graphProven || directlyProven ? relation.proof : "direct-revision-equality", + reason: graphProven ? "commits-equal" : "revisions-equal", + contextReason: graphProven + ? null + : directlyProven + ? stringOrNull(relation.contextReason) + : originalReason, + warnings, + valuesPrinted: false, + }; +} + function exactSkipMarker(records, event) { return [...records].reverse().find((item) => item.event === event && item.status === "skipped" @@ -633,7 +680,7 @@ function evaluatePacStatus(inputValue) { const argo = record(input.argo); const runtime = record(input.runtime); const sourceObservation = record(artifact.sourceObservation); - const revisionRelation = record(argo.revisionRelation); + const revisionRelation = normalizeGitopsRevisionRelation(argo.revisionRelation); const gitopsRelation = stringOrNull(revisionRelation.relation) || "unknown"; const sourceCommit = stringOrNull(input.sourceCommit); const registryDigest = stringOrNull(input.registryDigest); @@ -1017,6 +1064,16 @@ function runPacStatusFixtureChecks() { expectedCode: "pac-ready-gitops-exact", input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }), }, + { + id: "equal-revisions-ignore-owning-git-fetch-failure", + expectedOk: true, + expectedCode: "pac-ready-gitops-exact", + input: fixtureInput({ + artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, + argo: { ...exactArgo, revisionRelation: { relation: "unknown", expectedRevision: revision, observedRevision: revision, proof: "unavailable", reason: "owning-git-fetch-failed", fetchOk: false } }, + runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 }, + }), + }, { id: "gitops-only-registry-not-configured", expectedOk: true, @@ -1041,6 +1098,16 @@ function runPacStatusFixtureChecks() { expectedCode: "pac-argo-revision-stale", input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, argo: { ...exactArgo, revision: "d".repeat(40), revisionRelation: { relation: "stale", expectedRevision: revision, observedRevision: "d".repeat(40) } }, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }), }, + { + id: "different-revisions-fetch-failure-remains-unknown", + expectedOk: false, + expectedCode: "pac-argo-revision-unknown", + input: fixtureInput({ + artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, + argo: { ...exactArgo, revision: "d".repeat(40), revisionRelation: { relation: "unknown", expectedRevision: revision, observedRevision: "d".repeat(40), proof: "unavailable", reason: "owning-git-fetch-failed", fetchOk: false } }, + runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 }, + }), + }, { id: "delivery-gitops-missing", expectedOk: false, @@ -1245,6 +1312,7 @@ module.exports = { evaluatePacStatus, extractPacArtifactEvidence, extractPacSourceObservation, + normalizeGitopsRevisionRelation, parsePacLogRecords, runPacStatusFixtureChecks, pipelineRunMatchesConsumer, diff --git a/scripts/src/platform-infra-pac-provenance.test.ts b/scripts/src/platform-infra-pac-provenance.test.ts index 2f79b8b7..3199cec4 100644 --- a/scripts/src/platform-infra-pac-provenance.test.ts +++ b/scripts/src/platform-infra-pac-provenance.test.ts @@ -15,6 +15,7 @@ const evaluator = createRequire(import.meta.url)("../native/cicd/pac-status-eval evaluatePacAdmissionState: (input: Record) => Record; evaluatePacStatus: (input: Record) => Record; extractPacArtifactEvidence: (records: unknown[], logText: string) => Record; + normalizeGitopsRevisionRelation: (input: Record) => Record; taskTerminalRecord: (taskRun: Record) => Record | null; runPacStatusFixtureChecks: () => { ok: boolean; checks: Array<{ id: string; ok: boolean }> }; }; @@ -347,11 +348,41 @@ test("AgentRun rendered delivery plan and real TaskRun terminals close the statu test("provenance fixtures fail closed for pre-bootstrap, forged marker, wrong SA, and policy mismatch", () => { const result = evaluator.runPacStatusFixtureChecks(); expect(result.ok).toBe(true); - for (const id of ["gitops-only-registry-not-configured", "image-registry-missing-fails-closed", "admission-provenance-verified", "pre-bootstrap-run-fails-closed", "forged-name-candidate-fails-closed", "forged-label-candidate-fails-closed", "forged-pipeline-ref-candidate-fails-closed", "wrong-service-account-fails-closed", "policy-identity-mismatch-fails-closed"]) { + for (const id of ["gitops-only-registry-not-configured", "image-registry-missing-fails-closed", "equal-revisions-ignore-owning-git-fetch-failure", "different-revisions-fetch-failure-remains-unknown", "admission-provenance-verified", "pre-bootstrap-run-fails-closed", "forged-name-candidate-fails-closed", "forged-label-candidate-fails-closed", "forged-pipeline-ref-candidate-fails-closed", "wrong-service-account-fails-closed", "policy-identity-mismatch-fails-closed"]) { expect(result.checks.find((item) => item.id === id)?.ok).toBe(true); } }); +test("equal GitOps revisions stay exact when the supplemental owning Git fetch fails", () => { + const revision = "b".repeat(40); + const raw = { + relation: "unknown", + expectedRevision: revision, + observedRevision: revision, + proof: "unavailable", + reason: "owning-git-fetch-failed", + fetchOk: false, + }; + const normalized = evaluator.normalizeGitopsRevisionRelation(raw); + expect(normalized).toMatchObject({ + relation: "exact", + proof: "direct-revision-equality", + reason: "revisions-equal", + contextReason: "owning-git-fetch-failed", + warnings: [{ + warning: true, + blocking: false, + code: "pac-owning-git-fetch-failed", + mutation: false, + valuesPrinted: false, + }], + valuesPrinted: false, + }); + expect(evaluator.normalizeGitopsRevisionRelation(normalized)).toEqual(normalized); + const remote = readFileSync(resolve(root, "scripts/src/platform-infra-pipelines-as-code-remote.sh"), "utf8"); + expect(remote).toContain("normalizeGitopsRevisionRelation({"); +}); + test("history evaluates admission and default RBAC in each consumer namespace", () => { const remote = readFileSync(resolve(root, "scripts/src/platform-infra-pipelines-as-code-remote.sh"), "utf8"); expect(remote).toContain("consumer.admissionState = admissionStateForConsumer(consumer)"); diff --git a/scripts/src/platform-infra-pipelines-as-code-remote.sh b/scripts/src/platform-infra-pipelines-as-code-remote.sh index 51639095..292b3bd1 100644 --- a/scripts/src/platform-infra-pipelines-as-code-remote.sh +++ b/scripts/src/platform-infra-pipelines-as-code-remote.sh @@ -1258,7 +1258,8 @@ NODE UNIDESK_PAC_GITOPS_PROOF="$proof" \ UNIDESK_PAC_GITOPS_REASON="$reason" \ UNIDESK_PAC_GITOPS_FETCH_OK="$fetch_ok" node <<'NODE' -process.stdout.write(JSON.stringify({ +const { normalizeGitopsRevisionRelation } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH); +process.stdout.write(JSON.stringify(normalizeGitopsRevisionRelation({ relation: process.env.UNIDESK_PAC_GITOPS_RELATION || 'unknown', expectedRevision: process.env.UNIDESK_PAC_GITOPS_EXPECTED || null, observedRevision: process.env.UNIDESK_PAC_GITOPS_OBSERVED || null, @@ -1272,7 +1273,7 @@ process.stdout.write(JSON.stringify({ reason: process.env.UNIDESK_PAC_GITOPS_REASON || null, fetchOk: process.env.UNIDESK_PAC_GITOPS_FETCH_OK === 'true', valuesPrinted: false, -})); +}))); NODE }