package reporter
import (
"bytes"
"context"
"fmt"
"log/slog"
"strconv"
"strings"
"time"
"github.com/google/go-github/v55/github"
"golang.org/x/oauth2"
"github.com/cloudflare/pint/internal/checks"
"github.com/cloudflare/pint/internal/git"
"github.com/cloudflare/pint/internal/output"
)
var reviewBody = "### This pull request was validated by [pint](https://github.com/cloudflare/pint).\n"
type GithubReporter struct {
version string
baseURL string
uploadURL string
timeout time.Duration
authToken string
owner string
repo string
prNum int
gitCmd git.CommandRunner
client *github.Client
}
// NewGithubReporter creates a new GitHub reporter that reports
// problems via comments on a given pull request number (integer).
func NewGithubReporter(version, baseURL, uploadURL string, timeout time.Duration, token, owner, repo string, prNum int, gitCmd git.CommandRunner) (_ GithubReporter, err error) {
gr := GithubReporter{
version: version,
baseURL: baseURL,
uploadURL: uploadURL,
timeout: timeout,
authToken: token,
owner: owner,
repo: repo,
prNum: prNum,
gitCmd: gitCmd,
}
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: gr.authToken},
)
tc := oauth2.NewClient(context.Background(), ts)
gr.client = github.NewClient(tc)
if gr.uploadURL != "" && gr.baseURL != "" {
gr.client, err = gr.client.WithEnterpriseURLs(gr.baseURL, gr.uploadURL)
if err != nil {
return gr, fmt.Errorf("creating new GitHub client: %w", err)
}
}
return gr, nil
}
// Submit submits the summary to GitHub.
func (gr GithubReporter) Submit(summary Summary) error {
headCommit, err := git.HeadCommit(gr.gitCmd)
if err != nil {
return fmt.Errorf("failed to get HEAD commit: %w", err)
}
slog.Info("Got HEAD commit from git", slog.String("commit", headCommit))
review, err := gr.findExistingReview()
if err != nil {
return fmt.Errorf("failed to list pull request reviews: %w", err)
}
if review != nil {
if err = gr.updateReview(review, summary); err != nil {
return err
}
} else {
if err = gr.createReview(headCommit, summary); err != nil {
return err
}
}
return gr.addReviewComments(headCommit, summary)
}
func (gr GithubReporter) findExistingReview() (*github.PullRequestReview, error) {
ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
defer cancel()
reviews, _, err := gr.client.PullRequests.ListReviews(ctx, gr.owner, gr.repo, gr.prNum, nil)
if err != nil {
return nil, err
}
for _, review := range reviews {
if strings.HasPrefix(review.GetBody(), reviewBody) {
return review, nil
}
}
return nil, nil
}
func (gr GithubReporter) updateReview(review *github.PullRequestReview, summary Summary) error {
slog.Info("Updating pull request review", slog.String("repo", fmt.Sprintf("%s/%s", gr.owner, gr.repo)))
ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
defer cancel()
_, _, err := gr.client.PullRequests.UpdateReview(
ctx,
gr.owner,
gr.repo,
gr.prNum,
review.GetID(),
formatGHReviewBody(gr.version, summary),
)
return err
}
func (gr GithubReporter) addReviewComments(headCommit string, summary Summary) error {
slog.Info("Creating review comments")
existingComments, err := gr.getReviewComments()
if err != nil {
return err
}
for _, rep := range summary.Reports() {
comment := reportToGitHubComment(headCommit, rep)
var found bool
for _, ec := range existingComments {
if ec.GetBody() == comment.GetBody() && ec.GetCommitID() == comment.GetCommitID() {
found = true
break
}
}
if found {
slog.Debug("Comment already exist", slog.String("body", comment.GetBody()), slog.String("commit", comment.GetCommitID()))
continue
}
if err := gr.createComment(comment); err != nil {
return err
}
}
return nil
}
func (gr GithubReporter) getReviewComments() ([]*github.PullRequestComment, error) {
ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
defer cancel()
comments, _, err := gr.client.PullRequests.ListComments(ctx, gr.owner, gr.repo, gr.prNum, nil)
return comments, err
}
func (gr GithubReporter) createComment(comment *github.PullRequestComment) error {
slog.Debug("Creating review comment", slog.String("body", comment.GetBody()), slog.String("commit", comment.GetCommitID()))
ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
defer cancel()
_, _, err := gr.client.PullRequests.CreateComment(ctx, gr.owner, gr.repo, gr.prNum, comment)
return err
}
func (gr GithubReporter) createReview(headCommit string, summary Summary) error {
slog.Info("Creating pull request review", slog.String("repo", fmt.Sprintf("%s/%s", gr.owner, gr.repo)), slog.String("commit", headCommit))
ctx, cancel := context.WithTimeout(context.Background(), gr.timeout)
defer cancel()
_, resp, err := gr.client.PullRequests.CreateReview(
ctx,
gr.owner,
gr.repo,
gr.prNum,
&github.PullRequestReviewRequest{
CommitID: github.String(headCommit),
Body: github.String(formatGHReviewBody(gr.version, summary)),
Event: github.String("COMMENT"),
},
)
if err != nil {
return fmt.Errorf("failed to create review: %w", err)
}
slog.Info("Pull request review created", slog.String("status", resp.Status))
return nil
}
func formatGHReviewBody(version string, summary Summary) string {
var b strings.Builder
b.WriteString(reviewBody)
bySeverity := summary.CountBySeverity()
if len(bySeverity) > 0 {
b.WriteString(":heavy_exclamation_mark: Problems found.\n")
b.WriteString("| Severity | Number of problems |\n")
b.WriteString("| --- | --- |\n")
for _, s := range []checks.Severity{checks.Fatal, checks.Bug, checks.Warning, checks.Information} {
if bySeverity[s] > 0 {
b.WriteString("| ")
b.WriteString(s.String())
b.WriteString(" | ")
b.WriteString(strconv.Itoa(bySeverity[s]))
b.WriteString(" |\n")
}
}
} else {
b.WriteString(":heavy_check_mark: No problems found\n")
}
b.WriteString("<details><summary>Stats</summary>\n<p>\n\n")
b.WriteString("| Stat | Value |\n")
b.WriteString("| --- | --- |\n")
b.WriteString("| Version | ")
b.WriteString(version)
b.WriteString(" |\n")
b.WriteString("| Number of rules checked | ")
b.WriteString(strconv.Itoa(summary.Entries))
b.WriteString(" |\n")
b.WriteString("| Number of problems found | ")
b.WriteString(strconv.Itoa(len(summary.Reports())))
b.WriteString(" |\n")
b.WriteString("| Number of offline checks | ")
b.WriteString(strconv.FormatInt(summary.OfflineChecks, 10))
b.WriteString(" |\n")
b.WriteString("| Number of online checks | ")
b.WriteString(strconv.FormatInt(summary.OnlineChecks, 10))
b.WriteString(" |\n")
b.WriteString("| Checks duration | ")
b.WriteString(output.HumanizeDuration(summary.Duration))
b.WriteString(" |\n")
b.WriteString("\n</p>\n</details>\n\n")
b.WriteString("<details><summary>Problems</summary>\n<p>\n\n")
if summary.Entries > 0 {
buf := bytes.NewBuffer(nil)
cr := NewConsoleReporter(buf, checks.Information)
err := cr.Submit(summary)
if err != nil {
b.WriteString(fmt.Sprintf("Failed to generate list of problems: %s", err))
} else {
b.WriteString("```\n")
b.WriteString(buf.String())
b.WriteString("```\n")
}
} else {
b.WriteString("No problems reported")
}
b.WriteString("\n</p>\n</details>\n\n")
return b.String()
}
func reportToGitHubComment(headCommit string, rep Report) *github.PullRequestComment {
var msgPrefix string
reportLine, srcLine := moveReportedLine(rep)
if reportLine != srcLine {
msgPrefix = fmt.Sprintf("Problem reported on unmodified line %d, annotation moved here: ", srcLine)
}
c := github.PullRequestComment{
CommitID: github.String(headCommit),
Path: github.String(rep.ReportedPath),
Body: github.String(fmt.Sprintf(
"[%s](https://cloudflare.github.io/pint/checks/%s.html): %s%s",
rep.Problem.Reporter,
rep.Problem.Reporter,
msgPrefix,
rep.Problem.Text,
)),
Line: github.Int(reportLine),
}
return &c
}cloudflare/pint
Publicmirrored from https://github.com/cloudflare/pintAvailable
internal/reporter/github.go
295lines · modepreview