github.com/abayer/test-infra@v0.0.5/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/pluginhelp" 26 "k8s.io/test-infra/prow/plugins" 27 28 "github.com/sirupsen/logrus" 29 ) 30 31 var ( 32 labelPrefixes = []string{"sig/", "committee/", "wg/"} 33 34 sigCommandRe = regexp.MustCompile(`(?m)^/sig\s*(.*)$`) 35 ) 36 37 const ( 38 pluginName = "require-sig" 39 needsSigLabel = "needs-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 } 60 61 type commentPruner interface { 62 PruneComments(shouldPrune func(github.IssueComment) bool) 63 } 64 65 func init() { 66 plugins.RegisterIssueHandler(pluginName, handleIssue, helpProvider) 67 } 68 69 func helpProvider(config *plugins.Configuration, _ []string) (*pluginhelp.PluginHelp, error) { 70 url := config.RequireSIG.GroupListURL 71 if url == "" { 72 url = "<no url provided>" 73 } 74 // Only the 'Description' and 'Config' fields are necessary because this plugin does not react 75 // to any commands. 76 return &pluginhelp.PluginHelp{ 77 Description: fmt.Sprintf( 78 `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. 79 <br>Once a SIG label has been added to an issue, this plugin removes the %q label and deletes the comment it made previously.`, 80 needsSigLabel, 81 labelPrefixes, 82 needsSigLabel, 83 ), 84 Config: map[string]string{ 85 "": fmt.Sprintf("The comment the plugin creates includes this link to a list of the existing groups: %s", url), 86 }, 87 }, 88 nil 89 } 90 91 func handleIssue(pc plugins.PluginClient, ie github.IssueEvent) error { 92 return handle(pc.Logger, pc.GitHubClient, pc.CommentPruner, &ie, pc.PluginConfig.SigMention.Re) 93 } 94 95 func isSigLabel(label string) bool { 96 for i := range labelPrefixes { 97 if strings.HasPrefix(label, labelPrefixes[i]) { 98 return true 99 } 100 } 101 return false 102 } 103 104 func hasSigLabel(labels []github.Label) bool { 105 for i := range labels { 106 if isSigLabel(labels[i].Name) { 107 return true 108 } 109 } 110 return false 111 } 112 113 func shouldReact(mentionRe *regexp.Regexp, ie *github.IssueEvent) bool { 114 // Ignore PRs and closed issues. 115 if ie.Issue.IsPullRequest() || ie.Issue.State == "closed" { 116 return false 117 } 118 119 switch ie.Action { 120 case github.IssueActionOpened: 121 // Don't react if the new issue has a /sig command or sig team mention. 122 return !mentionRe.MatchString(ie.Issue.Body) && !sigCommandRe.MatchString(ie.Issue.Body) 123 case github.IssueActionLabeled, github.IssueActionUnlabeled: 124 // Only react to (un)label events for sig labels. 125 return isSigLabel(ie.Label.Name) 126 default: 127 return false 128 } 129 } 130 131 // handle is the workhorse notifying issue owner to add a sig label if there is none 132 // The algorithm: 133 // (1) return if this is not an opened, labelled, or unlabelled event or if the issue is closed. 134 // (2) find if the issue has a sig label 135 // (3) find if the issue has a needs-sig label 136 // (4) if the issue has both the sig and needs-sig labels, remove the needs-sig label and delete the comment. 137 // (5) if the issue has none of the labels, add the needs-sig label and comment 138 // (6) if the issue has only the sig label, do nothing 139 // (7) if the issue has only the needs-sig label, do nothing 140 func handle(log *logrus.Entry, ghc githubClient, cp commentPruner, ie *github.IssueEvent, mentionRe *regexp.Regexp) error { 141 // Ignore PRs, closed issues, and events that aren't new issues or sig label 142 // changes. 143 if !shouldReact(mentionRe, ie) { 144 return nil 145 } 146 147 org := ie.Repo.Owner.Login 148 repo := ie.Repo.Name 149 number := ie.Issue.Number 150 151 hasSigLabel := hasSigLabel(ie.Issue.Labels) 152 hasNeedsSigLabel := github.HasLabel(needsSigLabel, ie.Issue.Labels) 153 154 if hasSigLabel && hasNeedsSigLabel { 155 if err := ghc.RemoveLabel(org, repo, number, needsSigLabel); err != nil { 156 log.WithError(err).Errorf("Failed to remove %s label.", needsSigLabel) 157 } 158 botName, err := ghc.BotName() 159 if err != nil { 160 return fmt.Errorf("error getting bot name: %v", err) 161 } 162 cp.PruneComments(shouldPrune(log, botName)) 163 } else if !hasSigLabel && !hasNeedsSigLabel { 164 if err := ghc.AddLabel(org, repo, number, needsSigLabel); err != nil { 165 log.WithError(err).Errorf("Failed to add %s label.", needsSigLabel) 166 } 167 msg := plugins.FormatResponse(ie.Issue.User.Login, needsSIGMessage, needsSIGDetails) 168 if err := ghc.CreateComment(org, repo, number, msg); err != nil { 169 log.WithError(err).Error("Failed to create comment.") 170 } 171 } 172 return nil 173 } 174 175 // shouldPrune finds comments left by this plugin. 176 func shouldPrune(log *logrus.Entry, botName string) func(github.IssueComment) bool { 177 return func(comment github.IssueComment) bool { 178 if comment.User.Login != botName { 179 return false 180 } 181 return strings.Contains(comment.Body, needsSIGMessage) 182 } 183 }