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 }