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  }