cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.57.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/reporter/github.go

336lines · modecode

1package reporter
2
3import (
4 "bytes"
5 "context"
6 "fmt"
7 "log/slog"
8 "strconv"
9 "strings"
10 "time"
11
12 "github.com/google/go-github/v57/github"
13 "golang.org/x/oauth2"
14
15 "github.com/cloudflare/pint/internal/checks"
16 "github.com/cloudflare/pint/internal/git"
17 "github.com/cloudflare/pint/internal/output"
18)
19
20var reviewBody = "### This pull request was validated by [pint](https://github.com/cloudflare/pint).\n"
21
22type GithubReporter struct {
23 gitCmd git.CommandRunner
24
25 client *github.Client
26 version string
27 baseURL string
28 uploadURL string
29 authToken string
30 owner string
31 repo string
32 timeout time.Duration
33 prNum int
34 maxComments int
35}
36
37// NewGithubReporter creates a new GitHub reporter that reports
38// problems via comments on a given pull request number (integer).
39func NewGithubReporter(version, baseURL, uploadURL string, timeout time.Duration, token, owner, repo string, prNum, maxComments int, gitCmd git.CommandRunner) (_ GithubReporter, err error) {
40 gr := GithubReporter{
41 version: version,
42 baseURL: baseURL,
43 uploadURL: uploadURL,
44 timeout: timeout,
45 authToken: token,
46 owner: owner,
47 repo: repo,
48 prNum: prNum,
49 maxComments: maxComments,
50 gitCmd: gitCmd,
51 }
52
53 ts := oauth2.StaticTokenSource(
54 &oauth2.Token{AccessToken: gr.authToken},
55 )
56 tc := oauth2.NewClient(context.Background(), ts)
57 gr.client = github.NewClient(tc)
58
59 if gr.uploadURL != "" && gr.baseURL != "" {
60 gr.client, err = gr.client.WithEnterpriseURLs(gr.baseURL, gr.uploadURL)
61 if err != nil {
62 return gr, fmt.Errorf("creating new GitHub client: %w", err)
63 }
64 }
65
66 return gr, nil
67}
68
69// Submit submits the summary to GitHub.
70func (gr GithubReporter) Submit(summary Summary) error {
71 headCommit, err := git.HeadCommit(gr.gitCmd)
72 if err != nil {
73 return fmt.Errorf("failed to get HEAD commit: %w", err)
74 }
75 slog.Info("Got HEAD commit from git", slog.String("commit", headCommit))
76
77 review, err := gr.findExistingReview()
78 if err != nil {
79 return fmt.Errorf("failed to list pull request reviews: %w", err)
80 }
81 if review != nil {
82 if err = gr.updateReview(review, summary); err != nil {
83 return fmt.Errorf("failed to update pull request review: %w", err)
84 }
85 } else {
86 if err = gr.createReview(headCommit, summary); err != nil {
87 return fmt.Errorf("failed to create pull request review: %w", err)
88 }
89 }
90
91 return gr.addReviewComments(headCommit, summary)
92}
93
94func (gr GithubReporter) findExistingReview() (*github.PullRequestReview, error) {
95 ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
96 defer cancel()
97
98 reviews, _, err := gr.client.PullRequests.ListReviews(ctx, gr.owner, gr.repo, gr.prNum, nil)
99 if err != nil {
100 return nil, err
101 }
102
103 for _, review := range reviews {
104 if strings.HasPrefix(review.GetBody(), reviewBody) {
105 return review, nil
106 }
107 }
108
109 return nil, nil
110}
111
112func (gr GithubReporter) updateReview(review *github.PullRequestReview, summary Summary) error {
113 slog.Info("Updating pull request review", slog.String("repo", fmt.Sprintf("%s/%s", gr.owner, gr.repo)))
114
115 ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
116 defer cancel()
117
118 _, _, err := gr.client.PullRequests.UpdateReview(
119 ctx,
120 gr.owner,
121 gr.repo,
122 gr.prNum,
123 review.GetID(),
124 formatGHReviewBody(gr.version, summary),
125 )
126 return err
127}
128
129func (gr GithubReporter) addReviewComments(headCommit string, summary Summary) error {
130 slog.Info("Creating review comments")
131
132 existingComments, err := gr.getReviewComments()
133 if err != nil {
134 return err
135 }
136
137 var added int
138 for _, rep := range summary.Reports() {
139 comment := reportToGitHubComment(headCommit, rep)
140
141 var found bool
142 for _, ec := range existingComments {
143 if ec.GetBody() == comment.GetBody() &&
144 ec.GetCommitID() == comment.GetCommitID() &&
145 ec.GetLine() == comment.GetLine() {
146 found = true
147 break
148 }
149 }
150 if found {
151 slog.Debug("Comment already exist",
152 slog.String("path", comment.GetPath()),
153 slog.Int("line", comment.GetLine()),
154 slog.String("commit", comment.GetCommitID()),
155 slog.String("body", comment.GetBody()),
156 )
157 continue
158 }
159
160 if err := gr.createComment(comment); err != nil {
161 return err
162 }
163 added++
164
165 if added >= gr.maxComments {
166 return gr.tooManyComments(headCommit, len(summary.Reports()))
167 }
168 }
169
170 return nil
171}
172
173func (gr GithubReporter) getReviewComments() ([]*github.PullRequestComment, error) {
174 ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
175 defer cancel()
176
177 comments, _, err := gr.client.PullRequests.ListComments(ctx, gr.owner, gr.repo, gr.prNum, nil)
178 return comments, err
179}
180
181func (gr GithubReporter) createComment(comment *github.PullRequestComment) error {
182 slog.Debug("Creating review comment", slog.String("body", comment.GetBody()), slog.String("commit", comment.GetCommitID()))
183
184 ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
185 defer cancel()
186
187 _, _, err := gr.client.PullRequests.CreateComment(ctx, gr.owner, gr.repo, gr.prNum, comment)
188 return err
189}
190
191func (gr GithubReporter) createReview(headCommit string, summary Summary) error {
192 slog.Info("Creating pull request review", slog.String("repo", fmt.Sprintf("%s/%s", gr.owner, gr.repo)), slog.String("commit", headCommit))
193
194 ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
195 defer cancel()
196
197 _, resp, err := gr.client.PullRequests.CreateReview(
198 ctx,
199 gr.owner,
200 gr.repo,
201 gr.prNum,
202 &github.PullRequestReviewRequest{
203 CommitID: github.String(headCommit),
204 Body: github.String(formatGHReviewBody(gr.version, summary)),
205 Event: github.String("COMMENT"),
206 },
207 )
208 if err != nil {
209 return err
210 }
211 slog.Info("Pull request review created", slog.String("status", resp.Status))
212 return nil
213}
214
215func formatGHReviewBody(version string, summary Summary) string {
216 var b strings.Builder
217
218 b.WriteString(reviewBody)
219
220 bySeverity := summary.CountBySeverity()
221 if len(bySeverity) > 0 {
222 b.WriteString(":heavy_exclamation_mark: Problems found.\n")
223 b.WriteString("| Severity | Number of problems |\n")
224 b.WriteString("| --- | --- |\n")
225
226 for _, s := range []checks.Severity{checks.Fatal, checks.Bug, checks.Warning, checks.Information} {
227 if bySeverity[s] > 0 {
228 b.WriteString("| ")
229 b.WriteString(s.String())
230 b.WriteString(" | ")
231 b.WriteString(strconv.Itoa(bySeverity[s]))
232 b.WriteString(" |\n")
233 }
234 }
235 } else {
236 b.WriteString(":heavy_check_mark: No problems found\n")
237 }
238
239 b.WriteString("<details><summary>Stats</summary>\n<p>\n\n")
240 b.WriteString("| Stat | Value |\n")
241 b.WriteString("| --- | --- |\n")
242
243 b.WriteString("| Version | ")
244 b.WriteString(version)
245 b.WriteString(" |\n")
246
247 b.WriteString("| Number of rules parsed | ")
248 b.WriteString(strconv.Itoa(summary.TotalEntries))
249 b.WriteString(" |\n")
250
251 b.WriteString("| Number of rules checked | ")
252 b.WriteString(strconv.FormatInt(summary.CheckedEntries, 10))
253 b.WriteString(" |\n")
254
255 b.WriteString("| Number of problems found | ")
256 b.WriteString(strconv.Itoa(len(summary.Reports())))
257 b.WriteString(" |\n")
258
259 b.WriteString("| Number of offline checks | ")
260 b.WriteString(strconv.FormatInt(summary.OfflineChecks, 10))
261 b.WriteString(" |\n")
262
263 b.WriteString("| Number of online checks | ")
264 b.WriteString(strconv.FormatInt(summary.OnlineChecks, 10))
265 b.WriteString(" |\n")
266
267 b.WriteString("| Checks duration | ")
268 b.WriteString(output.HumanizeDuration(summary.Duration))
269 b.WriteString(" |\n")
270
271 b.WriteString("\n</p>\n</details>\n\n")
272
273 b.WriteString("<details><summary>Problems</summary>\n<p>\n\n")
274 if len(summary.Reports()) > 0 {
275 buf := bytes.NewBuffer(nil)
276 cr := NewConsoleReporter(buf, checks.Information)
277 err := cr.Submit(summary)
278 if err != nil {
279 b.WriteString(fmt.Sprintf("Failed to generate list of problems: %s", err))
280 } else {
281 b.WriteString("```\n")
282 b.WriteString(buf.String())
283 b.WriteString("```\n")
284 }
285 } else {
286 b.WriteString("No problems reported")
287 }
288 b.WriteString("\n</p>\n</details>\n\n")
289
290 return b.String()
291}
292
293func reportToGitHubComment(headCommit string, rep Report) *github.PullRequestComment {
294 var msgPrefix, msgSuffix string
295 reportLine, srcLine := moveReportedLine(rep)
296 if reportLine != srcLine {
297 msgPrefix = fmt.Sprintf("Problem reported on unmodified line %d, comment moved here: ", srcLine)
298 }
299 if rep.Problem.Details != "" {
300 msgSuffix = "\n\n" + rep.Problem.Details
301 }
302
303 var side string
304 if rep.Problem.Anchor == checks.AnchorBefore {
305 side = "LEFT"
306 } else {
307 side = "RIGHT"
308 }
309
310 c := github.PullRequestComment{
311 CommitID: github.String(headCommit),
312 Path: github.String(rep.ReportedPath),
313 Body: github.String(fmt.Sprintf(
314 "%s [%s](https://cloudflare.github.io/pint/checks/%s.html): %s%s%s",
315 problemIcon(rep.Problem.Severity),
316 rep.Problem.Reporter,
317 rep.Problem.Reporter,
318 msgPrefix,
319 rep.Problem.Text,
320 msgSuffix,
321 )),
322 Line: github.Int(reportLine),
323 Side: github.String(side),
324 }
325
326 return &c
327}
328
329func (gr GithubReporter) tooManyComments(headCommit string, nrComments int) error {
330 comment := github.PullRequestComment{
331 CommitID: github.String(headCommit),
332 Body: github.String(fmt.Sprintf(`This pint run would create %d comment(s), which is more than %d limit configured for pint.
333%d comments were skipped and won't be visibile on this PR.`, nrComments, gr.maxComments, nrComments-gr.maxComments)),
334 }
335 return gr.createComment(&comment)
336}
337