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 }