github.com/alexey-mercari/reviewdog@v0.10.1-0.20200514053941-928943b10766/doghouse/server/doghouse.go (about)

     1  package server
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"net/http"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/google/go-github/v31/github"
    12  
    13  	"github.com/reviewdog/reviewdog"
    14  	"github.com/reviewdog/reviewdog/diff"
    15  	"github.com/reviewdog/reviewdog/difffilter"
    16  	"github.com/reviewdog/reviewdog/doghouse"
    17  	"github.com/reviewdog/reviewdog/service/github/githubutils"
    18  )
    19  
    20  // GitHub check runs API cannot handle too large requests.
    21  // Set max number of filtered findings to be shown in check-run summary.
    22  // ERROR:
    23  //  https://api.github.com/repos/easymotion/vim-easymotion/check-runs: 422
    24  //  Invalid request.
    25  //  Only 65535 characters are allowed; 250684 were supplied. []
    26  const maxFilteredFinding = 150
    27  
    28  // > The Checks API limits the number of annotations to a maximum of 50 per API
    29  // > request.
    30  // https://developer.github.com/v3/checks/runs/#output-object
    31  const maxAnnotationsPerRequest = 50
    32  
    33  type Checker struct {
    34  	req *doghouse.CheckRequest
    35  	gh  checkerGitHubClientInterface
    36  }
    37  
    38  func NewChecker(req *doghouse.CheckRequest, gh *github.Client) *Checker {
    39  	return &Checker{req: req, gh: &checkerGitHubClient{Client: gh}}
    40  }
    41  
    42  func (ch *Checker) Check(ctx context.Context) (*doghouse.CheckResponse, error) {
    43  	var filediffs []*diff.FileDiff
    44  	if ch.req.PullRequest != 0 {
    45  		var err error
    46  		filediffs, err = ch.pullRequestDiff(ctx, ch.req.PullRequest)
    47  		if err != nil {
    48  			return nil, fmt.Errorf("fail to parse diff: %v", err)
    49  		}
    50  	}
    51  
    52  	results := annotationsToCheckResults(ch.req.Annotations)
    53  	filterMode := ch.req.FilterMode
    54  	if ch.req.PullRequest == 0 || ch.req.OutsideDiff {
    55  		// If it's not Pull Request run, do not filter results by diff regardless
    56  		// of the filter mode.
    57  		filterMode = difffilter.ModeNoFilter
    58  	}
    59  	filtered := reviewdog.FilterCheck(results, filediffs, 1, "", filterMode)
    60  	check, err := ch.createCheck(ctx)
    61  	if err != nil {
    62  		// If this error is StatusForbidden (403) here, it means reviewdog is
    63  		// running on GitHub Actions and has only read permission (because it's
    64  		// running for Pull Requests from forked repository). If the token itself
    65  		// is invalid, reviewdog should return an error earlier (e.g. when reading
    66  		// Pull Requests diff), so it should be ok not to return error here and
    67  		// return results instead.
    68  		if err, ok := err.(*github.ErrorResponse); ok && err.Response.StatusCode == http.StatusForbidden {
    69  			return &doghouse.CheckResponse{CheckedResults: filtered}, nil
    70  		}
    71  		return nil, fmt.Errorf("failed to create check: %v", err)
    72  	}
    73  
    74  	checkRun, conclusion, err := ch.postCheck(ctx, check.GetID(), filtered)
    75  	if err != nil {
    76  		return nil, fmt.Errorf("failed to post result: %v", err)
    77  	}
    78  	res := &doghouse.CheckResponse{
    79  		ReportURL:  checkRun.GetHTMLURL(),
    80  		Conclusion: conclusion,
    81  	}
    82  	return res, nil
    83  }
    84  
    85  func (ch *Checker) postCheck(ctx context.Context, checkID int64, checks []*reviewdog.FilteredCheck) (*github.CheckRun, string, error) {
    86  	var annotations []*github.CheckRunAnnotation
    87  	for _, c := range checks {
    88  		if !c.ShouldReport {
    89  			continue
    90  		}
    91  		annotations = append(annotations, ch.toCheckRunAnnotation(c))
    92  	}
    93  	if len(annotations) > 0 {
    94  		if err := ch.postAnnotations(ctx, checkID, annotations); err != nil {
    95  			return nil, "", fmt.Errorf("failed to post annotations: %v", err)
    96  		}
    97  	}
    98  
    99  	conclusion := "success"
   100  	if len(annotations) > 0 {
   101  		conclusion = ch.conclusion()
   102  	}
   103  	opt := github.UpdateCheckRunOptions{
   104  		Name:        ch.checkName(),
   105  		Status:      github.String("completed"),
   106  		Conclusion:  github.String(conclusion),
   107  		CompletedAt: &github.Timestamp{Time: time.Now()},
   108  		Output: &github.CheckRunOutput{
   109  			Title:   github.String(ch.checkTitle()),
   110  			Summary: github.String(ch.summary(checks)),
   111  		},
   112  	}
   113  	checkRun, err := ch.gh.UpdateCheckRun(ctx, ch.req.Owner, ch.req.Repo, checkID, opt)
   114  	if err != nil {
   115  		return nil, "", err
   116  	}
   117  	return checkRun, conclusion, nil
   118  }
   119  
   120  func (ch *Checker) createCheck(ctx context.Context) (*github.CheckRun, error) {
   121  	opt := github.CreateCheckRunOptions{
   122  		Name:    ch.checkName(),
   123  		HeadSHA: ch.req.SHA,
   124  		Status:  github.String("in_progress"),
   125  	}
   126  	return ch.gh.CreateCheckRun(ctx, ch.req.Owner, ch.req.Repo, opt)
   127  }
   128  
   129  func (ch *Checker) postAnnotations(ctx context.Context, checkID int64, annotations []*github.CheckRunAnnotation) error {
   130  	opt := github.UpdateCheckRunOptions{
   131  		Name: ch.checkName(),
   132  		Output: &github.CheckRunOutput{
   133  			Title:       github.String(ch.checkTitle()),
   134  			Summary:     github.String(""), // Post summary with the last request.
   135  			Annotations: annotations[:min(maxAnnotationsPerRequest, len(annotations))],
   136  		},
   137  	}
   138  	if _, err := ch.gh.UpdateCheckRun(ctx, ch.req.Owner, ch.req.Repo, checkID, opt); err != nil {
   139  		return err
   140  	}
   141  	if len(annotations) > maxAnnotationsPerRequest {
   142  		return ch.postAnnotations(ctx, checkID, annotations[maxAnnotationsPerRequest:])
   143  	}
   144  	return nil
   145  }
   146  
   147  func (ch *Checker) checkName() string {
   148  	if ch.req.Name != "" {
   149  		return ch.req.Name
   150  	}
   151  	return "reviewdog"
   152  }
   153  
   154  func (ch *Checker) checkTitle() string {
   155  	if name := ch.checkName(); name != "reviewdog" {
   156  		return fmt.Sprintf("reviewdog [%s] report", name)
   157  	}
   158  	return "reviewdog report"
   159  }
   160  
   161  // https://developer.github.com/v3/checks/runs/#parameters-1
   162  func (ch *Checker) conclusion() string {
   163  	switch strings.ToLower(ch.req.Level) {
   164  	case "info", "warning":
   165  		return "neutral"
   166  	}
   167  	return "failure"
   168  }
   169  
   170  // https://developer.github.com/v3/checks/runs/#annotations-object
   171  func (ch *Checker) annotationLevel() string {
   172  	switch strings.ToLower(ch.req.Level) {
   173  	case "info":
   174  		return "notice"
   175  	case "warning":
   176  		return "warning"
   177  	case "failure":
   178  		return "failure"
   179  	}
   180  	return "failure"
   181  }
   182  
   183  func (ch *Checker) summary(checks []*reviewdog.FilteredCheck) string {
   184  	var lines []string
   185  	lines = append(lines, "reported by [reviewdog](https://github.com/reviewdog/reviewdog) :dog:")
   186  
   187  	var findings []*reviewdog.FilteredCheck
   188  	var filteredFindings []*reviewdog.FilteredCheck
   189  	for _, c := range checks {
   190  		if c.ShouldReport {
   191  			findings = append(findings, c)
   192  		} else {
   193  			filteredFindings = append(filteredFindings, c)
   194  		}
   195  	}
   196  	lines = append(lines, ch.summaryFindings("Findings", findings)...)
   197  	lines = append(lines, ch.summaryFindings("Filtered Findings", filteredFindings)...)
   198  
   199  	return strings.Join(lines, "\n")
   200  }
   201  
   202  func (ch *Checker) summaryFindings(name string, checks []*reviewdog.FilteredCheck) []string {
   203  	var lines []string
   204  	lines = append(lines, "<details>")
   205  	lines = append(lines, fmt.Sprintf("<summary>%s (%d)</summary>", name, len(checks)))
   206  	lines = append(lines, "")
   207  	for i, c := range checks {
   208  		if i >= maxFilteredFinding {
   209  			lines = append(lines, "... (Too many findings. Dropped some findings)")
   210  			break
   211  		}
   212  		lines = append(lines, githubutils.LinkedMarkdownCheckResult(
   213  			ch.req.Owner, ch.req.Repo, ch.req.SHA, c.CheckResult))
   214  	}
   215  	lines = append(lines, "</details>")
   216  	return lines
   217  }
   218  
   219  func (ch *Checker) toCheckRunAnnotation(c *reviewdog.FilteredCheck) *github.CheckRunAnnotation {
   220  	a := &github.CheckRunAnnotation{
   221  		Path:            github.String(c.Path),
   222  		StartLine:       github.Int(c.Lnum),
   223  		EndLine:         github.Int(c.Lnum),
   224  		AnnotationLevel: github.String(ch.annotationLevel()),
   225  		Message:         github.String(c.Message),
   226  	}
   227  	if ch.req.Name != "" {
   228  		a.Title = github.String(fmt.Sprintf("[%s] %s#L%d", ch.req.Name, c.Path, c.Lnum))
   229  	}
   230  	if s := strings.Join(c.Lines, "\n"); s != "" {
   231  		a.RawDetails = github.String(s)
   232  	}
   233  	return a
   234  }
   235  
   236  func (ch *Checker) pullRequestDiff(ctx context.Context, pr int) ([]*diff.FileDiff, error) {
   237  	d, err := ch.rawPullRequestDiff(ctx, pr)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  	filediffs, err := diff.ParseMultiFile(bytes.NewReader(d))
   242  	if err != nil {
   243  		return nil, fmt.Errorf("fail to parse diff: %v", err)
   244  	}
   245  	return filediffs, nil
   246  }
   247  
   248  func (ch *Checker) rawPullRequestDiff(ctx context.Context, pr int) ([]byte, error) {
   249  	d, err := ch.gh.GetPullRequestDiff(ctx, ch.req.Owner, ch.req.Repo, pr)
   250  	if err != nil {
   251  		return nil, err
   252  	}
   253  	return d, nil
   254  }
   255  
   256  func annotationsToCheckResults(as []*doghouse.Annotation) []*reviewdog.CheckResult {
   257  	cs := make([]*reviewdog.CheckResult, 0, len(as))
   258  	for _, a := range as {
   259  		cs = append(cs, &reviewdog.CheckResult{
   260  			Path:    a.Path,
   261  			Lnum:    a.Line,
   262  			Message: a.Message,
   263  			Lines:   strings.Split(a.RawMessage, "\n"),
   264  		})
   265  	}
   266  	return cs
   267  }
   268  
   269  func min(x, y int) int {
   270  	if x > y {
   271  		return y
   272  	}
   273  	return x
   274  }