github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/requiresig/requiresig.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 requiresig 18 19 import ( 20 "fmt" 21 "regexp" 22 "strings" 23 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 "github.com/sirupsen/logrus" 30 ) 31 32 var ( 33 labelPrefixes = []string{"sig/", "committee/", "wg/"} 34 35 sigCommandRe = regexp.MustCompile(`(?m)^/sig\s*(.*)$`) 36 ) 37 38 const ( 39 pluginName = "require-sig" 40 41 needsSIGMessage = "There are no sig labels on this issue. Please add a sig label." 42 needsSIGDetails = `A sig label can be added by either: 43 44 1. mentioning a sig: ` + "`@kubernetes/sig-<group-name>-<group-suffix>`" + ` 45 e.g., ` + "`@kubernetes/sig-contributor-experience-<group-suffix>`" + ` to notify the contributor experience sig, OR 46 47 2. specifying the label manually: ` + "`/sig <group-name>`" + ` 48 e.g., ` + "`/sig scalability`" + ` to apply the ` + "`sig/scalability`" + ` label 49 50 Note: Method 1 will trigger an email to the group. See the [group list](https://git.k8s.io/community/sig-list.md). 51 The ` + "`<group-suffix>`" + ` in method 1 has to be replaced with one of these: _**bugs, feature-requests, pr-reviews, test-failures, proposals**_` 52 ) 53 54 type githubClient interface { 55 BotName() (string, error) 56 AddLabel(org, repo string, number int, label string) error 57 RemoveLabel(org, repo string, number int, label string) error 58 CreateComment(org, repo string, number int, content string) error 59 ListIssueComments(org, repo string, number int) ([]github.IssueComment, error) 60 DeleteComment(org, repo string, id int) error 61 } 62 63 type commentPruner interface { 64 PruneComments(shouldPrune func(github.IssueComment) bool) 65 } 66 67 func init() { 68 plugins.RegisterIssueHandler(pluginName, handleIssue, helpProvider) 69 } 70 71 func helpProvider(config *plugins.Configuration, _ []string) (*pluginhelp.PluginHelp, error) { 72 url := config.RequireSIG.GroupListURL 73 if url == "" { 74 url = "<no url provided>" 75 } 76 // Only the 'Description' and 'Config' fields are necessary because this plugin does not react 77 // to any commands. 78 return &pluginhelp.PluginHelp{ 79 Description: fmt.Sprintf( 80 `When a new issue is opened the require-sig plugin adds the %q label and leaves a comment requesting that a SIG (Special Interest Group) label be added to the issue. SIG labels are labels that have one of the following prefixes: %q. 81 <br>Once a SIG label has been added to an issue, this plugin removes the %q label and deletes the comment it made previously.`, 82 labels.NeedsSig, 83 labelPrefixes, 84 labels.NeedsSig, 85 ), 86 Config: map[string]string{ 87 "": fmt.Sprintf("The comment the plugin creates includes this link to a list of the existing groups: %s", url), 88 }, 89 }, 90 nil 91 } 92 93 func handleIssue(pc plugins.Agent, ie github.IssueEvent) error { 94 cp, err := pc.CommentPruner() 95 if err != nil { 96 return err 97 } 98 return handle(pc.Logger, pc.GitHubClient, cp, &ie, pc.PluginConfig.SigMention.Re) 99 } 100 101 func isSigLabel(label string) bool { 102 for i := range labelPrefixes { 103 if strings.HasPrefix(label, labelPrefixes[i]) { 104 return true 105 } 106 } 107 return false 108 } 109 110 func hasSigLabel(labels []github.Label) bool { 111 for i := range labels { 112 if isSigLabel(labels[i].Name) { 113 return true 114 } 115 } 116 return false 117 } 118 119 func shouldReact(mentionRe *regexp.Regexp, ie *github.IssueEvent) bool { 120 // Ignore PRs and closed issues. 121 if ie.Issue.IsPullRequest() || ie.Issue.State == "closed" { 122 return false 123 } 124 125 switch ie.Action { 126 case github.IssueActionOpened: 127 // Don't react if the new issue has a /sig command or sig team mention. 128 return !mentionRe.MatchString(ie.Issue.Body) && !sigCommandRe.MatchString(ie.Issue.Body) 129 case github.IssueActionLabeled, github.IssueActionUnlabeled: 130 // Only react to (un)label events for sig labels. 131 return isSigLabel(ie.Label.Name) 132 default: 133 return false 134 } 135 } 136 137 // handle is the workhorse notifying issue owner to add a sig label if there is none 138 // The algorithm: 139 // (1) return if this is not an opened, labelled, or unlabelled event or if the issue is closed. 140 // (2) find if the issue has a sig label 141 // (3) find if the issue has a needs-sig label 142 // (4) if the issue has both the sig and needs-sig labels, remove the needs-sig label and delete the comment. 143 // (5) if the issue has none of the labels, add the needs-sig label and comment 144 // (6) if the issue has only the sig label, do nothing 145 // (7) if the issue has only the needs-sig label, do nothing 146 func handle(log *logrus.Entry, ghc githubClient, cp commentPruner, ie *github.IssueEvent, mentionRe *regexp.Regexp) error { 147 // Ignore PRs, closed issues, and events that aren't new issues or sig label 148 // changes. 149 if !shouldReact(mentionRe, ie) { 150 return nil 151 } 152 153 org := ie.Repo.Owner.Login 154 repo := ie.Repo.Name 155 number := ie.Issue.Number 156 157 hasSigLabel := hasSigLabel(ie.Issue.Labels) 158 hasNeedsSigLabel := github.HasLabel(labels.NeedsSig, ie.Issue.Labels) 159 160 if hasSigLabel && hasNeedsSigLabel { 161 if err := ghc.RemoveLabel(org, repo, number, labels.NeedsSig); err != nil { 162 log.WithError(err).Errorf("Failed to remove %s label.", labels.NeedsSig) 163 } 164 botName, err := ghc.BotName() 165 if err != nil { 166 return fmt.Errorf("error getting bot name: %v", err) 167 } 168 cp.PruneComments(shouldPrune(log, botName)) 169 } else if !hasSigLabel && !hasNeedsSigLabel { 170 if err := ghc.AddLabel(org, repo, number, labels.NeedsSig); err != nil { 171 log.WithError(err).Errorf("Failed to add %s label.", labels.NeedsSig) 172 } 173 msg := plugins.FormatResponse(ie.Issue.User.Login, needsSIGMessage, needsSIGDetails) 174 if err := ghc.CreateComment(org, repo, number, msg); err != nil { 175 log.WithError(err).Error("Failed to create comment.") 176 } 177 } 178 return nil 179 } 180 181 // shouldPrune finds comments left by this plugin. 182 func shouldPrune(log *logrus.Entry, botName string) func(github.IssueComment) bool { 183 return func(comment github.IssueComment) bool { 184 if comment.User.Login != botName { 185 return false 186 } 187 return strings.Contains(comment.Body, needsSIGMessage) 188 } 189 }