sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/trigger/generic-comment.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  	"fmt"
    21  
    22  	"github.com/sirupsen/logrus"
    23  	"sigs.k8s.io/prow/pkg/kube"
    24  
    25  	"k8s.io/apimachinery/pkg/util/sets"
    26  	"sigs.k8s.io/prow/pkg/config"
    27  	"sigs.k8s.io/prow/pkg/github"
    28  	"sigs.k8s.io/prow/pkg/labels"
    29  	"sigs.k8s.io/prow/pkg/pjutil"
    30  	"sigs.k8s.io/prow/pkg/plugins"
    31  )
    32  
    33  func handleGenericComment(c Client, trigger plugins.Trigger, gc github.GenericCommentEvent) error {
    34  	org := gc.Repo.Owner.Login
    35  	repo := gc.Repo.Name
    36  	number := gc.Number
    37  	commentAuthor := gc.User.Login
    38  	// Only take action when a comment is first created,
    39  	// when it belongs to a PR,
    40  	// and the PR is open.
    41  	if gc.Action != github.GenericCommentActionCreated || !gc.IsPR || gc.IssueState != "open" {
    42  		return nil
    43  	}
    44  
    45  	// Skip bot comments.
    46  	botUserChecker, err := c.GitHubClient.BotUserChecker()
    47  	if err != nil {
    48  		return err
    49  	}
    50  
    51  	if botUserChecker(commentAuthor) {
    52  		c.Logger.Debug("Comment is made by the bot, skipping.")
    53  		return nil
    54  	}
    55  
    56  	refGetter := config.NewRefGetterForGitHubPullRequest(c.GitHubClient, org, repo, number)
    57  	presubmits := getPresubmits(c.Logger, c.GitClient, c.Config, org+"/"+repo, refGetter.BaseSHA, refGetter.HeadSHA)
    58  
    59  	// Skip comments not germane to this plugin
    60  	if !pjutil.RetestRe.MatchString(gc.Body) &&
    61  		!pjutil.RetestRequiredRe.MatchString(gc.Body) &&
    62  		!pjutil.OkToTestRe.MatchString(gc.Body) &&
    63  		!pjutil.TestAllRe.MatchString(gc.Body) &&
    64  		!pjutil.MayNeedHelpComment(gc.Body) {
    65  		matched := false
    66  		for _, presubmit := range presubmits {
    67  			matched = matched || presubmit.TriggerMatches(gc.Body)
    68  			if matched {
    69  				break
    70  			}
    71  		}
    72  		if !matched {
    73  			c.Logger.Debug("Comment doesn't match any triggering regex, skipping.")
    74  			return nil
    75  		}
    76  	}
    77  
    78  	// Skip untrusted users comments.
    79  	trustedResponse, err := TrustedUser(c.GitHubClient, trigger.OnlyOrgMembers, trigger.TrustedApps, trigger.TrustedOrg, commentAuthor, org, repo)
    80  	if err != nil {
    81  		return fmt.Errorf("error checking trust of %s: %w", commentAuthor, err)
    82  	}
    83  
    84  	trusted := trustedResponse.IsTrusted
    85  	var l []github.Label
    86  	if !trusted {
    87  		// Skip untrusted PRs.
    88  		l, trusted, err = TrustedPullRequest(c.GitHubClient, trigger, gc.IssueAuthor.Login, org, repo, number, nil)
    89  		if err != nil {
    90  			return err
    91  		}
    92  		if !trusted {
    93  			resp := "Cannot trigger testing until a trusted user reviews the PR and leaves an `/ok-to-test` message."
    94  			c.Logger.Infof("Commenting \"%s\".", resp)
    95  			return c.GitHubClient.CreateComment(org, repo, number, plugins.FormatResponseRaw(gc.Body, gc.HTMLURL, gc.User.Login, resp))
    96  		}
    97  	}
    98  
    99  	// At this point we can trust the PR, so we eventually update labels.
   100  	// Ensure we have labels before test, because TrustedPullRequest() won't be called
   101  	// when commentAuthor is trusted.
   102  	if l == nil {
   103  		l, err = c.GitHubClient.GetIssueLabels(org, repo, number)
   104  		if err != nil {
   105  			return err
   106  		}
   107  	}
   108  	isOkToTest := HonorOkToTest(trigger) && pjutil.OkToTestRe.MatchString(gc.Body)
   109  	if isOkToTest && !github.HasLabel(labels.OkToTest, l) {
   110  		if err := c.GitHubClient.AddLabel(org, repo, number, labels.OkToTest); err != nil {
   111  			return err
   112  		}
   113  	}
   114  	if (isOkToTest || github.HasLabel(labels.OkToTest, l)) && github.HasLabel(labels.NeedsOkToTest, l) {
   115  		if err := c.GitHubClient.RemoveLabel(org, repo, number, labels.NeedsOkToTest); err != nil {
   116  			return err
   117  		}
   118  	}
   119  
   120  	pr, err := refGetter.PullRequest()
   121  	if err != nil {
   122  		return err
   123  	}
   124  	baseSHA, err := refGetter.BaseSHA()
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	toTest, err := FilterPresubmits(HonorOkToTest(trigger), c.GitHubClient, gc.Body, pr, presubmits, c.Logger)
   130  	if err != nil {
   131  		return err
   132  	}
   133  	if needsHelp, note := pjutil.ShouldRespondWithHelp(gc.Body, len(toTest)); needsHelp {
   134  		return addHelpComment(c.GitHubClient, gc.Body, org, repo, pr.Base.Ref, pr.Number, presubmits, gc.HTMLURL, commentAuthor, note, c.Logger)
   135  	}
   136  	// we want to be able to track re-tests separately from the general body of tests
   137  	additionalLabels := map[string]string{}
   138  	if pjutil.RetestRe.MatchString(gc.Body) || pjutil.RetestRequiredRe.MatchString(gc.Body) {
   139  		additionalLabels[kube.RetestLabel] = "true"
   140  	}
   141  	// run failed github actions
   142  	if trigger.TriggerGitHubWorkflows && (pjutil.RetestRe.MatchString(gc.Body) || pjutil.TestAllRe.MatchString(gc.Body)) {
   143  		headSHA, err := refGetter.HeadSHA()
   144  		if err != nil {
   145  			c.Logger.Warnf("headSHA unvailable, failed github actions for pr will not be triggered: %v", pr)
   146  		} else {
   147  			failedRuns, err := c.GitHubClient.GetFailedActionRunsByHeadBranch(org, repo, pr.Head.Ref, headSHA)
   148  			if err != nil {
   149  				c.Logger.Errorf("%v: unable to get failed github action runs for branch %v", err, pr.Head.Ref)
   150  			} else {
   151  				for _, run := range failedRuns {
   152  					log := c.Logger.WithFields(logrus.Fields{
   153  						"runID":   run.ID,
   154  						"runName": run.Name,
   155  						"org":     org,
   156  						"repo":    repo,
   157  					})
   158  					runID := run.ID
   159  					go func() {
   160  						if err := c.GitHubClient.TriggerFailedGitHubWorkflow(org, repo, runID); err != nil {
   161  							log.Errorf("attempt to trigger github run failed: %v", err)
   162  						} else {
   163  							log.Infof("successfully triggered action run")
   164  						}
   165  					}()
   166  				}
   167  			}
   168  		}
   169  	}
   170  	return RunRequestedWithLabels(c, pr, baseSHA, toTest, gc.GUID, additionalLabels)
   171  }
   172  
   173  func HonorOkToTest(trigger plugins.Trigger) bool {
   174  	return !trigger.IgnoreOkToTest
   175  }
   176  
   177  type GitHubClient interface {
   178  	GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error)
   179  	GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
   180  }
   181  
   182  // FilterPresubmits determines which presubmits should run. We only want to
   183  // trigger jobs that should run, but the pool of jobs we filter to those that
   184  // should run depends on the type of trigger we just got:
   185  //   - if we get a /test foo, we only want to consider those jobs that match;
   186  //     jobs will default to run unless we can determine they shouldn't
   187  //   - if we got a /retest, we only want to consider those jobs that have
   188  //     already run and posted failing contexts to the PR or those jobs that
   189  //     have not yet run but would otherwise match /test all; jobs will default
   190  //     to run unless we can determine they shouldn't
   191  //   - if we got a /test all or an /ok-to-test, we want to consider any job
   192  //     that doesn't explicitly require a human trigger comment; jobs will
   193  //     default to not run unless we can determine that they should
   194  //
   195  // If a comment that we get matches more than one of the above patterns, we
   196  // consider the set of matching presubmits the union of the results from the
   197  // matching cases.
   198  func FilterPresubmits(honorOkToTest bool, gitHubClient GitHubClient, body string, pr *github.PullRequest, presubmits []config.Presubmit, logger *logrus.Entry) ([]config.Presubmit, error) {
   199  	org, repo, sha := pr.Base.Repo.Owner.Login, pr.Base.Repo.Name, pr.Head.SHA
   200  
   201  	contextGetter := func() (sets.Set[string], sets.Set[string], error) {
   202  		combinedStatus, err := gitHubClient.GetCombinedStatus(org, repo, sha)
   203  		if err != nil {
   204  			return nil, nil, err
   205  		}
   206  		failedContexts, allContexts := getContexts(combinedStatus)
   207  		return failedContexts, allContexts, nil
   208  	}
   209  
   210  	filter, err := pjutil.PresubmitFilter(honorOkToTest, contextGetter, body, logger)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	number, branch := pr.Number, pr.Base.Ref
   216  	changes := config.NewGitHubDeferredChangedFilesProvider(gitHubClient, org, repo, number)
   217  	return pjutil.FilterPresubmits(filter, changes, branch, presubmits, logger)
   218  }
   219  
   220  func getContexts(combinedStatus *github.CombinedStatus) (sets.Set[string], sets.Set[string]) {
   221  	allContexts := sets.Set[string]{}
   222  	failedContexts := sets.Set[string]{}
   223  	if combinedStatus != nil {
   224  		for _, status := range combinedStatus.Statuses {
   225  			allContexts.Insert(status.Context)
   226  			if status.State == github.StatusError || status.State == github.StatusFailure {
   227  				failedContexts.Insert(status.Context)
   228  			}
   229  		}
   230  	}
   231  	return failedContexts, allContexts
   232  }
   233  
   234  func addHelpComment(githubClient githubClient, body, org, repo, branch string, number int, presubmits []config.Presubmit, HTMLURL, user, note string, logger *logrus.Entry) error {
   235  	changes := config.NewGitHubDeferredChangedFilesProvider(githubClient, org, repo, number)
   236  	testAllNames, optionalJobsCommands, requiredJobsCommands, err := pjutil.AvailablePresubmits(changes, branch, presubmits, logger)
   237  	if err != nil {
   238  		return err
   239  	}
   240  
   241  	resp := pjutil.HelpMessage(org, repo, branch, note, testAllNames, optionalJobsCommands, requiredJobsCommands)
   242  	return githubClient.CreateComment(org, repo, number, plugins.FormatResponseRaw(body, HTMLURL, user, resp))
   243  }