Skip to content

Commit c0f65c7

Browse files
zakiskchmouel
authored andcommitted
feat: report invalid CEL expression errors as PR comments
This commit introduces a feature that automatically reports CEL expression validation errors directly on pull requests as comments, providing immediate feedback to users about what went wrong with their pipeline run configurations. Changes: - Add CEL validation error collection and reporting mechanism - Display validation errors in markdown table format on PR comments - Implement error message sanitization for proper markdown rendering - Enhance error formatting to escape special characters and remove line breaks - Streamline error notification flow to reduce duplicate messages https://issues.redhat.com/browse/SRVKP-7308 Signed-off-by: Zaki Shaikh <[email protected]>
1 parent 5034528 commit c0f65c7

File tree

9 files changed

+181
-14
lines changed

9 files changed

+181
-14
lines changed

pkg/matcher/annotation_matcher.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,19 @@ import (
66
"regexp"
77
"strings"
88

9-
"github.com/gobwas/glob"
10-
"github.com/google/cel-go/common/types"
119
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode"
1210
"github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/keys"
1311
apipac "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
12+
pacerrors "github.com/openshift-pipelines/pipelines-as-code/pkg/errors"
1413
"github.com/openshift-pipelines/pipelines-as-code/pkg/events"
1514
"github.com/openshift-pipelines/pipelines-as-code/pkg/opscomments"
1615
"github.com/openshift-pipelines/pipelines-as-code/pkg/params"
1716
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
1817
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/triggertype"
1918
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider"
19+
20+
"github.com/gobwas/glob"
21+
"github.com/google/cel-go/common/types"
2022
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
2123
"go.uber.org/zap"
2224
)
@@ -205,6 +207,7 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
205207
}
206208
logger.Info(infomsg)
207209

210+
celValidationErrors := []*pacerrors.PacYamlValidations{}
208211
for _, prun := range pruns {
209212
prMatch := Match{
210213
PipelineRun: prun,
@@ -277,6 +280,12 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
277280
out, err := celEvaluate(ctx, celExpr, event, vcx)
278281
if err != nil {
279282
logger.Errorf("there was an error evaluating the CEL expression, skipping: %v", err)
283+
if checkIfCELEvaluateError(err) {
284+
celValidationErrors = append(celValidationErrors, &pacerrors.PacYamlValidations{
285+
Name: prName,
286+
Err: fmt.Errorf("CEL expression evaluation error: %s", sanitizeErrorAsMarkdown(err)),
287+
})
288+
}
280289
continue
281290
}
282291
if out != types.True {
@@ -352,6 +361,10 @@ func MatchPipelinerunByAnnotation(ctx context.Context, logger *zap.SugaredLogger
352361
matchedPRs = append(matchedPRs, prMatch)
353362
}
354363

364+
if len(celValidationErrors) > 0 {
365+
reportCELValidationErrors(ctx, repo, celValidationErrors, eventEmitter, vcx, event)
366+
}
367+
355368
if len(matchedPRs) > 0 {
356369
return matchedPRs, nil
357370
}

pkg/matcher/annotation_matcher_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,6 +1644,35 @@ func TestMatchPipelinerunByAnnotation(t *testing.T) {
16441644
`Warning: The PipelineRun 'pipeline-on-cel-test' has 'on-cel-expression' defined along with [on-event, on-target-branch] annotation(s). The 'on-cel-expression' will take precedence and these annotations will be ignored`,
16451645
},
16461646
},
1647+
{
1648+
name: "invalid-cel-expression-error",
1649+
args: args{
1650+
pruns: []*tektonv1.PipelineRun{
1651+
{
1652+
ObjectMeta: metav1.ObjectMeta{
1653+
Name: "pipeline-on-cel-test",
1654+
Annotations: map[string]string{
1655+
keys.OnEvent: "[pull_request]",
1656+
keys.OnTargetBranch: "[main]",
1657+
keys.OnCelExpression: `event == "pull_request" &`,
1658+
},
1659+
},
1660+
},
1661+
},
1662+
runevent: info.Event{
1663+
URL: "https://hello/moto",
1664+
TriggerTarget: "pull_request",
1665+
EventType: "pull_request",
1666+
BaseBranch: "main",
1667+
HeadBranch: "warn-for-cel",
1668+
PullRequestNumber: 10,
1669+
Request: &info.Request{
1670+
Header: http.Header{},
1671+
},
1672+
},
1673+
},
1674+
wantErr: true,
1675+
},
16471676
{
16481677
name: "no-match-on-label",
16491678
args: args{

pkg/matcher/errors.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package matcher
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
apipac "github.com/openshift-pipelines/pipelines-as-code/pkg/apis/pipelinesascode/v1alpha1"
9+
pacerrors "github.com/openshift-pipelines/pipelines-as-code/pkg/errors"
10+
"github.com/openshift-pipelines/pipelines-as-code/pkg/events"
11+
"github.com/openshift-pipelines/pipelines-as-code/pkg/params/info"
12+
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider"
13+
14+
"go.uber.org/zap"
15+
)
16+
17+
// checkCELEvaluateError checks if error is from CEL evaluation stages.
18+
func checkIfCELEvaluateError(err error) bool {
19+
if err == nil {
20+
return false
21+
}
22+
23+
errMsg := err.Error()
24+
25+
patterns := []string{
26+
`failed to parse expression`,
27+
`check failed`,
28+
`failed to create a Program`,
29+
`failed to evaluate`,
30+
}
31+
32+
for _, pattern := range patterns {
33+
if strings.Contains(errMsg, pattern) {
34+
return true
35+
}
36+
}
37+
38+
return false
39+
}
40+
41+
func reportCELValidationErrors(ctx context.Context, repo *apipac.Repository, validationErrors []*pacerrors.PacYamlValidations, eventEmitter *events.EventEmitter, vcx provider.Interface, event *info.Event) {
42+
errorRows := make([]string, 0, len(validationErrors))
43+
for _, err := range validationErrors {
44+
errorRows = append(errorRows, fmt.Sprintf("| %s | `%s` |", err.Name, err.Err.Error()))
45+
}
46+
if len(errorRows) == 0 {
47+
return
48+
}
49+
markdownErrMessage := fmt.Sprintf(`%s
50+
%s`, provider.ValidationErrorTemplate, strings.Join(errorRows, "\n"))
51+
if err := vcx.CreateComment(ctx, event, markdownErrMessage, provider.ValidationErrorTemplate); err != nil {
52+
eventEmitter.EmitMessage(repo, zap.ErrorLevel, "PipelineRunCommentCreationError",
53+
fmt.Sprintf("failed to create comment: %s", err.Error()))
54+
}
55+
}
56+
57+
// sanitizeErrorAsMarkdown prepares a CEL evaluation error string to be rendered
58+
// inside a GitHub / GitLab markdown table without breaking its layout.
59+
//
60+
// Markdown tables use the vertical bar character (`|`) as a column delimiter. If
61+
// the original error message contains an un-escaped pipe the markdown renderer
62+
// interprets it as the start of a new column or row which distorts the table
63+
// produced by Pipelines-as-Code when reporting validation errors.
64+
//
65+
// To avoid this we escape every pipe with a backslash (\|). We also replace any
66+
// newline or carriage-return characters with a single space so that the whole
67+
// error is kept on one row, preserving readability in the rendered comment.
68+
func sanitizeErrorAsMarkdown(err error) string {
69+
errStr := err.Error()
70+
errStr = strings.ReplaceAll(errStr, "|", "\\|")
71+
errStr = strings.ReplaceAll(errStr, "\n", " ")
72+
errStr = strings.ReplaceAll(errStr, "\r", " ")
73+
return errStr
74+
}

pkg/pipelineascode/errors.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,12 @@ import (
1414
)
1515

1616
const (
17-
validationErrorTemplate = `> [!CAUTION]
18-
> There are some errors in your PipelineRun template.
19-
20-
| PipelineRun | Error |
21-
|------|-------|`
2217
tektonDirMissingError = ".tekton/ directory doesn't exist in repository's root directory"
2318
)
2419

2520
var regexpIgnoreErrors = regexp.MustCompile(`.*no kind.*is registered for version.*in scheme.*`)
2621

27-
func (p *PacRun) checkAccessOrErrror(ctx context.Context, repo *v1alpha1.Repository, status provider.StatusOpts, viamsg string) (bool, error) {
22+
func (p *PacRun) checkAccessOrError(ctx context.Context, repo *v1alpha1.Repository, status provider.StatusOpts, viamsg string) (bool, error) {
2823
allowed, err := p.vcx.IsAllowed(ctx, p.event)
2924
if err != nil {
3025
return false, fmt.Errorf("unable to verify event authorization: %w", err)
@@ -64,8 +59,8 @@ func (p *PacRun) reportValidationErrors(ctx context.Context, repo *v1alpha1.Repo
6459
return
6560
}
6661
markdownErrMessage := fmt.Sprintf(`%s
67-
%s`, validationErrorTemplate, strings.Join(errorRows, "\n"))
68-
if err := p.vcx.CreateComment(ctx, p.event, markdownErrMessage, validationErrorTemplate); err != nil {
62+
%s`, provider.ValidationErrorTemplate, strings.Join(errorRows, "\n"))
63+
if err := p.vcx.CreateComment(ctx, p.event, markdownErrMessage, provider.ValidationErrorTemplate); err != nil {
6964
p.eventEmitter.EmitMessage(repo, zap.ErrorLevel, "PipelineRunCommentCreationError",
7065
fmt.Sprintf("failed to create comment: %s", err.Error()))
7166
}

pkg/pipelineascode/errors_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func TestCheckAccessOrErrror(t *testing.T) {
9595
// Call the function
9696
repo := &v1alpha1.Repository{}
9797
status := provider.StatusOpts{}
98-
allowed, err := p.checkAccessOrErrror(context.Background(), repo, status, "via test")
98+
allowed, err := p.checkAccessOrError(context.Background(), repo, status, "via test")
9999

100100
// Verify results
101101
if tt.expectedErr {

pkg/pipelineascode/match.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ is that what you want? make sure you use -n when generating the secret, eg: echo
144144
DetailsURL: p.event.URL,
145145
AccessDenied: true,
146146
}
147-
if allowed, err := p.checkAccessOrErrror(ctx, repo, status, "by GitOps comment on push commit"); !allowed {
147+
if allowed, err := p.checkAccessOrError(ctx, repo, status, "by GitOps comment on push commit"); !allowed {
148148
return nil, err
149149
}
150150
}
@@ -160,7 +160,7 @@ is that what you want? make sure you use -n when generating the secret, eg: echo
160160
DetailsURL: p.event.URL,
161161
AccessDenied: true,
162162
}
163-
if allowed, err := p.checkAccessOrErrror(ctx, repo, status, "via "+p.event.TriggerTarget.String()); !allowed {
163+
if allowed, err := p.checkAccessOrError(ctx, repo, status, "via "+p.event.TriggerTarget.String()); !allowed {
164164
return nil, err
165165
}
166166
}
@@ -307,7 +307,7 @@ func (p *PacRun) getPipelineRunsFromRepo(ctx context.Context, repo *v1alpha1.Rep
307307
DetailsURL: p.event.URL,
308308
AccessDenied: true,
309309
}
310-
if allowed, err := p.checkAccessOrErrror(ctx, repo, status, "by GitOps comment on push commit"); !allowed {
310+
if allowed, err := p.checkAccessOrError(ctx, repo, status, "by GitOps comment on push commit"); !allowed {
311311
return nil, err
312312
}
313313
}

pkg/provider/provider.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import (
1111
"gopkg.in/yaml.v2"
1212
)
1313

14+
const ValidationErrorTemplate = `> [!CAUTION]
15+
> There are some errors in your PipelineRun template.
16+
17+
| PipelineRun | Error |
18+
|------|-------|`
19+
1420
var (
1521
testRetestAllRegex = regexp.MustCompile(`(?m)^(/retest|/test)\s*$`)
1622
testRetestSingleRegex = regexp.MustCompile(`(?m)^(/test|/retest)[ \t]+\S+`)

test/github_pullrequest_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,39 @@ func TestGithubSecondPullRequestBadYaml(t *testing.T) {
193193
t.Fatal("No comments with the pipelinerun error found on the pull request")
194194
}
195195

196+
func TestGithubInvalidCELExpressionReportingOnPR(t *testing.T) {
197+
ctx := context.Background()
198+
g := &tgithub.PRTest{
199+
Label: "Github PullRequest Invalid CEL expression",
200+
YamlFiles: []string{"testdata/failures/pipelinerun-invalid-cel.yaml"},
201+
NoStatusCheck: true,
202+
}
203+
g.RunPullRequest(ctx, t)
204+
defer g.TearDown(ctx, t)
205+
206+
maxLoop := 10
207+
for i := 0; i < maxLoop; i++ {
208+
comments, _, err := g.Provider.Client().Issues.ListComments(
209+
ctx, g.Options.Organization, g.Options.Repo, g.PRNumber,
210+
&github.IssueListCommentsOptions{})
211+
assert.NilError(t, err)
212+
213+
if len(comments) > 0 {
214+
assert.Assert(t, len(comments) == 1, "Should have only one comment created we got way too many: %+v", comments)
215+
commentBody := comments[0].GetBody()
216+
commentRegexp := regexp.MustCompile(`CEL expression evaluation error: failed to parse expression "event == \"pull request\" |": ERROR: <input>:1:25: Syntax error: token recognition error at: '|' | event == "pull request" | | ........................^`)
217+
assert.Check(t, commentRegexp.MatchString(commentBody), commentBody, t)
218+
g.Logger.Info("CEL expression validation comment has been matched")
219+
return
220+
}
221+
222+
g.Cnx.Clients.Log.Infof("Looping %d/%d waiting for a comment to appear", i, maxLoop)
223+
time.Sleep(6 * time.Second)
224+
}
225+
226+
t.Fatal("No comments with the pipelinerun error found on the pull request")
227+
}
228+
196229
// TestGithubPullRequestInvalidSpecValues tests invalid field values of a PipelinRun and
197230
// ensures that these validation errors are reported on UI.
198231
func TestGithubPullRequestInvalidSpecValues(t *testing.T) {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
apiVersion: tekton.dev/v1beta1
3+
kind: PipelineRun
4+
metadata:
5+
name: "\\ .PipelineName //"
6+
annotations:
7+
pipelinesascode.tekton.dev/on-cel-expression: event == "pull request" |
8+
spec:
9+
pipelineSpec:
10+
tasks:
11+
- name: task
12+
displayName: "The Task name is Task"
13+
taskSpec:
14+
steps:
15+
- name: task
16+
image: registry.access.redhat.com/ubi9/ubi-micro
17+
command: ["/bin/echo", "HELLOMOTO"]

0 commit comments

Comments
 (0)