sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/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 }