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  }