Merge pull request #2743 from pikasTech/fix/cicd-plan-runtime-scope
Pipelines as Code CI / hwlab-nc01-v03-ci-poll-769921f313290165741122c8c439a73a7f7277f8 Success
Pipelines as Code CI / hwlab-nc01-v03-ci-poll-769921f313290165741122c8c439a73a7f7277f8 Success
fix(cicd): 收敛运行面 rollout 计划范围
This commit is contained in:
@@ -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 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);
|
||||||
@@ -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"]);
|
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", "hwlab-cloud-web"]);
|
assert.deepEqual(plan.affectedServices, ["hwlab-cloud-api"]);
|
||||||
assert.deepEqual(plan.rolloutServices, ["hwlab-cloud-api", "hwlab-cloud-web"]);
|
assert.deepEqual(plan.rolloutServices, ["hwlab-cloud-api"]);
|
||||||
assert.deepEqual(plan.buildServices, []);
|
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);
|
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");
|
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(cloudWeb.affected, false);
|
||||||
assert.equal(plan.ciCdPlan.noImageBuildReason, "gitops-render-only-change");
|
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 () => {
|
test("planner rebuilds when catalog digest is missing instead of blocking reuse", async () => {
|
||||||
|
|||||||
+36
-12
@@ -75,6 +75,12 @@ export const DEFAULT_GITOPS_RENDER_PATHS = Object.freeze([
|
|||||||
"tsconfig.gitops.json"
|
"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 = {}) {
|
export async function createCiPlan(options = {}) {
|
||||||
const repoRoot = options.repoRoot ?? process.cwd();
|
const repoRoot = options.repoRoot ?? process.cwd();
|
||||||
const deployConfigPath = options.deployConfigPath ?? "deploy/deploy.yaml";
|
const deployConfigPath = options.deployConfigPath ?? "deploy/deploy.yaml";
|
||||||
@@ -113,7 +119,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 globalRuntimeRenderChanged = hasGlobalRuntimeRenderChange(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,
|
||||||
@@ -134,7 +140,7 @@ export async function createCiPlan(options = {}) {
|
|||||||
const envArtifactGroup = envReuse ? envArtifactGroupByService.get(model.serviceId) ?? null : null;
|
const envArtifactGroup = envReuse ? envArtifactGroupByService.get(model.serviceId) ?? null : null;
|
||||||
const bootSh = envReuse ? bootShForService(deployJson, model.serviceId, lane) : null;
|
const bootSh = envReuse ? bootShForService(deployJson, model.serviceId, lane) : null;
|
||||||
const serviceEnvReuseRecipe = envReuse ? envReuseRecipeForService(envReuseRecipe.publicRecipe, model) : 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([
|
const envInputPaths = envReuse ? uniqueSorted([
|
||||||
...model.runtimeDeps,
|
...model.runtimeDeps,
|
||||||
...envReuseRecipe.launcherInputPaths
|
...envReuseRecipe.launcherInputPaths
|
||||||
@@ -292,12 +298,14 @@ 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 affectedByServiceInputs = runtimeReuseDecision.runtimeReuseHit === true && reuseArtifactReady
|
const affectedByServiceInputs = runtimeConfigChanged === true || (
|
||||||
? false
|
runtimeReuseDecision.runtimeReuseHit === true && reuseArtifactReady
|
||||||
: runtimeReuseDecision.envReuseHit === true && reuseArtifactReady
|
? false
|
||||||
? runtimeReuseDecision.sourceIdentityHit === false || runtimeConfigChanged === true
|
: runtimeReuseDecision.envReuseHit === true && reuseArtifactReady
|
||||||
: serviceInputAffected;
|
? runtimeReuseDecision.sourceIdentityHit === false
|
||||||
const affected = gitopsRenderChanged || affectedByServiceInputs;
|
: serviceInputAffected
|
||||||
|
);
|
||||||
|
const affected = globalRuntimeRenderChanged || 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,
|
||||||
@@ -363,7 +371,7 @@ export async function createCiPlan(options = {}) {
|
|||||||
envChangedPaths: relevantEnvMatches,
|
envChangedPaths: relevantEnvMatches,
|
||||||
codeChangedPaths: relevantCodeMatches,
|
codeChangedPaths: relevantCodeMatches,
|
||||||
reason: affected
|
reason: affected
|
||||||
? gitopsRenderChanged && !affectedByServiceInputs
|
? globalRuntimeRenderChanged && !affectedByServiceInputs
|
||||||
? ["gitops-render-input-changed"]
|
? ["gitops-render-input-changed"]
|
||||||
: reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse, envReuse, envChanged, runtimeConfigChanged, environmentInputChanged: effectiveEnvironmentInputChanged, codeChanged, reuseRegistry })
|
: reasonForService({ directMatches, sharedMatches, runtimeDepMatches, buildSystemMatches, catalogReuse, envReuse, envChanged, runtimeConfigChanged, environmentInputChanged: effectiveEnvironmentInputChanged, codeChanged, reuseRegistry })
|
||||||
: reasonForUnchanged(globalChange),
|
: 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 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 runtimeConfigRollout = services.some((service) => service.affected && service.runtimeConfigChanged === true);
|
||||||
return {
|
return {
|
||||||
planVersion: CI_PLAN_VERSION,
|
planVersion: CI_PLAN_VERSION,
|
||||||
sourceCommitId,
|
sourceCommitId,
|
||||||
@@ -429,9 +438,15 @@ export async function createCiPlan(options = {}) {
|
|||||||
willRolloutWithoutImageBuild: rolloutWithoutImageBuildServices,
|
willRolloutWithoutImageBuild: rolloutWithoutImageBuildServices,
|
||||||
willReuse: reusedServices,
|
willReuse: reusedServices,
|
||||||
envArtifactGroups: envArtifactGroupPlans,
|
envArtifactGroups: envArtifactGroupPlans,
|
||||||
willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0 || gitopsRenderChanged,
|
willRunGitopsPromote: globalChange.gitopsOnly || affectedServices.length > 0 || globalRuntimeRenderChanged,
|
||||||
noImageBuildReason: buildServices.length === 0
|
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
|
: null
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -979,6 +994,15 @@ export function hasGitOpsRenderChange(changedPaths) {
|
|||||||
return changedPaths.some((item) => isGitOpsRenderPath(item));
|
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) {
|
export async function changedPathsBetween(repoRoot, baseRef, targetRef) {
|
||||||
const diff = await gitValue(repoRoot, ["diff", "--name-only", baseRef, targetRef]);
|
const diff = await gitValue(repoRoot, ["diff", "--name-only", baseRef, targetRef]);
|
||||||
return diff.split("\n").map((line) => line.trim()).filter(Boolean);
|
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 && environmentInputChanged) reasons.push("environment-input-mismatch");
|
||||||
if (envReuse && reuseRegistry && reuseRegistry.status !== "present") reasons.push("reuse-registry-missing");
|
if (envReuse && reuseRegistry && reuseRegistry.status !== "present") reasons.push("reuse-registry-missing");
|
||||||
if (envReuse && envChanged && reasons.length === 0) reasons.push("environment-input-changed");
|
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 (envReuse && codeChanged) reasons.push("code-input-changed");
|
||||||
if (directMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("component-path-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");
|
if (sharedMatches.some((item) => !isTestOnlyPath(item) && !isDocsOnlyPath(item))) reasons.push("shared-runtime-changed");
|
||||||
|
|||||||
Reference in New Issue
Block a user