fix: report GitOps renderer rollout scope

This commit is contained in:
root
2026-07-21 11:10:20 +02:00
parent 72f595c7f1
commit b9415654cd
2 changed files with 48 additions and 9 deletions
+36 -2
View File
@@ -87,6 +87,35 @@ test("v03 planner keeps affected code rollouts out of catalog reuse", async () =
assert.equal(plan.artifactCatalog.serviceCount, 3); 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 () => { test("tracked PaC remote pipelines pass selected services to catalog restore", async () => {
const targets = [ 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 repo = await createFixtureRepo();
const deployPath = path.join(repo, "deploy/deploy.yaml"); const deployPath = path.join(repo, "deploy/deploy.yaml");
const deploy = await readStructuredFile(repo, deployPath); 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"]); await git(repo, ["commit", "-m", "change cloud api runtime env"]);
const plan = await planV02CoreServices(repo, { baseRef: "HEAD~1", targetRef: "HEAD" }); 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.buildServices, []);
assert.deepEqual(plan.rolloutWithoutImageBuildServices, ["hwlab-cloud-api", "hwlab-cloud-web"]);
assert.equal(plan.imageBuildRequired, false); assert.equal(plan.imageBuildRequired, false);
const cloudApi = plan.services.find((service) => service.serviceId === "hwlab-cloud-api"); 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.runtimeConfigChanged, true);
assert.equal(cloudApi.envChanged, false); assert.equal(cloudApi.envChanged, false);
assert.deepEqual(cloudApi.reason, ["runtime-config-changed"]); 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 () => { test("planner rebuilds when catalog digest is missing instead of blocking reuse", async () => {
+12 -7
View File
@@ -59,6 +59,7 @@ export const DEFAULT_GITOPS_ONLY_PATHS = Object.freeze([
"deploy/gitops/", "deploy/gitops/",
"deploy/frp/", "deploy/frp/",
"scripts/gitops-render.mjs", "scripts/gitops-render.mjs",
"scripts/src/gitops-render/",
"scripts/src/runtime-lane.ts", "scripts/src/runtime-lane.ts",
"tsconfig.gitops.json" "tsconfig.gitops.json"
]); ]);
@@ -69,6 +70,7 @@ export const DEFAULT_GITOPS_RENDER_PATHS = Object.freeze([
"deploy/gitops/", "deploy/gitops/",
"deploy/frp/", "deploy/frp/",
"scripts/gitops-render.mjs", "scripts/gitops-render.mjs",
"scripts/src/gitops-render/",
"scripts/src/runtime-lane.ts", "scripts/src/runtime-lane.ts",
"tsconfig.gitops.json" "tsconfig.gitops.json"
]); ]);
@@ -111,6 +113,7 @@ export async function createCiPlan(options = {}) {
const envReuseServices = enabledEnvReuseServices(deployJson, lane, serviceIdResolution.serviceIds); const envReuseServices = enabledEnvReuseServices(deployJson, lane, serviceIdResolution.serviceIds);
const envArtifactGroupByService = envArtifactGroupsByService(runtimeReuseConfig, serviceIdResolution.serviceIds); const envArtifactGroupByService = envArtifactGroupsByService(runtimeReuseConfig, serviceIdResolution.serviceIds);
const globalChange = classifyGlobalChange(normalizedChangedPaths); const globalChange = classifyGlobalChange(normalizedChangedPaths);
const gitopsRenderChanged = hasGitOpsRenderChange(normalizedChangedPaths);
const hasGoService = componentModels.some((model) => model.runtimeKind === "go-service"); const hasGoService = componentModels.some((model) => model.runtimeKind === "go-service");
const dockerfileHash = await hashGitPaths(repoRoot, targetRef, [ const dockerfileHash = await hashGitPaths(repoRoot, targetRef, [
...envReuseRecipe.additionalEnvPaths, ...envReuseRecipe.additionalEnvPaths,
@@ -261,10 +264,10 @@ export async function createCiPlan(options = {}) {
const catalogReuse = envReuse const catalogReuse = envReuse
? envReuseCandidate({ catalogRecord, envChanged, environmentImage, environmentDigest, environmentInputHash }) ? envReuseCandidate({ catalogRecord, envChanged, environmentImage, environmentDigest, environmentInputHash })
: reuseCandidate(catalogRecordForReuse, imageRelevantChangedPaths.length > 0, componentInputChanged); : reuseCandidate(catalogRecordForReuse, imageRelevantChangedPaths.length > 0, componentInputChanged);
const plannedAffected = envReuse const serviceInputAffected = envReuse
? Boolean(envChanged || codeChanged || runtimeConfigChanged) ? Boolean(envChanged || codeChanged || runtimeConfigChanged)
: imageRelevantChangedPaths.length > 0 || componentInputChanged || catalogReuse.status === "candidate-no-catalog" || catalogReuse.status === "candidate-unverified-digest"; : 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({ const runtimeReuseDecision = await runtimeReuseDecisionForService({
repoRoot, repoRoot,
baseRef, baseRef,
@@ -273,11 +276,12 @@ export async function createCiPlan(options = {}) {
config: runtimeReuseByService.get(model.serviceId) ?? null config: runtimeReuseByService.get(model.serviceId) ?? null
}); });
const reuseArtifactReady = !envReuse || environmentReady === true; const reuseArtifactReady = !envReuse || environmentReady === true;
const affected = runtimeReuseDecision.runtimeReuseHit === true && reuseArtifactReady const affectedByServiceInputs = runtimeReuseDecision.runtimeReuseHit === true && reuseArtifactReady
? false ? false
: runtimeReuseDecision.envReuseHit === true && reuseArtifactReady : runtimeReuseDecision.envReuseHit === true && reuseArtifactReady
? runtimeReuseDecision.sourceIdentityHit === false || runtimeConfigChanged === true ? runtimeReuseDecision.sourceIdentityHit === false || runtimeConfigChanged === true
: plannedAffected; : serviceInputAffected;
const affected = gitopsRenderChanged || affectedByServiceInputs;
const buildRequired = runtimeReuseDecision.skipImageBuild === true && reuseArtifactReady ? false : plannedBuildRequired; const buildRequired = runtimeReuseDecision.skipImageBuild === true && reuseArtifactReady ? false : plannedBuildRequired;
services.push({ services.push({
serviceId: model.serviceId, serviceId: model.serviceId,
@@ -340,7 +344,9 @@ export async function createCiPlan(options = {}) {
envChangedPaths: relevantEnvMatches, envChangedPaths: relevantEnvMatches,
codeChangedPaths: relevantCodeMatches, codeChangedPaths: relevantCodeMatches,
reason: affected 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), : reasonForUnchanged(globalChange),
reuse: catalogReuse 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 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 reusedServices = services.filter((service) => !service.affected && !service.buildRequired).map((service) => service.serviceId);
const imageBuildSkippedServices = services.filter((service) => !service.buildRequired).map((service) => service.serviceId); const imageBuildSkippedServices = services.filter((service) => !service.buildRequired).map((service) => service.serviceId);
const gitopsRenderChanged = hasGitOpsRenderChange(normalizedChangedPaths);
return { return {
planVersion: CI_PLAN_VERSION, planVersion: CI_PLAN_VERSION,
sourceCommitId, sourceCommitId,
@@ -407,7 +412,7 @@ export async function createCiPlan(options = {}) {
envArtifactGroups: envArtifactGroupPlans, envArtifactGroups: envArtifactGroupPlans,
willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0 || gitopsRenderChanged, willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0 || gitopsRenderChanged,
noImageBuildReason: buildServices.length === 0 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 : null
} }
}; };