diff --git a/internal/clients/gitea_sdk/gitea_sdk.go b/internal/clients/gitea_sdk/gitea_sdk.go index 068bd9a..1b566c9 100644 --- a/internal/clients/gitea_sdk/gitea_sdk.go +++ b/internal/clients/gitea_sdk/gitea_sdk.go @@ -3,14 +3,15 @@ package gitea_sdk import ( "fmt" "gitea-sonarqube-pr-bot/internal/settings" - webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" + "log" "code.gitea.io/sdk/gitea" ) type GiteaSdkInterface interface { PostComment(settings.GiteaRepository, int, string) error - UpdateStatus(settings.GiteaRepository, *webhook.Webhook) error + UpdateStatus(settings.GiteaRepository, string, string, string, gitea.StatusState) error + DetermineHEAD(settings.GiteaRepository, int64) (string, error) } type GiteaSdk struct { @@ -27,26 +28,31 @@ func (sdk *GiteaSdk) PostComment(repo settings.GiteaRepository, idx int, msg str return err } -func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, w *webhook.Webhook) error { - status := gitea.StatusPending - switch w.QualityGate.Status { - case "OK": - status = gitea.StatusSuccess - case "ERROR": - status = gitea.StatusFailure - } +func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, ref string, targetUrl string, description string, status gitea.StatusState) error { opt := gitea.CreateStatusOption{ - TargetURL: w.Branch.Url, + TargetURL: targetUrl, Context: "gitea-sonarqube-pr-bot", - Description: w.QualityGate.Status, + Description: description, State: status, } - _, _, err := sdk.client.CreateStatus(repo.Owner, repo.Name, w.Revision, opt) + _, _, err := sdk.client.CreateStatus(repo.Owner, repo.Name, ref, opt) + if err != nil { + log.Printf("Error updating status: %s", err.Error()) + } return err } +func (sdk *GiteaSdk) DetermineHEAD(repo settings.GiteaRepository, idx int64) (string, error) { + pr, _, err := sdk.client.GetPullRequest(repo.Owner, repo.Name, idx) + if err != nil { + return "", err + } + + return pr.Head.Sha, nil +} + func New() *GiteaSdk { client, err := gitea.NewClient(settings.Gitea.Url, gitea.SetToken(settings.Gitea.Token.Value)) if err != nil { diff --git a/internal/webhook_handler/gitea.go b/internal/webhook_handler/gitea.go new file mode 100644 index 0000000..60737cf --- /dev/null +++ b/internal/webhook_handler/gitea.go @@ -0,0 +1,60 @@ +package webhook_handler + +import ( + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + webhook "gitea-sonarqube-pr-bot/internal/webhooks/gitea" +) + +type GiteaWebhookHandler struct { + giteaSdk giteaSdk.GiteaSdkInterface + sqSdk sqSdk.SonarQubeSdkInterface +} + +func (h *GiteaWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + + if r.Body != nil { + defer r.Body.Close() + } + + raw, err := ioutil.ReadAll(r.Body) + + if err != nil { + log.Printf("Error reading request body %s", err.Error()) + rw.WriteHeader(http.StatusInternalServerError) + io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) + return + } + + w, ok := webhook.New(raw) + if !ok { + rw.WriteHeader(http.StatusUnprocessableEntity) + io.WriteString(rw, `{"message": "Error parsing POST body."}`) + return + } + + if err := w.Validate(); err != nil { + rw.WriteHeader(http.StatusOK) + io.WriteString(rw, fmt.Sprintf(`{"message": "%s"}`, err.Error())) + return + } + + rw.WriteHeader(http.StatusOK) + io.WriteString(rw, `{"message": "Processing data. See bot logs for details."}`) + + w.ProcessData(h.giteaSdk, h.sqSdk) +} + +func NewGiteaWebhookHandler(g giteaSdk.GiteaSdkInterface, sq sqSdk.SonarQubeSdkInterface) *GiteaWebhookHandler { + return &GiteaWebhookHandler{ + giteaSdk: g, + sqSdk: sq, + } +} diff --git a/internal/webhook_handler/main.go b/internal/webhook_handler/main.go index 9e27466..873a117 100644 --- a/internal/webhook_handler/main.go +++ b/internal/webhook_handler/main.go @@ -38,6 +38,13 @@ func addSonarQubeEndpoint(r *gin.Engine) { }) } +func addGiteaEndpoint(r *gin.Engine) { + webhookHandler := NewGiteaWebhookHandler(giteaSdk.New(), sqSdk.New()) + r.POST("/hooks/gitea", func(c *gin.Context) { + webhookHandler.Handle(c.Writer, c.Request) + }) +} + func Serve(c *cli.Context) error { fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.") @@ -45,6 +52,7 @@ func Serve(c *cli.Context) error { addPingApi(r) addSonarQubeEndpoint(r) + addGiteaEndpoint(r) return endless.ListenAndServe(":3000", r) } diff --git a/internal/webhook_handler/sonarqube.go b/internal/webhook_handler/sonarqube.go index e901cbf..723a365 100644 --- a/internal/webhook_handler/sonarqube.go +++ b/internal/webhook_handler/sonarqube.go @@ -12,6 +12,8 @@ import ( sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" + + "code.gitea.io/sdk/gitea" ) type SonarQubeWebhookHandler struct { @@ -31,7 +33,7 @@ func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) (strin 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`" + message[4] = "- If you want the bot to check again, post `/sq-bot review`" return strings.Join(message, "\n\n"), nil } @@ -54,10 +56,14 @@ func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings. h.fetchDetails(w) - err := h.giteaSdk.UpdateStatus(repo, w) - if err != nil { - log.Printf("Error updating status: %s", err.Error()) + status := gitea.StatusPending + switch w.QualityGate.Status { + case "OK": + status = gitea.StatusSuccess + case "ERROR": + status = gitea.StatusFailure } + _ = h.giteaSdk.UpdateStatus(repo, w.Revision, w.Branch.Url, w.QualityGate.Status, status) comment, err := h.composeGiteaComment(w) if err != nil { @@ -82,8 +88,11 @@ func (h *SonarQubeWebhookHandler) Handle(rw http.ResponseWriter, r *http.Request log.Printf("Received hook for project '%s'. Processing data.", projectName) + if r.Body != nil { + defer r.Body.Close() + } + raw, err := ioutil.ReadAll(r.Body) - defer r.Body.Close() if err != nil { log.Printf("Error reading request body %s", err.Error()) rw.WriteHeader(http.StatusInternalServerError) diff --git a/internal/webhook_handler/sonarqube_test.go b/internal/webhook_handler/sonarqube_test.go index 631936a..b403277 100644 --- a/internal/webhook_handler/sonarqube_test.go +++ b/internal/webhook_handler/sonarqube_test.go @@ -10,6 +10,7 @@ import ( "gitea-sonarqube-pr-bot/internal/settings" webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube" + "code.gitea.io/sdk/gitea" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -30,7 +31,11 @@ func (h *GiteaSdkMock) PostComment(_ settings.GiteaRepository, _ int, _ string) return nil } -func (h *GiteaSdkMock) UpdateStatus(_ settings.GiteaRepository, w *webhook.Webhook) error { +func (h *GiteaSdkMock) DetermineHEAD(_ settings.GiteaRepository, _ int64) (string, error) { + return "", nil +} + +func (h *GiteaSdkMock) UpdateStatus(_ settings.GiteaRepository, _ string, _ string, _ string, _ gitea.StatusState) error { return nil } diff --git a/internal/webhooks/gitea/webhook.go b/internal/webhooks/gitea/webhook.go new file mode 100644 index 0000000..addfddf --- /dev/null +++ b/internal/webhooks/gitea/webhook.go @@ -0,0 +1,96 @@ +package gitea + +import ( + "encoding/json" + "fmt" + "log" + "strings" + + giteaSdk "gitea-sonarqube-pr-bot/internal/clients/gitea_sdk" + sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk" + "gitea-sonarqube-pr-bot/internal/settings" + + "code.gitea.io/sdk/gitea" +) + +type BotAction string + +const ( + ActionReview BotAction = "/pr-bot review" +) + +type issue struct { + Number int64 `json:"number"` + Repository settings.GiteaRepository `json:"repository"` +} + +type comment struct { + Body string `json:"body"` +} + +type Webhook struct { + Action string `json:"action"` + IsPR bool `json:"is_pull"` + Issue issue `json:"issue"` + Comment comment `json:"comment"` + ConfiguredProject settings.Project +} + +func (w *Webhook) inProjectsMapping(p []settings.Project) (bool, int) { + owner := w.Issue.Repository.Owner + name := w.Issue.Repository.Name + for idx, proj := range p { + if proj.Gitea.Owner == owner && proj.Gitea.Name == name { + return true, idx + } + } + + return false, 0 +} + +func (w *Webhook) Validate() error { + if !w.IsPR { + return fmt.Errorf("ignore non-PR hook") + } + + found, pIdx := w.inProjectsMapping(settings.Projects) + if !found { + return fmt.Errorf("ignore hook for non-configured project '%s/%s'", w.Issue.Repository.Owner, w.Issue.Repository.Name) + } + + if w.Action != "created" { + return fmt.Errorf("ignore hook for action others than created") + } + + if !strings.HasPrefix(w.Comment.Body, "/pr-bot") { + return fmt.Errorf("ignore hook for non-bot action comment") + } + + w.ConfiguredProject = settings.Projects[pIdx] + + return nil +} + +func (w *Webhook) ProcessData(gSDK giteaSdk.GiteaSdkInterface, sqSDK sqSdk.SonarQubeSdkInterface) { + headRef, err := gSDK.DetermineHEAD(w.ConfiguredProject.Gitea, w.Issue.Number) + if err != nil { + log.Printf("Error retrieving HEAD ref: %s", err.Error()) + return + } + _ = gSDK.UpdateStatus(w.ConfiguredProject.Gitea, headRef, "", "Analysis pending...", gitea.StatusPending) + + log.Printf("Fetching SonarQube data...") + + _ = gSDK.UpdateStatus(w.ConfiguredProject.Gitea, headRef, "", "OK", gitea.StatusSuccess) +} + +func New(raw []byte) (*Webhook, bool) { + w := &Webhook{} + err := json.Unmarshal(raw, &w) + if err != nil { + log.Printf("Error parsing Gitea webhook: %s", err.Error()) + return w, false + } + + return w, true +}