github.com/shashidharatd/test-infra@v0.0.0-20171006011030-71304e1ca560/mungegithub/mungers/approval-handler.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 mungers 18 19 import ( 20 "regexp" 21 "sort" 22 "strconv" 23 24 githubapi "github.com/google/go-github/github" 25 26 "k8s.io/kubernetes/pkg/util/sets" 27 "k8s.io/test-infra/mungegithub/features" 28 "k8s.io/test-infra/mungegithub/github" 29 "k8s.io/test-infra/mungegithub/mungers/approvers" 30 c "k8s.io/test-infra/mungegithub/mungers/matchers/comment" 31 "k8s.io/test-infra/mungegithub/mungers/matchers/event" 32 "k8s.io/test-infra/mungegithub/options" 33 ) 34 35 const ( 36 approveCommand = "APPROVE" 37 lgtmCommand = "LGTM" 38 cancelArgument = "cancel" 39 noIssueArgument = "no-issue" 40 ) 41 42 var AssociatedIssueRegex = regexp.MustCompile(`(?:kubernetes/[^/]+/issues/|#)(\d+)`) 43 44 // ApprovalHandler will try to add "approved" label once 45 // all files of change has been approved by approvers. 46 type ApprovalHandler struct { 47 botName string 48 features *features.Features 49 issueRequired bool 50 implicitSelfApproval bool 51 } 52 53 func init() { 54 h := &ApprovalHandler{} 55 RegisterMungerOrDie(h) 56 } 57 58 // Name is the name usable in --pr-mungers 59 func (*ApprovalHandler) Name() string { return "approval-handler" } 60 61 // RequiredFeatures is a slice of 'features' that must be provided 62 func (*ApprovalHandler) RequiredFeatures() []string { 63 return []string{features.RepoFeatureName} 64 } 65 66 // Initialize will initialize the munger 67 func (h *ApprovalHandler) Initialize(config *github.Config, features *features.Features) error { 68 h.botName = config.BotName 69 h.features = features 70 return nil 71 } 72 73 // EachLoop is called at the start of every munge loop 74 func (*ApprovalHandler) EachLoop() error { return nil } 75 76 // RegisterOptions registers options for this munger; returns any that require a restart when changed. 77 func (h *ApprovalHandler) RegisterOptions(opts *options.Options) sets.String { 78 opts.RegisterBool(&h.issueRequired, "approval-requires-issue", false, "[approval-handler] flag indicating if all "+ 79 "PRs must be associated with an issue in order to get approved label") 80 opts.RegisterBool(&h.implicitSelfApproval, "implicit-self-approval", false, "[approval-handler] flag indicating if "+ 81 "a PR author implicitly approves their own PR") 82 return nil 83 } 84 85 // Returns associated issue, or 0 if it can't find any. 86 // This is really simple, and could be improved later. 87 func findAssociatedIssue(body *string) int { 88 if body == nil { 89 return 0 90 } 91 match := AssociatedIssueRegex.FindStringSubmatch(*body) 92 if len(match) == 0 { 93 return 0 94 } 95 v, err := strconv.Atoi(match[1]) 96 if err != nil { 97 return 0 98 } 99 return v 100 } 101 102 // Munge is the workhorse the will actually make updates to the PR 103 // The algorithm goes as: 104 // - Initially, we build an approverSet 105 // - Go through all comments in order of creation. 106 // - (Issue/PR comments, PR review comments, and PR review bodies are considered as comments) 107 // - If anyone said "/approve" or "/lgtm", add them to approverSet. 108 // - Then, for each file, we see if any approver of this file is in approverSet and keep track of files without approval 109 // - An approver of a file is defined as: 110 // - Someone listed as an "approver" in an OWNERS file in the files directory OR 111 // - in one of the file's parent directorie 112 // - Iff all files have been approved, the bot will add the "approved" label. 113 // - Iff a cancel command is found, that reviewer will be removed from the approverSet 114 // and the munger will remove the approved label if it has been applied 115 func (h *ApprovalHandler) Munge(obj *github.MungeObject) { 116 if !obj.IsPR() { 117 return 118 } 119 filenames := []string{} 120 files, ok := obj.ListFiles() 121 if !ok { 122 return 123 } 124 for _, fn := range files { 125 filenames = append(filenames, *fn.Filename) 126 } 127 issueComments, ok := obj.ListComments() 128 if !ok { 129 return 130 } 131 reviewComments, ok := obj.ListReviewComments() 132 if !ok { 133 return 134 } 135 reviews, ok := obj.ListReviews() 136 if !ok { 137 return 138 } 139 commentsFromIssueComments := c.FromIssueComments(issueComments) 140 comments := append(c.FromReviewComments(reviewComments), commentsFromIssueComments...) 141 comments = append(comments, c.FromReviews(reviews)...) 142 sort.SliceStable(comments, func(i, j int) bool { 143 return comments[i].CreatedAt.Before(*comments[j].CreatedAt) 144 }) 145 approveComments := getApproveComments(comments, h.botName) 146 147 approversHandler := approvers.NewApprovers( 148 approvers.NewOwners( 149 filenames, 150 h.features.Repos, 151 int64(*obj.Issue.Number))) 152 approversHandler.AssociatedIssue = findAssociatedIssue(obj.Issue.Body) 153 approversHandler.RequireIssue = h.issueRequired 154 addApprovers(&approversHandler, approveComments) 155 // Author implicitly approves their own PR if config allows it 156 if h.implicitSelfApproval { 157 if obj.Issue.User != nil && obj.Issue.User.Login != nil { 158 url := "" 159 if obj.Issue.HTMLURL != nil { 160 // Append extra # so that it doesn't reload the page. 161 url = *obj.Issue.HTMLURL + "#" 162 } 163 approversHandler.AddAuthorSelfApprover(*obj.Issue.User.Login, url) 164 } 165 } 166 167 for _, user := range obj.Issue.Assignees { 168 if user != nil && user.Login != nil { 169 approversHandler.AddAssignees(*user.Login) 170 } 171 } 172 173 notificationMatcher := c.MungerNotificationName(approvers.ApprovalNotificationName, h.botName) 174 175 notifications := c.FilterComments(commentsFromIssueComments, notificationMatcher) 176 latestNotification := notifications.GetLast() 177 latestApprove := approveComments.GetLast() 178 newMessage := h.updateNotification(obj.Org(), obj.Project(), latestNotification, latestApprove, approversHandler) 179 if newMessage != nil { 180 for _, notif := range notifications { 181 obj.DeleteComment(notif.Source.(*githubapi.IssueComment)) 182 } 183 obj.WriteComment(*newMessage) 184 } 185 186 if !approversHandler.IsApproved() { 187 if obj.HasLabel(approvedLabel) && !humanAddedApproved(obj) { 188 obj.RemoveLabel(approvedLabel) 189 } 190 } else { 191 //pr is fully approved 192 if !obj.HasLabel(approvedLabel) { 193 obj.AddLabel(approvedLabel) 194 } 195 } 196 197 } 198 199 func humanAddedApproved(obj *github.MungeObject) bool { 200 events, ok := obj.GetEvents() 201 if !ok { 202 return false 203 } 204 approveAddedMatcher := event.And([]event.Matcher{event.AddLabel{}, event.LabelName(approvedLabel)}) 205 labelEvents := event.FilterEvents(events, approveAddedMatcher) 206 lastAdded := labelEvents.GetLast() 207 if lastAdded == nil || lastAdded.Actor == nil || lastAdded.Actor.Login == nil { 208 return false 209 } 210 return !obj.IsRobot(lastAdded.Actor) 211 } 212 213 func getApproveComments(comments []*c.Comment, botName string) c.FilteredComments { 214 approverMatcher := c.CommandName(approveCommand) 215 lgtmMatcher := c.CommandName(lgtmLabel) 216 return c.FilterComments(comments, c.And{c.HumanActor(botName), c.Or{approverMatcher, lgtmMatcher}}) 217 } 218 219 func (h *ApprovalHandler) updateNotification(org, project string, latestNotification, latestApprove *c.Comment, approversHandler approvers.Approvers) *string { 220 if latestNotification != nil && (latestApprove == nil || latestApprove.CreatedAt.Before(*latestNotification.CreatedAt)) { 221 // if we have an existing notification AND 222 // the latestApprove happened before we updated 223 // the notification, we do NOT need to update 224 return nil 225 } 226 return approvers.GetMessage(approversHandler, org, project) 227 } 228 229 // addApprovers iterates through the list of comments on a PR 230 // and identifies all of the people that have said /approve and adds 231 // them to the Approvers. The function uses the latest approve or cancel comment 232 // to determine the Users intention 233 func addApprovers(approversHandler *approvers.Approvers, approveComments c.FilteredComments) { 234 for _, comment := range approveComments { 235 commands := c.ParseCommands(comment) 236 for _, cmd := range commands { 237 if cmd.Name != approveCommand && cmd.Name != lgtmCommand { 238 continue 239 } 240 if comment.Author == nil { 241 continue 242 } 243 244 if cmd.Arguments == cancelArgument { 245 approversHandler.RemoveApprover(*comment.Author) 246 } else { 247 url := "" 248 if comment.HTMLURL != nil { 249 url = *comment.HTMLURL 250 } 251 252 if cmd.Name == approveCommand { 253 approversHandler.AddApprover( 254 *comment.Author, 255 url, 256 cmd.Arguments == noIssueArgument, 257 ) 258 } else { 259 approversHandler.AddLGTMer( 260 *comment.Author, 261 url, 262 cmd.Arguments == noIssueArgument, 263 ) 264 } 265 266 } 267 } 268 } 269 }