fix: 防止 artifact 复用污染组件证明
This commit is contained in:
@@ -1481,6 +1481,7 @@ function artifactRecordForCatalogReuse({ service, catalogService, commitId }) {
|
||||
const digest = catalogService?.digest ?? "not_published";
|
||||
const imageTag = catalogService?.imageTag ?? imageTagFromReference(image);
|
||||
const catalogCommitId = catalogService?.commitId ?? imageTag ?? null;
|
||||
const catalogProvenance = serviceImageCatalogProvenance(catalogService);
|
||||
if (!image || !imageTag || !shaDigestPattern.test(digest)) {
|
||||
return {
|
||||
...service,
|
||||
@@ -1491,9 +1492,10 @@ function artifactRecordForCatalogReuse({ service, catalogService, commitId }) {
|
||||
imageTag,
|
||||
buildCreatedAt: catalogService?.buildCreatedAt ?? null,
|
||||
buildSource: catalogService?.buildSource ?? null,
|
||||
buildBackend: "reused-catalog",
|
||||
digest,
|
||||
repositoryDigest: repositoryDigestFor(image, digest),
|
||||
...catalogProvenance,
|
||||
buildBackend: "reused-catalog",
|
||||
reusedFrom: null,
|
||||
notPublishedReason: "reuse_catalog_digest_unavailable"
|
||||
};
|
||||
@@ -1509,11 +1511,24 @@ function artifactRecordForCatalogReuse({ service, catalogService, commitId }) {
|
||||
buildSource: catalogService?.buildSource ?? null,
|
||||
digest,
|
||||
repositoryDigest: catalogService?.repositoryDigest ?? repositoryDigestFor(image, digest),
|
||||
...catalogProvenance,
|
||||
buildBackend: "reused-catalog",
|
||||
reusedFrom: service.reuse?.reusedFrom ?? catalogService?.componentInputHash ?? catalogCommitId,
|
||||
notPublishedReason: null
|
||||
};
|
||||
}
|
||||
|
||||
function serviceImageCatalogProvenance(catalogService) {
|
||||
return {
|
||||
componentCommitId: catalogService?.componentCommitId ?? null,
|
||||
componentInputHash: catalogService?.componentInputHash ?? null,
|
||||
dockerfileHash: catalogService?.dockerfileHash ?? null,
|
||||
baseImageReference: catalogService?.baseImageReference ?? null,
|
||||
baseImageDigest: catalogService?.baseImageDigest ?? null,
|
||||
buildArgsHash: catalogService?.buildArgsHash ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function artifactRecordForEnvReuseCatalogReuse({ service, catalogService, commitId }) {
|
||||
const environmentImage = catalogService?.environmentImage ?? service.environmentImage ?? null;
|
||||
const environmentDigest = catalogService?.environmentDigest ?? service.environmentDigest ?? "not_published";
|
||||
@@ -1651,6 +1666,7 @@ function artifactRecordFromServiceResultsEnv({ service, env }) {
|
||||
const image = serviceResultValue(env, service.serviceId, "IMAGE");
|
||||
const digest = serviceResultValue(env, service.serviceId, "DIGEST") ?? "not_published";
|
||||
const imageTag = serviceResultValue(env, service.serviceId, "IMAGE_TAG") ?? imageTagFromReference(image);
|
||||
const reusedServiceImage = status === "reused" && service.envReuse !== true;
|
||||
const artifact = {
|
||||
...service,
|
||||
serviceId: service.serviceId,
|
||||
@@ -1662,7 +1678,7 @@ function artifactRecordFromServiceResultsEnv({ service, env }) {
|
||||
sourceCommitId: serviceResultValue(env, service.serviceId, "SOURCE_COMMIT_ID"),
|
||||
commitId: imageTag,
|
||||
buildCreatedAt: serviceResultValue(env, service.serviceId, "BUILD_CREATED_AT"),
|
||||
componentInputHash: serviceResultValue(env, service.serviceId, "COMPONENT_INPUT_HASH") ?? service.componentInputHash ?? null,
|
||||
componentInputHash: serviceResultValue(env, service.serviceId, "COMPONENT_INPUT_HASH") ?? (reusedServiceImage ? null : service.componentInputHash ?? null),
|
||||
environmentInputHash: serviceResultValue(env, service.serviceId, "ENVIRONMENT_INPUT_HASH") ?? service.environmentInputHash ?? null,
|
||||
codeInputHash: serviceResultValue(env, service.serviceId, "CODE_INPUT_HASH") ?? service.codeInputHash ?? null,
|
||||
runtimeMode: serviceResultValue(env, service.serviceId, "RUNTIME_MODE") ?? service.runtimeMode ?? "service-image",
|
||||
@@ -1671,6 +1687,13 @@ function artifactRecordFromServiceResultsEnv({ service, env }) {
|
||||
bootSh: serviceResultValue(env, service.serviceId, "BOOT_SH") ?? service.bootSh ?? null,
|
||||
reusedFrom: serviceResultValue(env, service.serviceId, "REUSED_FROM"),
|
||||
buildBackend: serviceResultValue(env, service.serviceId, "BUILD_BACKEND"),
|
||||
...(reusedServiceImage ? {
|
||||
componentCommitId: null,
|
||||
dockerfileHash: null,
|
||||
baseImageReference: null,
|
||||
baseImageDigest: null,
|
||||
buildArgsHash: null
|
||||
} : {}),
|
||||
notPublishedReason: null
|
||||
};
|
||||
if (service.envReuse) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
@@ -69,6 +69,7 @@ test("planner rebuilds service image when catalog component input hash is stale"
|
||||
const initialCloudApi = initialPlan.services.find((service) => service.serviceId === "hwlab-cloud-api");
|
||||
const staleHash = initialCloudApi.componentInputHash;
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
|
||||
sourceCommitId: initialPlan.sourceCommitId,
|
||||
cloudApiComponentInputHash: staleHash
|
||||
}), null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
|
||||
@@ -99,6 +100,61 @@ test("planner rebuilds service image when catalog component input hash is stale"
|
||||
assert.ok(plan.buildServices.includes("hwlab-cloud-api"));
|
||||
});
|
||||
|
||||
test("planner rejects polluted catalog hash that does not match catalog source tree", async () => {
|
||||
const repo = await createFixtureRepo({ includeDevicePod: true });
|
||||
const oldSourceCommit = (await git(repo, ["rev-parse", "HEAD~1"])).stdout.trim();
|
||||
|
||||
await writeFile(path.join(repo, "tools/src/device-pod-cli-lib.ts"), "export const hwpod = true;\n");
|
||||
await git(repo, ["add", "tools/src/device-pod-cli-lib.ts"]);
|
||||
await git(repo, ["commit", "-m", "change shared hwpod runtime"]);
|
||||
|
||||
const currentPlan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
lane: "v02",
|
||||
baseRef: "HEAD",
|
||||
targetRef: "HEAD",
|
||||
artifactCatalogPath: "deploy/nonexistent-artifact-catalog.json",
|
||||
services: ["hwlab-cloud-api"]
|
||||
});
|
||||
const currentCloudApi = currentPlan.services.find((service) => service.serviceId === "hwlab-cloud-api");
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
|
||||
sourceCommitId: oldSourceCommit,
|
||||
cloudApiComponentInputHash: currentCloudApi.componentInputHash
|
||||
}), null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.v02.json"]);
|
||||
await git(repo, ["commit", "-m", "pollute catalog provenance"]);
|
||||
|
||||
await mkdir(path.join(repo, "docs/reference"), { recursive: true });
|
||||
await writeFile(path.join(repo, "docs/reference/g14-gitops-cicd.md"), "docs only after provenance pollution\n");
|
||||
await git(repo, ["add", "docs/reference/g14-gitops-cicd.md"]);
|
||||
await git(repo, ["commit", "-m", "docs after polluted catalog"]);
|
||||
|
||||
const plan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
lane: "v02",
|
||||
baseRef: "HEAD~1",
|
||||
targetRef: "HEAD",
|
||||
artifactCatalogPath: "deploy/artifact-catalog.v02.json",
|
||||
services: ["hwlab-cloud-api"]
|
||||
});
|
||||
const cloudApi = plan.services.find((service) => service.serviceId === "hwlab-cloud-api");
|
||||
assert.equal(cloudApi.affected, true);
|
||||
assert.equal(cloudApi.buildRequired, true);
|
||||
assert.equal(cloudApi.reuse.status, "component-source-mismatch");
|
||||
assert.deepEqual(cloudApi.reason, ["component-source-mismatch"]);
|
||||
});
|
||||
|
||||
test("artifact reuse paths preserve catalog provenance instead of current planner provenance", async () => {
|
||||
const artifactPublish = await readFile("scripts/artifact-publish.mjs", "utf8");
|
||||
assert.match(artifactPublish, /const catalogProvenance = serviceImageCatalogProvenance\(catalogService\);/u);
|
||||
assert.match(artifactPublish, /\.\.\.catalogProvenance,[\s\S]{0,80}buildBackend: "reused-catalog"/u);
|
||||
assert.match(artifactPublish, /componentInputHash: serviceResultValue\(env, service\.serviceId, "COMPONENT_INPUT_HASH"\) \?\? \(reusedServiceImage \? null : service\.componentInputHash \?\? null\)/u);
|
||||
|
||||
const gitopsRender = await readFile("scripts/g14-gitops-render.mjs", "utf8");
|
||||
assert.match(gitopsRender, /const componentInputHash = envReuse \? \(planned\.componentInputHash \|\| service\.componentInputHash \|\| ""\) : \(service\.componentInputHash \|\| ""\);/u);
|
||||
assert.doesNotMatch(gitopsRender, /"component-input-hash": planned\.componentInputHash \|\| service\.componentInputHash \|\| ""/u);
|
||||
});
|
||||
|
||||
test("component model uses built-in service paths", () => {
|
||||
const models = componentModelsForServices(["hwlab-cloud-api"]);
|
||||
assert.deepEqual(models[0].componentPaths, [
|
||||
@@ -139,6 +195,7 @@ test("planner ignores package script cleanup for runtime images", async () => {
|
||||
}, null, 2));
|
||||
await git(repo, ["add", "package.json"]);
|
||||
await git(repo, ["commit", "-m", "seed package scripts"]);
|
||||
await refreshFixtureCatalogForHead(repo, { includeDevicePod: true });
|
||||
|
||||
await writeFile(path.join(repo, "package.json"), JSON.stringify({
|
||||
type: "module",
|
||||
@@ -310,7 +367,10 @@ test("v02 planner keeps hwlab-cli-only changes out of runtime service builds", a
|
||||
});
|
||||
const initialCloudWeb = initialPlan.services.find((service) => service.serviceId === "hwlab-cloud-web");
|
||||
const initialDevicePod = initialPlan.services.find((service) => service.serviceId === "hwlab-device-pod");
|
||||
const initialHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.componentInputHash]));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
|
||||
sourceCommitId: initialPlan.sourceCommitId,
|
||||
componentInputHashes: initialHashes,
|
||||
cloudWebEnvironmentInputHash: initialCloudWeb.environmentInputHash,
|
||||
cloudWebCodeInputHash: initialCloudWeb.codeInputHash,
|
||||
cloudWebBootCommit: initialPlan.sourceCommitId,
|
||||
@@ -360,7 +420,10 @@ test("v02 planner treats hwpod runner CLI changes as runtime inputs", async () =
|
||||
});
|
||||
const initialCloudWeb = initialPlan.services.find((service) => service.serviceId === "hwlab-cloud-web");
|
||||
const initialDevicePod = initialPlan.services.find((service) => service.serviceId === "hwlab-device-pod");
|
||||
const initialHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.componentInputHash]));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture("ready", {
|
||||
sourceCommitId: initialPlan.sourceCommitId,
|
||||
componentInputHashes: initialHashes,
|
||||
cloudWebEnvironmentInputHash: initialCloudWeb.environmentInputHash,
|
||||
cloudWebCodeInputHash: initialCloudWeb.codeInputHash,
|
||||
cloudWebBootCommit: initialPlan.sourceCommitId,
|
||||
@@ -411,10 +474,6 @@ async function createFixtureRepo(options = {}) {
|
||||
await mkdir(path.join(repo, "deploy"), { recursive: true });
|
||||
await writeFile(path.join(repo, "deploy/deploy.json"), JSON.stringify(createDeployFixture({ deployServices, k3sServiceMappings, includeDevicePod }), null, 2));
|
||||
const catalogMode = options.catalog ?? "ready";
|
||||
if (catalogMode !== false) {
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.dev.json"), JSON.stringify(createCatalogFixture(catalogMode), null, 2));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(createCatalogFixture(catalogMode), null, 2));
|
||||
}
|
||||
await writeFile(path.join(repo, "package.json"), JSON.stringify({ type: "module" }, null, 2));
|
||||
await writeFile(path.join(repo, "package-lock.json"), JSON.stringify({ lockfileVersion: 3 }, null, 2));
|
||||
await writeFile(path.join(repo, "cmd/hwlab-cloud-api/main.ts"), "console.log('api');\n");
|
||||
@@ -440,9 +499,43 @@ async function createFixtureRepo(options = {}) {
|
||||
await git(repo, ["init"]);
|
||||
await git(repo, ["add", "."]);
|
||||
await git(repo, ["commit", "-m", "initial"]);
|
||||
if (catalogMode !== false) {
|
||||
const initialSha = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
|
||||
const initialPlan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
baseRef: "HEAD",
|
||||
targetRef: "HEAD",
|
||||
artifactCatalogPath: "deploy/nonexistent-artifact-catalog.json",
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web", ...(includeDevicePod ? ["hwlab-device-pod"] : [])]
|
||||
});
|
||||
const componentInputHashes = Object.fromEntries(initialPlan.services.map((service) => [service.serviceId, service.componentInputHash]));
|
||||
const catalog = createCatalogFixture(catalogMode, { sourceCommitId: initialSha, componentInputHashes });
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.dev.json"), JSON.stringify(catalog, null, 2));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(catalog, null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.dev.json", "deploy/artifact-catalog.v02.json"]);
|
||||
await git(repo, ["commit", "-m", "seed artifact catalogs"]);
|
||||
}
|
||||
return repo;
|
||||
}
|
||||
|
||||
async function refreshFixtureCatalogForHead(repo, { includeDevicePod = true } = {}) {
|
||||
const sourceCommitId = (await git(repo, ["rev-parse", "HEAD"])).stdout.trim();
|
||||
const services = ["hwlab-cloud-api", "hwlab-cloud-web", ...(includeDevicePod ? ["hwlab-device-pod"] : [])];
|
||||
const plan = await createG14CiPlan({
|
||||
repoRoot: repo,
|
||||
baseRef: "HEAD",
|
||||
targetRef: "HEAD",
|
||||
artifactCatalogPath: "deploy/nonexistent-artifact-catalog.json",
|
||||
services
|
||||
});
|
||||
const componentInputHashes = Object.fromEntries(plan.services.map((service) => [service.serviceId, service.componentInputHash]));
|
||||
const catalog = createCatalogFixture("ready", { sourceCommitId, componentInputHashes });
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.dev.json"), JSON.stringify(catalog, null, 2));
|
||||
await writeFile(path.join(repo, "deploy/artifact-catalog.v02.json"), JSON.stringify(catalog, null, 2));
|
||||
await git(repo, ["add", "deploy/artifact-catalog.dev.json", "deploy/artifact-catalog.v02.json"]);
|
||||
await git(repo, ["commit", "-m", "refresh fixture artifact catalogs"]);
|
||||
}
|
||||
|
||||
function createDeployFixture({ deployServices, k3sServiceMappings, includeDevicePod }) {
|
||||
const deploy = {
|
||||
lanes: {
|
||||
@@ -481,11 +574,12 @@ function createCatalogFixture(mode, options = {}) {
|
||||
services: ["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-device-pod"].map((serviceId) => ({
|
||||
serviceId,
|
||||
commitId: "abc1234",
|
||||
sourceCommitId: options.sourceCommitId ?? "abc1234",
|
||||
image: `127.0.0.1:5000/hwlab/${serviceId}:abc1234`,
|
||||
imageTag: "abc1234",
|
||||
digest,
|
||||
...((serviceId === "hwlab-cloud-api" && options.cloudApiComponentInputHash) ? {
|
||||
componentInputHash: options.cloudApiComponentInputHash
|
||||
...((options.componentInputHashes?.[serviceId] || (serviceId === "hwlab-cloud-api" && options.cloudApiComponentInputHash)) ? {
|
||||
componentInputHash: options.cloudApiComponentInputHash ?? options.componentInputHashes?.[serviceId]
|
||||
} : {}),
|
||||
...((serviceId === "hwlab-cloud-web" || serviceId === "hwlab-device-pod") ? {
|
||||
runtimeMode: "env-reuse-git-mirror-checkout",
|
||||
|
||||
@@ -1396,6 +1396,9 @@ const digest = envReuse ? (service.environmentDigest || service.digest || "not_p
|
||||
const imageTag = service.imageTag || (image.includes(":") ? image.slice(image.lastIndexOf(":") + 1) : "");
|
||||
const repository = image.includes(":") ? image.slice(0, image.lastIndexOf(":")) : image;
|
||||
const revision = process.env.HWLAB_SOURCE_REVISION || planned.bootCommit || service.bootCommit || service.sourceCommitId || service.commitId || imageTag;
|
||||
const componentInputHash = envReuse ? (planned.componentInputHash || service.componentInputHash || "") : (service.componentInputHash || "");
|
||||
const environmentInputHash = envReuse ? (service.environmentInputHash || planned.environmentInputHash || "") : (service.environmentInputHash || "");
|
||||
const codeInputHash = envReuse ? (planned.codeInputHash || service.codeInputHash || "") : (service.codeInputHash || "");
|
||||
const values = {
|
||||
"service-id": serviceId,
|
||||
status: /^sha256:[a-f0-9]{64}$/.test(digest) ? "reused" : "blocked_reuse_unavailable",
|
||||
@@ -1404,9 +1407,9 @@ const values = {
|
||||
digest,
|
||||
"repository-digest": /^sha256:[a-f0-9]{64}$/.test(digest) && repository ? repository + "@" + digest : "",
|
||||
"source-commit-id": envReuse ? revision : (service.sourceCommitId || service.commitId || imageTag),
|
||||
"component-input-hash": planned.componentInputHash || service.componentInputHash || "",
|
||||
"environment-input-hash": planned.environmentInputHash || service.environmentInputHash || "",
|
||||
"code-input-hash": planned.codeInputHash || service.codeInputHash || "",
|
||||
"component-input-hash": componentInputHash,
|
||||
"environment-input-hash": environmentInputHash,
|
||||
"code-input-hash": codeInputHash,
|
||||
"runtime-mode": envReuse ? "env-reuse-git-mirror-checkout" : "service-image",
|
||||
"boot-repo": planned.bootRepo || service.bootRepo || "",
|
||||
"boot-commit": envReuse ? revision : (service.bootCommit || ""),
|
||||
|
||||
@@ -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" };
|
||||
|
||||
Reference in New Issue
Block a user