sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/plugins/assign/assign.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 assign 18 19 import ( 20 "fmt" 21 "regexp" 22 "strings" 23 24 "github.com/sirupsen/logrus" 25 26 "sigs.k8s.io/prow/pkg/config" 27 "sigs.k8s.io/prow/pkg/github" 28 "sigs.k8s.io/prow/pkg/pluginhelp" 29 "sigs.k8s.io/prow/pkg/plugins" 30 ) 31 32 const pluginName = "assign" 33 34 var ( 35 assignRe = regexp.MustCompile(`(?mi)^/(un)?assign(( @?[-\w]+?)*)\s*$`) 36 // CCRegexp parses and validates /cc commands, also used by blunderbuss 37 CCRegexp = regexp.MustCompile(`(?mi)^/(un)?cc(( +@?[-/\w]+?)*)\s*$`) 38 ) 39 40 func init() { 41 plugins.RegisterGenericCommentHandler(pluginName, handleGenericComment, helpProvider) 42 } 43 44 func helpProvider(config *plugins.Configuration, _ []config.OrgRepo) (*pluginhelp.PluginHelp, error) { 45 // The Config field is omitted because this plugin is not configurable. 46 pluginHelp := &pluginhelp.PluginHelp{ 47 Description: "The assign plugin assigns or requests reviews from users. Specific users can be assigned with the command '/assign @user1' or have reviews requested of them with the command '/cc @user1'. If no users are specified, the commands default to targeting the user who created the command. Assignments and requested reviews can be removed in the same way that they are added by prefixing the commands with 'un'.", 48 } 49 pluginHelp.AddCommand(pluginhelp.Command{ 50 Usage: "/[un]assign [[@]<username>...]", 51 Description: "Assigns assignee(s) to the PR", 52 Featured: true, 53 WhoCanUse: "Anyone can use the command, but the target user(s) must be an org member, a repo collaborator, or should have previously commented on the issue or PR.", 54 Examples: []string{"/assign", "/unassign", "/assign @spongebob", "/assign spongebob patrick"}, 55 }) 56 pluginHelp.AddCommand(pluginhelp.Command{ 57 Usage: "/[un]cc [[@]<username>...]", 58 Description: "Requests a review from the user(s).", 59 Featured: true, 60 WhoCanUse: "Anyone can use the command, but the target user(s) must be a member of the org that owns the repository.", 61 Examples: []string{"/cc", "/uncc", "/cc @spongebob", "/cc spongebob patrick"}, 62 }) 63 return pluginHelp, nil 64 } 65 66 type githubClient interface { 67 AssignIssue(owner, repo string, number int, logins []string) error 68 UnassignIssue(owner, repo string, number int, logins []string) error 69 70 RequestReview(org, repo string, number int, logins []string) error 71 UnrequestReview(org, repo string, number int, logins []string) error 72 73 CreateComment(owner, repo string, number int, comment string) error 74 } 75 76 func handleGenericComment(pc plugins.Agent, e github.GenericCommentEvent) error { 77 if e.Action != github.GenericCommentActionCreated { 78 return nil 79 } 80 err := handle(newAssignHandler(e, pc.GitHubClient, pc.Logger)) 81 if e.IsPR { 82 err = combineErrors(err, handle(newReviewHandler(e, pc.GitHubClient, pc.Logger))) 83 } 84 return err 85 } 86 87 func parseLogins(text string) []string { 88 var parts []string 89 for _, p := range strings.Split(text, " ") { 90 t := strings.Trim(p, "@ ") 91 if t == "" { 92 continue 93 } 94 parts = append(parts, t) 95 } 96 return parts 97 } 98 99 func combineErrors(err1, err2 error) error { 100 if err1 != nil && err2 != nil { 101 return fmt.Errorf("two errors: 1) %v 2) %w", err1, err2) 102 } else if err1 != nil { 103 return err1 104 } else { 105 return err2 106 } 107 } 108 109 // handle is the generic handler for the assign plugin. It uses the handler's regexp and affectedLogins 110 // functions to identify the users to add and/or remove and then passes the appropriate users to the 111 // handler's add and remove functions. If add fails to add some of the users, a response comment is 112 // created where the body of the response is generated by the handler's addFailureResponse function. 113 func handle(h *handler) error { 114 e := h.event 115 org := e.Repo.Owner.Login 116 repo := e.Repo.Name 117 matches := h.regexp.FindAllStringSubmatch(e.Body, -1) 118 if matches == nil { 119 return nil 120 } 121 users := make(map[string]bool) 122 for _, re := range matches { 123 add := re[1] != "un" // un<cmd> == !add 124 if re[2] == "" { 125 users[e.User.Login] = add 126 } else { 127 for _, login := range parseLogins(re[2]) { 128 users[login] = add 129 } 130 } 131 } 132 var toAdd, toRemove []string 133 for login, add := range users { 134 if add { 135 toAdd = append(toAdd, login) 136 } else { 137 toRemove = append(toRemove, login) 138 } 139 } 140 141 if len(toRemove) > 0 { 142 h.log.Printf("Removing %s from %s/%s#%d: %v", h.userType, org, repo, e.Number, toRemove) 143 if err := h.remove(org, repo, e.Number, toRemove); err != nil { 144 return err 145 } 146 } 147 if len(toAdd) > 0 { 148 h.log.Printf("Adding %s to %s/%s#%d: %v", h.userType, org, repo, e.Number, toAdd) 149 if err := h.add(org, repo, e.Number, toAdd); err != nil { 150 if mu, ok := err.(github.MissingUsers); ok { 151 msg := h.addFailureResponse(mu) 152 if len(msg) == 0 { 153 return nil 154 } 155 h.log.Printf("Failed to add %s to %s/%s#%d: %s", h.userType, org, repo, e.Number, mu.Error()) 156 if err := h.gc.CreateComment(org, repo, e.Number, plugins.FormatResponseRaw(e.Body, e.HTMLURL, e.User.Login, msg)); err != nil { 157 return fmt.Errorf("comment err: %w", err) 158 } 159 return nil 160 } 161 return err 162 } 163 } 164 return nil 165 } 166 167 // handler is a struct that contains data about a github event and provides functions to help handle it. 168 type handler struct { 169 // addFailureResponse generates the body of a response comment in the event that the add function fails. 170 addFailureResponse func(mu github.MissingUsers) string 171 // remove is the function that is called on the affected logins for a command prefixed with 'un'. 172 remove func(org, repo string, number int, users []string) error 173 // add is the function that is called on the affected logins for a command with no 'un' prefix. 174 add func(org, repo string, number int, users []string) error 175 176 // event is a pointer to the github.GenericCommentEvent struct that triggered the handler. 177 event *github.GenericCommentEvent 178 // regexp is the regular expression describing the command. It must have an optional 'un' prefix 179 // as the first subgroup and the arguments to the command as the second subgroup. 180 regexp *regexp.Regexp 181 // gc is the githubClient to use for creating response comments in the event of a failure. 182 gc githubClient 183 184 // log is a logrus.Entry used to record actions the handler takes. 185 log *logrus.Entry 186 // userType is a string that represents the type of users affected by this handler. (e.g. 'assignees') 187 userType string 188 } 189 190 func newAssignHandler(e github.GenericCommentEvent, gc githubClient, log *logrus.Entry) *handler { 191 org := e.Repo.Owner.Login 192 addFailureResponse := func(mu github.MissingUsers) string { 193 return fmt.Sprintf("GitHub didn't allow me to assign the following users: %s.\n\nNote that only [%s members](https://github.com/orgs/%s/people) with read permissions, repo collaborators and people who have commented on this issue/PR can be assigned. Additionally, issues/PRs can only have 10 assignees at the same time.\nFor more information please see [the contributor guide](https://git.k8s.io/community/contributors/guide/first-contribution.md#issue-assignment-in-github)", strings.Join(mu.Users, ", "), org, org) 194 } 195 196 return &handler{ 197 addFailureResponse: addFailureResponse, 198 remove: gc.UnassignIssue, 199 add: gc.AssignIssue, 200 event: &e, 201 regexp: assignRe, 202 gc: gc, 203 log: log, 204 userType: "assignee(s)", 205 } 206 } 207 208 func newReviewHandler(e github.GenericCommentEvent, gc githubClient, log *logrus.Entry) *handler { 209 org := e.Repo.Owner.Login 210 addFailureResponse := func(mu github.MissingUsers) string { 211 return fmt.Sprintf("GitHub didn't allow me to request PR reviews from the following users: %s.\n\nNote that only [%s members](https://github.com/orgs/%s/people) and repo collaborators can review this PR, and authors cannot review their own PRs.", strings.Join(mu.Users, ", "), org, org) 212 } 213 214 return &handler{ 215 addFailureResponse: addFailureResponse, 216 remove: gc.UnrequestReview, 217 add: gc.RequestReview, 218 event: &e, 219 regexp: CCRegexp, 220 gc: gc, 221 log: log, 222 userType: "reviewer(s)", 223 } 224 }