diff --git a/internal/api/sonarqube_test.go b/internal/api/sonarqube_test.go index 0c8816b..30f0a93 100644 --- a/internal/api/sonarqube_test.go +++ b/internal/api/sonarqube_test.go @@ -47,6 +47,14 @@ func (h *SQSdkMock) GetMeasures(project string, branch string) (*sqSdk.MeasuresR return &sqSdk.MeasuresResponse{}, nil } +func (h *SQSdkMock) GetPullRequestUrl(project string, index int64) string { + return "" +} + +func (h *SQSdkMock) GetPullRequest(project string, index int64) (*sqSdk.PullRequest, error) { + return nil, nil +} + func defaultMockPreparation(h *HandlerPartialMock) { h.On("fetchDetails", mock.Anything).Return(nil) } diff --git a/internal/clients/sonarqube/pulls.go b/internal/clients/sonarqube/pulls.go new file mode 100644 index 0000000..567ef8d --- /dev/null +++ b/internal/clients/sonarqube/pulls.go @@ -0,0 +1,21 @@ +package sonarqube + +type PullRequest struct { + Key string `json:"key"` + Status struct { + QualityGateStatus string `json:"qualityGateStatus"` + } `json:"status"` +} + +type PullsResponse struct { + PullRequests []PullRequest `json:"pullRequests"` +} + +func (r *PullsResponse) GetPullRequest(name string) *PullRequest { + for _, pr := range r.PullRequests { + if pr.Key == name { + return &pr + } + } + return nil +} diff --git a/internal/clients/sonarqube/sonarqube.go b/internal/clients/sonarqube/sonarqube.go index 3c0dfb4..628be0a 100644 --- a/internal/clients/sonarqube/sonarqube.go +++ b/internal/clients/sonarqube/sonarqube.go @@ -5,13 +5,32 @@ import ( "encoding/json" "fmt" "io" + "log" "net/http" + "regexp" + "strconv" "gitea-sonarqube-pr-bot/internal/settings" ) +func ParsePRIndex(name string) (int, error) { + re := regexp.MustCompile(`^PR-(\d+)$`) + res := re.FindSubmatch([]byte(name)) + if len(res) != 2 { + return 0, fmt.Errorf("branch name '%s' does not match regex '%s'", name, re.String()) + } + + return strconv.Atoi(string(res[1])) +} + +func PRNameFromIndex(index int64) string { + return fmt.Sprintf("PR-%d", index) +} + type SonarQubeSdkInterface interface { GetMeasures(string, string) (*MeasuresResponse, error) + GetPullRequestUrl(string, int64) string + GetPullRequest(string, int64) (*PullRequest, error) } type SonarQubeSdk struct { @@ -20,6 +39,49 @@ type SonarQubeSdk struct { token string } +func (sdk *SonarQubeSdk) GetPullRequestUrl(project string, index int64) string { + return fmt.Sprintf("%s/dashboard?id=%s&pullRequest=%s", sdk.baseUrl, project, PRNameFromIndex(index)) +} + +func (sdk *SonarQubeSdk) fetchPullRequests(project string) *PullsResponse { + url := fmt.Sprintf("%s/api/project_pull_requests/list?project=%s", sdk.baseUrl, project) + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + log.Printf("Cannot initialize Request: %s", err.Error()) + return nil + } + req.Header.Add("Authorization", sdk.basicAuth()) + rawResp, _ := sdk.client.Do(req) + if rawResp.Body != nil { + defer rawResp.Body.Close() + } + + body, _ := io.ReadAll(rawResp.Body) + response := &PullsResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + log.Printf("cannot parse response from SonarQube: %s", err.Error()) + return nil + } + + return response +} + +func (sdk *SonarQubeSdk) GetPullRequest(project string, index int64) (*PullRequest, error) { + response := sdk.fetchPullRequests(project) + if response == nil { + return nil, fmt.Errorf("unable to retrieve pull requests from SonarQube") + } + + name := PRNameFromIndex(index) + pr := response.GetPullRequest(name) + if pr == nil { + return nil, fmt.Errorf("no pull request found with name '%s'", name) + } + + return pr, nil +} + func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (*MeasuresResponse, error) { url := fmt.Sprintf("%s/api/measures/component?additionalFields=metrics&metricKeys=bugs,vulnerabilities,new_security_hotspots,code_smells&component=%s&pullRequest=%s", sdk.baseUrl, project, branch) req, err := http.NewRequest(http.MethodGet, url, nil) diff --git a/internal/webhooks/gitea/comment.go b/internal/webhooks/gitea/comment.go index c7a5436..4e4eff6 100644 --- a/internal/webhooks/gitea/comment.go +++ b/internal/webhooks/gitea/comment.go @@ -72,10 +72,21 @@ func (w *CommentWebhook) ProcessData(gSDK giteaSdk.GiteaSdkInterface, sqSDK sqSd } log.Printf("Fetching SonarQube data...") + pr, err := sqSDK.GetPullRequest(w.ConfiguredProject.SonarQube.Key, w.Issue.Number) + if err != nil { + log.Printf("Error loading PR data from SonarQube: %s", err.Error()) + return + } + + status := giteaSdk.StatusOK + if pr.Status.QualityGateStatus != "OK" { + status = giteaSdk.StatusFailure + } + _ = gSDK.UpdateStatus(w.ConfiguredProject.Gitea, headRef, giteaSdk.StatusDetails{ - Url: "", - Message: "OK", - State: giteaSdk.StatusOK, + Url: sqSDK.GetPullRequestUrl(w.ConfiguredProject.SonarQube.Key, w.Issue.Number), + Message: pr.Status.QualityGateStatus, + State: status, }) } diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index d05117d..5bcdd17 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -4,8 +4,8 @@ import ( "bytes" "fmt" "log" - "regexp" - "strconv" + + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube" "github.com/spf13/viper" ) @@ -55,7 +55,7 @@ func New(raw []byte) (*Webhook, bool) { return w, false } - idx, err1 := parsePRIndex(w) + idx, err1 := sqSdk.ParsePRIndex(w.Branch.Name) if err1 != nil { log.Printf("Error parsing PR index: %s", err1.Error()) return w, false @@ -65,13 +65,3 @@ func New(raw []byte) (*Webhook, bool) { return w, true } - -func parsePRIndex(w *Webhook) (int, error) { - re := regexp.MustCompile(`^PR-(\d+)$`) - res := re.FindSubmatch([]byte(w.Branch.Name)) - if len(res) != 2 { - return 0, fmt.Errorf("branch name '%s' does not match regex '%s'", w.Branch.Name, re.String()) - } - - return strconv.Atoi(string(res[1])) -}