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  }