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  }