diff --git a/.DEREK.yml b/.DEREK.yml index a51ff29..187ad51 100644 --- a/.DEREK.yml +++ b/.DEREK.yml @@ -5,4 +5,7 @@ curators: features: - dco_check - comments - + - merge + +mergers: + - alexellis diff --git a/commentHandler.go b/commentHandler.go index a910453..3af9720 100644 --- a/commentHandler.go +++ b/commentHandler.go @@ -30,6 +30,7 @@ const ( addLabelConstant string = "AddLabel" setMilestoneConstant string = "SetMilestone" removeMilestoneConstant string = "RemoveMilestone" + mergePRConstant string = "Merge" ) func makeClient(installation int) (*github.Client, context.Context) { @@ -98,6 +99,10 @@ func handleComment(req types.IssueCommentOuter) { feedback, err = updateMilestone(req, command.Type, command.Value) break + case mergePRConstant: + merger := merge{} + feedback, err = merger.Merge(req, command.Type, command.Value) + default: feedback = "Unable to work with comment: " + req.Comment.Body err = nil @@ -329,6 +334,7 @@ func parse(body string) *types.CommentAction { "Derek unlock": unlockConstant, "Derek set milestone: ": setMilestoneConstant, "Derek remove milestone: ": removeMilestoneConstant, + "Derek merge": mergePRConstant, } for trigger, commandType := range commands { diff --git a/derek.yml b/derek.yml index 66493f4..14e1c5a 100644 --- a/derek.yml +++ b/derek.yml @@ -5,7 +5,7 @@ provider: functions: derek: handler: ./ - image: alexellis/derek:0.5.2 + image: alexellis/derek:0.6.0 lang: dockerfile environment: debug: true @@ -13,6 +13,9 @@ functions: validate_hmac: false validate_customers: true secret_path: /var/openfaas/secrets/ # use /run/secrets/ for older OpenFaaS versions + application: 10167 + write_debug: true + combined_output: false environment_file: - secrets.yml # See secrets.example.yml diff --git a/main.go b/main.go index c7ee9a2..a15cf71 100644 --- a/main.go +++ b/main.go @@ -27,30 +27,6 @@ const ( const derekSecretKeyFile = "derek-secret-key" const privateKeyFile = "derek-private-key" -func getSecretPath() (string, error) { - secretPath := os.Getenv("secret_path") - - if len(secretPath) == 0 { - return "", fmt.Errorf("secret_path not set, this must be /var/openfaas/secrets or /run/secrets") - - } - - return secretPath, nil -} - -func hmacValidation() bool { - val := os.Getenv("validate_hmac") - return len(val) > 0 && (val == "1" || val == "true") -} - -func getFirstLine(secret []byte) []byte { - stringSecret := string(secret) - if newLine := strings.Index(stringSecret, "\n"); newLine != -1 { - secret = secret[:newLine] - } - return secret -} - func main() { bytesIn, _ := ioutil.ReadAll(os.Stdin) @@ -152,3 +128,27 @@ func handleEvent(eventType string, bytesIn []byte) error { return nil } + +func getSecretPath() (string, error) { + secretPath := os.Getenv("secret_path") + + if len(secretPath) == 0 { + return "", fmt.Errorf("secret_path not set, this must be /var/openfaas/secrets or /run/secrets") + + } + + return secretPath, nil +} + +func hmacValidation() bool { + val := os.Getenv("validate_hmac") + return len(val) > 0 && (val == "1" || val == "true") +} + +func getFirstLine(secret []byte) []byte { + stringSecret := string(secret) + if newLine := strings.Index(stringSecret, "\n"); newLine != -1 { + secret = secret[:newLine] + } + return secret +} diff --git a/merge.go b/merge.go new file mode 100644 index 0000000..0e1a213 --- /dev/null +++ b/merge.go @@ -0,0 +1,92 @@ +package main + +import ( + "context" + "fmt" + + "github.com/alexellis/derek/auth" + "github.com/alexellis/derek/types" + "github.com/google/go-github/github" +) + +type merge struct { +} + +func (m *merge) Merge(req types.IssueCommentOuter, cmdType string, cmdValue string) (string, error) { + result := "" + + if req.Issue.PullRequest == nil { + return "can't merge a non-PR issue", nil + } + + token := getAccessToken(req.Installation.ID) + client := auth.MakeClient(context.Background(), token) + pr, _, err := client.PullRequests.Get(context.Background(), req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number) + if err != nil { + return "unable to get pull request", err + } + + if pr.GetMerged() == false { + + if pr.GetMergeable() == true { + + if validMergePolicy(req) == false { + sendComment(client, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number, + "I am unable to merge this PR due to merge-policy exception(s)") + + return "invalid merge policy", nil + } + + pullRequestOptions := github.PullRequestOptions{ + MergeMethod: "rebase", + CommitTitle: fmt.Sprintf("Merge PR #%d", req.Issue.Number), + } + mergeRes, _, err := client.PullRequests.Merge(context.Background(), + req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number, + fmt.Sprintf(`Merging PR #%d by Derek +This is an automated merge by the bot Derek, find more +https://github.com/alexellis/derek/ + +Signed-off-by: derek@openfaas.com`, req.Issue.Number), &pullRequestOptions) + + if err != nil { + + body := fmt.Sprintf(`I have been unable to merge the requested PR: %s`, err.Error()) + + sendComment(client, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number, + body) + + return fmt.Sprintf("Merge issue: %s, %t", mergeRes.GetMessage(), mergeRes.GetMerged()), err + } + + sendComment(client, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number, + `I have merged the pull request using the rebase strategy.`) + } else { + sendComment(client, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number, + "This pull request cannot be merged. Rebase your work and try again.") + } + } + + return result, err +} + +func sendComment(client *github.Client, login string, repo string, issue int, comment string) { + + issueComment := &github.IssueComment{ + Body: &comment, + } + client.Issues.CreateComment(context.Background(), + login, repo, issue, issueComment) +} + +func validMergePolicy(req types.IssueCommentOuter) bool { + validDCO := true + for _, label := range req.Issue.Labels { + if label.Name == "no-dco" { + validDCO = false + break + } + } + + return validDCO +} diff --git a/pullRequestHandler.go b/pullRequestHandler.go index f15ad74..346f71b 100644 --- a/pullRequestHandler.go +++ b/pullRequestHandler.go @@ -16,16 +16,14 @@ import ( "github.com/google/go-github/github" ) -func handlePullRequest(req types.PullRequestOuter) { - ctx := context.Background() - +func getAccessToken(installationID int) string { token := os.Getenv("access_token") if len(token) == 0 { keyPath, _ := getSecretPath() newToken, tokenErr := auth.MakeAccessTokenForInstallation( os.Getenv("application"), - req.Installation.ID, + installationID, keyPath+privateKeyFile) if tokenErr != nil { @@ -34,6 +32,13 @@ func handlePullRequest(req types.PullRequestOuter) { token = newToken } + return token +} + +func handlePullRequest(req types.PullRequestOuter) { + ctx := context.Background() + + token := getAccessToken(req.Installation.ID) client := auth.MakeClient(ctx, token) diff --git a/types/types.go b/types/types.go index c721f5a..d50e045 100644 --- a/types/types.go +++ b/types/types.go @@ -38,21 +38,27 @@ type IssueCommentOuter struct { Comment Comment `json:"comment"` Action string `json:"action"` Issue Issue `json:"issue"` + InstallationRequest } +type PullRequestIssueLink struct { + URL string `json:"url"` +} + type IssueLabel struct { Name string `json:"name"` } type Issue struct { - Labels []IssueLabel `json:"labels"` - Number int `json:"number"` - Title string `json:"title"` - Locked bool `json:"locked"` - State string `json:"state"` - Milestone Milestone `json:"milestone"` - URL string `json:"url"` + Labels []IssueLabel `json:"labels"` + Number int `json:"number"` + Title string `json:"title"` + Locked bool `json:"locked"` + State string `json:"state"` + Milestone Milestone `json:"milestone"` + URL string `json:"url"` + PullRequest *PullRequestIssueLink `json:"pull_request"` } type Milestone struct {