github.com/yoheimuta/protolint@v0.49.8-0.20240515023657-4ecaebb7575d/internal/linter/report/reporters/ciReporter.go (about) 1 package reporters 2 3 import ( 4 "bytes" 5 "io" 6 "log" 7 "os" 8 "strconv" 9 "strings" 10 "text/template" 11 12 "github.com/yoheimuta/protolint/linter/report" 13 ) 14 15 type CiPipelineLogTemplate string 16 17 const ( 18 // problemMatcher provides a generic issue line that can be parsed in a problem matcher or jenkins pipeline 19 problemMatcher CiPipelineLogTemplate = "Protolint {{ .Rule }} ({{ .Severity }}): {{ .File }}[{{ .Line }},{{ .Column }}]: {{ .Message }}" 20 // azureDevOps provides a log message according to https://learn.microsoft.com/en-us/azure/devops/pipelines/scripts/logging-commands?view=azure-devops&tabs=bash#task-commands 21 azureDevOps CiPipelineLogTemplate = "{{ if ne \"info\" .Severity }}##vso[task.logissue type={{ .Severity }};sourcepath={{ .File }};linenumber={{ .Line }};columnnumber={{ .Column }};code={{ .Rule }};]{{ .Message }}{{end}}" 22 // gitlabCiCd provides an issue template where the severity is written in upper case. This is matched by the pipeline 23 gitlabCiCd CiPipelineLogTemplate = "{{ .Severity | ToUpper }}: {{ .Rule }} {{ .File }}({{ .Line }},{{ .Column }}) : {{ .Message }}" 24 // githubActions provides an issue template according to https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message 25 githubActions CiPipelineLogTemplate = "::{{ if ne \"info\" .Severity }}{{ .Severity }}{{ else }}notice{{ end }} file={{ .File }},line={{ .Line }},col={{ .Column }},title={{ .Rule }}::{{ .Message }}" 26 // empty provides default value for invalid returns 27 empty CiPipelineLogTemplate = "" 28 // env provides a marker for processing CI Templates from environment 29 env CiPipelineLogTemplate = "[ENV]" 30 ) 31 32 type CiReporter struct { 33 pattern CiPipelineLogTemplate 34 } 35 36 func NewCiReporterForAzureDevOps() CiReporter { 37 return CiReporter{pattern: azureDevOps} 38 } 39 40 func NewCiReporterForGitlab() CiReporter { 41 return CiReporter{pattern: gitlabCiCd} 42 } 43 44 func NewCiReporterForGithubActions() CiReporter { 45 return CiReporter{pattern: githubActions} 46 } 47 48 func NewCiReporterWithGenericFormat() CiReporter { 49 return CiReporter{pattern: problemMatcher} 50 } 51 52 func NewCiReporterFromEnv() CiReporter { 53 return CiReporter{pattern: env} 54 } 55 56 type ciReportedFailure struct { 57 Severity string 58 File string 59 Line int 60 Column int 61 Rule string 62 Message string 63 } 64 65 func (c CiReporter) Report(w io.Writer, fs []report.Failure) error { 66 template, err := c.getTemplate() 67 if err != nil { 68 return err 69 } 70 71 for _, failure := range fs { 72 reportedFailure := ciReportedFailure{ 73 Severity: getSeverity(failure.Severity()), 74 File: failure.Pos().Filename, 75 Line: failure.Pos().Line, 76 Column: failure.Pos().Column, 77 Rule: failure.RuleID(), 78 Message: strings.Trim(strconv.Quote(failure.Message()), `"`), // Ensure message is on a single line without quotes 79 } 80 81 var buffer bytes.Buffer 82 err = template.Execute(&buffer, reportedFailure) 83 if err != nil { 84 return err 85 } 86 87 written, err := w.Write(buffer.Bytes()) 88 if err != nil { 89 return err 90 } 91 92 if written > 0 { 93 _, err = w.Write([]byte("\n")) 94 if err != nil { 95 return err 96 } 97 } 98 } 99 100 return nil 101 } 102 103 func getSeverity(s string) string { 104 if s == "note" { 105 return "info" 106 } 107 108 return s 109 } 110 111 func (c CiReporter) getTemplateString() CiPipelineLogTemplate { 112 if c.pattern == env { 113 template, err := getPatternFromEnv() 114 if err != nil { 115 log.Printf("[ERROR] Failed to process template from Environment: %s\n", err.Error()) 116 return problemMatcher 117 } 118 119 if template == empty { 120 return problemMatcher 121 } 122 123 return template 124 } 125 if c.pattern != empty { 126 return c.pattern 127 } 128 return problemMatcher 129 } 130 131 func (c CiReporter) getTemplate() (*template.Template, error) { 132 toParse := c.getTemplateString() 133 134 toUpper := template.FuncMap{"ToUpper": strings.ToUpper} 135 136 template := template.New("Failure").Funcs(toUpper) 137 evaluate, err := template.Parse(string(toParse)) 138 if err != nil { 139 return nil, err 140 } 141 return evaluate, nil 142 } 143 144 func getPatternFromEnv() (CiPipelineLogTemplate, error) { 145 templateString := os.Getenv("PROTOLINT_CIREPORTER_TEMPLATE_STRING") 146 if templateString != "" { 147 return CiPipelineLogTemplate(templateString), nil 148 } 149 150 templateFile := os.Getenv("PROTOLINT_CIREPORTER_TEMPLATE_FILE") 151 if templateFile != "" { 152 content, err := os.ReadFile(templateFile) 153 154 if err != nil { 155 if os.IsNotExist(err) { 156 log.Printf("[ERROR] Failed to open file %s from 'PROTOLINT_CIREPORTER_TEMPLATE_FILE'. File does not exist.\n", templateFile) 157 log.Println("[WARN] Starting output with default processor.") 158 return empty, nil 159 } 160 if os.IsPermission(err) { 161 log.Printf("[ERROR] Failed to open file %s from 'PROTOLINT_CIREPORTER_TEMPLATE_FILE'. Insufficient permissions.\n", templateFile) 162 log.Println("[WARN] Starting output with default processor.") 163 return empty, nil 164 } 165 166 return empty, err 167 } 168 169 content_string := string(content) 170 171 return CiPipelineLogTemplate(content_string), nil 172 } 173 174 return empty, nil 175 }