github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/plugins/skip/skip.go (about)

     1  /*
     2  Copyright 2017 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 skip implements the `/skip` command which allows users
    18  // to clean up commit statuses of non-blocking presubmits on PRs.
    19  package skip
    20  
    21  import (
    22  	"fmt"
    23  	"regexp"
    24  
    25  	"github.com/sirupsen/logrus"
    26  	"sigs.k8s.io/prow/pkg/config"
    27  	"sigs.k8s.io/prow/pkg/git/v2"
    28  	"sigs.k8s.io/prow/pkg/github"
    29  	"sigs.k8s.io/prow/pkg/pluginhelp"
    30  	"sigs.k8s.io/prow/pkg/plugins"
    31  	"sigs.k8s.io/prow/pkg/plugins/trigger"
    32  )
    33  
    34  const pluginName = "skip"
    35  
    36  var (
    37  	skipRe = regexp.MustCompile(`(?mi)^/skip\s*$`)
    38  )
    39  
    40  type githubClient interface {
    41  	CreateComment(owner, repo string, number int, comment string) error
    42  	CreateStatus(org, repo, ref string, s github.Status) error
    43  	GetPullRequest(org, repo string, number int) (*github.PullRequest, error)
    44  	GetCombinedStatus(org, repo, ref string) (*github.CombinedStatus, error)
    45  	GetPullRequestChanges(org, repo string, number int) ([]github.PullRequestChange, error)
    46  	GetRef(org, repo, ref string) (string, error)
    47  }
    48  
    49  func init() {
    50  	plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider)
    51  }
    52  
    53  func helpProvider(config *plugins.Configuration, _ []config.OrgRepo) (*pluginhelp.PluginHelp, error) {
    54  	pluginHelp := &pluginhelp.PluginHelp{
    55  		Description: "The skip plugin allows users to clean up GitHub stale commit statuses for non-blocking jobs on a PR.",
    56  	}
    57  	pluginHelp.AddCommand(pluginhelp.Command{
    58  		Usage:       "/skip",
    59  		Description: "Cleans up GitHub stale commit statuses for non-blocking jobs on a PR.",
    60  		Featured:    false,
    61  		WhoCanUse:   "Anyone can trigger this command on a PR.",
    62  		Examples:    []string{"/skip"},
    63  	})
    64  	return pluginHelp, nil
    65  }
    66  
    67  func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error {
    68  	honorOkToTest := trigger.HonorOkToTest(pc.PluginConfig.TriggerFor(e.Repo.Owner.Login, e.Repo.Name))
    69  	return handle(pc.GitHubClient, pc.Logger, &e, pc.Config, pc.GitClient, honorOkToTest)
    70  }
    71  
    72  func handle(gc githubClient, log *logrus.Entry, e *github.GenericCommentEvent, c *config.Config, gitClient git.ClientFactory, honorOkToTest bool) error {
    73  	if !e.IsPR || e.IssueState != "open" || e.Action != github.GenericCommentActionCreated {
    74  		return nil
    75  	}
    76  
    77  	if !skipRe.MatchString(e.Body) {
    78  		return nil
    79  	}
    80  
    81  	org := e.Repo.Owner.Login
    82  	repo := e.Repo.Name
    83  	number := e.Number
    84  
    85  	pr, err := gc.GetPullRequest(org, repo, number)
    86  	if err != nil {
    87  		resp := fmt.Sprintf("Cannot get PR #%d in %s/%s: %v", number, org, repo, err)
    88  		log.Warn(resp)
    89  		return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, resp))
    90  	}
    91  	baseSHAGetter := func() (string, error) {
    92  		baseSHA, err := gc.GetRef(org, repo, "heads/"+pr.Base.Ref)
    93  		if err != nil {
    94  			return "", fmt.Errorf("failed to get baseSHA: %w", err)
    95  		}
    96  		return baseSHA, nil
    97  	}
    98  	headSHAGetter := func() (string, error) {
    99  		return pr.Head.SHA, nil
   100  	}
   101  	presubmits, err := c.GetPresubmits(gitClient, org+"/"+repo, pr.Base.Ref, baseSHAGetter, headSHAGetter)
   102  	if err != nil {
   103  		return fmt.Errorf("failed to get presubmits: %w", err)
   104  	}
   105  
   106  	combinedStatus, err := gc.GetCombinedStatus(org, repo, pr.Head.SHA)
   107  	if err != nil {
   108  		resp := fmt.Sprintf("Cannot get combined commit statuses for PR #%d in %s/%s: %v", number, org, repo, err)
   109  		log.Warn(resp)
   110  		return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, resp))
   111  	}
   112  	if combinedStatus.State == github.StatusSuccess {
   113  		return nil
   114  	}
   115  	statuses := combinedStatus.Statuses
   116  
   117  	filteredPresubmits, err := trigger.FilterPresubmits(honorOkToTest, gc, e.Body, pr, presubmits, log)
   118  	if err != nil {
   119  		resp := fmt.Sprintf("Cannot get combined status for PR #%d in %s/%s: %v", number, org, repo, err)
   120  		log.Warn(resp)
   121  		return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, resp))
   122  	}
   123  	triggerWillHandle := func(p config.Presubmit) bool {
   124  		for _, presubmit := range filteredPresubmits {
   125  			if p.Name == presubmit.Name && p.Context == presubmit.Context {
   126  				return true
   127  			}
   128  		}
   129  		return false
   130  	}
   131  
   132  	for _, job := range presubmits {
   133  		// Only consider jobs that have already posted a failed status
   134  		if !statusExists(job, statuses) || isSuccess(job, statuses) {
   135  			continue
   136  		}
   137  		// Ignore jobs that will be handled by the trigger plugin
   138  		// for this specific comment, regardless of whether they
   139  		// are required or not. This allows a comment like
   140  		// >/skip
   141  		// >/test foo
   142  		// To end up testing foo instead of skipping it
   143  		if triggerWillHandle(job) {
   144  			continue
   145  		}
   146  		// Only skip jobs that are not required
   147  		if job.ContextRequired() {
   148  			continue
   149  		}
   150  		context := job.Context
   151  		status := github.Status{
   152  			State:       github.StatusSuccess,
   153  			Description: "Skipped",
   154  			Context:     context,
   155  		}
   156  		if err := gc.CreateStatus(org, repo, pr.Head.SHA, status); err != nil {
   157  			resp := fmt.Sprintf("Cannot update PR status for context %s: %v", context, err)
   158  			log.Warn(resp)
   159  			return gc.CreateComment(org, repo, number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, resp))
   160  		}
   161  	}
   162  	return nil
   163  }
   164  
   165  func statusExists(job config.Presubmit, statuses []github.Status) bool {
   166  	for _, status := range statuses {
   167  		if status.Context == job.Context {
   168  			return true
   169  		}
   170  	}
   171  	return false
   172  }
   173  
   174  func isSuccess(job config.Presubmit, statuses []github.Status) bool {
   175  	for _, status := range statuses {
   176  		if status.Context == job.Context && status.State == github.StatusSuccess {
   177  			return true
   178  		}
   179  	}
   180  	return false
   181  }