github.com/abayer/test-infra@v0.0.5/prow/plugins/trigger/pr.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 "strings" 23 24 "k8s.io/test-infra/prow/config" 25 "k8s.io/test-infra/prow/github" 26 "k8s.io/test-infra/prow/plugins" 27 ) 28 29 const ( 30 needsOkToTest = "needs-ok-to-test" 31 ) 32 33 func handlePR(c client, trigger *plugins.Trigger, pr github.PullRequestEvent) error { 34 org, repo, a := orgRepoAuthor(pr.PullRequest) 35 author := string(a) 36 num := pr.PullRequest.Number 37 switch pr.Action { 38 case github.PullRequestActionOpened: 39 // When a PR is opened, if the author is in the org then build it. 40 // Otherwise, ask for "/ok-to-test". There's no need to look for previous 41 // "/ok-to-test" comments since the PR was just opened! 42 member, err := trustedUser(c.GitHubClient, trigger, author, org, repo) 43 if err != nil { 44 return fmt.Errorf("could not check membership: %s", err) 45 } 46 if member { 47 c.Logger.Info("Starting all jobs for new PR.") 48 return buildAll(c, &pr.PullRequest, pr.GUID) 49 } 50 c.Logger.Infof("Welcome message to PR author %q.", author) 51 if err := welcomeMsg(c.GitHubClient, trigger, pr.PullRequest); err != nil { 52 return fmt.Errorf("could not welcome non-org member %q: %v", author, err) 53 } 54 case github.PullRequestActionReopened: 55 // When a PR is reopened, check that the user is in the org or that an org 56 // member had said "/ok-to-test" before building. 57 comments, err := c.GitHubClient.ListIssueComments(org, repo, num) 58 if err != nil { 59 return err 60 } 61 trusted, err := trustedPullRequest(c.GitHubClient, trigger, author, org, repo, comments) 62 if err != nil { 63 return fmt.Errorf("could not validate PR: %s", err) 64 } else if trusted { 65 err = clearStaleComments(c.GitHubClient, pr.PullRequest, comments) 66 if err != nil { 67 c.Logger.Warnf("Failed to clear stale comments: %v.", err) 68 } 69 // Just try to remove "needs-ok-to-test" label if existing, we don't care about the result. 70 c.GitHubClient.RemoveLabel(org, repo, num, needsOkToTest) 71 c.Logger.Info("Starting all jobs for updated PR.") 72 return buildAll(c, &pr.PullRequest, pr.GUID) 73 } 74 case github.PullRequestActionEdited: 75 // if someone changes the base of their PR, we will get this 76 // event and the changes field will list that the base SHA and 77 // ref changes so we can detect such a case and retrigger tests 78 var changes struct { 79 Base struct { 80 Ref struct { 81 From string `json:"from"` 82 } `json:"ref"` 83 Sha struct { 84 From string `json:"from"` 85 } `json:"sha"` 86 } `json:"base"` 87 } 88 if err := json.Unmarshal(pr.Changes, &changes); err != nil { 89 // we're detecting this best-effort so we can forget about 90 // the event 91 return nil 92 } else if changes.Base.Ref.From != "" || changes.Base.Sha.From != "" { 93 // the base of the PR changed and we need to re-test it 94 return buildAllIfTrusted(c, trigger, pr) 95 } 96 case github.PullRequestActionSynchronize: 97 return buildAllIfTrusted(c, trigger, pr) 98 case github.PullRequestActionLabeled: 99 comments, err := c.GitHubClient.ListIssueComments(org, repo, num) 100 if err != nil { 101 return err 102 } 103 // When a PR is LGTMd, if it is untrusted then build it once. 104 if pr.Label.Name == lgtmLabel { 105 trusted, err := trustedPullRequest(c.GitHubClient, trigger, author, org, repo, comments) 106 if err != nil { 107 return fmt.Errorf("could not validate PR: %s", err) 108 } else if !trusted { 109 c.Logger.Info("Starting all jobs for untrusted PR with LGTM.") 110 return buildAll(c, &pr.PullRequest, pr.GUID) 111 } 112 } 113 } 114 return nil 115 } 116 117 type login string 118 119 func orgRepoAuthor(pr github.PullRequest) (string, string, login) { 120 org := pr.Base.Repo.Owner.Login 121 repo := pr.Base.Repo.Name 122 author := pr.User.Login 123 return org, repo, login(author) 124 } 125 126 func buildAllIfTrusted(c client, trigger *plugins.Trigger, pr github.PullRequestEvent) error { 127 // When a PR is updated, check that the user is in the org or that an org 128 // member has said "/ok-to-test" before building. There's no need to ask 129 // for "/ok-to-test" because we do that once when the PR is created. 130 org, repo, a := orgRepoAuthor(pr.PullRequest) 131 author := string(a) 132 comments, err := c.GitHubClient.ListIssueComments(org, repo, pr.PullRequest.Number) 133 if err != nil { 134 return err 135 } 136 trusted, err := trustedPullRequest(c.GitHubClient, trigger, author, org, repo, comments) 137 if err != nil { 138 return fmt.Errorf("could not validate PR: %s", err) 139 } else if trusted { 140 err = clearStaleComments(c.GitHubClient, pr.PullRequest, comments) 141 if err != nil { 142 c.Logger.Warnf("Failed to clear stale comments: %v.", err) 143 } 144 c.Logger.Info("Starting all jobs for updated PR.") 145 return buildAll(c, &pr.PullRequest, pr.GUID) 146 } 147 return nil 148 } 149 150 func welcomeMsg(ghc githubClient, trigger *plugins.Trigger, pr github.PullRequest) error { 151 commentTemplate := `Hi @%s. Thanks for your PR. 152 153 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. 154 155 I understand the commands that are listed [here](https://go.k8s.io/bot-commands). 156 157 <details> 158 159 %s 160 </details> 161 ` 162 org, repo, a := orgRepoAuthor(pr) 163 author := string(a) 164 var more string 165 if trigger != nil && trigger.TrustedOrg != "" && trigger.TrustedOrg != org { 166 more = fmt.Sprintf("or [%s](https://github.com/orgs/%s/people) ", trigger.TrustedOrg, trigger.TrustedOrg) 167 } 168 169 var joinOrgURL string 170 if trigger != nil && trigger.JoinOrgURL != "" { 171 joinOrgURL = trigger.JoinOrgURL 172 } else { 173 joinOrgURL = fmt.Sprintf("https://github.com/orgs/%s/people", org) 174 } 175 comment := fmt.Sprintf(commentTemplate, author, org, org, more, joinOrgURL, plugins.AboutThisBotWithoutCommands) 176 177 err1 := ghc.AddLabel(org, repo, pr.Number, needsOkToTest) 178 err2 := ghc.CreateComment(org, repo, pr.Number, comment) 179 if err1 != nil || err2 != nil { 180 return fmt.Errorf("welcomeMsg: error adding label: %v, error creating comment: %v", err1, err2) 181 } 182 return nil 183 } 184 185 // trustedPullRequest returns whether or not the given PR should be tested. 186 // It first checks if the author is in the org, then looks for "/ok-to-test" 187 // comments by org members. 188 func trustedPullRequest(ghc githubClient, trigger *plugins.Trigger, author, org, repo string, comments []github.IssueComment) (bool, error) { 189 // First check if the author is a member of the org. 190 if orgMember, err := trustedUser(ghc, trigger, author, org, repo); err != nil { 191 return false, fmt.Errorf("error checking %s for trust: %v", author, err) 192 } else if orgMember { 193 return true, nil 194 } 195 botName, err := ghc.BotName() 196 if err != nil { 197 return false, fmt.Errorf("error finding bot name: %v", err) 198 } 199 // Next look for "/ok-to-test" comments on the PR. 200 for _, comment := range comments { 201 commentAuthor := comment.User.Login 202 // Skip comments: by the PR author, or by bot, or not matching "/ok-to-test". 203 if commentAuthor == author || commentAuthor == botName || !okToTestRe.MatchString(comment.Body) { 204 continue 205 } 206 // Ensure that the commenter is in the org. 207 if commentAuthorMember, err := trustedUser(ghc, trigger, commentAuthor, org, repo); err != nil { 208 return false, fmt.Errorf("error checking %s for trust: %v", commentAuthor, err) 209 } else if commentAuthorMember { 210 return true, nil 211 } 212 } 213 return false, nil 214 } 215 216 func buildAll(c client, pr *github.PullRequest, eventGUID string) error { 217 var matchingJobs []config.Presubmit 218 for _, job := range c.Config.Presubmits[pr.Base.Repo.FullName] { 219 if job.AlwaysRun || job.RunIfChanged != "" { 220 matchingJobs = append(matchingJobs, job) 221 } 222 } 223 return runOrSkipRequested(c, pr, matchingJobs, nil, "", eventGUID) 224 } 225 226 // clearStaleComments deletes old comments that are no longer applicable. 227 func clearStaleComments(gc githubClient, pr github.PullRequest, comments []github.IssueComment) error { 228 botName, err := gc.BotName() 229 if err != nil { 230 return err 231 } 232 233 org, repo, _ := orgRepoAuthor(pr) 234 const waitingComment = "member to verify that this patch is reasonable to test." 235 236 return gc.DeleteStaleComments( 237 org, 238 repo, 239 pr.Number, 240 comments, 241 func(c github.IssueComment) bool { // isStale function 242 return c.User.Login == botName && strings.Contains(c.Body, waitingComment) 243 }, 244 ) 245 }