Merge pull request #1813 from pikasTech/fix/1811-pac-dynamic-pipeline-spec
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

fix(cicd): 修正 PaC pipelineSpec CEL 动态访问
This commit is contained in:
Lyon
2026-07-12 07:01:23 +08:00
committed by GitHub
2 changed files with 29 additions and 1 deletions
@@ -88,10 +88,15 @@ test("versioned admission desired state protects controller proof, candidate ide
const oldRun = { spec: { pipelineSpec: { tasks: [{ name: "plan-artifacts" }] }, workspaces: [], timeouts: { pipeline: "1h" } } };
const mutatedRun = structuredClone(oldRun);
mutatedRun.spec.pipelineSpec.tasks[0].name = "forged-task";
for (const field of ["managedBy", "params", "pipelineRef", "pipelineSpec", "taskRunSpecs", "taskRunTemplate", "timeouts", "workspaces"]) {
for (const field of ["managedBy", "params", "pipelineRef", "taskRunSpecs", "taskRunTemplate", "timeouts", "workspaces"]) {
expect(specGuard.expression).toContain(`has(object.spec.${field})`);
expect(specGuard.expression).toContain(`object.spec.${field} == oldObject.spec.${field}`);
}
expect(specGuard.expression).toContain("has(dyn(object.spec).pipelineSpec)");
expect(specGuard.expression).toContain("has(dyn(oldObject.spec).pipelineSpec)");
expect(specGuard.expression).toContain("dyn(object.spec).pipelineSpec == dyn(oldObject.spec).pipelineSpec");
expect(specGuard.expression).not.toContain("has(object.spec.pipelineSpec)");
expect(specGuard.expression).not.toContain("object.spec.pipelineSpec == oldObject.spec.pipelineSpec");
expect(specGuard.expression).not.toContain("pipelinesascode.tekton.dev/state");
expect(mutatedRun.spec).not.toEqual(oldRun.spec);
expect(queueGuard.expression).toContain('request.userInfo.username == "system:serviceaccount:pipelines-as-code:pipelines-as-code-watcher"');
@@ -141,6 +146,21 @@ test("live admission evaluator requires exact desired hashes, observed generatio
};
const input = { policy, binding, role: rbac[0], roleBinding: rbac[1], namespace: "agentrun-ci", defaultCanMutate: false, expected };
expect(evaluator.evaluatePacAdmissionState(input)).toMatchObject({ ready: true, activeAfter: "2026-01-01T00:00:01Z", defaultCanMutate: false });
expect(evaluator.evaluatePacAdmissionState({
...input,
policy: {
...policy,
status: {
...policy.status,
typeChecking: {
expressionWarnings: [{
fieldRef: "spec.validations[7].expression",
warning: "undefined field 'pipelineSpec'",
}],
},
},
},
})).toMatchObject({ ready: false, reasons: expect.arrayContaining(["policy-expression-warning"]) });
const apiDefaultedPolicy = {
...policy,
spec: {
@@ -319,11 +319,19 @@ function pipelineRefEqual(): string {
}
function specFieldEqual(field: string): string {
if (field === "pipelineSpec") return schemalessSpecFieldEqual(field);
const current = `has(object.spec.${field})`;
const previous = `has(oldObject.spec.${field})`;
return `((${current}) == (${previous}) && (!(${current}) || object.spec.${field} == oldObject.spec.${field}))`;
}
function schemalessSpecFieldEqual(field: string): string {
// Tekton pipelineSpec 保留未知字段且没有静态类型,必须在 dyn 根上检查存在性并做完整深度相等。
const current = `has(dyn(object.spec).${field})`;
const previous = `has(dyn(oldObject.spec).${field})`;
return `((${current}) == (${previous}) && (!(${current}) || dyn(object.spec).${field} == dyn(oldObject.spec).${field}))`;
}
function specStatusState(root: "object" | "oldObject", state: string): string {
if (state === "absent") return `!has(${root}.spec.status)`;
return `has(${root}.spec.status) && ${root}.spec.status == ${cel(state)}`;