fix: 防止 artifact 复用污染组件证明

This commit is contained in:
Codex
2026-05-31 22:28:06 +08:00
parent a234145983
commit d5baeb461b
5 changed files with 218 additions and 16 deletions
+84 -3
View File
@@ -195,10 +195,32 @@ export async function createG14CiPlan(options = {}) {
runtimeKind: model.runtimeKind,
entrypoint: model.entrypoint
});
const componentInputChanged = !envReuse && typeof catalogRecord?.componentInputHash === "string" && catalogRecord.componentInputHash !== componentInputHash;
const catalogSourceCommitId = !envReuse ? text(catalogRecord?.sourceCommitId) : "";
const catalogComponentInputHash = !envReuse ? text(catalogRecord?.componentInputHash) : "";
const catalogComponentInputHashAtSource = !envReuse && catalogSourceCommitId
? await hashGitPathsIfCommitExists(repoRoot, catalogSourceCommitId, componentInputPaths, {
skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath),
semanticPackageJson: true
})
: null;
const catalogComponentProvenance = !envReuse ? catalogComponentProvenanceStatus({
catalogRecord,
catalogSourceCommitId,
catalogComponentInputHash,
catalogComponentInputHashAtSource,
componentInputHash,
dockerfileHash,
buildArgsHash
}) : null;
const catalogRecordForReuse = catalogRecord && catalogComponentProvenance ? {
...catalogRecord,
componentProvenanceStatus: catalogComponentProvenance.status,
componentProvenanceReason: catalogComponentProvenance.reason
} : catalogRecord;
const componentInputChanged = !envReuse && catalogComponentProvenance?.safeToReuse !== true;
const catalogReuse = envReuse
? envReuseCandidate(catalogRecord, envChanged)
: reuseCandidate(catalogRecord, imageRelevantChangedPaths.length > 0, componentInputChanged);
: reuseCandidate(catalogRecordForReuse, imageRelevantChangedPaths.length > 0, componentInputChanged);
const affected = envReuse
? Boolean(envChanged || codeChanged)
: imageRelevantChangedPaths.length > 0 || componentInputChanged || catalogReuse.status === "candidate-no-catalog" || catalogReuse.status === "candidate-unverified-digest";
@@ -213,6 +235,10 @@ export async function createG14CiPlan(options = {}) {
buildSystemPaths: model.buildSystemPaths,
componentCommitId,
componentInputHash,
catalogSourceCommitId: catalogSourceCommitId || null,
catalogComponentInputHash: catalogComponentInputHash || null,
catalogComponentInputHashAtSource,
catalogComponentProvenance,
dockerfileHash,
baseImageReference: baseImage,
baseImageDigest: digestFromImageReference(baseImage),
@@ -501,6 +527,10 @@ function reasonForService({ directMatches, sharedMatches, runtimeDepMatches, bui
if (runtimeDepMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("runtime-deps-changed");
if (buildSystemMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("build-system-changed");
if (catalogReuse?.status === "component-input-mismatch") reasons.push("component-input-mismatch");
if (catalogReuse?.status === "component-source-mismatch") reasons.push("component-source-mismatch");
if (catalogReuse?.status === "component-source-unavailable") reasons.push("component-source-unavailable");
if (catalogReuse?.status === "component-provenance-missing") reasons.push("component-provenance-missing");
if (catalogReuse?.status === "component-build-mismatch") reasons.push("component-build-mismatch");
if (catalogReuse?.status === "candidate-no-catalog") reasons.push("catalog-record-missing");
if (catalogReuse?.status === "candidate-unverified-digest") reasons.push("catalog-digest-missing");
return reasons.length ? reasons : ["affected"];
@@ -517,7 +547,16 @@ function reasonForUnchanged(globalChange) {
function reuseCandidate(catalogRecord, affected, componentInputChanged = false) {
if (affected) return { status: "not-reused", reason: "component-affected" };
if (!catalogRecord) return { status: "candidate-no-catalog", reason: "no-previous-artifact-record" };
if (componentInputChanged) {
if (componentInputChanged && catalogRecord?.componentProvenanceStatus) {
return {
status: catalogRecord.componentProvenanceStatus,
reason: catalogRecord.componentProvenanceReason,
image: catalogRecord.image ?? null,
digest: catalogRecord.digest ?? null,
reusedFrom: catalogRecord.componentInputHash ?? catalogRecord.sourceCommitId ?? catalogRecord.commitId ?? catalogRecord.imageTag ?? null
};
}
if (componentInputChanged && typeof catalogRecord?.componentInputHash === "string") {
return {
status: "component-input-mismatch",
reason: "catalog-component-input-hash-differs-from-current-plan",
@@ -526,6 +565,15 @@ function reuseCandidate(catalogRecord, affected, componentInputChanged = false)
reusedFrom: catalogRecord.componentInputHash ?? catalogRecord.commitId ?? catalogRecord.imageTag ?? null
};
}
if (componentInputChanged) {
return {
status: "component-source-mismatch",
reason: "catalog-source-commit-does-not-contain-current-component-input",
image: catalogRecord.image ?? null,
digest: catalogRecord.digest ?? null,
reusedFrom: catalogRecord.sourceCommitId ?? catalogRecord.commitId ?? catalogRecord.imageTag ?? null
};
}
if (!/^sha256:[a-f0-9]{64}$/u.test(catalogRecord.digest ?? "")) {
return { status: "candidate-unverified-digest", image: catalogRecord.image ?? null, digest: catalogRecord.digest ?? null };
}
@@ -537,6 +585,39 @@ function reuseCandidate(catalogRecord, affected, componentInputChanged = false)
};
}
function catalogComponentProvenanceStatus({ catalogRecord, catalogSourceCommitId, catalogComponentInputHash, catalogComponentInputHashAtSource, componentInputHash, dockerfileHash, buildArgsHash }) {
if (!catalogRecord) return { safeToReuse: false, status: "candidate-no-catalog", reason: "no-previous-artifact-record" };
if (!catalogSourceCommitId) return { safeToReuse: false, status: "component-provenance-missing", reason: "catalog-source-commit-missing" };
if (!catalogComponentInputHash) return { safeToReuse: false, status: "component-provenance-missing", reason: "catalog-component-input-hash-missing" };
if (!catalogComponentInputHashAtSource) return { safeToReuse: false, status: "component-source-unavailable", reason: "catalog-source-commit-unavailable" };
if (catalogComponentInputHashAtSource !== catalogComponentInputHash) {
return { safeToReuse: false, status: "component-source-mismatch", reason: "catalog-component-input-hash-does-not-match-catalog-source-tree" };
}
if (catalogComponentInputHash !== componentInputHash) {
return { safeToReuse: false, status: "component-input-mismatch", reason: "catalog-component-input-hash-differs-from-current-plan" };
}
if (text(catalogRecord.dockerfileHash) && catalogRecord.dockerfileHash !== dockerfileHash) {
return { safeToReuse: false, status: "component-build-mismatch", reason: "catalog-dockerfile-hash-differs-from-current-plan" };
}
if (text(catalogRecord.buildArgsHash) && catalogRecord.buildArgsHash !== buildArgsHash) {
return { safeToReuse: false, status: "component-build-mismatch", reason: "catalog-build-args-hash-differs-from-current-plan" };
}
return { safeToReuse: true, status: "safe-reuse", reason: "catalog-source-tree-and-current-component-input-match" };
}
async function hashGitPathsIfCommitExists(repoRoot, targetRef, paths, options = {}) {
try {
await gitValue(repoRoot, ["rev-parse", "--verify", `${targetRef}^{commit}`]);
return await hashGitPaths(repoRoot, targetRef, paths, options);
} catch {
return null;
}
}
function text(value) {
return String(value ?? "").trim();
}
function envReuseCandidate(catalogRecord, envChanged) {
if (envChanged) return { status: "not-reused", reason: "environment-affected" };
if (!catalogRecord) return { status: "candidate-no-catalog", reason: "no-previous-artifact-record" };