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 }