From e608a8f969ac14a458ea84e799c3643807f96498 Mon Sep 17 00:00:00 2001 From: justusbunsi <61625851+justusbunsi@users.noreply.github.com> Date: Sat, 9 Oct 2021 18:09:54 +0200 Subject: [PATCH] Retrieve actual data from SonarQube for comment Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com> --- internal/clients/gitea_sdk/gitea_sdk.go | 2 +- internal/clients/sonarqube_sdk/measures.go | 53 +++++++++++++++++++ .../clients/sonarqube_sdk/sonarqube_sdk.go | 26 +++++---- internal/webhook_handler/main.go | 1 + internal/webhook_handler/sonarqube.go | 41 ++++++-------- internal/webhook_handler/sonarqube_test.go | 5 +- internal/webhooks/sonarqube/webhook.go | 13 ++++- 7 files changed, 102 insertions(+), 39 deletions(-) create mode 100644 internal/clients/sonarqube_sdk/measures.go diff --git a/internal/clients/gitea_sdk/gitea_sdk.go b/internal/clients/gitea_sdk/gitea_sdk.go index 5486301..e879761 100644 --- a/internal/clients/gitea_sdk/gitea_sdk.go +++ b/internal/clients/gitea_sdk/gitea_sdk.go @@ -28,7 +28,7 @@ func (sdk *GiteaSdk) PostComment(repo settings.GiteaRepository, idx int, msg str func New() *GiteaSdk { client, err := gitea.NewClient(settings.Gitea.Url, gitea.SetToken(settings.Gitea.Token.Value)) if err != nil { - panic(fmt.Errorf("Cannot initialize Gitea client: %w", err)) + panic(fmt.Errorf("cannot initialize Gitea client: %w", err)) } return &GiteaSdk{client} diff --git a/internal/clients/sonarqube_sdk/measures.go b/internal/clients/sonarqube_sdk/measures.go new file mode 100644 index 0000000..d0d20ce --- /dev/null +++ b/internal/clients/sonarqube_sdk/measures.go @@ -0,0 +1,53 @@ +package sonarqube_sdk + +import ( + "fmt" + "strings" +) + +type period struct { + Value string `json:"value"` +} + +type MeasuresComponentMeasure struct { + Metric string `json:"metric"` + Value string `json:"value"` + Period *period `json:"period,omitempty"` +} + +type MeasuresComponentMetric struct { + Key string `json:"key"` + Name string `json:"name"` +} + +type MeasuresComponent struct { + PullRequest string `json:"pullRequest"` + Measures []MeasuresComponentMeasure `json:"measures"` +} + +type MeasuresResponse struct { + Component MeasuresComponent `json:"component"` + Metrics []MeasuresComponentMetric `json:"metrics"` +} + +func (mr *MeasuresResponse) GetRenderedMarkdownTable() string { + metricsTranslations := map[string]string{} + for _, metric := range mr.Metrics { + metricsTranslations[metric.Key] = metric.Name + } + measures := make([]string, len(mr.Component.Measures)) + for i, measure := range mr.Component.Measures { + value := measure.Value + if measure.Period != nil { + value = measure.Period.Value + } + measures[i] = fmt.Sprintf("| %s | %s |", metricsTranslations[measure.Metric], value) + } + + table := ` +| Metric | Current | +| -------- | -------- | +%s` + + return fmt.Sprintf(table, strings.Join(measures, "\n")) +} diff --git a/internal/clients/sonarqube_sdk/sonarqube_sdk.go b/internal/clients/sonarqube_sdk/sonarqube_sdk.go index b6d683b..2fd1a3a 100644 --- a/internal/clients/sonarqube_sdk/sonarqube_sdk.go +++ b/internal/clients/sonarqube_sdk/sonarqube_sdk.go @@ -2,16 +2,16 @@ package sonarqube_sdk import ( "encoding/base64" + "encoding/json" "fmt" "io" - "log" "net/http" "gitea-sonarqube-pr-bot/internal/settings" ) type SonarQubeSdkInterface interface { - GetMeasures(string, string) (string, error) + GetMeasures(string, string) (*MeasuresResponse, error) } type SonarQubeSdk struct { @@ -20,20 +20,26 @@ type SonarQubeSdk struct { token string } -func (sdk *SonarQubeSdk) GetMeasures(project string, branch string) (string, error) { +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,violations&component=%s&pullRequest=%s", sdk.baseUrl, project, branch) - log.Println(url) - req, err := http.NewRequest("GET", url, nil) + req, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { - panic(fmt.Errorf("Cannot initialize Request: %w", err)) + return nil, fmt.Errorf("cannot initialize Request: %w", err) } req.Header.Add("Authorization", sdk.basicAuth()) - resp, _ := sdk.client.Do(req) + rawResp, _ := sdk.client.Do(req) + if rawResp.Body != nil { + defer rawResp.Body.Close() + } - defer resp.Body.Close() - body, _ := io.ReadAll(resp.Body) + body, _ := io.ReadAll(rawResp.Body) + response := &MeasuresResponse{} + err = json.Unmarshal(body, &response) + if err != nil { + return nil, fmt.Errorf("cannot parse response from SonarQube: %w", err) + } - return string(body), nil + return response, nil } func (sdk *SonarQubeSdk) basicAuth() string { diff --git a/internal/webhook_handler/main.go b/internal/webhook_handler/main.go index db4c492..9977d2a 100644 --- a/internal/webhook_handler/main.go +++ b/internal/webhook_handler/main.go @@ -12,6 +12,7 @@ import ( giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + "github.com/gorilla/mux" "github.com/urfave/cli/v2" ) diff --git a/internal/webhook_handler/sonarqube.go b/internal/webhook_handler/sonarqube.go index 439ed9d..2cb6f8a 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -20,34 +20,23 @@ type SonarQubeWebhookHandler struct { sqSdk sqSdk.SonarQubeSdkInterface } -func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) string { - a, _ := h.sqSdk.GetMeasures(w.Project.Key, w.Branch.Name) - - log.Println(a) - - status := ":white_check_mark:" - if w.QualityGate.Status != "OK" { - status = ":x:" +func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) (string, error) { + m, err := h.sqSdk.GetMeasures(w.Project.Key, w.Branch.Name) + if err != nil { + return "", err } - measures := `| Metric | Current | -| -------- | -------- | -| Bugs | 123 | -| Code Smells | 1 | -| Vulnerabilities | 1 | -` + message := make([]string, 5) + message[0] = w.GetRenderedQualityGate() + message[1] = m.GetRenderedMarkdownTable() + message[2] = fmt.Sprintf("See [SonarQube](%s) for details.", w.Branch.Url) + message[3] = "---" + message[4] = "- If you want the bot to check again, post `/sqbot review`" - msg := `**Quality Gate**: %s - -**Measures** - -%s - -See [SonarQube](https://example.com/sonarqube/dashboard?id=pr-bot&pullRequest=PR-1) for details.` - return fmt.Sprintf(msg, status, measures) + return strings.Join(message, "\n\n"), nil } -func (_ *SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) (bool, int) { +func (*SonarQubeWebhookHandler) inProjectsMapping(p []settings.Project, n string) (bool, int) { for idx, proj := range p { if proj.SonarQube.Key == n { return true, idx @@ -65,7 +54,11 @@ func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings. h.fetchDetails(w) - comment := h.composeGiteaComment(w) + comment, err := h.composeGiteaComment(w) + if err != nil { + log.Printf("Error composing Gitea comment: %s", err.Error()) + return + } h.giteaSdk.PostComment(repo, w.PRIndex, comment) } diff --git a/internal/webhook_handler/sonarqube_test.go b/internal/webhook_handler/sonarqube_test.go index dce3b3f..91c594c 100644 --- a/internal/webhook_handler/sonarqube_test.go +++ b/internal/webhook_handler/sonarqube_test.go @@ -6,6 +6,7 @@ import ( "net/http/httptest" "testing" + sqSDK "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" @@ -33,8 +34,8 @@ type SQSdkMock struct { mock.Mock } -func (h *SQSdkMock) GetMeasures(project string, branch string) (string, error) { - return "", nil +func (h *SQSdkMock) GetMeasures(project string, branch string) (*sqSDK.MeasuresResponse, error) { + return &sqSDK.MeasuresResponse{}, nil } func defaultMockPreparation(h *HandlerPartialMock) { diff --git a/internal/webhooks/sonarqube/webhook.go b/internal/webhooks/sonarqube/webhook.go index 4ccb3bf..d05117d 100644 --- a/internal/webhooks/sonarqube/webhook.go +++ b/internal/webhooks/sonarqube/webhook.go @@ -33,6 +33,15 @@ type Webhook struct { PRIndex int } +func (w *Webhook) GetRenderedQualityGate() string { + status := ":white_check_mark:" + if w.QualityGate.Status != "OK" { + status = ":x:" + } + + return fmt.Sprintf("**Quality Gate**: %s", status) +} + func New(raw []byte) (*Webhook, bool) { v := viper.New() v.SetConfigType("json") @@ -61,8 +70,8 @@ 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 0, fmt.Errorf("branch name '%s' does not match regex '%s'", w.Branch.Name, re.String()) } - return strconv.Atoi(fmt.Sprintf("%s", res[1])) + return strconv.Atoi(string(res[1])) }