Skip to content

Commit c379ccd

Browse files
committed
feat: Add CEL expression evaluator command
This commit introduces a new command `tkn pac cel` that allows users to interactively evaluate CEL (Common Expression Language) expressions. The command is designed to help users test and debug CEL expressions, which are commonly used in Pipelines-as-Code. Key features include: - Interactive and non-interactive modes. - Support for webhook payloads and headers. - Provider auto-detection (GitHub, GitLab, Bitbucket Cloud, Bitbucket Data Center, Gitea). - Direct access to variables as per PAC documentation. - Cross-platform history with readline experience. - Comprehensive help and example expressions. Note: this evaluator is primarily intended for repository or GitHub app admins, as it requires access to webhook payloads and headers. Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
1 parent fbb4d9e commit c379ccd

File tree

99 files changed

+9400
-117
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+9400
-117
lines changed

docs/content/docs/guide/cli.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,114 @@ You can specify a different directory than the current one by using the -d/--dir
409409

410410
{{< /details >}}
411411

412+
{{< details "tkn pac cel" >}}
413+
414+
### CEL Expression Evaluator
415+
416+
{{< hint danger >}}
417+
The CEL evaluator is only useful for admins, since the payload and headers as seen by Pipelines-as-Code needs to be provided and is only available to the repository or GitHub app admins.
418+
{{< /hint >}}
419+
`tkn pac cel` — Evaluate CEL (Common Expression Language) expressions interactively with webhook payloads.
420+
421+
This command allows you to test and debug CEL expressions as they would be evaluated by Pipelines-as-Code, using real webhook payloads and headers. It supports interactive and non-interactive modes, provider auto-detection, and persistent history.
422+
423+
To be able to have the CEL evaluator working, you need to have the payload and the headers available in a file. The best way to do this is to go to the webhook configuration on your git provider and copy the payload and headers to different files.
424+
425+
The payload is the JSON content of the webhook request, The headers file supports multiple formats:
426+
427+
1. **Plain HTTP headers format** (as shown above)
428+
2. **JSON format**:
429+
430+
```json
431+
{
432+
"X-GitHub-Event": "pull_request",
433+
"Content-Type": "application/json",
434+
"User-Agent": "GitHub-Hookshot/2d5e4d4"
435+
}
436+
```
437+
438+
3. **Gosmee-generated shell scripts**: The command automatically detects and parses shell scripts generated by [gosmee](https://github.com/chmouel/gosmee) which are generated when using the `--save` feature, extracting headers from curl commands with `-H` flags:
439+
440+
```bash
441+
#!/usr/bin/env bash
442+
curl -X POST "http://localhost:8080/" \
443+
-H "X-GitHub-Event: pull_request" \
444+
-H "Content-Type: application/json" \
445+
-H "User-Agent: GitHub-Hookshot/2d5e4d4" \
446+
-d @payload.json
447+
```
448+
449+
#### Usage
450+
451+
```shell
452+
tkn pac cel -b <body.json> -H <headers.txt>
453+
```
454+
455+
* `-b, --body`: Path to JSON body file (webhook payload)
456+
* `-H, --headers`: Path to headers file (plain text, JSON, or gosmee script)
457+
* `-p, --provider`: Provider (auto, github, gitlab, bitbucket-cloud, bitbucket-datacenter, gitea)
458+
459+
#### Interactive Mode
460+
461+
If run in a terminal, you'll get a prompt:
462+
463+
```console
464+
CEL expression>
465+
```
466+
467+
* Use ↑/↓ arrows to navigate history.
468+
* History is saved and loaded automatically.
469+
* Press Enter on an empty line to exit.
470+
471+
#### Non-Interactive Mode
472+
473+
Pipe expressions via stdin:
474+
475+
```shell
476+
echo 'event == "pull_request"' | tkn pac cel -b body.json -H headers.txt
477+
```
478+
479+
#### Available Variables
480+
481+
* **Direct variables** (top-level, as per PAC documentation):
482+
* `event` — event type (push, pull_request)
483+
* `target_branch` — target branch name
484+
* `source_branch` — source branch name
485+
* `target_url` — target repository URL
486+
* `source_url` — source repository URL
487+
* `event_title` — PR title or commit message
488+
489+
* **Webhook payload** (`body.*`): All fields from the webhook JSON.
490+
* **HTTP headers** (`headers.*`): All HTTP headers.
491+
* **Files** (`files.*`): Always empty in CLI mode.
492+
**Note:** `fileChanged`, `fileDeleted`, `fileModified` and similar functions are **not implemented yet** in the CLI.
493+
* **PAC Parameters** (`pac.*`): All variables for backward compatibility.
494+
495+
#### Example Expressions
496+
497+
```text
498+
event == "pull_request" && target_branch == "main"
499+
event == "pull_request" && source_branch.matches(".*feat/.*")
500+
body.action == "synchronize"
501+
!body.pull_request.draft
502+
headers['x-github-event'] == "pull_request"
503+
event == "pull_request" && target_branch != "experimental"
504+
```
505+
506+
#### Limitations
507+
508+
* `files.*` variables are always empty in CLI mode.
509+
* Functions like `fileChanged`, `fileDeleted`, `fileModified` are **not implemented yet** in the CLI.
510+
511+
#### Cross-Platform History
512+
513+
* History is saved in a cache directory:
514+
* Linux/macOS: `~/.cache/tkn-pac/cel-history`
515+
* Windows: `%USERPROFILE%\.cache\tkn-pac\cel-history`
516+
* The directory is created automatically if it does not exist.
517+
518+
{{< /details >}}
519+
412520
## Screenshot
413521

414522
![tkn-plug-in](/images/tkn-pac-cli.png)

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
code.gitea.io/sdk/gitea v0.21.0
1010
github.com/AlecAivazis/survey/v2 v2.3.7
1111
github.com/bradleyfalzon/ghinstallation/v2 v2.15.0
12+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
1213
github.com/cloudevents/sdk-go/v2 v2.16.0
1314
github.com/fvbommel/sortorder v1.1.0
1415
github.com/gobwas/glob v0.2.3
@@ -128,8 +129,8 @@ require (
128129
go.uber.org/multierr v1.11.0 // indirect
129130
golang.org/x/crypto v0.37.0 // indirect
130131
golang.org/x/net v0.39.0 // indirect
131-
golang.org/x/sys v0.33.0 // indirect
132-
golang.org/x/term v0.31.0 // indirect
132+
golang.org/x/sys v0.35.0 // indirect
133+
golang.org/x/term v0.34.0
133134
golang.org/x/time v0.11.0 // indirect
134135
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
135136
google.golang.org/api v0.231.0 // indirect

go.sum

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,11 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
8787
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
8888
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
8989
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
90+
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
9091
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
92+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
9193
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
94+
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
9295
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
9396
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
9497
github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.15.2 h1:AbtPqiUDzKup5JpTZzO297/QXgL/TAdpdXQCNwLzlaM=
@@ -685,8 +688,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
685688
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
686689
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
687690
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
688-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
689-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
691+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
692+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
690693
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
691694
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
692695
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -696,8 +699,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
696699
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
697700
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
698701
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
699-
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
700-
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
702+
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
703+
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
701704
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
702705
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
703706
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

pkg/cel/cel.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,50 @@ func Value(query string, body any, headers, pacParams map[string]string, changed
5555
decls.NewVariable("headers", mapStrDyn),
5656
decls.NewVariable("pac", mapStrDyn),
5757
decls.NewVariable("files", mapStrDyn),
58+
// Direct variables as per documentation
59+
decls.NewVariable("event", types.StringType),
60+
decls.NewVariable("event_type", types.StringType),
61+
decls.NewVariable("target_branch", types.StringType),
62+
decls.NewVariable("source_branch", types.StringType),
63+
decls.NewVariable("target_url", types.StringType),
64+
decls.NewVariable("source_url", types.StringType),
65+
decls.NewVariable("event_title", types.StringType),
66+
decls.NewVariable("revision", types.StringType),
67+
decls.NewVariable("repo_owner", types.StringType),
68+
decls.NewVariable("repo_name", types.StringType),
69+
decls.NewVariable("sender", types.StringType),
70+
decls.NewVariable("repo_url", types.StringType),
71+
decls.NewVariable("git_tag", types.StringType),
72+
decls.NewVariable("target_namespace", types.StringType),
73+
decls.NewVariable("trigger_comment", types.StringType),
74+
decls.NewVariable("pull_request_labels", types.StringType),
75+
decls.NewVariable("pull_request_number", types.StringType),
76+
decls.NewVariable("git_auth_secret", types.StringType),
5877
))
5978
val, err := evaluate(query, celDec, map[string]any{
6079
"body": jsonMap,
6180
"pac": pacParams,
6281
"headers": headers,
6382
"files": changedFiles,
83+
// Direct variables - all from pacParams
84+
"event": pacParams["event"],
85+
"event_type": pacParams["event_type"],
86+
"target_branch": pacParams["target_branch"],
87+
"source_branch": pacParams["source_branch"],
88+
"target_url": pacParams["target_url"],
89+
"source_url": pacParams["source_url"],
90+
"event_title": pacParams["event_title"],
91+
"revision": pacParams["revision"],
92+
"repo_owner": pacParams["repo_owner"],
93+
"repo_name": pacParams["repo_name"],
94+
"sender": pacParams["sender"],
95+
"repo_url": pacParams["repo_url"],
96+
"git_tag": pacParams["git_tag"],
97+
"target_namespace": pacParams["target_namespace"],
98+
"trigger_comment": pacParams["trigger_comment"],
99+
"pull_request_labels": pacParams["pull_request_labels"],
100+
"pull_request_number": pacParams["pull_request_number"],
101+
"git_auth_secret": pacParams["git_auth_secret"],
64102
})
65103
if err != nil {
66104
return nil, err
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package cel
2+
3+
import (
4+
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
5+
)
6+
7+
// eventFromBitbucketCloud parses Bitbucket Cloud webhook payload for CEL evaluation.
8+
func eventFromBitbucketCloud(bodyBytes []byte, headers map[string]string) (*info.Event, error) {
9+
return parseWebhookForCEL(bodyBytes, headers, &BitbucketCloudParser{})
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package cel
2+
3+
import (
4+
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
5+
)
6+
7+
// eventFromBitbucketDataCenter parses Bitbucket Data Center webhook payload for CEL evaluation.
8+
func eventFromBitbucketDataCenter(bodyBytes []byte, headers map[string]string) (*info.Event, error) {
9+
return parseWebhookForCEL(bodyBytes, headers, &BitbucketDataCenterParser{})
10+
}

0 commit comments

Comments
 (0)