From fd392f4f7a48d4bd36f92be61d2c31e007e652cf Mon Sep 17 00:00:00 2001 From: root Date: Tue, 21 Jul 2026 14:36:06 +0200 Subject: [PATCH] =?UTF-8?q?fix(cicd):=20=E6=94=B6=E6=95=9B=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E9=9D=A2=20rollout=20=E8=AE=A1=E5=88=92=E8=8C=83?= =?UTF-8?q?=E5=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ci-plan.test.mjs | 26 +++++++++++++++----- scripts/src/ci-plan-lib.mjs | 48 +++++++++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 18 deletions(-) diff --git a/scripts/ci-plan.test.mjs b/scripts/ci-plan.test.mjs index 981643f5..3d88595c 100644 --- a/scripts/ci-plan.test.mjs +++ b/scripts/ci-plan.test.mjs @@ -419,7 +419,7 @@ test("artifact catalog restore rejects a non-bootstrap source catalog when GitOp ); }); -test("node CI planner reports runtime config rendering as full rollout without image builds", async () => { +test("node CI planner scopes runtime config rendering to the changed service without image builds", async () => { const repo = await createFixtureRepo(); const deployPath = path.join(repo, "deploy/deploy.yaml"); const deploy = await readStructuredFile(repo, deployPath); @@ -429,18 +429,32 @@ test("node CI planner reports runtime config rendering as full rollout without i await git(repo, ["commit", "-m", "change cloud api runtime env"]); const plan = await planV02CoreServices(repo, { baseRef: "HEAD~1", targetRef: "HEAD" }); - assert.deepEqual(plan.affectedServices, ["hwlab-cloud-api", "hwlab-cloud-web"]); - assert.deepEqual(plan.rolloutServices, ["hwlab-cloud-api", "hwlab-cloud-web"]); + assert.deepEqual(plan.affectedServices, ["hwlab-cloud-api"]); + assert.deepEqual(plan.rolloutServices, ["hwlab-cloud-api"]); assert.deepEqual(plan.buildServices, []); - assert.deepEqual(plan.rolloutWithoutImageBuildServices, ["hwlab-cloud-api", "hwlab-cloud-web"]); + assert.deepEqual(plan.rolloutWithoutImageBuildServices, ["hwlab-cloud-api"]); assert.equal(plan.imageBuildRequired, false); const cloudApi = plan.services.find((service) => service.serviceId === "hwlab-cloud-api"); const cloudWeb = plan.services.find((service) => service.serviceId === "hwlab-cloud-web"); assert.equal(cloudApi.runtimeConfigChanged, true); assert.equal(cloudApi.envChanged, false); assert.deepEqual(cloudApi.reason, ["runtime-config-changed"]); - assert.deepEqual(cloudWeb.reason, ["gitops-render-input-changed"]); - assert.equal(plan.ciCdPlan.noImageBuildReason, "gitops-render-only-change"); + assert.equal(cloudWeb.affected, false); + assert.equal(plan.ciCdPlan.noImageBuildReason, "runtime-config-only-change"); +}); + +test("node CI planner keeps Tekton renderer changes out of runtime rollout scope", async () => { + const repo = await createFixtureRepo(); + await mkdir(path.join(repo, "scripts/src/gitops-render"), { recursive: true }); + await writeFile(path.join(repo, "scripts/src/gitops-render/tekton-manifests.mjs"), "export const pipeline = 2;\n"); + await git(repo, ["add", "scripts/src/gitops-render/tekton-manifests.mjs"]); + await git(repo, ["commit", "-m", "change Tekton renderer"]); + + const plan = await planV02CoreServices(repo, { baseRef: "HEAD~1", targetRef: "HEAD" }); + assert.deepEqual(plan.affectedServices, []); + assert.deepEqual(plan.rolloutServices, []); + assert.deepEqual(plan.buildServices, []); + assert.equal(plan.ciCdPlan.willRunGitopsPromote, true); }); test("planner rebuilds when catalog digest is missing instead of blocking reuse", async () => { diff --git a/scripts/src/ci-plan-lib.mjs b/scripts/src/ci-plan-lib.mjs index c225a7fb..08c784dc 100644 --- a/scripts/src/ci-plan-lib.mjs +++ b/scripts/src/ci-plan-lib.mjs @@ -75,6 +75,12 @@ export const DEFAULT_GITOPS_RENDER_PATHS = Object.freeze([ "tsconfig.gitops.json" ]); +export const DEFAULT_GLOBAL_RUNTIME_RENDER_PATHS = Object.freeze([ + "scripts/gitops-render.mjs", + "scripts/src/gitops-render/runtime-manifests.mjs", + "scripts/src/runtime-lane.ts" +]); + export async function createCiPlan(options = {}) { const repoRoot = options.repoRoot ?? process.cwd(); const deployConfigPath = options.deployConfigPath ?? "deploy/deploy.yaml"; @@ -113,7 +119,7 @@ export async function createCiPlan(options = {}) { const envReuseServices = enabledEnvReuseServices(deployJson, lane, serviceIdResolution.serviceIds); const envArtifactGroupByService = envArtifactGroupsByService(runtimeReuseConfig, serviceIdResolution.serviceIds); const globalChange = classifyGlobalChange(normalizedChangedPaths); - const gitopsRenderChanged = hasGitOpsRenderChange(normalizedChangedPaths); + const globalRuntimeRenderChanged = hasGlobalRuntimeRenderChange(normalizedChangedPaths); const hasGoService = componentModels.some((model) => model.runtimeKind === "go-service"); const dockerfileHash = await hashGitPaths(repoRoot, targetRef, [ ...envReuseRecipe.additionalEnvPaths, @@ -134,7 +140,7 @@ export async function createCiPlan(options = {}) { const envArtifactGroup = envReuse ? envArtifactGroupByService.get(model.serviceId) ?? null : null; const bootSh = envReuse ? bootShForService(deployJson, model.serviceId, lane) : null; const serviceEnvReuseRecipe = envReuse ? envReuseRecipeForService(envReuseRecipe.publicRecipe, model) : null; - const runtimeConfigChanged = envReuse ? await serviceRuntimeConfigChanged(repoRoot, deployConfigPath, baseRef, targetRef, lane, model.serviceId) : null; + const runtimeConfigChanged = await serviceRuntimeConfigChanged(repoRoot, deployConfigPath, baseRef, targetRef, lane, model.serviceId); const envInputPaths = envReuse ? uniqueSorted([ ...model.runtimeDeps, ...envReuseRecipe.launcherInputPaths @@ -292,12 +298,14 @@ export async function createCiPlan(options = {}) { config: runtimeReuseByService.get(model.serviceId) ?? null }); const reuseArtifactReady = !envReuse || environmentReady === true; - const affectedByServiceInputs = runtimeReuseDecision.runtimeReuseHit === true && reuseArtifactReady - ? false - : runtimeReuseDecision.envReuseHit === true && reuseArtifactReady - ? runtimeReuseDecision.sourceIdentityHit === false || runtimeConfigChanged === true - : serviceInputAffected; - const affected = gitopsRenderChanged || affectedByServiceInputs; + const affectedByServiceInputs = runtimeConfigChanged === true || ( + runtimeReuseDecision.runtimeReuseHit === true && reuseArtifactReady + ? false + : runtimeReuseDecision.envReuseHit === true && reuseArtifactReady + ? runtimeReuseDecision.sourceIdentityHit === false + : serviceInputAffected + ); + const affected = globalRuntimeRenderChanged || affectedByServiceInputs; const buildRequired = runtimeReuseDecision.skipImageBuild === true && reuseArtifactReady ? false : plannedBuildRequired; services.push({ serviceId: model.serviceId, @@ -363,7 +371,7 @@ export async function createCiPlan(options = {}) { envChangedPaths: relevantEnvMatches, codeChangedPaths: relevantCodeMatches, reason: affected - ? gitopsRenderChanged && !affectedByServiceInputs + ? globalRuntimeRenderChanged && !affectedByServiceInputs ? ["gitops-render-input-changed"] : reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse, envReuse, envChanged, runtimeConfigChanged, environmentInputChanged: effectiveEnvironmentInputChanged, codeChanged, reuseRegistry }) : reasonForUnchanged(globalChange), @@ -378,6 +386,7 @@ export async function createCiPlan(options = {}) { const rolloutWithoutImageBuildServices = services.filter((service) => service.affected && !service.buildRequired).map((service) => service.serviceId); const reusedServices = services.filter((service) => !service.affected && !service.buildRequired).map((service) => service.serviceId); const imageBuildSkippedServices = services.filter((service) => !service.buildRequired).map((service) => service.serviceId); + const runtimeConfigRollout = services.some((service) => service.affected && service.runtimeConfigChanged === true); return { planVersion: CI_PLAN_VERSION, sourceCommitId, @@ -429,9 +438,15 @@ export async function createCiPlan(options = {}) { willRolloutWithoutImageBuild: rolloutWithoutImageBuildServices, willReuse: reusedServices, envArtifactGroups: envArtifactGroupPlans, - willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0 || gitopsRenderChanged, + willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0 || globalRuntimeRenderChanged, noImageBuildReason: buildServices.length === 0 - ? (gitopsRenderChanged ? "gitops-render-only-change" : affectedServices.length > 0 ? "env-reuse-code-only-rollout" : globalChange.summary) + ? (globalRuntimeRenderChanged + ? "gitops-render-only-change" + : runtimeConfigRollout + ? "runtime-config-only-change" + : affectedServices.length > 0 + ? "env-reuse-code-only-rollout" + : globalChange.summary) : null } }; @@ -979,6 +994,15 @@ export function hasGitOpsRenderChange(changedPaths) { return changedPaths.some((item) => isGitOpsRenderPath(item)); } +export function isGlobalRuntimeRenderPath(filePath) { + const normalized = normalizeRepoPath(filePath); + return DEFAULT_GLOBAL_RUNTIME_RENDER_PATHS.some((pattern) => pathMatches(pattern, normalized)); +} + +export function hasGlobalRuntimeRenderChange(changedPaths) { + return changedPaths.some((item) => isGlobalRuntimeRenderPath(item)); +} + export async function changedPathsBetween(repoRoot, baseRef, targetRef) { const diff = await gitValue(repoRoot, ["diff", "--name-only", baseRef, targetRef]); return diff.split("\n").map((line) => line.trim()).filter(Boolean); @@ -1095,7 +1119,7 @@ function reasonForService({ directMatches, sharedMatches, runtimeDepMatches, bui if (envReuse && environmentInputChanged) reasons.push("environment-input-mismatch"); if (envReuse && reuseRegistry && reuseRegistry.status !== "present") reasons.push("reuse-registry-missing"); if (envReuse && envChanged && reasons.length === 0) reasons.push("environment-input-changed"); - if (envReuse && runtimeConfigChanged) reasons.push("runtime-config-changed"); + if (runtimeConfigChanged) reasons.push("runtime-config-changed"); if (envReuse && codeChanged) reasons.push("code-input-changed"); if (directMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("component-path-changed"); if (sharedMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("shared-runtime-changed");