cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.43.1

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/reporter/github.go

280lines · modecode

1package reporter
2
3import (
4 "context"
5 "fmt"
6 "sort"
7 "strconv"
8 "strings"
9 "time"
10
11 "github.com/google/go-github/v50/github"
12 "github.com/rs/zerolog/log"
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
56 if gr.uploadURL != "" && gr.baseURL != "" {
57 gr.client, err = github.NewEnterpriseClient(gr.baseURL, gr.uploadURL, tc)
58 if err != nil {
59 return gr, fmt.Errorf("creating new GitHub client: %w", err)
60 }
61 } else {
62 gr.client = github.NewClient(tc)
63 }
64
65 return gr, nil
66}
67
68// Submit submits the summary to GitHub.
69func (gr GithubReporter) Submit(summary Summary) error {
70 headCommit, err := git.HeadCommit(gr.gitCmd)
71 if err != nil {
72 return fmt.Errorf("failed to get HEAD commit: %w", err)
73 }
74 log.Info().Str("commit", headCommit).Msg("Got HEAD commit from git")
75
76 review, err := gr.findExistingReview()
77 if err != nil {
78 return fmt.Errorf("failed to list pull request reviews: %w", err)
79 }
80 if review != nil {
81 if err = gr.updateReview(review, summary); err != nil {
82 return err
83 }
84 } else {
85 if err = gr.createReview(headCommit, summary); err != nil {
86 return err
87 }
88 }
89
90 return gr.addReviewComments(headCommit, summary)
91}
92
93func (gr GithubReporter) findExistingReview() (*github.PullRequestReview, error) {
94 ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
95 defer cancel()
96
97 reviews, _, err := gr.client.PullRequests.ListReviews(ctx, gr.owner, gr.repo, gr.prNum, nil)
98 if err != nil {
99 return nil, err
100 }
101
102 for _, review := range reviews {
103 if strings.HasPrefix(review.GetBody(), reviewBody) {
104 return review, nil
105 }
106 }
107
108 return nil, nil
109}
110
111func (gr GithubReporter) updateReview(review *github.PullRequestReview, summary Summary) error {
112 log.Info().Str("repo", fmt.Sprintf("%s/%s", gr.owner, gr.repo)).Msg("Updating pull request review")
113
114 ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
115 defer cancel()
116
117 _, _, err := gr.client.PullRequests.UpdateReview(
118 ctx,
119 gr.owner,
120 gr.repo,
121 gr.prNum,
122 review.GetID(),
123 formatGHReviewBody(gr.version, summary),
124 )
125 return err
126}
127
128func (gr GithubReporter) addReviewComments(headCommit string, summary Summary) error {
129 log.Info().Msg("Creating review comments")
130
131 existingComments, err := gr.getReviewComments()
132 if err != nil {
133 return err
134 }
135
136 for _, rep := range summary.Reports() {
137 comment := reportToGitHubComment(headCommit, rep)
138
139 var found bool
140 for _, ec := range existingComments {
141 if ec.GetBody() == comment.GetBody() && ec.GetCommitID() == comment.GetCommitID() {
142 found = true
143 break
144 }
145 }
146 if found {
147 log.Debug().Str("body", comment.GetBody()).Str("commit", comment.GetCommitID()).Msg("Comment already exist")
148 continue
149 }
150
151 if err := gr.createComment(comment); err != nil {
152 return err
153 }
154 }
155
156 return nil
157}
158
159func (gr GithubReporter) getReviewComments() ([]*github.PullRequestComment, error) {
160 ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
161 defer cancel()
162
163 comments, _, err := gr.client.PullRequests.ListComments(ctx, gr.owner, gr.repo, gr.prNum, nil)
164 return comments, err
165}
166
167func (gr GithubReporter) createComment(comment *github.PullRequestComment) error {
168 log.Debug().Str("body", comment.GetBody()).Str("commit", comment.GetCommitID()).Msg("Creating review comment")
169
170 ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
171 defer cancel()
172
173 _, _, err := gr.client.PullRequests.CreateComment(ctx, gr.owner, gr.repo, gr.prNum, comment)
174 return err
175}
176
177func (gr GithubReporter) createReview(headCommit string, summary Summary) error {
178 log.Info().Str("repo", fmt.Sprintf("%s/%s", gr.owner, gr.repo)).Str("commit", headCommit).Msg("Creating pull request review")
179
180 ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
181 defer cancel()
182
183 _, resp, err := gr.client.PullRequests.CreateReview(
184 ctx,
185 gr.owner,
186 gr.repo,
187 gr.prNum,
188 &github.PullRequestReviewRequest{
189 CommitID: github.String(headCommit),
190 Body: github.String(formatGHReviewBody(gr.version, summary)),
191 Event: github.String("COMMENT"),
192 },
193 )
194 if err != nil {
195 return fmt.Errorf("failed to create review: %w", err)
196 }
197 log.Info().Str("status", resp.Status).Msg("Pull request review created")
198 return nil
199}
200
201func formatGHReviewBody(version string, summary Summary) string {
202 var b strings.Builder
203
204 b.WriteString(reviewBody)
205
206 bySeverity := summary.CountBySeverity()
207 if len(bySeverity) > 0 {
208 b.WriteString(":heavy_exclamation_mark: Problems found.\n")
209 b.WriteString("| Severity | Number of problems |\n")
210 b.WriteString("| --- | --- |\n")
211
212 for _, s := range []checks.Severity{checks.Fatal, checks.Bug, checks.Warning, checks.Information} {
213 if bySeverity[s] > 0 {
214 b.WriteString("| ")
215 b.WriteString(s.String())
216 b.WriteString(" | ")
217 b.WriteString(strconv.Itoa(bySeverity[s]))
218 b.WriteString(" |\n")
219 }
220 }
221 } else {
222 b.WriteString(":heavy_check_mark: No problems found\n")
223 }
224
225 b.WriteString("<details><summary>Stats</summary>\n<p>\n\n")
226 b.WriteString("| Stat | Value |\n")
227 b.WriteString("| --- | --- |\n")
228
229 b.WriteString("| Version | ")
230 b.WriteString(version)
231 b.WriteString(" |\n")
232
233 b.WriteString("| Number of rules checked | ")
234 b.WriteString(strconv.Itoa(summary.Entries))
235 b.WriteString(" |\n")
236
237 b.WriteString("| Number of problems found | ")
238 b.WriteString(strconv.Itoa(len(summary.Reports())))
239 b.WriteString(" |\n")
240
241 b.WriteString("| Number of offline checks | ")
242 b.WriteString(strconv.FormatInt(summary.OfflineChecks, 10))
243 b.WriteString(" |\n")
244
245 b.WriteString("| Number of online checks | ")
246 b.WriteString(strconv.FormatInt(summary.OnlineChecks, 10))
247 b.WriteString(" |\n")
248
249 b.WriteString("| Checks duration | ")
250 b.WriteString(output.HumanizeDuration(summary.Duration))
251 b.WriteString(" |\n")
252
253 b.WriteString("\n</p>\n</details>\n\n")
254
255 return b.String()
256}
257
258func reportToGitHubComment(headCommit string, rep Report) *github.PullRequestComment {
259 c := github.PullRequestComment{
260 CommitID: github.String(headCommit),
261 Path: github.String(rep.ReportedPath),
262 Body: github.String(fmt.Sprintf(
263 "[%s](https://cloudflare.github.io/pint/checks/%s.html): %s",
264 rep.Problem.Reporter,
265 rep.Problem.Reporter,
266 rep.Problem.Text,
267 )),
268 }
269
270 if len(rep.ModifiedLines) == 1 {
271 c.Line = github.Int(rep.ModifiedLines[0])
272 } else {
273 sort.Ints(rep.ModifiedLines)
274 start, end := rep.ModifiedLines[0], rep.ModifiedLines[len(rep.ModifiedLines)-1]
275 c.Line = github.Int(end)
276 c.StartLine = github.Int(start)
277 }
278
279 return &c
280}