github.com/abayer/test-infra@v0.0.5/mungegithub/mungers/inactive-review-handler.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 mungers 18 19 import ( 20 "fmt" 21 "time" 22 23 "github.com/golang/glog" 24 githubapi "github.com/google/go-github/github" 25 "k8s.io/apimachinery/pkg/util/sets" 26 "k8s.io/test-infra/mungegithub/features" 27 "k8s.io/test-infra/mungegithub/github" 28 "k8s.io/test-infra/mungegithub/mungers/matchers" 29 "k8s.io/test-infra/mungegithub/options" 30 ) 31 32 const ( 33 NOTIFNAME = "INACTIVE-PULL-REQUEST" 34 CREATIONTIMECAP = 36 * 30 * 24 * time.Hour //period since PR creation time 35 COMMENTTIMECAP = 7 * 24 * time.Hour //period since last IssueComment and PullRequestComment being posted 36 REMINDERNUMCAP = 5 //maximum number of times this munger will post reminder IssueComment 37 LEAFOWNERSONLY = false //setting for Blunderbuss to fetch only leaf owners or all owners 38 ) 39 40 type InactiveReviewHandler struct { 41 botName string 42 features *features.Features 43 } 44 45 func init() { 46 h := &InactiveReviewHandler{} 47 RegisterMungerOrDie(h) 48 } 49 50 func (i *InactiveReviewHandler) Name() string { return "inactive-review-handler" } 51 52 func (i *InactiveReviewHandler) RequiredFeatures() []string { 53 return []string{features.RepoFeatureName} 54 } 55 56 func (i *InactiveReviewHandler) Initialize(config *github.Config, features *features.Features) error { 57 i.botName = config.BotName 58 i.features = features 59 return nil 60 } 61 62 // EachLoop is called at the start of every munge loop 63 func (i *InactiveReviewHandler) EachLoop() error { return nil } 64 65 // RegisterOptions registers options for this munger; returns any that require a restart when changed. 66 func (*InactiveReviewHandler) RegisterOptions(opts *options.Options) sets.String { return nil } 67 68 func (i *InactiveReviewHandler) haveNonAuthorHuman(authorName *string, comments []*githubapi.IssueComment, reviewComments []*githubapi.PullRequestComment) bool { 69 return !matchers.Items{}. 70 AddComments(comments...). 71 AddReviewComments(reviewComments...). 72 Filter(matchers.HumanActor(i.botName)). 73 Filter(matchers.Not(matchers.AuthorLogin(*authorName))). 74 IsEmpty() 75 } 76 77 // Suggest a new reviewer who is NOT any of the existing reviewers 78 // (1) get all current assignees for the PR 79 // (2) get potential owners of the PR using Blunderbuss algorithm (calling getPotentialOwners() function) 80 // (3) filter out current assignees from the potential owners 81 // (4) if there is no any new reviewer available, the bot will encourage the PR author to ping all existing assignees 82 // (5) otherwise, select a new reviewer using Blunderbuss algorithm (calling selectMultipleOwners() function with number of assignees parameter of one) 83 // Note: the munger will suggest a new reviewer when the PR currently does not have any reviewer 84 func (i *InactiveReviewHandler) suggestNewReviewer(issue *githubapi.Issue, potentialOwners weightMap, weightSum int64) string { 85 var newReviewer string 86 87 if len(issue.Assignees) > 0 { 88 for _, oldReviewer := range issue.Assignees { 89 login := *oldReviewer.Login 90 91 for potentialOwner := range potentialOwners { 92 if login == potentialOwner { 93 weightSum -= potentialOwners[login] 94 delete(potentialOwners, login) 95 break 96 } 97 } 98 } 99 } 100 101 if len(potentialOwners) > 0 { 102 newReviewer = selectMultipleOwners(potentialOwners, weightSum, 1)[0] 103 } 104 105 return newReviewer 106 } 107 108 // Munge is the workhorse encouraging PR author to assign a new reviewer 109 // after getting no response from current reviewer for "COMMENTTIMECAP" duration 110 // The algorithm: 111 // (1) find latest comment posting time 112 // (2) if the time is "COMMENTTIMECAP" or longer before today's time, create a comment 113 // encouraging the author to assign a new reviewer and unassign the old reviewer 114 // (3) suggest the new reviewer using Blunderbuss algorithm, making sure the old reviewer is not suggested 115 // Note: the munger will post at most "REMINDERNUMCAP" number of times 116 func (i *InactiveReviewHandler) Munge(obj *github.MungeObject) { 117 issue := obj.Issue 118 119 // do not suggest new reviewer if it is not a PR, the PR has no author information, or 120 // the PR has been created more than 3 years ago (36 months with 30 days per month) 121 if !obj.IsPR() || issue.User == nil || issue.User.Login == nil || 122 time.Since(*issue.CreatedAt) > CREATIONTIMECAP { 123 return 124 } 125 126 comments, ok := obj.ListComments() 127 if !ok { 128 return 129 } 130 131 reviewComments, ok := obj.ListReviewComments() 132 if !ok { 133 return 134 } 135 136 // return if there is at least a non-author human 137 if i.haveNonAuthorHuman(issue.User.Login, comments, reviewComments) { 138 return 139 } 140 141 files, ok := obj.ListFiles() 142 if !ok || len(files) == 0 { 143 glog.Errorf("failed to detect any changed file when assigning a new reviewer for inactive PR #%v", *obj.Issue.Number) 144 return 145 } 146 147 pinger := matchers.NewPinger(NOTIFNAME, i.botName).SetTimePeriod(COMMENTTIMECAP).SetMaxCount(REMINDERNUMCAP) 148 notification := pinger.PingNotification(comments, "", issue.CreatedAt) 149 150 // return if the munger has created comments for "REMINDERNUMCAP" number of times, or 151 // the munger has created the comment within "COMMENTTIMECAP", or 152 // the PR is created within "CREATIONTIMECAP" 153 if notification == nil { 154 return 155 } 156 157 // only run Blunderbuss algorithm when ping limit is not reached 158 potentialOwners, weightSum := getPotentialOwners(*issue.User.Login, i.features, files, LEAFOWNERSONLY) 159 newReviewer := i.suggestNewReviewer(issue, potentialOwners, weightSum) 160 var msg string 161 162 if len(issue.Assignees) == 0 { 163 msg = fmt.Sprintf("To expedite a review, consider assigning _%s_.", newReviewer) 164 } else if len(newReviewer) == 0 { 165 msg = fmt.Sprintf("Sorry the review process for your PR has stalled. Your reviewer(s) may be on vacation or otherwise occupied. Consider pinging them.") 166 } else { 167 msg = fmt.Sprintf("Sorry the review process for your PR has stalled. Your reviewer(s) may be on vacation or otherwise occupied. Consider unassigning them using `/unassign` command, and assigning _%s_.", newReviewer) 168 } 169 170 //reinsert the message if the munger can create the comment 171 notification.Arguments = msg 172 173 if err := notification.Post(obj); err != nil { 174 glog.Errorf("failed to leave comment encouraging %s to assign a new reviewer for inactive PR #%v", *issue.User.Login, *issue.Number) 175 } 176 }