Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions docs/content/docs/guide/authoringprs.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,77 @@ for example, this will show the GitHub event type for a GitHub event:

and then you can do the same conditional or access as described above for the `body` keyword.

## Using the cel: prefix for advanced CEL expressions

For more complex CEL expressions that go beyond simple property access, you can
use the `cel:` prefix. This allows you to write arbitrary CEL expressions with
access to all available data sources.

The `cel:` prefix provides access to:

- `body` - The full webhook payload
- `headers` - HTTP request headers
- `files` - Changed files information (`files.all`, `files.added`, `files.deleted`, `files.modified`, `files.renamed`)
- `pac` - Standard PAC parameters (`pac.revision`, `pac.target_branch`, `pac.source_branch`, etc.)

### Examples

**Conditional values based on event action:**

```yaml
params:
- name: pr-status
value: "{{ cel: body.action == \"opened\" ? \"new-pr\" : \"updated-pr\" }}"
```

**Environment selection based on target branch:**

```yaml
params:
- name: environment
value: "{{ cel: pac.target_branch == \"main\" ? \"production\" : \"staging\" }}"
```

**Safe field access with has() function:**

Use the `has()` function to safely check if a field exists before accessing it:

```yaml
params:
- name: commit-type
value: "{{ cel: has(body.head_commit) && body.head_commit.message.startsWith(\"Merge\") ? \"merge\" : \"regular\" }}"
```

**Check if Go files were modified:**

```yaml
params:
- name: run-go-tests
value: "{{ cel: files.all.exists(f, f.endsWith(\".go\")) ? \"true\" : \"false\" }}"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The ternary operator ? \"true\" : \"false\" is redundant in this CEL expression. Boolean results from CEL expressions are automatically converted to the strings "true" or "false" during template rendering. Simplifying the expression makes the example cleaner and demonstrates a more idiomatic usage of the new feature.

Suggested change
value: "{{ cel: files.all.exists(f, f.endsWith(\".go\")) ? \"true\" : \"false\" }}"
value: "{{ cel: files.all.exists(f, f.endsWith(\".go\")) }}"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not so sure about that, i let this be explicit

```

**String concatenation:**

```yaml
params:
- name: greeting
value: "{{ cel: \"Build for \" + pac.repo_name + \" on \" + pac.target_branch }}"
```

**Count changed files:**

```yaml
params:
- name: file-count
value: "{{ cel: files.all.size() }}"
```

{{< hint info >}}
If a `cel:` expression has a syntax error or fails to evaluate, it returns an
empty string. This allows PipelineRuns to continue even if an optional dynamic
value cannot be computed.
{{< /hint >}}

## Using the temporary GitHub APP Token for GitHub API operations

You can use the temporary installation token that is generated by Pipelines as
Expand Down
30 changes: 23 additions & 7 deletions pkg/cel/cel.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package cel

import (
"bytes"
"encoding/json"
"fmt"

Expand Down Expand Up @@ -38,14 +39,29 @@ func evaluate(expr string, env *cel.Env, data map[string]any) (ref.Val, error) {
// / pacParams, it will output a Cel value or an error if selectedjm.
func Value(query string, body any, headers, pacParams map[string]string, changedFiles map[string]any) (ref.Val, error) {
// Marshal/Unmarshal the body to a map[string]any so we can access it from the CEL
nbody, err := json.Marshal(body)
if err != nil {
return nil, err
}
var jsonMap map[string]any
err = json.Unmarshal(nbody, &jsonMap)
if err != nil {
return nil, err
switch b := body.(type) {
case nil:
jsonMap = map[string]any{}
case map[string]any:
jsonMap = b
default:
nbody, err := json.Marshal(body)
if err != nil {
return nil, err
}
trimmed := bytes.TrimSpace(nbody)
if len(trimmed) == 0 || bytes.Equal(trimmed, []byte("null")) {
jsonMap = map[string]any{}
} else {
err = json.Unmarshal(nbody, &jsonMap)
if err != nil {
return nil, err
}
if jsonMap == nil {
jsonMap = map[string]any{}
}
Comment on lines +61 to +63

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This check for jsonMap == nil is redundant. The json.Unmarshal function, when unmarshaling a JSON object into a *map[string]any, allocates a new map if the pointer is nil. The case where the JSON content is null (which would make jsonMap nil after unmarshaling) is already handled on line 54. Therefore, if json.Unmarshal succeeds at this point, jsonMap is guaranteed to be non-nil. Removing this check simplifies the code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd rather be explicit than caught by surprise

}
}

mapStrDyn := types.NewMapType(types.StringType, types.DynType)
Expand Down
5 changes: 5 additions & 0 deletions pkg/cel/cel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ func TestValue(t *testing.T) {
assert.NilError(t, err)
assert.Equal(t, ref.Val(val), val)

// Test pac-only query with nil body
val, err = Value("pac.param", nil, headers, pacParams, changedFiles)
assert.NilError(t, err)
assert.Equal(t, val.Value(), "value")

// Test an invalid query
_, err = Value("invalid query", body, headers, pacParams, changedFiles)
assert.ErrorContains(t, err, "failed to parse expression")
Expand Down
27 changes: 23 additions & 4 deletions pkg/templates/templating.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ var (
// value.
//
// The function first checks if the key in the placeholder has a prefix of
// "body", "headers", or "files". If it does and both `rawEvent` and `headers`
// "body", "headers", "files", or "cel:". If it does and both `rawEvent` and `headers`
// are not nil, it attempts to retrieve the value for the key using the
// `cel.Value` function and returns the corresponding string
// representation. If the key does not have any of the mentioned prefixes, the
// representation. The "cel:" prefix allows evaluating arbitrary CEL expressions
// with access to body, headers, files, and pac (standard PAC parameters) namespaces.
// If the key does not have any of the mentioned prefixes, the
// function checks if the key exists in the `dico` map. If it does, the
// function replaces the placeholder with the corresponding value from the
// `dico` map.
Expand All @@ -52,10 +54,18 @@ func ReplacePlaceHoldersVariables(template string, dico map[string]string, rawEv
return keys.ParamsRe.ReplaceAllStringFunc(template, func(s string) string {
parts := keys.ParamsRe.FindStringSubmatch(s)
key := strings.TrimSpace(parts[1])
if strings.HasPrefix(key, "body") || strings.HasPrefix(key, "headers") || strings.HasPrefix(key, "files") {

// Check for cel: prefix first - it allows arbitrary CEL expressions
isCelExpr := strings.HasPrefix(key, "cel:")

if strings.HasPrefix(key, "body") || strings.HasPrefix(key, "headers") || strings.HasPrefix(key, "files") || isCelExpr {
// Check specific requirements for each prefix
canEvaluate := false
celExpr := key
switch {
case isCelExpr:
canEvaluate = true
celExpr = strings.TrimSpace(strings.TrimPrefix(key, "cel:"))
case strings.HasPrefix(key, "body") && rawEvent != nil:
canEvaluate = true
case strings.HasPrefix(key, "headers") && headers != nil:
Expand All @@ -70,8 +80,17 @@ func ReplacePlaceHoldersVariables(template string, dico map[string]string, rawEv
for k, v := range headers {
headerMap[k] = v[0]
}
val, err := cel.Value(key, rawEvent, headerMap, map[string]string{}, changedFiles)
// For cel: prefix, pass dico as pacParams so pac.* variables are available
pacParams := map[string]string{}
if isCelExpr {
pacParams = dico
}
val, err := cel.Value(celExpr, rawEvent, headerMap, pacParams, changedFiles)
if err != nil {
// For cel: prefix, return empty string on error
if isCelExpr {
return ""
}
return s
}
var raw any
Expand Down
Loading
Loading