github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/help/help.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package help 18 19 import ( 20 "regexp" 21 "strings" 22 23 "github.com/sirupsen/logrus" 24 "k8s.io/test-infra/prow/github" 25 "k8s.io/test-infra/prow/labels" 26 "k8s.io/test-infra/prow/pluginhelp" 27 "k8s.io/test-infra/prow/plugins" 28 ) 29 30 const pluginName = "help" 31 32 var ( 33 helpRe = regexp.MustCompile(`(?mi)^/help\s*$`) 34 helpRemoveRe = regexp.MustCompile(`(?mi)^/remove-help\s*$`) 35 helpGoodFirstIssueRe = regexp.MustCompile(`(?mi)^/good-first-issue\s*$`) 36 helpGoodFirstIssueRemoveRe = regexp.MustCompile(`(?mi)^/remove-good-first-issue\s*$`) 37 helpGuidelinesURL = "https://git.k8s.io/community/contributors/devel/help-wanted.md" 38 helpMsgPruneMatch = "This request has been marked as needing help from a contributor." 39 helpMsg = ` 40 This request has been marked as needing help from a contributor. 41 42 Please ensure the request meets the requirements listed [here](` + helpGuidelinesURL + `). 43 44 If this request no longer meets these requirements, the label can be removed 45 by commenting with the ` + "`/remove-help`" + ` command. 46 ` 47 goodFirstIssueMsgPruneMatch = "This request has been marked as suitable for new contributors." 48 goodFirstIssueMsg = ` 49 This request has been marked as suitable for new contributors. 50 51 Please ensure the request meets the requirements listed [here](` + helpGuidelinesURL + "#good-first-issue" + `). 52 53 If this request no longer meets these requirements, the label can be removed 54 by commenting with the ` + "`/remove-good-first-issue`" + ` command. 55 ` 56 ) 57 58 func init() { 59 plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider) 60 } 61 62 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) { 63 // The Config field is omitted because this plugin is not configurable. 64 pluginHelp := &pluginhelp.PluginHelp{ 65 Description: "The help plugin provides commands that add or remove the '" + labels.Help + "' and the '" + labels.GoodFirstIssue + "' labels from issues.", 66 } 67 pluginHelp.AddCommand(pluginhelp.Command{ 68 Usage: "/[remove-](help|good-first-issue)", 69 Description: "Applies or removes the '" + labels.Help + "' and '" + labels.GoodFirstIssue + "' labels to an issue.", 70 Featured: false, 71 WhoCanUse: "Anyone can trigger this command on a PR.", 72 Examples: []string{"/help", "/remove-help", "/good-first-issue", "/remove-good-first-issue"}, 73 }) 74 return pluginHelp, nil 75 } 76 77 type githubClient interface { 78 BotName() (string, error) 79 CreateComment(owner, repo string, number int, comment string) error 80 AddLabel(owner, repo string, number int, label string) error 81 RemoveLabel(owner, repo string, number int, label string) error 82 GetIssueLabels(org, repo string, number int) ([]github.Label, error) 83 } 84 85 type commentPruner interface { 86 PruneComments(shouldPrune func(github.IssueComment) bool) 87 } 88 89 func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error { 90 cp, err := pc.CommentPruner() 91 if err != nil { 92 return err 93 } 94 return handle(pc.GitHubClient, pc.Logger, cp, &e) 95 } 96 97 func handle(gc githubClient, log *logrus.Entry, cp commentPruner, e *github.GenericCommentEvent) error { 98 // Only consider open issues and new comments. 99 if e.IsPR || e.IssueState != "open" || e.Action != github.GenericCommentActionCreated { 100 return nil 101 } 102 103 org := e.Repo.Owner.Login 104 repo := e.Repo.Name 105 commentAuthor := e.User.Login 106 107 // Determine if the issue has the help and the good-first-issue label 108 issueLabels, err := gc.GetIssueLabels(org, repo, e.Number) 109 if err != nil { 110 log.WithError(err).Errorf("Failed to get issue labels.") 111 } 112 hasHelp := github.HasLabel(labels.Help, issueLabels) 113 hasGoodFirstIssue := github.HasLabel(labels.GoodFirstIssue, issueLabels) 114 115 // If PR has help label and we're asking for it to be removed, remove label 116 if hasHelp && helpRemoveRe.MatchString(e.Body) { 117 if err := gc.RemoveLabel(org, repo, e.Number, labels.Help); err != nil { 118 log.WithError(err).Errorf("Github failed to remove the following label: %s", labels.Help) 119 } 120 121 botName, err := gc.BotName() 122 if err != nil { 123 log.WithError(err).Errorf("Failed to get bot name.") 124 } 125 cp.PruneComments(shouldPrune(log, botName, helpMsgPruneMatch)) 126 127 // if it has the good-first-issue label, remove it too 128 if hasGoodFirstIssue { 129 if err := gc.RemoveLabel(org, repo, e.Number, labels.GoodFirstIssue); err != nil { 130 log.WithError(err).Errorf("Github failed to remove the following label: %s", labels.GoodFirstIssue) 131 } 132 cp.PruneComments(shouldPrune(log, botName, goodFirstIssueMsgPruneMatch)) 133 } 134 135 return nil 136 } 137 138 // If PR does not have the good-first-issue label and we are asking for it to be added, 139 // add both the good-first-issue and help labels 140 if !hasGoodFirstIssue && helpGoodFirstIssueRe.MatchString(e.Body) { 141 if err := gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.IssueHTMLURL, commentAuthor, goodFirstIssueMsg)); err != nil { 142 log.WithError(err).Errorf("Failed to create comment \"%s\".", goodFirstIssueMsg) 143 } 144 145 if err := gc.AddLabel(org, repo, e.Number, labels.GoodFirstIssue); err != nil { 146 log.WithError(err).Errorf("Github failed to add the following label: %s", labels.GoodFirstIssue) 147 } 148 149 if !hasHelp { 150 if err := gc.AddLabel(org, repo, e.Number, labels.Help); err != nil { 151 log.WithError(err).Errorf("Github failed to add the following label: %s", labels.Help) 152 } 153 } 154 155 return nil 156 } 157 158 // If PR does not have the help label and we're asking it to be added, 159 // add the label 160 if !hasHelp && helpRe.MatchString(e.Body) { 161 if err := gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.IssueHTMLURL, commentAuthor, helpMsg)); err != nil { 162 log.WithError(err).Errorf("Failed to create comment \"%s\".", helpMsg) 163 } 164 if err := gc.AddLabel(org, repo, e.Number, labels.Help); err != nil { 165 log.WithError(err).Errorf("Github failed to add the following label: %s", labels.Help) 166 } 167 168 return nil 169 } 170 171 // If PR has good-first-issue label and we are asking for it to be removed, 172 // remove just the good-first-issue label 173 if hasGoodFirstIssue && helpGoodFirstIssueRemoveRe.MatchString(e.Body) { 174 if err := gc.RemoveLabel(org, repo, e.Number, labels.GoodFirstIssue); err != nil { 175 log.WithError(err).Errorf("Github failed to remove the following label: %s", labels.GoodFirstIssue) 176 } 177 178 botName, err := gc.BotName() 179 if err != nil { 180 log.WithError(err).Errorf("Failed to get bot name.") 181 } 182 cp.PruneComments(shouldPrune(log, botName, goodFirstIssueMsgPruneMatch)) 183 184 return nil 185 } 186 187 return nil 188 } 189 190 // shouldPrune finds comments left by this plugin. 191 func shouldPrune(log *logrus.Entry, botName, msgPruneMatch string) func(github.IssueComment) bool { 192 return func(comment github.IssueComment) bool { 193 if comment.User.Login != botName { 194 return false 195 } 196 return strings.Contains(comment.Body, msgPruneMatch) 197 } 198 }