diff --git a/docs/cel_expressions.md b/docs/cel_expressions.md
index bd61e82f3..0cc2f9a71 100644
--- a/docs/cel_expressions.md
+++ b/docs/cel_expressions.md
@@ -442,6 +442,35 @@ which can be accessed by indexing.
{"testing":"value"}.marshalJSON() == "{\"testing\": \"value\"}"
+
+
+ first()
+ |
+
+ <jsonArray>.first() -> <jsonObject>
+ |
+
+ Returns the first element in the array.
+ |
+
+ [1, 2, 3, 4, 5].first() == 1
+ |
+
+
+
+ last()
+ |
+
+ <jsonArray>.last() -> <jsonObject>
+ |
+
+ Returns the last element in the array.
+ |
+
+ [1, 2, 3, 4, 5].last() == 5
+ |
+
+
## Troubleshooting CEL expressions
diff --git a/pkg/interceptors/cel/cel_test.go b/pkg/interceptors/cel/cel_test.go
index 7b48ceef1..482fe5f10 100644
--- a/pkg/interceptors/cel/cel_test.go
+++ b/pkg/interceptors/cel/cel_test.go
@@ -440,6 +440,10 @@ func TestExpressionEvaluation(t *testing.T) {
},
},
},
+ "numbers": []int64{
+ 1, 2, 3, 4, 5,
+ },
+ "emptyList": []int64{},
}
refParts := strings.Split(testRef, "/")
@@ -621,6 +625,47 @@ func TestExpressionEvaluation(t *testing.T) {
expr: "body.jsonArray.join(', ')",
want: types.String("one, two"),
},
+ {
+ name: "last element in array",
+ expr: "body.testURL.parseURL().path.split('/').last()",
+ want: types.String("to"),
+ },
+ {
+ name: "last element in empty array",
+ expr: `body.emptyList.last()`,
+ want: types.NullValue,
+ },
+ {
+ name: "last element in numeric array",
+ expr: `body.numbers.last()`,
+ want: types.Int(5),
+ },
+ {
+ name: "last element in literal array",
+ expr: "[1, 2, 3, 4, 5].last()",
+ want: types.Int(5),
+ },
+ {
+ name: "first element in literal array",
+ expr: "[1, 2, 3, 4, 5].first()",
+ want: types.Int(1),
+ },
+ {
+ name: "first element in array",
+ // Splitting gets an empty route element so this filters it out.
+ expr: "body.testURL.parseURL().path.split('/').filter(t, t.size() > 0).first()",
+ want: types.String("path"),
+ },
+ {
+ name: "first element in empty array",
+ expr: `body.emptyList.first()`,
+ want: types.NullValue,
+ },
+ {
+ name: "first element in numeric array",
+ expr: `body.numbers.first()`,
+ want: types.Int(1),
+ },
}
for _, tt := range tests {
t.Run(tt.name, func(rt *testing.T) {
diff --git a/pkg/interceptors/cel/triggers.go b/pkg/interceptors/cel/triggers.go
index f1d95fd91..9b3736371 100644
--- a/pkg/interceptors/cel/triggers.go
+++ b/pkg/interceptors/cel/triggers.go
@@ -27,6 +27,7 @@ import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
+ "github.com/google/cel-go/common/types/traits"
"github.com/google/cel-go/interpreter/functions"
"github.com/tektoncd/triggers/pkg/interceptors"
"sigs.k8s.io/yaml"
@@ -187,6 +188,12 @@ func (t triggersLib) CompileOptions() []cel.EnvOption {
cel.UnaryBinding(marshalJSON)),
cel.MemberOverload("marshalJSON_list", []*cel.Type{listStrDyn}, cel.StringType,
cel.UnaryBinding(marshalJSON))),
+ cel.Function("last",
+ cel.MemberOverload("last_list", []*cel.Type{listStrDyn}, cel.DynType,
+ cel.UnaryBinding(listLast))),
+ cel.Function("first",
+ cel.MemberOverload("first_list", []*cel.Type{listStrDyn}, cel.DynType,
+ cel.UnaryBinding(listFirst))),
}
}
@@ -324,6 +331,28 @@ func marshalJSON(val ref.Val) ref.Val {
return types.String(marshaledVal)
}
+func listLast(val ref.Val) ref.Val {
+ l := val.(traits.Lister)
+ sz := l.Size().Value().(int64)
+
+ if sz == 0 {
+ return types.NullValue
+ }
+
+ return l.Get(types.Int(sz - 1))
+}
+
+func listFirst(val ref.Val) ref.Val {
+ l := val.(traits.Lister)
+ sz := l.Size().Value().(int64)
+
+ if sz == 0 {
+ return types.NullValue
+ }
+
+ return l.Get(types.Int(0))
+}
+
func max(x, y types.Int) types.Int {
switch x.Compare(y) {
case types.IntNegOne: