Bot listens to actions via Gitea comments
Signed-off-by: Steven Kriegler <61625851+justusbunsi@users.noreply.github.com>
This commit is contained in:
parent
3bab05bb71
commit
57fc8054b2
|
@ -3,14 +3,15 @@ package gitea_sdk
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"gitea-sonarqube-pr-bot/internal/settings"
|
"gitea-sonarqube-pr-bot/internal/settings"
|
||||||
webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube"
|
"log"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GiteaSdkInterface interface {
|
type GiteaSdkInterface interface {
|
||||||
PostComment(settings.GiteaRepository, int, string) error
|
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 {
|
type GiteaSdk struct {
|
||||||
|
@ -27,26 +28,31 @@ func (sdk *GiteaSdk) PostComment(repo settings.GiteaRepository, idx int, msg str
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, w *webhook.Webhook) error {
|
func (sdk *GiteaSdk) UpdateStatus(repo settings.GiteaRepository, ref string, targetUrl string, description string, status gitea.StatusState) error {
|
||||||
status := gitea.StatusPending
|
|
||||||
switch w.QualityGate.Status {
|
|
||||||
case "OK":
|
|
||||||
status = gitea.StatusSuccess
|
|
||||||
case "ERROR":
|
|
||||||
status = gitea.StatusFailure
|
|
||||||
}
|
|
||||||
opt := gitea.CreateStatusOption{
|
opt := gitea.CreateStatusOption{
|
||||||
TargetURL: w.Branch.Url,
|
TargetURL: targetUrl,
|
||||||
Context: "gitea-sonarqube-pr-bot",
|
Context: "gitea-sonarqube-pr-bot",
|
||||||
Description: w.QualityGate.Status,
|
Description: description,
|
||||||
State: status,
|
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
|
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 {
|
func New() *GiteaSdk {
|
||||||
client, err := gitea.NewClient(settings.Gitea.Url, gitea.SetToken(settings.Gitea.Token.Value))
|
client, err := gitea.NewClient(settings.Gitea.Url, gitea.SetToken(settings.Gitea.Token.Value))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
60
internal/webhook_handler/gitea.go
Normal file
60
internal/webhook_handler/gitea.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
func Serve(c *cli.Context) error {
|
||||||
fmt.Println("Hi! I'm the Gitea-SonarQube-PR bot. At your service.")
|
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)
|
addPingApi(r)
|
||||||
addSonarQubeEndpoint(r)
|
addSonarQubeEndpoint(r)
|
||||||
|
addGiteaEndpoint(r)
|
||||||
|
|
||||||
return endless.ListenAndServe(":3000", r)
|
return endless.ListenAndServe(":3000", r)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ import (
|
||||||
sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk"
|
sqSdk "gitea-sonarqube-pr-bot/internal/clients/sonarqube_sdk"
|
||||||
"gitea-sonarqube-pr-bot/internal/settings"
|
"gitea-sonarqube-pr-bot/internal/settings"
|
||||||
webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube"
|
webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SonarQubeWebhookHandler struct {
|
type SonarQubeWebhookHandler struct {
|
||||||
|
@ -31,7 +33,7 @@ func (h *SonarQubeWebhookHandler) composeGiteaComment(w *webhook.Webhook) (strin
|
||||||
message[1] = m.GetRenderedMarkdownTable()
|
message[1] = m.GetRenderedMarkdownTable()
|
||||||
message[2] = fmt.Sprintf("See [SonarQube](%s) for details.", w.Branch.Url)
|
message[2] = fmt.Sprintf("See [SonarQube](%s) for details.", w.Branch.Url)
|
||||||
message[3] = "---"
|
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
|
return strings.Join(message, "\n\n"), nil
|
||||||
}
|
}
|
||||||
|
@ -54,10 +56,14 @@ func (h *SonarQubeWebhookHandler) processData(w *webhook.Webhook, repo settings.
|
||||||
|
|
||||||
h.fetchDetails(w)
|
h.fetchDetails(w)
|
||||||
|
|
||||||
err := h.giteaSdk.UpdateStatus(repo, w)
|
status := gitea.StatusPending
|
||||||
if err != nil {
|
switch w.QualityGate.Status {
|
||||||
log.Printf("Error updating status: %s", err.Error())
|
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)
|
comment, err := h.composeGiteaComment(w)
|
||||||
if err != nil {
|
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)
|
log.Printf("Received hook for project '%s'. Processing data.", projectName)
|
||||||
|
|
||||||
|
if r.Body != nil {
|
||||||
|
defer r.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
raw, err := ioutil.ReadAll(r.Body)
|
raw, err := ioutil.ReadAll(r.Body)
|
||||||
defer r.Body.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error reading request body %s", err.Error())
|
log.Printf("Error reading request body %s", err.Error())
|
||||||
rw.WriteHeader(http.StatusInternalServerError)
|
rw.WriteHeader(http.StatusInternalServerError)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"gitea-sonarqube-pr-bot/internal/settings"
|
"gitea-sonarqube-pr-bot/internal/settings"
|
||||||
webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube"
|
webhook "gitea-sonarqube-pr-bot/internal/webhooks/sonarqube"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
@ -30,7 +31,11 @@ func (h *GiteaSdkMock) PostComment(_ settings.GiteaRepository, _ int, _ string)
|
||||||
return nil
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
96
internal/webhooks/gitea/webhook.go
Normal file
96
internal/webhooks/gitea/webhook.go
Normal file
|
@ -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
|
||||||
|
}
|
Loading…
Reference in a new issue