github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/cla/cla.go (about) 1 /* 2 Copyright 2016 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 cla 18 19 import ( 20 "fmt" 21 "time" 22 23 "github.com/sirupsen/logrus" 24 25 "regexp" 26 27 "k8s.io/test-infra/prow/github" 28 "k8s.io/test-infra/prow/labels" 29 "k8s.io/test-infra/prow/pluginhelp" 30 "k8s.io/test-infra/prow/plugins" 31 ) 32 33 const ( 34 pluginName = "cla" 35 claContextName = "cla/linuxfoundation" 36 cncfclaNotFoundMessage = `Thanks for your pull request. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). 37 38 :memo: **Please follow instructions at <https://git.k8s.io/community/CLA.md#the-contributor-license-agreement> to sign the CLA.** 39 40 It may take a couple minutes for the CLA signature to be fully registered; after that, please reply here with a new comment and we'll verify. Thanks. 41 42 --- 43 44 - If you've already signed a CLA, it's possible we don't have your GitHub username or you're using a different email address. Check your existing CLA data and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/). 45 - If you signed the CLA as a corporation, please sign in with your organization's credentials at <https://identity.linuxfoundation.org/projects/cncf> to be authorized. 46 - If you have done the above and are still having issues with the CLA being reported as unsigned, please email the CNCF helpdesk: helpdesk@rt.linuxfoundation.org 47 48 <!-- need_sender_cla --> 49 50 <details> 51 52 %s 53 </details> 54 ` 55 maxRetries = 5 56 ) 57 58 var ( 59 checkCLARe = regexp.MustCompile(`(?mi)^/check-cla\s*$`) 60 ) 61 62 func init() { 63 plugins.RegisterStatusEventHandler(pluginName, handleStatusEvent, helpProvider) 64 plugins.RegisterGenericCommentHandler(pluginName, handleCommentEvent, helpProvider) 65 } 66 67 func helpProvider(config *plugins.Configuration, enabledRepos []string) (*pluginhelp.PluginHelp, error) { 68 // The {WhoCanUse, Usage, Examples, Config} fields are omitted because this plugin cannot be 69 // manually triggered and is not configurable. 70 pluginHelp := &pluginhelp.PluginHelp{ 71 Description: "The cla plugin manages the application and removal of the 'cncf-cla' prefixed labels on pull requests as a reaction to the " + claContextName + " github status context. It is also responsible for warning unauthorized PR authors that they need to sign the CNCF CLA before their PR will be merged.", 72 } 73 pluginHelp.AddCommand(pluginhelp.Command{ 74 Usage: "/check-cla", 75 Description: "Forces rechecking of the CLA status.", 76 Featured: true, 77 WhoCanUse: "Anyone", 78 Examples: []string{"/check-cla"}, 79 }) 80 return pluginHelp, nil 81 } 82 83 type gitHubClient interface { 84 CreateComment(owner, repo string, number int, comment string) error 85 AddLabel(owner, repo string, number int, label string) error 86 RemoveLabel(owner, repo string, number int, label string) error 87 GetPullRequest(owner, repo string, number int) (*github.PullRequest, error) 88 FindIssues(query, sort string, asc bool) ([]github.Issue, error) 89 GetIssueLabels(org, repo string, number int) ([]github.Label, error) 90 ListStatuses(org, repo, ref string) ([]github.Status, error) 91 } 92 93 func handleStatusEvent(pc plugins.Agent, se github.StatusEvent) error { 94 return handle(pc.GitHubClient, pc.Logger, se) 95 } 96 97 // 1. Check that the status event received from the webhook is for the CNCF-CLA. 98 // 2. Use the github search API to search for the PRs which match the commit hash corresponding to the status event. 99 // 3. For each issue that matches, check that the PR's HEAD commit hash against the commit hash for which the status 100 // was received. This is because we only care about the status associated with the last (latest) commit in a PR. 101 // 4. Set the corresponding CLA label if needed. 102 func handle(gc gitHubClient, log *logrus.Entry, se github.StatusEvent) error { 103 if se.State == "" || se.Context == "" { 104 return fmt.Errorf("invalid status event delivered with empty state/context") 105 } 106 107 if se.Context != claContextName { 108 // Not the CNCF CLA context, do not process this. 109 return nil 110 } 111 112 if se.State == github.StatusPending { 113 // do nothing and wait for state to be updated. 114 return nil 115 } 116 117 org := se.Repo.Owner.Login 118 repo := se.Repo.Name 119 log.Info("Searching for PRs matching the commit.") 120 121 var issues []github.Issue 122 var err error 123 for i := 0; i < maxRetries; i++ { 124 issues, err = gc.FindIssues(fmt.Sprintf("%s repo:%s/%s type:pr state:open", se.SHA, org, repo), "", false) 125 if err != nil { 126 return fmt.Errorf("error searching for issues matching commit: %v", err) 127 } 128 if len(issues) > 0 { 129 break 130 } 131 time.Sleep(10 * time.Second) 132 } 133 log.Infof("Found %d PRs matching commit.", len(issues)) 134 135 for _, issue := range issues { 136 l := log.WithField("pr", issue.Number) 137 hasCncfYes := issue.HasLabel(labels.ClaYes) 138 hasCncfNo := issue.HasLabel(labels.ClaNo) 139 if hasCncfYes && se.State == github.StatusSuccess { 140 // Nothing to update. 141 l.Infof("PR has up-to-date %s label.", labels.ClaYes) 142 continue 143 } 144 145 if hasCncfNo && (se.State == github.StatusFailure || se.State == github.StatusError) { 146 // Nothing to update. 147 l.Infof("PR has up-to-date %s label.", labels.ClaNo) 148 continue 149 } 150 151 l.Info("PR labels may be out of date. Getting pull request info.") 152 pr, err := gc.GetPullRequest(org, repo, issue.Number) 153 if err != nil { 154 l.WithError(err).Warningf("Unable to fetch PR-%d from %s/%s.", issue.Number, org, repo) 155 continue 156 } 157 158 // Check if this is the latest commit in the PR. 159 if pr.Head.SHA != se.SHA { 160 l.Info("Event is not for PR HEAD, skipping.") 161 continue 162 } 163 164 number := pr.Number 165 if se.State == github.StatusSuccess { 166 if hasCncfNo { 167 if err := gc.RemoveLabel(org, repo, number, labels.ClaNo); err != nil { 168 l.WithError(err).Warningf("Could not remove %s label.", labels.ClaNo) 169 } 170 } 171 if err := gc.AddLabel(org, repo, number, labels.ClaYes); err != nil { 172 l.WithError(err).Warningf("Could not add %s label.", labels.ClaYes) 173 } 174 continue 175 } 176 177 // If we end up here, the status is a failure/error. 178 if hasCncfYes { 179 if err := gc.RemoveLabel(org, repo, number, labels.ClaYes); err != nil { 180 l.WithError(err).Warningf("Could not remove %s label.", labels.ClaYes) 181 } 182 } 183 if err := gc.CreateComment(org, repo, number, fmt.Sprintf(cncfclaNotFoundMessage, plugins.AboutThisBot)); err != nil { 184 l.WithError(err).Warning("Could not create CLA not found comment.") 185 } 186 if err := gc.AddLabel(org, repo, number, labels.ClaNo); err != nil { 187 l.WithError(err).Warningf("Could not add %s label.", labels.ClaNo) 188 } 189 } 190 return nil 191 } 192 193 func handleCommentEvent(pc plugins.Agent, ce github.GenericCommentEvent) error { 194 return handleComment(pc.GitHubClient, pc.Logger, &ce) 195 } 196 197 func handleComment(gc gitHubClient, log *logrus.Entry, e *github.GenericCommentEvent) error { 198 // Only consider open PRs and new comments. 199 if e.IssueState != "open" || e.Action != github.GenericCommentActionCreated { 200 return nil 201 } 202 // Only consider "/check-cla" comments. 203 if !checkCLARe.MatchString(e.Body) { 204 return nil 205 } 206 207 org := e.Repo.Owner.Login 208 repo := e.Repo.Name 209 number := e.Number 210 hasCLAYes := false 211 hasCLANo := false 212 213 // Check for existing cla labels. 214 issueLabels, err := gc.GetIssueLabels(org, repo, number) 215 if err != nil { 216 log.WithError(err).Errorf("Failed to get the labels on %s/%s#%d.", org, repo, number) 217 } 218 for _, candidate := range issueLabels { 219 if candidate.Name == labels.ClaYes { 220 hasCLAYes = true 221 } 222 // Could theoretically have both yes/no labels. 223 if candidate.Name == labels.ClaNo { 224 hasCLANo = true 225 } 226 } 227 228 pr, err := gc.GetPullRequest(org, repo, e.Number) 229 if err != nil { 230 log.WithError(err).Errorf("Unable to fetch PR-%d from %s/%s.", e.Number, org, repo) 231 } 232 233 // Check for the cla in past commit statuses, and add/remove corresponding cla label if necessary. 234 ref := pr.Head.SHA 235 statuses, err := gc.ListStatuses(org, repo, ref) 236 if err != nil { 237 log.WithError(err).Errorf("Failed to get statuses on %s/%s#%d", org, repo, number) 238 } 239 240 for _, status := range statuses { 241 242 // Only consider "cla/linuxfoundation" status. 243 if status.Context == claContextName { 244 245 // Success state implies that the cla exists, so label should be cncf-cla:yes. 246 if status.State == github.StatusSuccess { 247 248 // Remove cncf-cla:no (if label exists). 249 if hasCLANo { 250 if err := gc.RemoveLabel(org, repo, number, labels.ClaNo); err != nil { 251 log.WithError(err).Warningf("Could not remove %s label.", labels.ClaNo) 252 } 253 } 254 255 // Add cncf-cla:yes (if label doesn't exist). 256 if !hasCLAYes { 257 if err := gc.AddLabel(org, repo, number, labels.ClaYes); err != nil { 258 log.WithError(err).Warningf("Could not add %s label.", labels.ClaYes) 259 } 260 } 261 262 // Failure state implies that the cla does not exist, so label should be cncf-cla:no. 263 } else if status.State == github.StatusFailure { 264 265 // Remove cncf-cla:yes (if label exists). 266 if hasCLAYes { 267 if err := gc.RemoveLabel(org, repo, number, labels.ClaYes); err != nil { 268 log.WithError(err).Warningf("Could not remove %s label.", labels.ClaYes) 269 } 270 } 271 272 // Add cncf-cla:no (if label doesn't exist). 273 if !hasCLANo { 274 if err := gc.AddLabel(org, repo, number, labels.ClaNo); err != nil { 275 log.WithError(err).Warningf("Could not add %s label.", labels.ClaNo) 276 } 277 } 278 } 279 280 // Only consider the latest relevant status. 281 break 282 } 283 } 284 return nil 285 }