From 5ce138d47765c969e4ecfcbacff3ea4ee04be5ad Mon Sep 17 00:00:00 2001 From: Zero King Date: Sun, 16 Jul 2017 08:10:47 +0000 Subject: [PATCH] Add PR bot --- pr/{ => db}/dbutil.go | 24 +++++++++- pr/githubapi/pull_request.go | 72 +++++++++++++++++++++++++++-- pr/prbot/main.go | 10 ++-- pr/webhook/pull_request.go | 90 +++++++++++++++++++++++++++++++++++- pr/webhook/server.go | 17 ++++--- 5 files changed, 195 insertions(+), 18 deletions(-) rename pr/{ => db}/dbutil.go (82%) diff --git a/pr/dbutil.go b/pr/db/dbutil.go similarity index 82% rename from pr/dbutil.go rename to pr/db/dbutil.go index 749a27c..d03fa06 100644 --- a/pr/dbutil.go +++ b/pr/db/dbutil.go @@ -1,10 +1,11 @@ -package pr +package db import ( "database/sql" "log" "strings" + "errors" _ "github.com/lib/pq" ) @@ -16,8 +17,8 @@ type Maintainer struct { type PortMaintainer struct { Primary *Maintainer Others []*Maintainer - OpenMaintainer bool NoMaintainer bool + OpenMaintainer bool } var tracDB *sql.DB @@ -66,11 +67,21 @@ func GetMaintainer(port string) (*PortMaintainer, error) { maintainer := new(PortMaintainer) maintainerCursor := "" isPrimary := false + rowExist := false for rows.Next() { if err := rows.Scan(&maintainerCursor, &isPrimary); err != nil { return nil, err } + rowExist = true + switch maintainerCursor { + case "nomaintainer": + maintainer.NoMaintainer = true + continue + case "openmaintainer": + maintainer.OpenMaintainer = true + continue + } if isPrimary { maintainer.Primary = parseMaintainer(maintainerCursor) } else { @@ -78,6 +89,10 @@ func GetMaintainer(port string) (*PortMaintainer, error) { } } + if !rowExist { + return nil, errors.New("port not found") + } + return maintainer, nil } @@ -94,5 +109,10 @@ func parseMaintainer(maintainerFullString string) *Maintainer { maintainer.Email = maintainerString + "@macports.org" } } + if maintainer.GithubHandle == "" && maintainer.Email != "" { + if handle, err := GetGitHubHandle(maintainer.Email); err == nil { + maintainer.GithubHandle = handle + } + } return maintainer } diff --git a/pr/githubapi/pull_request.go b/pr/githubapi/pull_request.go index c984131..9f439ea 100644 --- a/pr/githubapi/pull_request.go +++ b/pr/githubapi/pull_request.go @@ -2,15 +2,37 @@ package githubapi import ( "context" - "github.com/google/go-github/github" "regexp" + + "github.com/google/go-github/github" + "golang.org/x/oauth2" ) -func ListChangedPorts(number int) []string { - client := github.NewClient(nil) +type Client struct { + *github.Client + ctx context.Context + owner, repo string +} + +func NewClient(botSecret, owner, repo string) *Client { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: botSecret}, + ) + tc := oauth2.NewClient(ctx, ts) + + return &Client{ + Client: github.NewClient(tc), + ctx: ctx, + owner: owner, + repo: repo, + } +} + +func (client *Client) ListChangedPorts(number int) ([]string, error) { files, _, err := client.PullRequests.ListFiles(context.Background(), "macports-staging", "macports-ports", number, nil) if err != nil { - return nil + return nil, err } ports := make([]string, 0, 1) portfileRegexp := regexp.MustCompile(`[^\._/][^/]*/([^/]+)/Portfile`) @@ -19,5 +41,45 @@ func ListChangedPorts(number int) []string { ports = append(ports, match[1]) } } - return ports + return ports, nil +} + +func (client *Client) CreateComment(number int, body *string) error { + _, _, err := client.Issues.CreateComment( + client.ctx, + client.owner, + client.repo, + number, + &github.IssueComment{Body: body}, + ) + return err +} + +func (client *Client) ReplaceLabels(number int, labels []string) error { + _, _, err := client.Issues.ReplaceLabelsForIssue( + client.ctx, + client.owner, + client.repo, + number, + labels, + ) + return err +} + +func (client *Client) ListLabels(number int) ([]string, error) { + labels, _, err := client.Issues.ListLabelsByIssue( + client.ctx, + client.owner, + client.repo, + number, + nil, + ) + if err != nil { + return nil, err + } + labelNames := make([]string, 0, 1) + for _, label := range labels { + labelNames = append(labelNames, *label.Name) + } + return labelNames, nil } diff --git a/pr/prbot/main.go b/pr/prbot/main.go index 711d49c..57f14d9 100644 --- a/pr/prbot/main.go +++ b/pr/prbot/main.go @@ -12,10 +12,14 @@ import ( func main() { webhookAddr := flag.String("l", "localhost:8081", "listen address for webhook events") flag.Parse() - hubSecret := []byte(os.Getenv("HUB_WEBHOOK_SECRET")) - if len(hubSecret) == 0 { + hookSecret := []byte(os.Getenv("HUB_WEBHOOK_SECRET")) + if len(hookSecret) == 0 { log.Fatal("HUB_WEBHOOK_SECRET not found") } + botSecret := os.Getenv("HUB_BOT_SECRET") + if botSecret == "" { + log.Fatal("HUB_BOT_SECRET not found") + } - webhook.NewReceiver(*webhookAddr, hubSecret).Start() + webhook.NewReceiver(*webhookAddr, hookSecret, botSecret).Start() } diff --git a/pr/webhook/pull_request.go b/pr/webhook/pull_request.go index fbca80b..495cda8 100644 --- a/pr/webhook/pull_request.go +++ b/pr/webhook/pull_request.go @@ -1,6 +1,92 @@ package webhook -func handlePullRequest(body []byte) { - // Use |= for openmaintainer/nomaintainer (init 1) flag, nomaintainer take precedence +import ( + "encoding/json" + "strings" + + "github.com/google/go-github/github" + "github.com/macports/mpbot-github/pr/db" + "log" + "strconv" +) + +func (receiver *Receiver) handlePullRequest(body []byte) { + // Use &&= for isOpenmaintainer/isNomaintainer (init true) flag, isNomaintainer take precedence // Loop over ports and aggregate related maintainers + // Use @_handle for now + + event := &github.PullRequestEvent{} + err := json.Unmarshal(body, event) + if err != nil { + // TODO: log + return + } + number := *event.Number + isOpenmaintainer := true + isNomaintainer := true + isMaintainer := false + ports, err := receiver.githubClient.ListChangedPorts(number) + if err != nil { + return + } + handles := make([]string, 0, 1) + for _, port := range ports { + maintainer, err := db.GetMaintainer(port) + if err != nil { + continue + } + isNomaintainer = isNomaintainer && maintainer.NoMaintainer + isOpenmaintainer = isOpenmaintainer && (maintainer.OpenMaintainer || maintainer.NoMaintainer) + if maintainer.NoMaintainer { + continue + } + if maintainer.Primary.GithubHandle != "" { + handles = append(handles, maintainer.Primary.GithubHandle) + if maintainer.Primary.GithubHandle == *event.Sender.Login { + // TODO: should be set only when the sender is maintainer of all modified ports + isMaintainer = true + } + } + } + + switch *event.Action { + case "opened": + // Notify maintainers + if len(handles) > 0 { + body := "Notifying maintainers: @_" + strings.Join(handles, " @_") + err = receiver.githubClient.CreateComment(number, &body) + if err != nil { + log.Println(err) + } + } + fallthrough + case "synchronize": + // Modify labels + labels, err := receiver.githubClient.ListLabels(number) + newLabels := make([]string, len(labels)) + copy(newLabels, labels) + if err != nil { + return + } + maintainerLabels := make([]string, 0) + if isMaintainer { + maintainerLabels = append(maintainerLabels, "maintainer") + } + if isNomaintainer { + maintainerLabels = append(maintainerLabels, "maintainer: none") + } else if isOpenmaintainer { + maintainerLabels = append(maintainerLabels, "maintainer: open") + } + for _, label := range labels { + if !strings.HasPrefix(label, "maintainer") { + newLabels = append(newLabels, label) + } + } + newLabels = append(newLabels, maintainerLabels...) + err = receiver.githubClient.ReplaceLabels(number, newLabels) + if err != nil { + log.Println(err) + } + } + log.Println("PR #" + strconv.Itoa(number) + " processed") } diff --git a/pr/webhook/server.go b/pr/webhook/server.go index 773645a..9b467ab 100644 --- a/pr/webhook/server.go +++ b/pr/webhook/server.go @@ -7,17 +7,22 @@ import ( "io/ioutil" "net/http" "strings" + + "github.com/macports/mpbot-github/pr/githubapi" ) type Receiver struct { - listenAddr string - hubSecret []byte + listenAddr string + hookSecret []byte + githubClient *githubapi.Client } -func NewReceiver(listenAddr string, hubSecret []byte) *Receiver { +func NewReceiver(listenAddr string, hookSecret []byte, botSecret string) *Receiver { return &Receiver{ listenAddr: listenAddr, - hubSecret: hubSecret, + hookSecret: hookSecret, + // TODO: canonical owner + githubClient: githubapi.NewClient(botSecret, "macports-staging", "macports-ports"), } } @@ -53,7 +58,7 @@ func (receiver *Receiver) Start() { w.WriteHeader(http.StatusBadRequest) return case "pull_request": - go handlePullRequest(body) + go receiver.handlePullRequest(body) } w.WriteHeader(http.StatusNoContent) @@ -64,7 +69,7 @@ func (receiver *Receiver) Start() { // checkMAC reports whether messageMAC is a valid HMAC tag for message. func (receiver *Receiver) checkMAC(message, messageMAC []byte) bool { - mac := hmac.New(sha1.New, receiver.hubSecret) + mac := hmac.New(sha1.New, receiver.hookSecret) mac.Write(message) expectedMAC := mac.Sum(nil) return hmac.Equal(messageMAC, expectedMAC)