diff --git a/scripts/ci-plan.test.mjs b/scripts/ci-plan.test.mjs index 75c87607..9e4623ca 100644 --- a/scripts/ci-plan.test.mjs +++ b/scripts/ci-plan.test.mjs @@ -87,6 +87,35 @@ test("v03 planner keeps affected code rollouts out of catalog reuse", async () = assert.equal(plan.artifactCatalog.serviceCount, 3); }); +test("v03 planner reports renderer changes as full rollout without image builds", async () => { + const selectedServices = ["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-gateway"]; + const repo = await createFixtureRepo({ serviceIds: selectedServices }); + await mkdir(path.join(repo, "scripts/src/gitops-render"), { recursive: true }); + await writeFile(path.join(repo, "scripts/src/gitops-render/runtime-manifests.mjs"), "export const renderer = 2;\n"); + await git(repo, ["add", "scripts/src/gitops-render/runtime-manifests.mjs"]); + await git(repo, ["commit", "-m", "change runtime manifest renderer"]); + + const plan = await createCiPlan({ + repoRoot: repo, + lane: "v03", + baseRef: "HEAD~1", + targetRef: "HEAD", + artifactCatalogPath: "deploy/artifact-catalog.v03.json", + services: selectedServices + }); + + assert.equal(plan.changedPathSummary.gitopsOnly, true); + assert.deepEqual(plan.affectedServices, selectedServices); + assert.deepEqual(plan.rolloutServices, selectedServices); + assert.deepEqual(plan.buildServices, []); + assert.deepEqual(plan.rolloutWithoutImageBuildServices, selectedServices); + assert.deepEqual(plan.reusedServices, []); + assert.equal(plan.imageBuildRequired, false); + assert.equal(plan.services.every((service) => service.buildRequired === false), true); + assert.equal(plan.services.every((service) => service.reason.includes("gitops-render-input-changed")), true); + assert.equal(plan.ciCdPlan.noImageBuildReason, "gitops-render-only-change"); +}); + test("tracked PaC remote pipelines pass selected services to catalog restore", async () => { const targets = [ { @@ -390,7 +419,7 @@ test("artifact catalog restore rejects a non-bootstrap source catalog when GitOp ); }); -test("node CI planner rolls service runtime config changes without rebuilding image", async () => { +test("node CI planner reports runtime config rendering as full rollout without image builds", async () => { const repo = await createFixtureRepo(); const deployPath = path.join(repo, "deploy/deploy.yaml"); const deploy = await readStructuredFile(repo, deployPath); @@ -400,13 +429,18 @@ test("node CI planner rolls service runtime config changes without rebuilding im 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"]); + assert.deepEqual(plan.affectedServices, ["hwlab-cloud-api", "hwlab-cloud-web"]); + assert.deepEqual(plan.rolloutServices, ["hwlab-cloud-api", "hwlab-cloud-web"]); assert.deepEqual(plan.buildServices, []); + assert.deepEqual(plan.rolloutWithoutImageBuildServices, ["hwlab-cloud-api", "hwlab-cloud-web"]); 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"); }); 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 181e3b1f..37a1a644 100644 --- a/scripts/src/ci-plan-lib.mjs +++ b/scripts/src/ci-plan-lib.mjs @@ -59,6 +59,7 @@ export const DEFAULT_GITOPS_ONLY_PATHS = Object.freeze([ "deploy/gitops/", "deploy/frp/", "scripts/gitops-render.mjs", + "scripts/src/gitops-render/", "scripts/src/runtime-lane.ts", "tsconfig.gitops.json" ]); @@ -69,6 +70,7 @@ export const DEFAULT_GITOPS_RENDER_PATHS = Object.freeze([ "deploy/gitops/", "deploy/frp/", "scripts/gitops-render.mjs", + "scripts/src/gitops-render/", "scripts/src/runtime-lane.ts", "tsconfig.gitops.json" ]); @@ -111,6 +113,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 hasGoService = componentModels.some((model) => model.runtimeKind === "go-service"); const dockerfileHash = await hashGitPaths(repoRoot, targetRef, [ ...envReuseRecipe.additionalEnvPaths, @@ -261,10 +264,10 @@ export async function createCiPlan(options = {}) { const catalogReuse = envReuse ? envReuseCandidate({ catalogRecord, envChanged, environmentImage, environmentDigest, environmentInputHash }) : reuseCandidate(catalogRecordForReuse, imageRelevantChangedPaths.length > 0, componentInputChanged); - const plannedAffected = envReuse + const serviceInputAffected = envReuse ? Boolean(envChanged || codeChanged || runtimeConfigChanged) : imageRelevantChangedPaths.length > 0 || componentInputChanged || catalogReuse.status === "candidate-no-catalog" || catalogReuse.status === "candidate-unverified-digest"; - const plannedBuildRequired = envReuse ? envChanged === true : plannedAffected; + const plannedBuildRequired = envReuse ? envChanged === true : serviceInputAffected; const runtimeReuseDecision = await runtimeReuseDecisionForService({ repoRoot, baseRef, @@ -273,11 +276,12 @@ export async function createCiPlan(options = {}) { config: runtimeReuseByService.get(model.serviceId) ?? null }); const reuseArtifactReady = !envReuse || environmentReady === true; - const affected = runtimeReuseDecision.runtimeReuseHit === true && reuseArtifactReady + const affectedByServiceInputs = runtimeReuseDecision.runtimeReuseHit === true && reuseArtifactReady ? false : runtimeReuseDecision.envReuseHit === true && reuseArtifactReady ? runtimeReuseDecision.sourceIdentityHit === false || runtimeConfigChanged === true - : plannedAffected; + : serviceInputAffected; + const affected = gitopsRenderChanged || affectedByServiceInputs; const buildRequired = runtimeReuseDecision.skipImageBuild === true && reuseArtifactReady ? false : plannedBuildRequired; services.push({ serviceId: model.serviceId, @@ -340,7 +344,9 @@ export async function createCiPlan(options = {}) { envChangedPaths: relevantEnvMatches, codeChangedPaths: relevantCodeMatches, reason: affected - ? reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse, envReuse, envChanged, runtimeConfigChanged, environmentInputChanged: effectiveEnvironmentInputChanged, codeChanged, reuseRegistry }) + ? gitopsRenderChanged && !affectedByServiceInputs + ? ["gitops-render-input-changed"] + : reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse, envReuse, envChanged, runtimeConfigChanged, environmentInputChanged: effectiveEnvironmentInputChanged, codeChanged, reuseRegistry }) : reasonForUnchanged(globalChange), reuse: catalogReuse }); @@ -353,7 +359,6 @@ 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 gitopsRenderChanged = hasGitOpsRenderChange(normalizedChangedPaths); return { planVersion: CI_PLAN_VERSION, sourceCommitId, @@ -407,7 +412,7 @@ export async function createCiPlan(options = {}) { envArtifactGroups: envArtifactGroupPlans, willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0 || gitopsRenderChanged, noImageBuildReason: buildServices.length === 0 - ? (affectedServices.length > 0 ? "env-reuse-code-only-rollout" : gitopsRenderChanged ? "gitops-render-only-change" : globalChange.summary) + ? (gitopsRenderChanged ? "gitops-render-only-change" : affectedServices.length > 0 ? "env-reuse-code-only-rollout" : globalChange.summary) : null } };