github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/prow/plugins/trigger/pull-request.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 trigger 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "net/url" 23 24 "k8s.io/test-infra/prow/config" 25 "k8s.io/test-infra/prow/errorutil" 26 "k8s.io/test-infra/prow/github" 27 "k8s.io/test-infra/prow/labels" 28 "k8s.io/test-infra/prow/plugins" 29 ) 30 31 func handlePR(c Client, trigger *plugins.Trigger, pr github.PullRequestEvent) error { 32 org, repo, a := orgRepoAuthor(pr.PullRequest) 33 author := string(a) 34 num := pr.PullRequest.Number 35 switch pr.Action { 36 case github.PullRequestActionOpened: 37 // When a PR is opened, if the author is in the org then build it. 38 // Otherwise, ask for "/ok-to-test". There's no need to look for previous 39 // "/ok-to-test" comments since the PR was just opened! 40 member, err := TrustedUser(c.GitHubClient, trigger, author, org, repo) 41 if err != nil { 42 return fmt.Errorf("could not check membership: %s", err) 43 } 44 if member { 45 c.Logger.Info("Starting all jobs for new PR.") 46 return buildAll(c, &pr.PullRequest, pr.GUID) 47 } 48 c.Logger.Infof("Welcome message to PR author %q.", author) 49 if err := welcomeMsg(c.GitHubClient, trigger, pr.PullRequest); err != nil { 50 return fmt.Errorf("could not welcome non-org member %q: %v", author, err) 51 } 52 case github.PullRequestActionReopened: 53 // When a PR is reopened, check that the user is in the org or that an org 54 // member had said "/ok-to-test" before building, resulting in label ok-to-test. 55 l, trusted, err := TrustedPullRequest(c.GitHubClient, trigger, author, org, repo, num, nil) 56 if err != nil { 57 return fmt.Errorf("could not validate PR: %s", err) 58 } else if trusted { 59 // Eventually remove need-ok-to-test 60 // Does not work for TrustedUser() == true since labels are not fetched in this case 61 if github.HasLabel(labels.NeedsOkToTest, l) { 62 if err := c.GitHubClient.RemoveLabel(org, repo, num, labels.NeedsOkToTest); err != nil { 63 return err 64 } 65 } 66 c.Logger.Info("Starting all jobs for updated PR.") 67 return buildAll(c, &pr.PullRequest, pr.GUID) 68 } 69 case github.PullRequestActionEdited: 70 // if someone changes the base of their PR, we will get this 71 // event and the changes field will list that the base SHA and 72 // ref changes so we can detect such a case and retrigger tests 73 var changes struct { 74 Base struct { 75 Ref struct { 76 From string `json:"from"` 77 } `json:"ref"` 78 Sha struct { 79 From string `json:"from"` 80 } `json:"sha"` 81 } `json:"base"` 82 } 83 if err := json.Unmarshal(pr.Changes, &changes); err != nil { 84 // we're detecting this best-effort so we can forget about 85 // the event 86 return nil 87 } else if changes.Base.Ref.From != "" || changes.Base.Sha.From != "" { 88 // the base of the PR changed and we need to re-test it 89 return buildAllIfTrusted(c, trigger, pr) 90 } 91 case github.PullRequestActionSynchronize: 92 return buildAllIfTrusted(c, trigger, pr) 93 case github.PullRequestActionLabeled: 94 // When a PR is LGTMd, if it is untrusted then build it once. 95 if pr.Label.Name == labels.LGTM { 96 _, trusted, err := TrustedPullRequest(c.GitHubClient, trigger, author, org, repo, num, nil) 97 if err != nil { 98 return fmt.Errorf("could not validate PR: %s", err) 99 } else if !trusted { 100 c.Logger.Info("Starting all jobs for untrusted PR with LGTM.") 101 return buildAll(c, &pr.PullRequest, pr.GUID) 102 } 103 } 104 } 105 return nil 106 } 107 108 type login string 109 110 func orgRepoAuthor(pr github.PullRequest) (string, string, login) { 111 org := pr.Base.Repo.Owner.Login 112 repo := pr.Base.Repo.Name 113 author := pr.User.Login 114 return org, repo, login(author) 115 } 116 117 func buildAllIfTrusted(c Client, trigger *plugins.Trigger, pr github.PullRequestEvent) error { 118 // When a PR is updated, check that the user is in the org or that an org 119 // member has said "/ok-to-test" before building. There's no need to ask 120 // for "/ok-to-test" because we do that once when the PR is created. 121 org, repo, a := orgRepoAuthor(pr.PullRequest) 122 author := string(a) 123 num := pr.PullRequest.Number 124 l, trusted, err := TrustedPullRequest(c.GitHubClient, trigger, author, org, repo, num, nil) 125 if err != nil { 126 return fmt.Errorf("could not validate PR: %s", err) 127 } else if trusted { 128 // Eventually remove needs-ok-to-test 129 // Will not work for org members since labels are not fetched in this case 130 if github.HasLabel(labels.NeedsOkToTest, l) { 131 if err := c.GitHubClient.RemoveLabel(org, repo, num, labels.NeedsOkToTest); err != nil { 132 return err 133 } 134 } 135 c.Logger.Info("Starting all jobs for updated PR.") 136 return buildAll(c, &pr.PullRequest, pr.GUID) 137 } 138 return nil 139 } 140 141 func welcomeMsg(ghc githubClient, trigger *plugins.Trigger, pr github.PullRequest) error { 142 var errors []error 143 org, repo, a := orgRepoAuthor(pr) 144 author := string(a) 145 encodedRepoFullName := url.QueryEscape(pr.Base.Repo.FullName) 146 var more string 147 if trigger != nil && trigger.TrustedOrg != "" && trigger.TrustedOrg != org { 148 more = fmt.Sprintf("or [%s](https://github.com/orgs/%s/people) ", trigger.TrustedOrg, trigger.TrustedOrg) 149 } 150 151 var joinOrgURL string 152 if trigger != nil && trigger.JoinOrgURL != "" { 153 joinOrgURL = trigger.JoinOrgURL 154 } else { 155 joinOrgURL = fmt.Sprintf("https://github.com/orgs/%s/people", org) 156 } 157 158 var comment string 159 if trigger.IgnoreOkToTest { 160 comment = fmt.Sprintf(`Hi @%s. Thanks for your PR. 161 162 PRs from untrusted users cannot be marked as trusted with `+"`/ok-to-test`"+` in this repo meaning untrusted PR authors can never trigger tests themselves. Collaborators can still trigger tests on the PR using `+"`/test all`"+`. 163 164 I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=%s). 165 166 <details> 167 168 %s 169 </details> 170 `, author, encodedRepoFullName, plugins.AboutThisBotWithoutCommands) 171 } else { 172 comment = fmt.Sprintf(`Hi @%s. Thanks for your PR. 173 174 I'm waiting for a [%s](https://github.com/orgs/%s/people) %smember to verify that this patch is reasonable to test. If it is, they should reply with `+"`/ok-to-test`"+` on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work. Regular contributors should [join the org](%s) to skip this step. 175 176 Once the patch is verified, the new status will be reflected by the `+"`%s`"+` label. 177 178 I understand the commands that are listed [here](https://go.k8s.io/bot-commands?repo=%s). 179 180 <details> 181 182 %s 183 </details> 184 `, author, org, org, more, joinOrgURL, labels.OkToTest, encodedRepoFullName, plugins.AboutThisBotWithoutCommands) 185 if err := ghc.AddLabel(org, repo, pr.Number, labels.NeedsOkToTest); err != nil { 186 errors = append(errors, err) 187 } 188 } 189 190 if err := ghc.CreateComment(org, repo, pr.Number, comment); err != nil { 191 errors = append(errors, err) 192 } 193 194 if len(errors) > 0 { 195 return errorutil.NewAggregate(errors...) 196 } 197 return nil 198 } 199 200 // TrustedPullRequest returns whether or not the given PR should be tested. 201 // It first checks if the author is in the org, then looks for "ok-to-test" label. 202 func TrustedPullRequest(ghc githubClient, trigger *plugins.Trigger, author, org, repo string, num int, l []github.Label) ([]github.Label, bool, error) { 203 // First check if the author is a member of the org. 204 if orgMember, err := TrustedUser(ghc, trigger, author, org, repo); err != nil { 205 return l, false, fmt.Errorf("error checking %s for trust: %v", author, err) 206 } else if orgMember { 207 return l, true, nil 208 } 209 // Then check if PR has ok-to-test label 210 if l == nil { 211 var err error 212 l, err = ghc.GetIssueLabels(org, repo, num) 213 if err != nil { 214 return l, false, err 215 } 216 } 217 if github.HasLabel(labels.OkToTest, l) { 218 return l, true, nil 219 } 220 botName, err := ghc.BotName() 221 if err != nil { 222 return l, false, fmt.Errorf("error finding bot name: %v", err) 223 } 224 // Next look for "/ok-to-test" comments on the PR. 225 comments, err := ghc.ListIssueComments(org, repo, num) 226 if err != nil { 227 return l, false, err 228 } 229 for _, comment := range comments { 230 commentAuthor := comment.User.Login 231 // Skip comments: by the PR author, or by bot, or not matching "/ok-to-test". 232 if commentAuthor == author || commentAuthor == botName || !okToTestRe.MatchString(comment.Body) { 233 continue 234 } 235 // Ensure that the commenter is in the org. 236 if commentAuthorMember, err := TrustedUser(ghc, trigger, commentAuthor, org, repo); err != nil { 237 return l, false, fmt.Errorf("error checking %s for trust: %v", commentAuthor, err) 238 } else if commentAuthorMember { 239 return l, true, nil 240 } 241 } 242 return l, false, nil 243 } 244 245 func buildAll(c Client, pr *github.PullRequest, eventGUID string) error { 246 var matchingJobs []config.Presubmit 247 for _, job := range c.Config.Presubmits[pr.Base.Repo.FullName] { 248 if job.AlwaysRun || job.RunIfChanged != "" { 249 matchingJobs = append(matchingJobs, job) 250 } 251 } 252 return RunOrSkipRequested(c, pr, matchingJobs, nil, "", eventGUID) 253 }