Skip to content

Commit 7afc027

Browse files
committed
feat(cel): add direct custom param variable access in expressions
Allow custom parameters from Repository CR to be accessed directly as CEL variables without template expansion. Parameters can now be used as: param_name == "value" on top of "{{param_name}}" == "value". Jira: https://issues.redhat.com/browse/SRVKP-9118 Signed-off-by: Akshay Pant <[email protected]>
1 parent 5b4173f commit 7afc027

File tree

6 files changed

+492
-19
lines changed

6 files changed

+492
-19
lines changed

docs/content/docs/guide/customparams.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,73 @@ and a pull request event.
122122
- [GitHub Documentation for webhook events](https://docs.github.com/webhooks-and-events/webhooks/webhook-events-and-payloads?actionType=auto_merge_disabled#pull_request)
123123
- [GitLab Documentation for webhook events](https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html)
124124
{{< /hint >}}
125+
126+
### Using custom parameters in CEL matching expressions
127+
128+
In addition to template expansion (`{{ param }}`), custom parameters defined in the Repository CR are available as CEL variables in the `on-cel-expression` annotation. This allows you to control which PipelineRuns are triggered based on repository-specific configuration.
129+
130+
For example, with this Repository CR configuration:
131+
132+
```yaml
133+
apiVersion: pipelinesascode.tekton.dev/v1alpha1
134+
kind: Repository
135+
metadata:
136+
name: my-repo
137+
spec:
138+
url: "https://github.com/owner/repo"
139+
params:
140+
- name: enable_ci
141+
value: "true"
142+
- name: environment
143+
value: "staging"
144+
```
145+
146+
You can use these parameters directly in your PipelineRun's CEL expression:
147+
148+
```yaml
149+
apiVersion: tekton.dev/v1
150+
kind: PipelineRun
151+
metadata:
152+
name: my-pipeline
153+
annotations:
154+
pipelinesascode.tekton.dev/on-cel-expression: |
155+
event == "push" && enable_ci == "true" && environment == "staging"
156+
spec:
157+
# ... pipeline spec
158+
```
159+
160+
This approach is particularly useful for:
161+
162+
- **Conditional CI**: Enable or disable CI for specific repositories without changing PipelineRun files
163+
- **Environment-specific matching**: Run different pipelines based on environment configuration
164+
- **Feature flags**: Control which pipelines run using repository-level feature flags
165+
166+
Custom parameters from secrets are also available:
167+
168+
```yaml
169+
apiVersion: pipelinesascode.tekton.dev/v1alpha1
170+
kind: Repository
171+
metadata:
172+
name: my-repo
173+
spec:
174+
url: "https://github.com/owner/repo"
175+
params:
176+
- name: api_key
177+
secret_ref:
178+
name: my-secret
179+
key: key
180+
```
181+
182+
```yaml
183+
apiVersion: tekton.dev/v1
184+
kind: PipelineRun
185+
metadata:
186+
name: my-pipeline-with-secret
187+
annotations:
188+
pipelinesascode.tekton.dev/on-cel-expression: |
189+
event == "push" && api_key != ""
190+
spec:
191+
# ... pipeline spec
192+
```
193+
194+
For more information on CEL expressions and event matching, see the [Advanced event matching using CEL]({{< relref "/docs/guide/matchingevents#advanced-event-matching-using-cel" >}}) documentation.

docs/content/docs/guide/matchingevents.md

Lines changed: 74 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -301,18 +301,20 @@ pipelinesascode.tekton.dev/on-cel-expression: |
301301

302302
The fields available are:
303303

304-
| **Field** | **Description** |
305-
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
306-
| `event` | `push`, `pull_request` or `incoming`. |
307-
| `target_branch` | The branch we are targeting. |
308-
| `source_branch` | The branch where this pull_request comes from. (On `push`, this is the same as `target_branch`.) |
309-
| `target_url` | The URL of the repository we are targeting. |
310-
| `source_url` | The URL of the repository where this pull_request comes from. (On `push`, this is the same as `target_url`.) |
311-
| `event_title` | Matches the title of the event. For `push`, it matches the commit title. For PR, it matches the Pull/Merge Request title. (Only supported for `GitHub`, `GitLab`, and `BitbucketCloud` providers.) |
312-
| `body` | The full body as passed by the Git provider. Example: `body.pull_request.number` retrieves the pull request number on GitHub. |
313-
| `headers` | The full set of headers as passed by the Git provider. Example: `headers['x-github-event']` retrieves the event type on GitHub. |
314-
| `.pathChanged` | A suffix function to a string that can be a glob of a path to check if changed. (Supported only for `GitHub` and `GitLab` providers.) |
315-
| `files` | The list of files that changed in the event (`all`, `added`, `deleted`, `modified`, and `renamed`). Example: `files.all` or `files.deleted`. For pull requests, every file belonging to the pull request will be listed. |
304+
| **Field** | **Description** |
305+
| --- | --- |
306+
| `event` | `push`, `pull_request` or `incoming`. |
307+
| `event_type` | The event type from the webhook payload header. Provider-specific (e.g., GitHub sends `pull_request`, GitLab is `Merge Request`, etc). |
308+
| `target_branch` | The branch we are targeting. |
309+
| `source_branch` | The branch where this pull_request comes from. (On `push`, this is the same as `target_branch`.) |
310+
| `target_url` | The URL of the repository we are targeting. |
311+
| `source_url` | The URL of the repository where this pull_request comes from. (On `push`, this is the same as `target_url`.) |
312+
| `event_title` | Matches the title of the event. For `push`, it matches the commit title. For PR, it matches the Pull/Merge Request title. (Only supported for `GitHub`, `GitLab`, and `BitbucketCloud` providers.) |
313+
| `body` | The full body as passed by the Git provider. Example: `body.pull_request.number` retrieves the pull request number on GitHub. |
314+
| `headers` | The full set of headers as passed by the Git provider. Example: `headers['x-github-event']` retrieves the event type on GitHub. |
315+
| `.pathChanged` | A suffix function to a string that can be a glob of a path to check if changed. (Supported only for `GitHub` and `GitLab` providers.) |
316+
| `files` | The list of files that changed in the event (`all`, `added`, `deleted`, `modified`, and `renamed`). Example: `files.all` or `files.deleted`. For pull requests, every file belonging to the pull request will be listed. |
317+
| Custom params | Any [custom parameters]({{< relref "/docs/guide/customparams" >}}) provided from the Repository CR `spec.params` are available as CEL variables. Example: `enable_ci == "true"`. See [Using custom parameters in CEL expressions: limitations](#using-custom-parameters-in-cel-expressions-limitations) below for important details. |
316318

317319
CEL expressions let you do more complex filtering compared to the simple `on-target` annotation matching and enable more advanced scenarios.
318320

@@ -330,6 +332,66 @@ You can find more information about the CEL language spec here:
330332
<https://github.com/google/cel-spec/blob/master/doc/langdef.md>
331333
{{< /hint >}}
332334

335+
### Using custom parameters in CEL expressions: limitations
336+
337+
#### Filtered custom parameters and CEL evaluation
338+
339+
When using a custom parameter with a `filter` in a CEL expression, be aware that if the filter condition
340+
is **not met**, the parameter will be **undefined**, causing a CEL evaluation error rather than evaluating to false.
341+
342+
For example, consider this Repository CR:
343+
344+
```yaml
345+
apiVersion: pipelinesascode.tekton.dev/v1alpha1
346+
kind: Repository
347+
metadata:
348+
name: my-repo
349+
spec:
350+
url: "https://github.com/owner/repo"
351+
params:
352+
- name: docker_registry
353+
value: "registry.staging.example.com"
354+
filter: pac.event_type == "pull_request"
355+
```
356+
357+
And this PipelineRun:
358+
359+
```yaml
360+
apiVersion: tekton.dev/v1
361+
kind: PipelineRun
362+
metadata:
363+
name: my-pipeline
364+
annotations:
365+
pipelinesascode.tekton.dev/on-cel-expression: |
366+
docker_registry == "registry.staging.example.com"
367+
spec:
368+
# ... pipeline spec
369+
```
370+
371+
On a **push event**, the `docker_registry` parameter will not be defined (since the filter only matches pull
372+
requests), and the CEL expression will produce an **error**, not `false`. The PipelineRun will not be
373+
evaluated and an error will be reported.
374+
375+
To avoid undefined parameter errors, ensure your CEL expressions only reference custom parameters when their
376+
filter conditions match, or use parameters without filters for CEL matching. We recommend testing your CEL
377+
expressions with different event types using the [tkn pac cel]({{< relref "/docs/guide/cli#tkn-pac-cel" >}})
378+
command to verify they work correctly across all scenarios
379+
380+
#### Custom parameters do not override standard CEL variables
381+
382+
Custom parameters defined in the Repository CR cannot override the built-in CEL variables provided by
383+
Pipelines-as-Code, such as:
384+
385+
* `event` (or `event_type`)
386+
* `target_branch`
387+
* `source_branch`
388+
* `trigger_target`
389+
* And other default variables documented in the table above
390+
391+
If you define a custom parameter with the same name as a standard CEL variable, the standard variable will
392+
take precedence in CEL expressions. Custom parameters should use unique names that don't conflict with
393+
built-in variables.
394+
333395
### Matching a PipelineRun to a branch with a regex
334396

335397
In a CEL expression, you can match a field name using a regular expression. For

pkg/matcher/annotation_matcher.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import (
99
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode"
1010
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
1111
apipac "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
12+
"github.com/openshift-pipelines/pipelines-as-code/pkg/customparams"
1213
pacerrors "github.com/openshift-pipelines/pipelines-as-code/pkg/errors"
1314
"github.com/openshift-pipelines/pipelines-as-code/pkg/events"
1415
"github.com/openshift-pipelines/pipelines-as-code/pkg/formatting"
16+
"github.com/openshift-pipelines/pipelines-as-code/pkg/kubeinteraction"
1517
"github.com/openshift-pipelines/pipelines-as-code/pkg/opscomments"
1618
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
1719
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
@@ -210,6 +212,12 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
210212
}
211213
logger.Info(infomsg)
212214

215+
// Resolve custom params once for all PipelineRuns (for use in CEL expressions)
216+
customParams := resolveCustomParamsForCEL(ctx, repo, event, cs, vcx, eventEmitter, logger)
217+
if len(customParams) > 0 {
218+
logger.Debugf("resolved %d custom params from repo for CEL", len(customParams))
219+
}
220+
213221
celValidationErrors := []*pacerrors.PacYamlValidations{}
214222
for _, prun := range pruns {
215223
prMatch := Match{
@@ -280,7 +288,7 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
280288
if celExpr, ok := prun.GetObjectMeta().GetAnnotations()[keys.OnCelExpression]; ok {
281289
checkPipelineRunAnnotation(prun, eventEmitter, repo)
282290

283-
out, err := celEvaluate(ctx, celExpr, event, vcx)
291+
out, err := celEvaluate(ctx, celExpr, event, vcx, customParams)
284292
if err != nil {
285293
logger.Errorf("there was an error evaluating the CEL expression, skipping: %v", err)
286294
if checkIfCELEvaluateError(err) {
@@ -508,3 +516,38 @@ func MatchRunningPipelineRunForIncomingWebhook(eventType, incomingPipelineRun st
508516
}
509517
return nil
510518
}
519+
520+
// resolveCustomParamsForCEL resolves custom parameters from the Repository CR for use in CEL expressions.
521+
// It returns a map of parameter names to values, excluding reserved keywords.
522+
// All parameters are returned as strings, including those from secret_ref.
523+
func resolveCustomParamsForCEL(ctx context.Context, repo *apipac.Repository, event *info.Event, cs *params.Run, vcx provider.Interface, eventEmitter *events.EventEmitter, logger *zap.SugaredLogger) map[string]string {
524+
if repo == nil || repo.Spec.Params == nil {
525+
return map[string]string{}
526+
}
527+
528+
// Create kubeinteraction interface
529+
kinteract, err := kubeinteraction.NewKubernetesInteraction(cs)
530+
if err != nil {
531+
logger.Warnf("failed to create kubernetes interaction for custom params: %s", err.Error())
532+
return map[string]string{}
533+
}
534+
535+
// Use existing customparams package to resolve all params
536+
cp := customparams.NewCustomParams(event, repo, cs, kinteract, eventEmitter, vcx)
537+
allParams, _, err := cp.GetParams(ctx)
538+
if err != nil {
539+
eventEmitter.EmitMessage(repo, zap.WarnLevel, "CustomParamsCELError",
540+
fmt.Sprintf("failed to resolve custom params for CEL: %s", err.Error()))
541+
return map[string]string{}
542+
}
543+
544+
// Filter to only include params defined in repo.Spec.Params (not standard PAC params)
545+
result := make(map[string]string)
546+
for _, param := range *repo.Spec.Params {
547+
if value, ok := allParams[param.Name]; ok {
548+
result[param.Name] = value
549+
}
550+
}
551+
552+
return result
553+
}

0 commit comments

Comments
 (0)