sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/cherrypickunapproved/cherrypick-unapproved.go (about) 1 /* 2 Copyright 2018 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 cherrypickunapproved adds the `do-not-merge/cherry-pick-not-approved` 18 // label to PRs against a release branch which do not have the 19 // `cherry-pick-approved` label. 20 package cherrypickunapproved 21 22 import ( 23 "encoding/json" 24 "fmt" 25 "regexp" 26 "strings" 27 28 "github.com/sirupsen/logrus" 29 30 "sigs.k8s.io/prow/pkg/config" 31 "sigs.k8s.io/prow/pkg/github" 32 "sigs.k8s.io/prow/pkg/labels" 33 "sigs.k8s.io/prow/pkg/pluginhelp" 34 "sigs.k8s.io/prow/pkg/plugins" 35 ) 36 37 const ( 38 // PluginName defines this plugin's registered name. 39 PluginName = "cherry-pick-unapproved" 40 ) 41 42 func init() { 43 plugins.RegisterPullRequestHandler(PluginName, handlePullRequest, helpProvider) 44 } 45 46 func helpProvider(config *plugins.Configuration, _ []config.OrgRepo) (*pluginhelp.PluginHelp, error) { 47 // Only the 'Config' and Description' fields are necessary because this 48 // plugin does not react to any commands. 49 yamlSnippet, err := plugins.CommentMap.GenYaml(&plugins.Configuration{ 50 CherryPickUnapproved: plugins.CherryPickUnapproved{ 51 BranchRegexp: "^release-*", 52 Comment: "This is why your cherry-pick cannot be approved.", 53 }, 54 }) 55 if err != nil { 56 logrus.WithError(err).Warnf("cannot generate comments for %s plugin", PluginName) 57 } 58 pluginHelp := &pluginhelp.PluginHelp{ 59 Description: "Label PRs against a release branch which do not have the `cherry-pick-approved` label with the `do-not-merge/cherry-pick-not-approved` label.", 60 Config: map[string]string{ 61 "": fmt.Sprintf( 62 "The cherry-pick-unapproved plugin treats PRs against branch names satisfying the regular expression `%s` as cherry-pick PRs and adds the following comment:\n%s", 63 config.CherryPickUnapproved.BranchRegexp, 64 config.CherryPickUnapproved.Comment, 65 ), 66 }, 67 Snippet: yamlSnippet, 68 } 69 return pluginHelp, nil 70 } 71 72 type githubClient interface { 73 CreateComment(owner, repo string, number int, comment string) error 74 AddLabel(owner, repo string, number int, label string) error 75 RemoveLabel(owner, repo string, number int, label string) error 76 GetIssueLabels(org, repo string, number int) ([]github.Label, error) 77 } 78 79 type commentPruner interface { 80 PruneComments(shouldPrune func(github.IssueComment) bool) 81 } 82 83 func handlePullRequest(pc plugins.Agent, pr github.PullRequestEvent) error { 84 cp, err := pc.CommentPruner() 85 if err != nil { 86 return err 87 } 88 return handlePR( 89 pc.GitHubClient, pc.Logger, &pr, cp, 90 pc.PluginConfig.CherryPickUnapproved.BranchRe, pc.PluginConfig.CherryPickUnapproved.Comment, 91 ) 92 } 93 94 func handlePR(gc githubClient, log *logrus.Entry, pr *github.PullRequestEvent, cp commentPruner, branchRe *regexp.Regexp, commentBody string) error { 95 var ( 96 org = pr.Repo.Owner.Login 97 repo = pr.Repo.Name 98 branch = pr.PullRequest.Base.Ref 99 ) 100 101 switch pr.Action { 102 case github.PullRequestActionOpened, github.PullRequestActionReopened: 103 if !branchRe.MatchString(branch) { 104 return nil 105 } 106 return ensureLabels(gc, org, repo, pr, log, cp, commentBody) 107 case github.PullRequestActionLabeled, github.PullRequestActionUnlabeled: 108 if !branchRe.MatchString(branch) { 109 return nil 110 } 111 if !(pr.Label.Name == labels.CpApproved || pr.Label.Name == labels.CpUnapproved) { 112 return nil 113 } 114 return ensureLabels(gc, org, repo, pr, log, cp, commentBody) 115 case github.PullRequestActionEdited: 116 // if someone changes the base of their PR, we will get this event 117 // and the changes field will list that the base SHA and ref changes 118 var changes struct { 119 Base struct { 120 Ref struct { 121 From string `json:"from"` 122 } `json:"ref"` 123 Sha struct { 124 From string `json:"from"` 125 } `json:"sha"` 126 } `json:"base"` 127 } 128 if err := json.Unmarshal(pr.Changes, &changes); err != nil { 129 // we're detecting this best-effort so we can forget about the event 130 return nil 131 } 132 133 if changes.Base.Ref.From == "" { 134 // PR base ref did not change, ignore the event 135 return nil 136 } 137 138 if branchRe.MatchString(branch) && !branchRe.MatchString(changes.Base.Ref.From) { 139 // base ref changed from a branch not allowed for cherry-picks to a branch that is allowed for cherry-picks 140 return ensureLabels(gc, org, repo, pr, log, cp, commentBody) 141 } else if !branchRe.MatchString(branch) && branchRe.MatchString(changes.Base.Ref.From) { 142 // base ref changed from a branch allowed for cherry-picks to a branch that is not allowed for cherry-picks 143 return pruneLabels(gc, org, repo, pr, log, cp, commentBody) 144 } 145 } 146 147 return nil 148 } 149 150 func ensureLabels(gc githubClient, org string, repo string, pr *github.PullRequestEvent, log *logrus.Entry, cp commentPruner, commentBody string) error { 151 issueLabels, err := gc.GetIssueLabels(org, repo, pr.Number) 152 if err != nil { 153 return err 154 } 155 hasCherryPickApprovedLabel := github.HasLabel(labels.CpApproved, issueLabels) 156 hasCherryPickUnapprovedLabel := github.HasLabel(labels.CpUnapproved, issueLabels) 157 158 // if it has the approved label, 159 // remove the unapproved label (if it exists) and 160 // remove any comments left by this plugin 161 if hasCherryPickApprovedLabel { 162 if hasCherryPickUnapprovedLabel { 163 if err := gc.RemoveLabel(org, repo, pr.Number, labels.CpUnapproved); err != nil { 164 log.WithError(err).Errorf("GitHub failed to remove the following label: %s", labels.CpUnapproved) 165 } 166 } 167 cp.PruneComments(func(comment github.IssueComment) bool { 168 return strings.Contains(comment.Body, commentBody) 169 }) 170 return nil 171 } 172 173 // if it already has the unapproved label, we are done here 174 if hasCherryPickUnapprovedLabel { 175 return nil 176 } 177 178 // only add the label and comment if none of the approved and unapproved labels are present 179 if err := gc.AddLabel(org, repo, pr.Number, labels.CpUnapproved); err != nil { 180 log.WithError(err).Errorf("GitHub failed to add the following label: %s", labels.CpUnapproved) 181 } 182 183 formattedComment := plugins.FormatSimpleResponse(commentBody) 184 if err := gc.CreateComment(org, repo, pr.Number, formattedComment); err != nil { 185 log.WithError(err).Errorf("Failed to comment %q", formattedComment) 186 } 187 188 return nil 189 } 190 191 func pruneLabels(gc githubClient, org string, repo string, pr *github.PullRequestEvent, log *logrus.Entry, cp commentPruner, commentBody string) error { 192 issueLabels, err := gc.GetIssueLabels(org, repo, pr.Number) 193 if err != nil { 194 return err 195 } 196 hasCherryPickApprovedLabel := github.HasLabel(labels.CpApproved, issueLabels) 197 hasCherryPickUnapprovedLabel := github.HasLabel(labels.CpUnapproved, issueLabels) 198 199 if hasCherryPickApprovedLabel { 200 if err := gc.RemoveLabel(org, repo, pr.Number, labels.CpApproved); err != nil { 201 log.WithError(err).Errorf("GitHub failed to remove the following label: %s", labels.CpApproved) 202 } 203 } 204 205 if hasCherryPickUnapprovedLabel { 206 if err := gc.RemoveLabel(org, repo, pr.Number, labels.CpUnapproved); err != nil { 207 log.WithError(err).Errorf("GitHub failed to remove the following label: %s", labels.CpUnapproved) 208 } 209 } 210 211 cp.PruneComments(func(comment github.IssueComment) bool { 212 return strings.Contains(comment.Body, commentBody) 213 }) 214 215 return nil 216 }