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