diff --git a/README.md b/README.md index 9f07e2d..8bcf38c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # :robot: Sonarqube PR Issues Review [![Build Status][ci-img]][ci] [![Coverage Status][cov-img]][cov] [![Go Report Card][report-card-img]][report-card] -Simple Webhook for Sonarqube which publishes the issues found in the PR as review requesting changes. +Simple Webhook for Sonarqube which publishes the issues found in the PR. ![Review screenshot](assets/review_screenshot.png) diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index 4027a24..cdcb511 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -13,12 +13,14 @@ var project string var branch string var publishReview bool var markAsPublished bool +var requestChanges bool func init() { CliCmd.PersistentFlags().StringVar(&project, "project", "my-project", "Sonarqube project name") CliCmd.PersistentFlags().StringVar(&branch, "branch", "my-branch", "SCM branch name") CliCmd.PersistentFlags().BoolVar(&publishReview, "publish", false, "Publish review in the SCM") CliCmd.PersistentFlags().BoolVar(&markAsPublished, "mark", false, "Mark the issue as published to avoid sending it again") + CliCmd.PersistentFlags().BoolVar(&requestChanges, "request-changes", true, "When issue is found, mark PR as changes requested") CliCmd.AddCommand(RunCmd) } diff --git a/cmd/cli/run.go b/cmd/cli/run.go index 9f33caa..c34b4c6 100644 --- a/cmd/cli/run.go +++ b/cmd/cli/run.go @@ -89,7 +89,7 @@ func Run(cmd *cobra.Command, args []string) { var gh scm2.SCM = scm2.NewGithub(ctx, sonar, ghToken) // Publish review - err = gh.PublishIssuesReviewFor(ctx, issues.Issues, pr) + err = gh.PublishIssuesReviewFor(ctx, issues.Issues, pr, requestChanges) if err != nil { logrus.WithError(err).Panicln("Failed to publish issues review") diff --git a/cmd/server/run.go b/cmd/server/run.go index 3b300f5..a332887 100644 --- a/cmd/server/run.go +++ b/cmd/server/run.go @@ -128,7 +128,7 @@ func WebhookHandler(webhookSecret string, sonar *sonarqube2.Sonarqube, gh scm2.S queue <- func() error { logrus.Infoln("Processing", webhook.Project.Key, "->", webhook.Branch.Name) - if err := PublishIssues(context.Background(), sonar, gh, webhook.Project.Key, webhook.Branch.Name); err != nil { + if err := PublishIssues(context.Background(), sonar, gh, webhook.Project.Key, webhook.Branch.Name, webhook.Branch.Type); err != nil { return err } @@ -153,11 +153,20 @@ func ProcessQueue(queue <-chan func() error) { } // PublishIssues publishes the issues in the PR for the given project branch -func PublishIssues(ctx context.Context, sonar *sonarqube2.Sonarqube, projectScm scm2.SCM, project string, branch string) error { +func PublishIssues(ctx context.Context, sonar *sonarqube2.Sonarqube, projectScm scm2.SCM, project string, branch string, branchType string) error { // Find PR - pr, err := sonar.FindPRForBranch(project, branch) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("failed to find PR for branch %s of the project %s", branch, project)) + var pr *sonarqube2.PullRequest + var err error + if branchType == sonarqube2.BRANCH_TYPE_PULL_REQUEST { + pr, err = sonar.FindPRForKey(project, branch) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to find PR for key %s of the project %s", branch, project)) + } + } else { + pr, err = sonar.FindPRForBranch(project, branch) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to find PR for branch %s of the project %s", branch, project)) + } } // List issues @@ -175,7 +184,7 @@ func PublishIssues(ctx context.Context, sonar *sonarqube2.Sonarqube, projectScm } // Publish review - err = projectScm.PublishIssuesReviewFor(ctx, issues.Issues, pr) + err = projectScm.PublishIssuesReviewFor(ctx, issues.Issues, pr, requestChanges) if err != nil { return errors.Wrap(err, fmt.Sprintf("Failed to publish issues review for branch %s of the project %s", branch, project)) } diff --git a/cmd/server/server.go b/cmd/server/server.go index 3aab48d..eae2e7a 100644 --- a/cmd/server/server.go +++ b/cmd/server/server.go @@ -6,6 +6,7 @@ import ( var serverPort int var workers int +var requestChanges bool var ServerCmd = &cobra.Command{ Use: "server", @@ -15,5 +16,6 @@ var ServerCmd = &cobra.Command{ func init() { ServerCmd.PersistentFlags().IntVarP(&serverPort, "port", "p", 8080, "Server port") ServerCmd.PersistentFlags().IntVarP(&workers, "workers", "w", 30, "Workers count") + ServerCmd.PersistentFlags().BoolVar(&requestChanges, "request-changes", true, "When issue is found, mark PR as changes requested") ServerCmd.AddCommand(RunCmd) } diff --git a/pkg/scm/github.go b/pkg/scm/github.go index 9f198e7..0150f40 100644 --- a/pkg/scm/github.go +++ b/pkg/scm/github.go @@ -14,6 +14,11 @@ import ( "github.com/herlon214/sonarqube-pr-issues/pkg/sonarqube" ) +const ( + REVIEW_EVENT_COMMENT = "COMMENT" + REVIEW_EVENT_REQUEST_CHANGES = "REQUEST_CHANGES" +) + type Github struct { client *github.Client sonar *sonarqube.Sonarqube @@ -43,8 +48,14 @@ func NewGithub(ctx context.Context, sonar *sonarqube.Sonarqube, token string) *G } // PublishIssuesReviewFor publishes a review with a comment for each issue -func (g *Github) PublishIssuesReviewFor(ctx context.Context, issues []sonarqube.Issue, pr *sonarqube.PullRequest) error { - event := "REQUEST_CHANGES" +func (g *Github) PublishIssuesReviewFor(ctx context.Context, issues []sonarqube.Issue, pr *sonarqube.PullRequest, requestChanges bool) error { + var reviewEvent string + if requestChanges { + reviewEvent = REVIEW_EVENT_REQUEST_CHANGES + } else { + reviewEvent = REVIEW_EVENT_COMMENT + } + comments := make([]*github.DraftReviewComment, 0) // Create a comment for each issue @@ -67,7 +78,7 @@ func (g *Github) PublishIssuesReviewFor(ctx context.Context, issues []sonarqube. reviewRequest := &github.PullRequestReviewRequest{ Body: &body, - Event: &event, + Event: &reviewEvent, Comments: comments, } diff --git a/pkg/scm/github_test.go b/pkg/scm/github_test.go index 0affce2..f5b02c5 100644 --- a/pkg/scm/github_test.go +++ b/pkg/scm/github_test.go @@ -54,7 +54,9 @@ func TestGithubPublishIssuesReview(t *testing.T) { }, } - err := gh.PublishIssuesReviewFor(ctx, issues, pr) + reviewEvent := REVIEW_EVENT_REQUEST_CHANGES + + err := gh.PublishIssuesReviewFor(ctx, issues, pr, reviewEvent) assert.NoError(t, err) } diff --git a/pkg/scm/scm.go b/pkg/scm/scm.go index 2a1338f..7ad43ac 100644 --- a/pkg/scm/scm.go +++ b/pkg/scm/scm.go @@ -7,5 +7,5 @@ import ( ) type SCM interface { - PublishIssuesReviewFor(ctx context.Context, issues []sonarqube.Issue, pr *sonarqube.PullRequest) error + PublishIssuesReviewFor(ctx context.Context, issues []sonarqube.Issue, pr *sonarqube.PullRequest, requestChanges bool) error } diff --git a/pkg/sonarqube/sonarqube.go b/pkg/sonarqube/sonarqube.go index 6195a63..eef49e1 100644 --- a/pkg/sonarqube/sonarqube.go +++ b/pkg/sonarqube/sonarqube.go @@ -12,7 +12,8 @@ import ( ) const ( - TAG_PUBLISHED = "published" + TAG_PUBLISHED = "published" + BRANCH_TYPE_PULL_REQUEST = "PULL_REQUEST" ) type BulkActionResponse struct { @@ -77,6 +78,24 @@ func (s *Sonarqube) ProjectPullRequests(projectId string) (*ProjectPullRequests, return &data, nil } +// FindPRForKey searches the pull request for the given project and key +func (s *Sonarqube) FindPRForKey(project string, key string) (*PullRequest, error) { + // Fetch project pull requests + pullRequests, err := s.ProjectPullRequests(project) + if err != nil { + return nil, err + } + + // Filter by key + for _, item := range pullRequests.PullRequests { + if item.Key == key { + return &item, nil + } + } + + return nil, errors.New("not found") +} + // FindPRForBranch searches the pull request for the given project and branch func (s *Sonarqube) FindPRForBranch(project string, branch string) (*PullRequest, error) { // Fetch project pull requests diff --git a/pkg/sonarqube/sonarqube_test.go b/pkg/sonarqube/sonarqube_test.go index 5b1baf3..8909aee 100644 --- a/pkg/sonarqube/sonarqube_test.go +++ b/pkg/sonarqube/sonarqube_test.go @@ -42,6 +42,27 @@ func TestSonarqubeProjectPRs(t *testing.T) { assert.Equal(t, "https://github.com/myorg/myproject/pull/2", prs.PullRequests[1].URL) } +func TestSonarqubeFindPRForKey(t *testing.T) { + // Mock response + expected := `{"pullRequests":[{"key":"3","title":"Feat/newtest","branch":"feat/newtest","base":"feat/mvp","status":{"qualityGateStatus":"ERROR","bugs":2,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2021-12-04T15:44:18+0000","url":"https://github.com/myorg/myproject/pull/3","target":"feat/mvp"},{"key":"2","title":"test PR","branch":"feat/test","base":"feat/mvp","status":{"qualityGateStatus":"ERROR","bugs":1,"vulnerabilities":0,"codeSmells":2},"analysisDate":"2021-12-03T18:10:59+0000","url":"https://github.com/myorg/myproject/pull/2","target":"feat/mvp"}]}` + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(expected)) + })) + defer svr.Close() + + // New sonar + sonar := New(svr.URL, "myapikey") + + // Read PRs + pr, err := sonar.FindPRForKey("myproject", "2") + assert.NoError(t, err) + + assert.NotNil(t, pr) + assert.Equal(t, "feat/test", pr.Branch) + assert.Equal(t, "2", pr.Key) + assert.Equal(t, "https://github.com/myorg/myproject/pull/2", pr.URL) +} + func TestSonarqubeFindPRForBranch(t *testing.T) { // Mock response expected := `{"pullRequests":[{"key":"3","title":"Feat/newtest","branch":"feat/newtest","base":"feat/mvp","status":{"qualityGateStatus":"ERROR","bugs":2,"vulnerabilities":0,"codeSmells":0},"analysisDate":"2021-12-04T15:44:18+0000","url":"https://github.com/myorg/myproject/pull/3","target":"feat/mvp"},{"key":"2","title":"test PR","branch":"feat/test","base":"feat/mvp","status":{"qualityGateStatus":"ERROR","bugs":1,"vulnerabilities":0,"codeSmells":2},"analysisDate":"2021-12-03T18:10:59+0000","url":"https://github.com/myorg/myproject/pull/2","target":"feat/mvp"}]}` diff --git a/pkg/sonarqube/webhook.go b/pkg/sonarqube/webhook.go index 2f8fa79..3fe1905 100644 --- a/pkg/sonarqube/webhook.go +++ b/pkg/sonarqube/webhook.go @@ -7,5 +7,6 @@ type WebhookData struct { } `json:"project"` Branch struct { Name string `json:"name"` + Type string `json:"type"` } `json:"branch"` }