github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/require-matching-label/require-matching-label.go (about) 1 /* 2 Copyright 2018 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 requirematchinglabel implements the `require-matching-label` plugin. 18 // This is a configurable plugin that applies a label (and possibly comments) 19 // when an issue or PR does not have any labels matching a regexp. If a label 20 // is added that matches the regexp, the 'MissingLabel' is removed and the comment 21 // is deleted. 22 package requirematchinglabel 23 24 import ( 25 "fmt" 26 "strings" 27 "time" 28 29 "k8s.io/test-infra/prow/github" 30 "k8s.io/test-infra/prow/pluginhelp" 31 "k8s.io/test-infra/prow/plugins" 32 33 "github.com/sirupsen/logrus" 34 ) 35 36 var ( 37 handlePRActions = map[github.PullRequestEventAction]bool{ 38 github.PullRequestActionOpened: true, 39 github.PullRequestActionReopened: true, 40 github.PullRequestActionLabeled: true, 41 github.PullRequestActionUnlabeled: true, 42 } 43 44 handleIssueActions = map[github.IssueEventAction]bool{ 45 github.IssueActionOpened: true, 46 github.IssueActionReopened: true, 47 github.IssueActionLabeled: true, 48 github.IssueActionUnlabeled: true, 49 } 50 ) 51 52 const ( 53 pluginName = "require-matching-label" 54 ) 55 56 type githubClient interface { 57 AddLabel(org, repo string, number int, label string) error 58 RemoveLabel(org, repo string, number int, label string) error 59 CreateComment(org, repo string, number int, content string) error 60 GetIssueLabels(org, repo string, number int) ([]github.Label, 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 plugins.RegisterPullRequestHandler(pluginName, handlePullRequest, helpProvider) 70 } 71 72 func helpProvider(config *plugins.Configuration, _ []string) (*pluginhelp.PluginHelp, error) { 73 descs := make([]string, 0, len(config.RequireMatchingLabel)) 74 for _, cfg := range config.RequireMatchingLabel { 75 descs = append(descs, cfg.Describe()) 76 } 77 // Only the 'Description' and 'Config' fields are necessary because this plugin does not react 78 // to any commands. 79 return &pluginhelp.PluginHelp{ 80 Description: `The require-matching-label plugin is a configurable plugin that applies a label to issues and/or PRs that do not have any labels matching a regular expression. An example of this is applying a 'needs-sig' label to all issues that do not have a 'sig/*' label. This plugin can have multiple configurations to provide this kind of behavior for multiple different label sets. The configuration allows issue type, PR branch, and an optional explanation comment to be specified.`, 81 Config: map[string]string{ 82 "": fmt.Sprintf("The plugin has the following configurations:\n<ul><li>%s</li></ul>", strings.Join(descs, "</li><li>")), 83 }, 84 }, 85 nil 86 } 87 88 type event struct { 89 org string 90 repo string 91 number int 92 author string 93 // The PR's base branch. If empty this is an Issue, not a PR. 94 branch string 95 // The label that was added or removed. If empty this is an open or reopen event. 96 label string 97 // The labels currently on the issue. For PRs this is not contained in the webhook payload and may be omitted. 98 currentLabels []github.Label 99 } 100 101 func handleIssue(pc plugins.Agent, ie github.IssueEvent) error { 102 if !handleIssueActions[ie.Action] { 103 return nil 104 } 105 e := &event{ 106 org: ie.Repo.Owner.Login, 107 repo: ie.Repo.Name, 108 number: ie.Issue.Number, 109 author: ie.Issue.User.Login, 110 label: ie.Label.Name, // This will be empty for non-label events. 111 currentLabels: ie.Issue.Labels, 112 } 113 cp, err := pc.CommentPruner() 114 if err != nil { 115 return err 116 } 117 return handle(pc.Logger, pc.GitHubClient, cp, pc.PluginConfig.RequireMatchingLabel, e) 118 } 119 120 func handlePullRequest(pc plugins.Agent, pre github.PullRequestEvent) error { 121 if !handlePRActions[pre.Action] { 122 return nil 123 } 124 e := &event{ 125 org: pre.Repo.Owner.Login, 126 repo: pre.Repo.Name, 127 number: pre.PullRequest.Number, 128 branch: pre.PullRequest.Base.Ref, 129 author: pre.PullRequest.User.Login, 130 label: pre.Label.Name, // This will be empty for non-label events. 131 } 132 cp, err := pc.CommentPruner() 133 if err != nil { 134 return err 135 } 136 return handle(pc.Logger, pc.GitHubClient, cp, pc.PluginConfig.RequireMatchingLabel, e) 137 } 138 139 // matchingConfigs filters irrelevant RequireMtchingLabel configs from 140 // the list of all configs. 141 // `branch` should be empty for Issues and non-empty for PRs. 142 // `label` should be omitted in the case of 'open' and 'reopen' actions. 143 func matchingConfigs(org, repo, branch, label string, allConfigs []plugins.RequireMatchingLabel) []plugins.RequireMatchingLabel { 144 var filtered []plugins.RequireMatchingLabel 145 for _, cfg := range allConfigs { 146 // Check if the config applies to this issue type. 147 if (branch == "" && !cfg.Issues) || (branch != "" && !cfg.PRs) { 148 continue 149 } 150 // Check if the config applies to this 'org[/repo][/branch]'. 151 if org != cfg.Org || 152 (cfg.Repo != "" && cfg.Repo != repo) || 153 (cfg.Branch != "" && branch != "" && cfg.Branch != branch) { 154 continue 155 } 156 // If we are reacting to a label event, see if it is relevant. 157 if label != "" && !cfg.Re.MatchString(label) { 158 continue 159 } 160 filtered = append(filtered, cfg) 161 } 162 return filtered 163 } 164 165 func handle(log *logrus.Entry, ghc githubClient, cp commentPruner, configs []plugins.RequireMatchingLabel, e *event) error { 166 // Find any configs that may be relevant to this event. 167 matchConfigs := matchingConfigs(e.org, e.repo, e.branch, e.label, configs) 168 if len(matchConfigs) == 0 { 169 return nil 170 } 171 172 if e.label == "" /* not a label event */ { 173 // If we are reacting to a PR or Issue being created or reopened, we should wait a 174 // few seconds to allow other automation to apply labels in order to minimize thrashing. 175 // We use the max grace period from applicable configs. 176 gracePeriod := time.Duration(0) 177 for _, cfg := range matchConfigs { 178 if cfg.GracePeriodDuration > gracePeriod { 179 gracePeriod = cfg.GracePeriodDuration 180 } 181 } 182 time.Sleep(gracePeriod) 183 // If currentLabels was populated it is now stale. 184 e.currentLabels = nil 185 } 186 if e.currentLabels == nil { 187 var err error 188 e.currentLabels, err = ghc.GetIssueLabels(e.org, e.repo, e.number) 189 if err != nil { 190 return fmt.Errorf("error getting the issue or pr's labels: %v", err) 191 } 192 } 193 194 // Handle the potentially relevant configs. 195 for _, cfg := range matchConfigs { 196 hasMissingLabel := false 197 hasMatchingLabel := false 198 for _, label := range e.currentLabels { 199 hasMissingLabel = hasMissingLabel || label.Name == cfg.MissingLabel 200 hasMatchingLabel = hasMatchingLabel || cfg.Re.MatchString(label.Name) 201 } 202 203 if hasMatchingLabel && hasMissingLabel { 204 if err := ghc.RemoveLabel(e.org, e.repo, e.number, cfg.MissingLabel); err != nil { 205 log.WithError(err).Errorf("Failed to remove %q label.", cfg.MissingLabel) 206 } 207 if cfg.MissingComment != "" { 208 cp.PruneComments(func(comment github.IssueComment) bool { 209 return strings.Contains(comment.Body, cfg.MissingComment) 210 }) 211 } 212 } else if !hasMatchingLabel && !hasMissingLabel { 213 if err := ghc.AddLabel(e.org, e.repo, e.number, cfg.MissingLabel); err != nil { 214 log.WithError(err).Errorf("Failed to add %q label.", cfg.MissingLabel) 215 } 216 if cfg.MissingComment != "" { 217 msg := plugins.FormatSimpleResponse(e.author, cfg.MissingComment) 218 if err := ghc.CreateComment(e.org, e.repo, e.number, msg); err != nil { 219 log.WithError(err).Error("Failed to create comment.") 220 } 221 } 222 } 223 224 } 225 return nil 226 }