cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.50.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/reporter/github.go

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