From 61ffb311c8c433f4d787c2cce74f8b139beaff28 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 21 Jul 2026 12:50:35 +0200 Subject: [PATCH] fix(cicd): absorb semantic code hash drift --- scripts/ci-plan.test.mjs | 7 +++++++ scripts/src/ci-plan-lib.mjs | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/scripts/ci-plan.test.mjs b/scripts/ci-plan.test.mjs index 6b1f6956..17eb7090 100644 --- a/scripts/ci-plan.test.mjs +++ b/scripts/ci-plan.test.mjs @@ -1178,6 +1178,12 @@ test("planner ignores package scripts-only changes even when package is a compon await git(repo, ["add", "deploy/deploy.yaml", "package.json"]); await git(repo, ["commit", "-m", "seed package scripts"]); await refreshFixtureCatalogForHead(repo); + const catalogPath = path.join(repo, "deploy/artifact-catalog.v02.json"); + const catalog = JSON.parse(await readFile(catalogPath, "utf8")); + for (const service of catalog.services) service.codeInputHash = "legacy-package-script-hash"; + await writeFile(catalogPath, JSON.stringify(catalog, null, 2)); + await git(repo, ["add", "deploy/artifact-catalog.v02.json"]); + await git(repo, ["commit", "-m", "seed legacy code input hashes"]); await writeFile(path.join(repo, "package.json"), JSON.stringify({ type: "module", @@ -1197,6 +1203,7 @@ test("planner ignores package scripts-only changes even when package is a compon assert.deepEqual(plan.affectedServices, []); assert.deepEqual(plan.buildServices, []); assert.equal(plan.services.every((service) => !service.changedPaths.includes("package.json")), true); + assert.equal(plan.services.every((service) => service.codeMetadataHashDrift === true), true); assert.equal(plan.imageBuildRequired, false); assert.equal(plan.buildSkippedCount, 2); }); diff --git a/scripts/src/ci-plan-lib.mjs b/scripts/src/ci-plan-lib.mjs index 3215a98f..1adc21a8 100644 --- a/scripts/src/ci-plan-lib.mjs +++ b/scripts/src/ci-plan-lib.mjs @@ -161,6 +161,7 @@ export async function createCiPlan(options = {}) { ...buildSystemMatches ].filter((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))); const catalogRecord = catalogByServiceId.get(model.serviceId) ?? null; + const catalogEnvironmentSourceCommitId = envReuse ? text(catalogRecord?.sourceCommitId) : ""; const environmentInputHash = envReuse ? await hashEnvReuseInputs(repoRoot, targetRef, envInputPaths, serviceEnvReuseRecipe, { skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath), semanticPackageJson: true @@ -169,10 +170,15 @@ export async function createCiPlan(options = {}) { skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath), semanticPackageJson: true }) : null; + const catalogCodeInputHashAtSource = envReuse && catalogEnvironmentSourceCommitId + ? await hashGitPathsIfCommitExists(repoRoot, catalogEnvironmentSourceCommitId, codeInputPaths, { + skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath), + semanticPackageJson: true + }) + : null; const currentEnvironmentImage = envReuse ? envReuseImageRef(registryPrefix, model.serviceId, environmentInputHash, envArtifactGroup) : null; const catalogEnvironmentImage = envReuse ? environmentImageFromCatalog(model.serviceId, catalogRecord) : null; const catalogEnvironmentDigest = envReuse ? environmentDigestFromCatalog(catalogRecord) : null; - const catalogEnvironmentSourceCommitId = envReuse ? text(catalogRecord?.sourceCommitId) : ""; const catalogEnvironmentInputHashAtSource = envReuse && catalogEnvironmentSourceCommitId ? await hashEnvReuseInputsIfCommitExists(repoRoot, catalogEnvironmentSourceCommitId, envInputPaths, serviceEnvReuseRecipe, { skipPath: (filePath) => isTestOnlyPath(filePath) || isDocsOnlyPath(filePath), @@ -224,7 +230,12 @@ export async function createCiPlan(options = {}) { const effectiveEnvironmentInputChanged = environmentInputChanged === true && !environmentMetadataHashDrift; const environmentReady = envReuse && Boolean(environmentImage) && (reuseRegistryProbe ? reuseRegistry?.status === "present" && digestReady : !effectiveEnvironmentInputChanged && digestReady); const envChanged = envReuse ? Boolean(relevantEnvMatches.length > 0 || !environmentReady || reuseRegistryUnavailable) : null; - const codeChanged = envReuse ? relevantCodeMatches.length > 0 || catalogRecord?.codeInputHash !== codeInputHash : null; + const codeInputChanged = envReuse ? catalogRecord?.codeInputHash !== codeInputHash : null; + const codeMetadataHashDrift = envReuse + && codeInputChanged === true + && Boolean(catalogCodeInputHashAtSource) + && catalogCodeInputHashAtSource === codeInputHash; + const codeChanged = envReuse ? relevantCodeMatches.length > 0 || (codeInputChanged === true && !codeMetadataHashDrift) : null; const componentInputPaths = uniqueSorted([ ...model.componentPaths, ...model.sharedPaths, @@ -338,6 +349,9 @@ export async function createCiPlan(options = {}) { catalogEnvironmentInputHashAtSource, environmentSourceUnchanged, codeChanged, + codeInputChanged, + codeMetadataHashDrift, + catalogCodeInputHashAtSource, reuseRegistry, environmentInputHash, codeInputHash,