diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 76640cd..b047986 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -165,6 +165,33 @@ Derek unlock > Note: once locked no further comments are allowed apart from users with admin access. +#### Add predefined message + +Have Derek add pre-configured comments to a PR or Issue thread, for example when you would like to direct someone towards the contributing guide. + +Configure the feature in `.DEREK.yml` file. It should look something like: + +``` +custom_messages: + - name: docs + value: Hello, please check out the docs ... + - name: slack + value: | + -- + To join our slack channel ... +``` + +Above are two examples which shows simple configuration, the first one is the method for single line messages, the second one is more specific multi line literal, which should be exactly below the `|` sign in order to be displayed and not having errors while parsing. + +Tell derek to send the message: + +``` +Derek message: docs +``` +``` +Derek msg: slack +``` + ### Notes on usage #### Editing the .DEREK.yml file diff --git a/handler/commentHandler.go b/handler/commentHandler.go index 6308618..ca630a3 100644 --- a/handler/commentHandler.go +++ b/handler/commentHandler.go @@ -35,6 +35,7 @@ const ( removeMilestoneConstant string = "RemoveMilestone" assignReviewerConstant string = "AssignReviewer" unassignReviewerConstant string = "UnassignReviewer" + messageConstant string = "message" noDCO string = "no-dco" labelLimitDefault int = 5 @@ -61,7 +62,7 @@ func makeClient(installation int, config config.Config) (*github.Client, context } // HandleComment handles a comment -func HandleComment(req types.IssueCommentOuter, config config.Config) { +func HandleComment(req types.IssueCommentOuter, config config.Config, derekConfig *types.DerekRepoConfig) { var feedback string var err error @@ -114,6 +115,11 @@ func HandleComment(req types.IssueCommentOuter, config config.Config) { feedback, err = editReviewers(prReq, command.Type, command.Value, config) break + case messageConstant: + + feedback, err = createMessage(req, command.Type, command.Value, config, derekConfig) + break + default: feedback = "Unable to work with comment: " + req.Comment.Body err = nil @@ -426,6 +432,8 @@ func parse(body string, commandTriggers []string) *types.CommentAction { commandTrigger + "remove milestone: ": removeMilestoneConstant, commandTrigger + "set reviewer: ": assignReviewerConstant, commandTrigger + "clear reviewer: ": unassignReviewerConstant, + commandTrigger + "message: ": messageConstant, + commandTrigger + "msg: ": messageConstant, } for trigger, commandType := range commands { @@ -506,3 +514,41 @@ func getMultiLabelLimit() int { } return labelLimitDefault } + +func createMessage(req types.IssueCommentOuter, cmdType, cmdValue string, config config.Config, derekConfig *types.DerekRepoConfig) (string, error) { + var err error + + var buffer bytes.Buffer + + buffer.WriteString(fmt.Sprintf("%s wants to add message of type '%s' on issue #%d \n", req.Comment.User.Login, cmdValue, req.Issue.Number)) + + messageValue, err := createIssueComment(derekConfig.Messages, cmdValue) + if err != nil { + return buffer.String(), fmt.Errorf("Error while filtering message: %s", err.Error()) + } + + buffer.WriteString(fmt.Sprintf("Message '%s' with content: \n%s\nfound.\n", cmdValue, *messageValue.Body)) + + client, ctx := makeClient(req.Installation.ID, config) + + comment, resp, err := client.Issues.CreateComment(ctx, req.Repository.Owner.Login, req.Repository.Name, req.Issue.Number, messageValue) + if err != nil { + return buffer.String(), err + } + buffer.WriteString(fmt.Sprintf("Successfully applied comment: \n%s\nstatus code: %d", + *comment.Body, + resp.StatusCode)) + + return buffer.String(), nil +} + +func createIssueComment(messages []types.Message, wantedMessage string) (*github.IssueComment, error) { + for _, message := range messages { + if message.Name == wantedMessage { + return &github.IssueComment{ + Body: &message.Value, + }, nil + } + } + return nil, fmt.Errorf("Message: `%s` is not configured", wantedMessage) +} diff --git a/handler/commentHandler_test.go b/handler/commentHandler_test.go index f49e346..8c8707e 100644 --- a/handler/commentHandler_test.go +++ b/handler/commentHandler_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/alexellis/derek/types" + "github.com/google/go-github/github" ) func Test_getCommandTrigger(t *testing.T) { @@ -803,5 +804,54 @@ func Test_getCommandValue(t *testing.T) { } }) } +} +func Test_createIssueComment(t *testing.T) { + desiredBody := "The content of the message with slack info" + tests := []struct { + title string + message []types.Message + wantedMessage string + wantedErr bool + desiredGitHubBody *github.IssueComment + }{ + { + title: "Desired message is found.", + message: []types.Message{ + {Name: "slack", Value: "The content of the message with slack info"}, + {Name: "docs", Value: "The content of the message with docs info"}, + }, + wantedMessage: "slack", + wantedErr: false, + desiredGitHubBody: &github.IssueComment{ + Body: &desiredBody, + }, + }, + { + title: "User tried to set non existing message", + message: []types.Message{ + {Name: "slack", Value: "The content of the message with slack info"}, + {Name: "docs", Value: "The content of the message with docs info"}, + }, + wantedMessage: "dco", + wantedErr: true, + desiredGitHubBody: nil, + }, + } + for _, test := range tests { + t.Run(test.title, func(t *testing.T) { + gitHubComment, err := createIssueComment(test.message, test.wantedMessage) + if gitHubComment != nil && test.desiredGitHubBody != nil { + if *gitHubComment.Body != *test.desiredGitHubBody.Body { + t.Errorf("Expected body to contain: %s got :%s", + *test.desiredGitHubBody.Body, + *gitHubComment.Body) + } + } + if err != nil && test.wantedErr == false { + t.Errorf("Unexpected error: %s", + err.Error()) + } + }) + } } diff --git a/main.go b/main.go index 0a4d304..80022b1 100644 --- a/main.go +++ b/main.go @@ -83,7 +83,10 @@ func handleEvent(eventType string, bytesIn []byte, config config.Config) error { derekConfig, err = handler.GetRepoConfig(req.Repository.Owner.Login, req.Repository.Name) } if err != nil { - return fmt.Errorf("Unable to access maintainers file at: %s/%s", req.Repository.Owner.Login, req.Repository.Name) + return fmt.Errorf("Unable to access maintainers file at: %s/%s\nError: %s", + req.Repository.Owner.Login, + req.Repository.Name, + err.Error()) } if req.Action != handler.ClosedConstant { contributingURL := getContributingURL(derekConfig.ContributingURL, req.Repository.Owner.Login, req.Repository.Name) @@ -116,12 +119,15 @@ func handleEvent(eventType string, bytesIn []byte, config config.Config) error { derekConfig, err = handler.GetRepoConfig(req.Repository.Owner.Login, req.Repository.Name) } if err != nil { - return fmt.Errorf("Unable to access maintainers file at: %s/%s", req.Repository.Owner.Login, req.Repository.Name) + return fmt.Errorf("Unable to access maintainers file at: %s/%s\nError: %s", + req.Repository.Owner.Login, + req.Repository.Name, + err.Error()) } if req.Action != deleted { if handler.PermittedUserFeature(comments, derekConfig, req.Comment.User.Login) { - handler.HandleComment(req, config) + handler.HandleComment(req, config, derekConfig) } } break diff --git a/types/types.go b/types/types.go index 3e88f0c..19ccf1b 100644 --- a/types/types.go +++ b/types/types.go @@ -91,6 +91,13 @@ type DerekRepoConfig struct { //ContributingURL url to contribution guide ContributingURL string `yaml:"contributing_url"` + + Messages []Message `yaml:"custom_messages"` +} + +type Message struct { + Name string `yaml:"name"` + Value string `yaml:"value"` } // FirstTimeContributor whether the contributor is new to the repo