diff --git a/config/300-repositories.yaml b/config/300-repositories.yaml index 3319d62d5..f2766ce3f 100644 --- a/config/300-repositories.yaml +++ b/config/300-repositories.yaml @@ -446,6 +446,34 @@ spec: - roles - secret_ref type: object + error_detection: + description: |- + ErrorDetection configures error detection for this repository. Error detection scans + container logs for error patterns and creates annotations (currently GitHub-only). + The global error-detection-from-container-logs setting must be enabled for this to work. + If not specified, uses only global error detection pattern. + properties: + max_number_of_lines: + description: |- + MaxNumberOfLines specifies how many lines to scan from the end of container logs + when looking for errors. This overrides the global error-detection-max-number-of-lines setting. + Higher values may increase memory usage. Use -1 for unlimited. + If not specified, uses the global setting (default: 50). + type: integer + patterns: + description: |- + Patterns is an array of regular expressions used to detect errors in container logs. + Each pattern must use named groups to capture: filename, line, and error. + The column group is optional. Example pattern: + ^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*) + + Multiple patterns can be specified to match different error formats. + Repository-specific patterns are tried first, followed by global patterns. + If not specified or empty, only the global error-detection-simple-regexp patterns are used. + items: + type: string + type: array + type: object github: properties: comment_strategy: diff --git a/config/302-pac-configmap.yaml b/config/302-pac-configmap.yaml index 97c8efe9b..022e864cc 100644 --- a/config/302-pac-configmap.yaml +++ b/config/302-pac-configmap.yaml @@ -101,7 +101,22 @@ data: # memory usage. Use -1 for unlimited lines. error-detection-max-number-of-lines: "50" - # The default regexp used when we use the simple error detection + # The default regexp(s) used for simple error detection. + # Supports multiple formats for backward compatibility: + # + # 1. Single pattern (backward compatible): + # error-detection-simple-regexp: '^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)' + # + # 2. YAML list format (multiple patterns, one per line): + # error-detection-simple-regexp: |- + # ^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*) + # ^ERROR: (?P[^ ]+) line (?P[0-9]+): (?P.*) + # ^\[(?P[^\]]+)\]:(?P[0-9]+) - (?P.*) + # + # 3. JSON array format: + # error-detection-simple-regexp: '["^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)", "^ERROR:.*"]' + # + # Each pattern must include named groups: filename, line, and error (column is optional) error-detection-simple-regexp: |- ^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*) diff --git a/docs/content/docs/guide/repositorycrd.md b/docs/content/docs/guide/repositorycrd.md index 1b683f964..34bd6543e 100644 --- a/docs/content/docs/guide/repositorycrd.md +++ b/docs/content/docs/guide/repositorycrd.md @@ -185,6 +185,104 @@ spec: comment_strategy: "disable_all" ``` +## Error Detection + +Error detection scans container logs for error patterns and creates inline +annotations on Pull Requests. This feature is currently **only supported for +GitHub Apps**. + +By default, error detection uses the global pattern configured in the +`pipelines-as-code` ConfigMap via the `error-detection-simple-regexp` setting. +However, you can customize error detection patterns on a per-repository basis +using the Repository CR. + +### Configuring Error Detection Patterns + +You can specify multiple regex patterns to detect different error formats in +your repository: + +```yaml +apiVersion: "pipelinesascode.tekton.dev/v1alpha1" +kind: Repository +metadata: + name: my-repo +spec: + url: "https://github.com/owner/repo" + settings: + error_detection: + patterns: + - "^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)" + - "^ERROR: (?P[^ ]+) line (?P[0-9]+): (?P.*)" + max_number_of_lines: 100 +``` + +**Pattern Requirements:** + +Each pattern must use [named groups](https://www.regular-expressions.info/named.html) to capture: + +- `filename`: The file path where the error occurred +- `line`: The line number +- `error`: The error message +- `column`: (optional) The column number + +**Configuration Options:** + +- `patterns`: Array of regex patterns. Repository-specific patterns are tried + first, followed by global patterns. If not specified or empty, only the + global patterns are used. **Note:** Providing an empty array does not disable + error detection; it falls back to using only the global patterns defined in + the `pipelines-as-code` ConfigMap. +- `max_number_of_lines`: Number of log lines to scan (overrides global + setting). Default is 50. Use -1 for unlimited. + +{{< hint info >}} +**Global Override:** The global `error-detection-from-container-logs` setting +must be enabled (default: `true`) for error detection to work. If disabled +globally, repository-level settings cannot override it. +{{< /hint >}} + +### Examples + +**Multiple error formats:** + +```yaml +spec: + settings: + error_detection: + patterns: + # Standard format (make, gcc, eslint, etc.) + - "^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)" + # Python traceback format + - 'File "(?P[^"]+)", line (?P[0-9]+).*\n.*(?P.*)' + # Custom CI format + - "^\\[(?P[^\\]]+)\\]:(?P[0-9]+) - (?P.*)" + max_number_of_lines: 200 +``` + +**Using only global patterns:** + +If you want to use only the global patterns defined in the `pipelines-as-code` +ConfigMap, simply omit the `error_detection` field or provide an empty +`patterns` array: + +```yaml +spec: + settings: + error_detection: + patterns: [] # Uses only global patterns + max_number_of_lines: 100 # Can still override line count +``` + +{{< hint info >}} +**Pattern Priority:** When repository-specific patterns are defined, they are +tried first for each log line. If no repository pattern matches, the global +patterns are then tried. This allows you to add repository-specific patterns +while still benefiting from the global patterns as a fallback. +{{< /hint >}} + +For more information about error detection and log snippets, see the +[Status documentation]({{< relref "/docs/guide/statuses.md" >}}). + ## Concurrency `concurrency_limit` allows you to define the maximum number of PipelineRuns running at any time for a Repository. diff --git a/docs/content/docs/guide/statuses.md b/docs/content/docs/guide/statuses.md index 3f887c6da..8e72e4e24 100644 --- a/docs/content/docs/guide/statuses.md +++ b/docs/content/docs/guide/statuses.md @@ -84,8 +84,10 @@ You can customize the regular expression used for detecting errors with the `error-detection-simple-regexp` setting. The regular expression uses [named groups](https://www.regular-expressions.info/named.html) to provide flexibility in specifying the matching criteria. The necessary groups for matching are -filename, line, and error (the column group is not used). The default regular -expression is defined in the configuration map. +filename, line, and error (the column group is optional). The default regular +expression is defined in the configuration map. Multiple patterns are supported +using multi-line YAML or JSON array format, allowing you to detect errors from +different tools with various output formats. By default, Pipelines-as-Code searches for errors in only the last 50 lines of the container logs. However, you can increase this limit by setting the @@ -94,6 +96,14 @@ system will search through all available lines for errors. Keep in mind that increasing this maximum number of lines may increase the memory usage of the watcher. +{{< hint info >}} +**Repository-level configuration:** You can also configure error detection +patterns on a per-repository basis using the Repository CR. This allows you to +define multiple patterns and customize settings for individual repositories. +See the [Repository CR documentation]({{< relref "/docs/guide/repositorycrd.md#error-detection" >}}) +for more details. +{{< /hint >}} + ![annotations](/images/github-annotation-error-failure-detection.png) ## Namespace Event stream diff --git a/docs/content/docs/install/operator_installation.md b/docs/content/docs/install/operator_installation.md index 3a6635455..cf09dbfb2 100644 --- a/docs/content/docs/install/operator_installation.md +++ b/docs/content/docs/install/operator_installation.md @@ -45,6 +45,7 @@ spec: hub-url: 'https://artifacthub.io' hub-catalog-type: 'artifacthub' error-detection-max-number-of-lines: '50' + # Single pattern example. For multiple patterns, use multi-line format (see settings docs) error-detection-simple-regexp: >- ^(?P[^:]*):(?P[0-9]+):(?P[0-9]+):([ ]*)?(?P.*) diff --git a/docs/content/docs/install/settings.md b/docs/content/docs/install/settings.md index 2aefa204c..2498c9c57 100644 --- a/docs/content/docs/install/settings.md +++ b/docs/content/docs/install/settings.md @@ -291,6 +291,39 @@ A few settings are available to configure this feature: By default the error detection only support a simple output, the way GCC or Make will output error, which is supported by most linters and command line tools. + **Multiple Patterns Support:** You can now specify multiple regex patterns to + match different error formats. The setting supports three formats: + + 1. **Single pattern** (backward compatible): + + ```yaml + error-detection-simple-regexp: '^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)' + ``` + + 2. **Multi-line YAML** (recommended for multiple patterns): + + ```yaml + error-detection-simple-regexp: |- + ^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*) + ^ERROR: (?P[^ ]+) line (?P[0-9]+): (?P.*) + ^\[(?P[^\]]+)\]:(?P[0-9]+) - (?P.*) + ``` + + 3. **JSON array format**: + + ```yaml + error-detection-simple-regexp: '["^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)", "^ERROR:.*"]' + ``` + + Each pattern will be tried in order until one matches. This allows you to detect + errors from multiple tools with different output formats. + + **Pattern Requirements:** Each pattern must use regexp named groups to capture: + * `(?P...)` - The file path where the error occurred + * `(?P...)` - The line number + * `(?P...)` - The error message + * `(?P...)` - Column number (optional) + An example of an error that is supported is : ```console diff --git a/pkg/apis/pipelinesascode/v1alpha1/types.go b/pkg/apis/pipelinesascode/v1alpha1/types.go index 16bc22e1d..b620a21aa 100644 --- a/pkg/apis/pipelinesascode/v1alpha1/types.go +++ b/pkg/apis/pipelinesascode/v1alpha1/types.go @@ -164,6 +164,13 @@ type Settings struct { // AIAnalysis contains AI/LLM analysis configuration for automated CI/CD pipeline analysis. // +optional AIAnalysis *AIAnalysisConfig `json:"ai,omitempty"` + + // ErrorDetection configures error detection for this repository. Error detection scans + // container logs for error patterns and creates annotations (currently GitHub-only). + // The global error-detection-from-container-logs setting must be enabled for this to work. + // If not specified, uses only global error detection pattern. + // +optional + ErrorDetection *ErrorDetectionSettings `json:"error_detection,omitempty"` } type GitlabSettings struct { @@ -184,6 +191,28 @@ type GithubSettings struct { CommentStrategy string `json:"comment_strategy,omitempty"` } +// ErrorDetectionSettings configures how errors are detected from container logs and +// exposed as annotations on Pull Requests. Currently only supported for GitHub Apps. +type ErrorDetectionSettings struct { + // Patterns is an array of regular expressions used to detect errors in container logs. + // Each pattern must use named groups to capture: filename, line, and error. + // The column group is optional. Example pattern: + // ^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*) + // + // Multiple patterns can be specified to match different error formats. + // Repository-specific patterns are tried first, followed by global patterns. + // If not specified or empty, only the global error-detection-simple-regexp patterns are used. + // +optional + Patterns []string `json:"patterns,omitempty"` + + // MaxNumberOfLines specifies how many lines to scan from the end of container logs + // when looking for errors. This overrides the global error-detection-max-number-of-lines setting. + // Higher values may increase memory usage. Use -1 for unlimited. + // If not specified, uses the global setting (default: 50). + // +optional + MaxNumberOfLines *int `json:"max_number_of_lines,omitempty"` +} + func (s *Settings) Merge(newSettings *Settings) { if newSettings.PipelineRunProvenance != "" && s.PipelineRunProvenance == "" { s.PipelineRunProvenance = newSettings.PipelineRunProvenance @@ -197,6 +226,9 @@ func (s *Settings) Merge(newSettings *Settings) { if newSettings.AIAnalysis != nil && s.AIAnalysis == nil { s.AIAnalysis = newSettings.AIAnalysis } + if newSettings.ErrorDetection != nil && s.ErrorDetection == nil { + s.ErrorDetection = newSettings.ErrorDetection + } } type Policy struct { diff --git a/pkg/params/settings/config.go b/pkg/params/settings/config.go index 6161a6bca..d70d8d184 100644 --- a/pkg/params/settings/config.go +++ b/pkg/params/settings/config.go @@ -1,6 +1,7 @@ package settings import ( + "encoding/json" "fmt" "net/url" "regexp" @@ -64,11 +65,11 @@ type Settings struct { SecretGHAppRepoScoped bool `default:"true" json:"secret-github-app-token-scoped"` SecretGhAppTokenScopedExtraRepos string `json:"secret-github-app-scope-extra-repos"` - ErrorLogSnippet bool `default:"true" json:"error-log-snippet"` - ErrorLogSnippetNumberOfLines int `default:"3" json:"error-log-snippet-number-of-lines"` - ErrorDetection bool `default:"true" json:"error-detection-from-container-logs"` - ErrorDetectionNumberOfLines int `default:"50" json:"error-detection-max-number-of-lines"` - ErrorDetectionSimpleRegexp string `default:"^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)" json:"error-detection-simple-regexp"` + ErrorLogSnippet bool `default:"true" json:"error-log-snippet"` + ErrorLogSnippetNumberOfLines int `default:"3" json:"error-log-snippet-number-of-lines"` + ErrorDetection bool `default:"true" json:"error-detection-from-container-logs"` + ErrorDetectionNumberOfLines int `default:"50" json:"error-detection-max-number-of-lines"` + ErrorDetectionSimpleRegexp []string `default:"^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)"` // Note: no json tag, handled specially EnableCancelInProgressOnPullRequests bool `json:"enable-cancel-in-progress-on-pull-requests"` EnableCancelInProgressOnPush bool `json:"enable-cancel-in-progress-on-push"` @@ -106,11 +107,10 @@ func DefaultSettings() Settings { func DefaultValidators() map[string]func(string) error { return map[string]func(string) error{ - "ErrorDetectionSimpleRegexp": isValidRegex, - "TektonDashboardURL": isValidURL, - "CustomConsoleURL": isValidURL, - "CustomConsolePRTaskLog": startWithHTTPorHTTPS, - "CustomConsolePRDetail": startWithHTTPorHTTPS, + "TektonDashboardURL": isValidURL, + "CustomConsoleURL": isValidURL, + "CustomConsolePRTaskLog": startWithHTTPorHTTPS, + "CustomConsolePRDetail": startWithHTTPorHTTPS, } } @@ -122,6 +122,13 @@ func SyncConfig(logger *zap.SugaredLogger, setting *Settings, config map[string] return fmt.Errorf("failed to validate and assign values: %w", err) } + // Parse error detection patterns (supports backward compatibility) + // Handling error detection specially as we want to keep backward compatibility while + // allowing []string as a valid input value. + if err := parseErrorDetectionPatterns(setting, config); err != nil { + return fmt.Errorf("failed to parse error detection patterns: %w", err) + } + value, _ := setting.HubCatalogs.Load("default") catalogDefault, ok := value.(HubCatalog) if ok { @@ -160,3 +167,61 @@ func startWithHTTPorHTTPS(url string) error { } return nil } + +// parseErrorDetectionPatterns parses the error-detection-simple-regexp from ConfigMap +// and populates ErrorDetectionSimpleRegexp as []string. It supports: +// 1. Single string pattern (backward compatible) +// 2. JSON array format: ["pattern1", "pattern2"] +// 3. Newline-separated patterns (YAML multi-line). +func parseErrorDetectionPatterns(setting *Settings, config map[string]string) error { + regexpValue := config["error-detection-simple-regexp"] + + // If not in config, use default + if regexpValue == "" { + regexpValue = "^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)" + } + + regexpValue = strings.TrimSpace(regexpValue) + + // Try to parse as JSON array first + if strings.HasPrefix(regexpValue, "[") && strings.HasSuffix(regexpValue, "]") { + var patterns []string + if err := json.Unmarshal([]byte(regexpValue), &patterns); err == nil { + // Validate each pattern + for _, pattern := range patterns { + if err := isValidRegex(pattern); err != nil { + return fmt.Errorf("invalid regex in array: %w", err) + } + } + setting.ErrorDetectionSimpleRegexp = patterns + return nil + } + } + + // Check if it contains newlines (multi-line YAML format) + if strings.Contains(regexpValue, "\n") { + lines := strings.Split(regexpValue, "\n") + var patterns []string + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } + if err := isValidRegex(line); err != nil { + return fmt.Errorf("invalid regex in multi-line format: %w", err) + } + patterns = append(patterns, line) + } + if len(patterns) > 0 { + setting.ErrorDetectionSimpleRegexp = patterns + return nil + } + } + + // Single pattern (backward compatible) + if err := isValidRegex(regexpValue); err != nil { + return err + } + setting.ErrorDetectionSimpleRegexp = []string{regexpValue} + return nil +} diff --git a/pkg/params/settings/config_test.go b/pkg/params/settings/config_test.go index e8e4cdaa0..7d2ba8697 100644 --- a/pkg/params/settings/config_test.go +++ b/pkg/params/settings/config_test.go @@ -38,7 +38,7 @@ func TestSyncConfig(t *testing.T) { ErrorLogSnippetNumberOfLines: 3, ErrorDetection: true, ErrorDetectionNumberOfLines: 50, - ErrorDetectionSimpleRegexp: "^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)", + ErrorDetectionSimpleRegexp: []string{"^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)"}, EnableCancelInProgressOnPullRequests: false, EnableCancelInProgressOnPush: false, SkipPushEventForPRCommits: true, @@ -98,7 +98,7 @@ func TestSyncConfig(t *testing.T) { ErrorLogSnippetNumberOfLines: 3, ErrorDetection: false, ErrorDetectionNumberOfLines: 100, - ErrorDetectionSimpleRegexp: "^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)", + ErrorDetectionSimpleRegexp: []string{"^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)"}, EnableCancelInProgressOnPullRequests: false, EnableCancelInProgressOnPush: false, SkipPushEventForPRCommits: true, @@ -130,7 +130,84 @@ func TestSyncConfig(t *testing.T) { configMap: map[string]string{ "error-detection-simple-regexp": "[", }, - expectedError: "custom validation failed for field ErrorDetectionSimpleRegexp: invalid regex: error parsing regexp: missing closing ]: `[`", + expectedError: "failed to parse error detection patterns: invalid regex: error parsing regexp: missing closing ]: `[`", + }, + { + name: "multiple patterns via multi-line YAML", + configMap: map[string]string{ + "error-detection-simple-regexp": "^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)\n^ERROR: (?P[^ ]+) line (?P[0-9]+): (?P.*)", + }, + expectedStruct: Settings{ + ApplicationName: "Pipelines as Code CI", + HubCatalogs: nil, + RemoteTasks: true, + MaxKeepRunsUpperLimit: 0, + DefaultMaxKeepRuns: 0, + BitbucketCloudCheckSourceIP: true, + BitbucketCloudAdditionalSourceIP: "", + TektonDashboardURL: "", + AutoConfigureNewGitHubRepo: false, + AutoConfigureRepoNamespaceTemplate: "", + SecretAutoCreation: true, + SecretGHAppRepoScoped: true, + SecretGhAppTokenScopedExtraRepos: "", + ErrorLogSnippet: true, + ErrorLogSnippetNumberOfLines: 3, + ErrorDetection: true, + ErrorDetectionNumberOfLines: 50, + ErrorDetectionSimpleRegexp: []string{ + "^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)", + "^ERROR: (?P[^ ]+) line (?P[0-9]+): (?P.*)", + }, + EnableCancelInProgressOnPullRequests: false, + EnableCancelInProgressOnPush: false, + SkipPushEventForPRCommits: true, + CustomConsoleName: "", + CustomConsoleURL: "", + CustomConsolePRdetail: "", + CustomConsolePRTaskLog: "", + CustomConsoleNamespaceURL: "", + RememberOKToTest: false, + }, + }, + { + name: "multiple patterns via JSON array", + configMap: map[string]string{ + "error-detection-simple-regexp": `["^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)", "^ERROR: (?P[^ ]+) line (?P[0-9]+): (?P.*)", "^\\[(?P[^\\]]+)\\]:(?P[0-9]+) - (?P.*)"]`, + }, + expectedStruct: Settings{ + ApplicationName: "Pipelines as Code CI", + HubCatalogs: nil, + RemoteTasks: true, + MaxKeepRunsUpperLimit: 0, + DefaultMaxKeepRuns: 0, + BitbucketCloudCheckSourceIP: true, + BitbucketCloudAdditionalSourceIP: "", + TektonDashboardURL: "", + AutoConfigureNewGitHubRepo: false, + AutoConfigureRepoNamespaceTemplate: "", + SecretAutoCreation: true, + SecretGHAppRepoScoped: true, + SecretGhAppTokenScopedExtraRepos: "", + ErrorLogSnippet: true, + ErrorLogSnippetNumberOfLines: 3, + ErrorDetection: true, + ErrorDetectionNumberOfLines: 50, + ErrorDetectionSimpleRegexp: []string{ + "^(?P[^:]*):(?P[0-9]+):(?P[0-9]+)?([ ]*)?(?P.*)", + "^ERROR: (?P[^ ]+) line (?P[0-9]+): (?P.*)", + "^\\[(?P[^\\]]+)\\]:(?P[0-9]+) - (?P.*)", + }, + EnableCancelInProgressOnPullRequests: false, + EnableCancelInProgressOnPush: false, + SkipPushEventForPRCommits: true, + CustomConsoleName: "", + CustomConsoleURL: "", + CustomConsolePRdetail: "", + CustomConsolePRTaskLog: "", + CustomConsoleNamespaceURL: "", + RememberOKToTest: false, + }, }, { name: "invalid value url", diff --git a/pkg/provider/github/status.go b/pkg/provider/github/status.go index 6838b89e0..b68c63a6d 100644 --- a/pkg/provider/github/status.go +++ b/pkg/provider/github/status.go @@ -140,64 +140,98 @@ func (v *Provider) createCheckRunStatus(ctx context.Context, runevent *info.Even func (v *Provider) getFailuresMessageAsAnnotations(ctx context.Context, pr *tektonv1.PipelineRun, pacopts *info.PacOpts) []*github.CheckRunAnnotation { annotations := []*github.CheckRunAnnotation{} - r, err := regexp.Compile(pacopts.ErrorDetectionSimpleRegexp) - if err != nil { - v.Logger.Errorf("invalid regexp for filtering failure messages: %v", pacopts.ErrorDetectionSimpleRegexp) + var patterns []string + maxNumberOfLines := pacopts.ErrorDetectionNumberOfLines + + // Check if repository has custom error detection settings + if v.repo != nil && v.repo.Spec.Settings != nil && v.repo.Spec.Settings.ErrorDetection != nil { + repoErrorDetection := v.repo.Spec.Settings.ErrorDetection + // Use repository-specific patterns if provided + if repoErrorDetection.Patterns != nil { + patterns = repoErrorDetection.Patterns + } + // Use repository-specific max number of lines if provided + if repoErrorDetection.MaxNumberOfLines != nil { + maxNumberOfLines = *repoErrorDetection.MaxNumberOfLines + } + } + + patterns = append(patterns, pacopts.ErrorDetectionSimpleRegexp...) + + compiledPatterns := make([]*regexp.Regexp, 0, len(patterns)) + for _, pattern := range patterns { + r, err := regexp.Compile(pattern) + if err != nil { + v.Logger.Errorf("invalid regexp for filtering failure messages: %v", pattern) + continue + } + compiledPatterns = append(compiledPatterns, r) + } + + if len(compiledPatterns) == 0 { + v.Logger.Error("no valid error detection patterns available") return annotations } + intf, err := kubeinteraction.NewKubernetesInteraction(v.Run) if err != nil { v.Logger.Errorf("failed to create kubeinteraction: %v", err) return annotations } - taskinfos := kstatus.CollectFailedTasksLogSnippet(ctx, v.Run, intf, pr, int64(pacopts.ErrorDetectionNumberOfLines)) + + taskinfos := kstatus.CollectFailedTasksLogSnippet(ctx, v.Run, intf, pr, int64(maxNumberOfLines)) for _, taskinfo := range taskinfos { for _, errline := range strings.Split(taskinfo.LogSnippet, "\n") { - results := map[string]string{} - if !r.MatchString(errline) { - continue - } - matches := r.FindStringSubmatch(errline) - for i, name := range r.SubexpNames() { - if i != 0 && name != "" { - results[name] = matches[i] + // Try each pattern until we find a match + for _, r := range compiledPatterns { + results := map[string]string{} + if !r.MatchString(errline) { + continue + } + matches := r.FindStringSubmatch(errline) + for i, name := range r.SubexpNames() { + if i != 0 && name != "" { + results[name] = matches[i] + } } - } - // check if we have file in results - var linenumber, errmsg, filename string - var ok bool + // check if we have file in results + var linenumber, errmsg, filename string + var ok bool - if filename, ok = results["filename"]; !ok { - v.Logger.Errorf("regexp for filtering failure messages does not contain a filename regexp group: %v", pacopts.ErrorDetectionSimpleRegexp) - continue - } - // remove ./ cause it would bug github otherwise - filename = strings.TrimPrefix(filename, "./") + if filename, ok = results["filename"]; !ok { + v.Logger.Errorf("regexp for filtering failure messages does not contain a filename regexp group: %v", r.String()) + continue + } + // remove ./ cause it would bug github otherwise + filename = strings.TrimPrefix(filename, "./") - if linenumber, ok = results["line"]; !ok { - v.Logger.Errorf("regexp for filtering failure messages does not contain a line regexp group: %v", pacopts.ErrorDetectionSimpleRegexp) - continue - } + if linenumber, ok = results["line"]; !ok { + v.Logger.Errorf("regexp for filtering failure messages does not contain a line regexp group: %v", r.String()) + continue + } - if errmsg, ok = results["error"]; !ok { - v.Logger.Errorf("regexp for filtering failure messages does not contain a error regexp group: %v", pacopts.ErrorDetectionSimpleRegexp) - continue - } + if errmsg, ok = results["error"]; !ok { + v.Logger.Errorf("regexp for filtering failure messages does not contain a error regexp group: %v", r.String()) + continue + } - ilinenumber, err := strconv.Atoi(linenumber) - if err != nil { - // can't do much regexp has probably failed to detect - v.Logger.Errorf("cannot convert %s as integer: %v", linenumber, err) - continue + ilinenumber, err := strconv.Atoi(linenumber) + if err != nil { + // can't do much regexp has probably failed to detect + v.Logger.Errorf("cannot convert %s as integer: %v", linenumber, err) + continue + } + annotations = append(annotations, &github.CheckRunAnnotation{ + Path: github.Ptr(filename), + StartLine: github.Ptr(ilinenumber), + EndLine: github.Ptr(ilinenumber), + AnnotationLevel: github.Ptr("failure"), + Message: github.Ptr(errmsg), + }) + // Pattern matched, no need to try other patterns for this line + break } - annotations = append(annotations, &github.CheckRunAnnotation{ - Path: github.Ptr(filename), - StartLine: github.Ptr(ilinenumber), - EndLine: github.Ptr(ilinenumber), - AnnotationLevel: github.Ptr("failure"), - Message: github.Ptr(errmsg), - }) } } return annotations