diff --git a/pkg/ciclassifier/classifier.go b/pkg/ciclassifier/classifier.go new file mode 100644 index 000000000..c2c9eeca8 --- /dev/null +++ b/pkg/ciclassifier/classifier.go @@ -0,0 +1,69 @@ +package ciclassifier + +import "strings" + +// Check contains normalized check fields required for CI state classification. +type Check struct { + Status string + Conclusion string + Name string + Context string + Details string + Title string + Summary string + Text string +} + +// Classify maps raw checks into deterministic triage categories. +func Classify(checks []Check) string { + if len(checks) == 0 { + return "no checks" + } + + pending := false + failed := false + policyBlocked := false + + for _, check := range checks { + status := strings.ToLower(check.Status) + conclusion := strings.ToLower(check.Conclusion) + summary := strings.ToLower(strings.Join([]string{ + check.Name, + check.Context, + check.Details, + check.Title, + check.Summary, + check.Text, + }, " ")) + + if strings.Contains(summary, "resource not accessible by integration") || + strings.Contains(summary, "insufficient permission") || + strings.Contains(summary, "insufficient permissions") || + strings.Contains(summary, "not authorized") || + strings.Contains(summary, "forbidden") || + strings.Contains(summary, "cla") { + policyBlocked = true + } + + switch status { + case "pending", "queued", "in_progress", "requested", "waiting": + pending = true + } + + switch conclusion { + case "failure", "timed_out", "cancelled", "action_required", "startup_failure", "stale": + failed = true + } + } + + if policyBlocked { + return "policy-blocked" + } + if failed { + return "failed" + } + if pending { + return "pending" + } + return "passed" +} diff --git a/pkg/ciclassifier/classifier_test.go b/pkg/ciclassifier/classifier_test.go new file mode 100644 index 000000000..52eb7ba71 --- /dev/null +++ b/pkg/ciclassifier/classifier_test.go @@ -0,0 +1,30 @@ +package ciclassifier + +import "testing" + +func TestClassifyNoChecks(t *testing.T) { + if got := Classify(nil); got != "no checks" { + t.Fatalf("expected no checks, got %q", got) + } +} + +func TestClassifyPending(t *testing.T) { + checks := []Check{{Status: "in_progress"}} + if got := Classify(checks); got != "pending" { + t.Fatalf("expected pending, got %q", got) + } +} + +func TestClassifyFailed(t *testing.T) { + checks := []Check{{Conclusion: "failure"}} + if got := Classify(checks); got != "failed" { + t.Fatalf("expected failed, got %q", got) + } +} + +func TestClassifyPolicyBlockedWins(t *testing.T) { + checks := []Check{{Conclusion: "failure", Summary: "Resource not accessible by integration"}} + if got := Classify(checks); got != "policy-blocked" { + t.Fatalf("expected policy-blocked, got %q", got) + } +}