Skip to content

Commit f8e4343

Browse files
theakshaypantchmouel
authored andcommitted
fix(llm): default analysis to failed pipelines only
Remove the hard-coded failed-only check from the reconciler and delegate it to the LLM orchestrator. When no on_cel expression is defined for a role, the orchestrator defaults to triggering only for failed PipelineRuns. An explicit on_cel expression overrides this, allowing roles to run on any pipeline outcome. Signed-off-by: Akshay Pant <akpant@redhat.com>
1 parent 091f843 commit f8e4343

3 files changed

Lines changed: 50 additions & 40 deletions

File tree

pkg/llm/analyzer.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
"github.com/openshift-pipelines/pipelines-as-code/pkg/provider"
1616
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
1717
"go.uber.org/zap"
18+
corev1 "k8s.io/api/core/v1"
19+
apis "knative.dev/pkg/apis"
1820
)
1921

2022
// AnalysisResult represents the result of an LLM analysis.
@@ -110,7 +112,7 @@ func (a *Analyzer) Analyze(ctx context.Context, request *AnalyzeRequest) ([]Anal
110112
for _, role := range config.Roles {
111113
roleLogger := analysisLogger.With("role", role.Name)
112114

113-
shouldTrigger, err := a.shouldTriggerRole(role, celContext)
115+
shouldTrigger, err := a.shouldTriggerRole(role, celContext, request.PipelineRun)
114116
if err != nil {
115117
roleLogger.With("error", err, "cel_expression", role.OnCEL).Warn("Failed to evaluate CEL expression")
116118
results = append(results, AnalysisResult{
@@ -296,9 +298,12 @@ func countFailedResults(results []AnalysisResult) int {
296298
}
297299

298300
// shouldTriggerRole evaluates the CEL expression to determine if a role should be triggered.
299-
func (a *Analyzer) shouldTriggerRole(role v1alpha1.AnalysisRole, celContext map[string]any) (bool, error) {
301+
// If no on_cel is provided, defaults to triggering only for failed PipelineRuns.
302+
func (a *Analyzer) shouldTriggerRole(role v1alpha1.AnalysisRole, celContext map[string]any, pr *tektonv1.PipelineRun) (bool, error) {
300303
if role.OnCEL == "" {
301-
return true, nil
304+
succeededCondition := pr.Status.GetCondition(apis.ConditionSucceeded)
305+
306+
return succeededCondition != nil && succeededCondition.Status == corev1.ConditionFalse, nil
302307
}
303308

304309
result, err := cel.Value(role.OnCEL, celContext["body"],

pkg/llm/analyzer_test.go

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
tektonv1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
1515
"gotest.tools/v3/assert"
1616
"k8s.io/client-go/kubernetes/fake"
17+
"knative.dev/pkg/apis"
1718
)
1819

1920
func TestAnalyzerAnalyze(t *testing.T) {
@@ -410,14 +411,17 @@ func TestAnalyzerShouldTriggerRoleEvaluations(t *testing.T) {
410411
},
411412
}
412413

414+
failedPR := &tektonv1.PipelineRun{}
415+
failedPR.Status.Conditions = append(failedPR.Status.Conditions, apis.Condition{Type: apis.ConditionSucceeded, Status: "False"})
416+
413417
tests := []struct {
414418
name string
415419
role v1alpha1.AnalysisRole
416420
want bool
417421
wantError bool
418422
}{
419423
{
420-
name: "no expression returns true",
424+
name: "no expression defaults to failed pipelines only",
421425
role: v1alpha1.AnalysisRole{},
422426
want: true,
423427
},
@@ -440,7 +444,7 @@ func TestAnalyzerShouldTriggerRoleEvaluations(t *testing.T) {
440444

441445
for _, tt := range tests {
442446
t.Run(tt.name, func(t *testing.T) {
443-
got, err := analyzer.shouldTriggerRole(tt.role, celContext)
447+
got, err := analyzer.shouldTriggerRole(tt.role, celContext, failedPR)
444448

445449
if tt.wantError {
446450
assert.Assert(t, err != nil, "expected error but got none")
@@ -459,57 +463,60 @@ func TestAnalyzerShouldTriggerRole(t *testing.T) {
459463
kinteract := &kubeinteraction.Interaction{}
460464
analyzer := NewAnalyzer(run, kinteract, logger)
461465

466+
failedPR := &tektonv1.PipelineRun{}
467+
failedPR.Status.Conditions = append(failedPR.Status.Conditions, apis.Condition{Type: apis.ConditionSucceeded, Status: "False"})
468+
469+
succeededPR := &tektonv1.PipelineRun{}
470+
succeededPR.Status.Conditions = append(succeededPR.Status.Conditions, apis.Condition{Type: apis.ConditionSucceeded, Status: "True"})
471+
462472
tests := []struct {
463473
name string
464474
role v1alpha1.AnalysisRole
465475
celContext map[string]any
476+
pr *tektonv1.PipelineRun
466477
wantTrigger bool
467478
wantError bool
468479
}{
469480
{
470-
name: "no cel expression - always trigger",
471-
role: v1alpha1.AnalysisRole{
472-
Name: "test-role",
473-
},
481+
name: "no cel expression - triggers for failed pipeline",
482+
role: v1alpha1.AnalysisRole{Name: "test-role"},
474483
celContext: map[string]any{},
484+
pr: failedPR,
475485
wantTrigger: true,
476-
wantError: false,
477486
},
478487
{
479-
name: "simple true expression",
480-
role: v1alpha1.AnalysisRole{
481-
Name: "test-role",
482-
OnCEL: "true",
483-
},
488+
name: "no cel expression - skips succeeded pipeline",
489+
role: v1alpha1.AnalysisRole{Name: "test-role"},
484490
celContext: map[string]any{},
485-
wantTrigger: true,
486-
wantError: false,
491+
pr: succeededPR,
492+
wantTrigger: false,
487493
},
488494
{
489-
name: "simple false expression",
490-
role: v1alpha1.AnalysisRole{
491-
Name: "test-role",
492-
OnCEL: "false",
493-
},
495+
name: "simple true expression",
496+
role: v1alpha1.AnalysisRole{Name: "test-role", OnCEL: "true"},
494497
celContext: map[string]any{},
495-
wantTrigger: false,
496-
wantError: false,
498+
pr: succeededPR,
499+
wantTrigger: true,
497500
},
498501
{
499-
name: "invalid cel expression",
500-
role: v1alpha1.AnalysisRole{
501-
Name: "test-role",
502-
OnCEL: "invalid syntax (",
503-
},
502+
name: "simple false expression",
503+
role: v1alpha1.AnalysisRole{Name: "test-role", OnCEL: "false"},
504504
celContext: map[string]any{},
505+
pr: failedPR,
505506
wantTrigger: false,
506-
wantError: true,
507+
},
508+
{
509+
name: "invalid cel expression",
510+
role: v1alpha1.AnalysisRole{Name: "test-role", OnCEL: "invalid syntax ("},
511+
celContext: map[string]any{},
512+
pr: failedPR,
513+
wantError: true,
507514
},
508515
}
509516

510517
for _, tt := range tests {
511518
t.Run(tt.name, func(t *testing.T) {
512-
shouldTrigger, err := analyzer.shouldTriggerRole(tt.role, tt.celContext)
519+
shouldTrigger, err := analyzer.shouldTriggerRole(tt.role, tt.celContext, tt.pr)
513520

514521
if tt.wantError {
515522
assert.Assert(t, err != nil, "expected error but got none")

pkg/reconciler/reconciler.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
pipelinerunreconciler "github.com/tektoncd/pipeline/pkg/client/injection/reconciler/pipeline/v1/pipelinerun"
1111
tektonv1lister "github.com/tektoncd/pipeline/pkg/client/listers/pipeline/v1"
1212
"go.uber.org/zap"
13-
corev1 "k8s.io/api/core/v1"
1413
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1514
"knative.dev/pkg/logging"
1615
pkgreconciler "knative.dev/pkg/reconciler"
@@ -239,14 +238,13 @@ func (r *Reconciler) reportFinalStatus(ctx context.Context, logger *zap.SugaredL
239238
finalState = kubeinteraction.StateFailed
240239
}
241240

242-
// Perform LLM analysis only for failed pipeline runs (best-effort, non-blocking)
243-
// Users can use CEL expressions in role configurations for more fine-grained control
244-
if len(newPr.Status.Conditions) > 0 && newPr.Status.Conditions[0].Status == corev1.ConditionFalse {
245-
if err := r.performLLMAnalysis(ctx, logger, repo, newPr, event, provider); err != nil {
246-
logger.Warnf("LLM analysis failed (non-blocking): %v", err)
247-
r.eventEmitter.EmitMessage(repo, zap.WarnLevel, "LLMAnalysisFailed",
248-
fmt.Sprintf("AI/LLM analysis failed for repository %s/%s and pipeline run %s: %v", repo.Namespace, repo.Name, newPr.Name, err))
249-
}
241+
// LLM Analysis orchestrator checks if it is enabled and if the CEL condition
242+
// is matched for the defined roles, defaults to failed PipelineRuns only if
243+
// no CEL expression is defined.
244+
if err := r.performLLMAnalysis(ctx, logger, repo, newPr, event, provider); err != nil {
245+
logger.Warnf("LLM analysis failed (non-blocking): %v", err)
246+
r.eventEmitter.EmitMessage(repo, zap.WarnLevel, "LLMAnalysisFailed",
247+
fmt.Sprintf("AI/LLM analysis failed for repository %s/%s and pipeline run %s: %v", repo.Namespace, repo.Name, newPr.Name, err))
250248
}
251249

252250
if err := r.updateRepoRunStatus(ctx, logger, newPr, repo, event); err != nil {

0 commit comments

Comments
 (0)