Skip to content

Commit 1aa9629

Browse files
zakiskclaude
andcommitted
feat(cel): enable string and list extension functions in CEL expressions
Previously, CEL expressions in Pipelines-as-Code only had access to the core CEL operators, which limited users to basic comparisons and logical expressions. Functions like join(), replace(), substring(), and other string/list manipulation operations were unavailable, forcing users to work around these limitations. This adds cel-go's ext.Strings() and ext.Lists() extensions to the CEL environment in pkg/cel/cel.go. This unlocks the full set of standard CEL string operations (join, replace, substring, split, trim, upperAscii, lowerAscii, etc.) and list operations, which can be used in both `on-cel-expression` annotations and `{{ cel: }}` template expressions. Changes: - pkg/cel/cel.go: Register ext.Strings() and ext.Lists() extensions on the CEL environment alongside existing variable declarations - pkg/templates/templating_test.go: Add unit tests for join(), replace(), and substring() via the template placeholder path; improve test failure message to show actual vs expected values - test/testdata/pipelinerun-cel-string-join.yaml: New e2e test fixture that uses files.all.join(", ") in a cel: template - test/github_pullrequest_test.go: Add GHE e2e test (TestGithubGHEPullRequestCELJoin) that verifies join() works end-to-end by checking the PipelineRun pod log output matches the expected changed file path Note: The e2e test uses regex matching (empty goldenFile param) rather than golden file comparison, since the output is a single dynamic file path that varies per test run. Signed-off-by: Zaki Shaikh <zashaikh@redhat.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 861a507 commit 1aa9629

46 files changed

Lines changed: 16015 additions & 3 deletions

Some content is hidden

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

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/google/cel-go v0.28.0
1414
github.com/google/go-cmp v0.7.0
1515
github.com/google/go-github/scrape v0.0.0-20260403152401-96a365122246
16+
github.com/google/go-github/v84 v84.0.0
1617
github.com/google/go-github/v85 v85.0.0
1718
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
1819
github.com/jenkins-x/go-scm v1.15.17
@@ -78,7 +79,6 @@ require (
7879
github.com/go-openapi/swag/typeutils v0.25.5 // indirect
7980
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
8081
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
81-
github.com/google/go-github/v84 v84.0.0 // indirect
8282
github.com/oklog/ulid/v2 v2.1.1 // indirect
8383
github.com/prometheus/otlptranslator v1.0.0 // indirect
8484
github.com/rickb777/plural v1.4.10 // indirect

pkg/cel/cel.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/google/cel-go/common/decls"
1010
"github.com/google/cel-go/common/types"
1111
"github.com/google/cel-go/common/types/ref"
12+
"github.com/google/cel-go/ext"
1213
)
1314

1415
func evaluate(expr string, env *cel.Env, data map[string]any) (ref.Val, error) {
@@ -90,7 +91,10 @@ func Value(query string, body any, headers, pacParams map[string]string, changed
9091
decls.NewVariable("pull_request_labels", types.StringType),
9192
decls.NewVariable("pull_request_number", types.StringType),
9293
decls.NewVariable("git_auth_secret", types.StringType),
93-
))
94+
),
95+
ext.Strings(),
96+
ext.Lists(),
97+
)
9498
val, err := evaluate(query, celDec, map[string]any{
9599
"body": jsonMap,
96100
"pac": pacParams,

pkg/templates/templating_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,59 @@ func TestReplacePlaceHoldersVariables(t *testing.T) {
175175
"hello": "world",
176176
},
177177
},
178+
{
179+
name: "Test CEL with string join",
180+
template: `all files: {{ files.all.join(", ") }}`,
181+
expected: `all files: file1.txt, file2.txt, file3.txt`,
182+
dicto: map[string]string{
183+
"revision": "main",
184+
},
185+
changedFiles: map[string]any{
186+
"all": []string{"file1.txt", "file2.txt", "file3.txt"},
187+
},
188+
headers: http.Header{
189+
"Test": []string{"value"},
190+
},
191+
rawEvent: map[string]any{
192+
"hello": "world",
193+
},
194+
},
195+
{
196+
name: "Test CEL with string replace",
197+
template: `{{ cel: trigger_comment.replace("bad", "good") }}`,
198+
expected: `a good comment!`,
199+
dicto: map[string]string{
200+
"trigger_comment": "a bad comment!",
201+
},
202+
changedFiles: map[string]any{},
203+
headers: http.Header{
204+
"Test": []string{"value"},
205+
},
206+
rawEvent: map[string]any{
207+
"hello": "world",
208+
},
209+
},
210+
{
211+
name: "Test CEL with string substring",
212+
template: `{{ cel: "HELLOWORLD".substring(0, 5) }}`,
213+
expected: `HELLO`,
214+
dicto: map[string]string{
215+
"trigger_comment": "a bad comment!",
216+
},
217+
changedFiles: map[string]any{},
218+
headers: http.Header{
219+
"Test": []string{"value"},
220+
},
221+
rawEvent: map[string]any{
222+
"hello": "world",
223+
},
224+
},
178225
}
179226
for _, tt := range tests {
180227
t.Run(tt.name, func(t *testing.T) {
181228
got := ReplacePlaceHoldersVariables(tt.template, tt.dicto, tt.rawEvent, tt.headers, tt.changedFiles)
182229
if d := cmp.Diff(got, tt.expected); d != "" {
183-
t.Fatalf("-got, +want: %v", d)
230+
t.Fatalf("-got %s, +want: %s", got, tt.expected)
184231
}
185232
})
186233
}

test/github_pullrequest_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -765,6 +765,34 @@ func TestGithubGHEPullRequestCelPrefix(t *testing.T) {
765765
assert.NilError(t, err)
766766
}
767767

768+
func TestGithubGHEPullRequestCELJoin(t *testing.T) {
769+
ctx := context.Background()
770+
g := &tgithub.PRTest{
771+
Label: "Github CEL String Join",
772+
YamlFiles: []string{"testdata/pipelinerun-cel-string-join.yaml"},
773+
GHE: true,
774+
}
775+
g.RunPullRequest(ctx, t)
776+
defer g.TearDown(ctx, t)
777+
778+
prs, err := g.Cnx.Clients.Tekton.TektonV1().PipelineRuns(g.TargetNamespace).List(ctx, metav1.ListOptions{})
779+
assert.NilError(t, err)
780+
assert.Assert(t, len(prs.Items) >= 1, "Expected at least one PipelineRun")
781+
782+
err = twait.RegexpMatchingInPodLog(
783+
ctx,
784+
g.Cnx,
785+
g.TargetNamespace,
786+
fmt.Sprintf("tekton.dev/pipelineRun=%s,tekton.dev/pipelineTask=cel-string-join-test", prs.Items[0].Name),
787+
"step-test-cel-string-join-values",
788+
*regexp.MustCompile(".*tekton/pipelinerun-cel-string-join.yaml"),
789+
"",
790+
2,
791+
nil,
792+
)
793+
assert.NilError(t, err)
794+
}
795+
768796
// Local Variables:
769797
// compile-command: "go test -tags=e2e -v -info TestGithubPullRequest$ ."
770798
// End:
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
apiVersion: tekton.dev/v1beta1
3+
kind: PipelineRun
4+
metadata:
5+
name: "\\ .PipelineName //"
6+
annotations:
7+
pipelinesascode.tekton.dev/target-namespace: "\\ .TargetNamespace //"
8+
pipelinesascode.tekton.dev/on-cel-expression: |
9+
target_branch == "\\ .TargetBranch //" && event == "\\ .TargetEvent //"
10+
spec:
11+
pipelineSpec:
12+
tasks:
13+
- name: cel-string-join-test
14+
taskSpec:
15+
steps:
16+
- name: test-cel-string-join-values
17+
image: registry.access.redhat.com/ubi10/ubi-micro
18+
script: |
19+
echo "{{ cel: files.all.join(", ") }}"
20+
exit 0

vendor/github.com/google/cel-go/ext/BUILD.bazel

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)