Skip to content

Commit 7ad19d7

Browse files
thehowlaeddi
andauthored
fix(github-bot): reviewer requirement fixes (#3706)
- deduplicate reviews, so that we only consider 1 review per author, and the one that is the most relevant (ie. the latest approval/rejection if available) - when requesting a review for the team members requirement, ensure that we don't already have reviews by people in that team before requesting it (should stop the spam of review requests on tech-staff) - for the review by user requirement, check first if we have a matching review by the given user, before requesting a review by them. --------- Co-authored-by: Antoine Eddi <[email protected]>
1 parent 3c604b4 commit 7ad19d7

File tree

2 files changed

+274
-64
lines changed

2 files changed

+274
-64
lines changed

contribs/github-bot/internal/requirements/reviewer.go

+83-43
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,38 @@ import (
1111
"github.com/xlab/treeprint"
1212
)
1313

14+
// deduplicateReviews returns a list of reviews with at most 1 review per
15+
// author, where approval/changes requested reviews are preferred over comments
16+
// and later reviews are preferred over earlier ones.
17+
func deduplicateReviews(reviews []*github.PullRequestReview) []*github.PullRequestReview {
18+
added := make(map[string]int)
19+
result := make([]*github.PullRequestReview, 0, len(reviews))
20+
for _, rev := range reviews {
21+
idx, ok := added[rev.User.GetLogin()]
22+
switch utils.ReviewState(rev.GetState()) {
23+
case utils.ReviewStateApproved, utils.ReviewStateChangesRequested:
24+
// this review changes the "approval state", and is more relevant,
25+
// so substitute it with the previous one if it exists.
26+
if ok {
27+
result[idx] = rev
28+
} else {
29+
result = append(result, rev)
30+
added[rev.User.GetLogin()] = len(result) - 1
31+
}
32+
case utils.ReviewStateCommented:
33+
// this review does not change the "approval state", so only append
34+
// it if a previous review doesn't exist.
35+
if !ok {
36+
result = append(result, rev)
37+
added[rev.User.GetLogin()] = len(result) - 1
38+
}
39+
default:
40+
panic(fmt.Sprintf("invalid review state %q", rev.GetState()))
41+
}
42+
}
43+
return result
44+
}
45+
1446
// ReviewByUserRequirement asserts that there is a review by the given user,
1547
// and if given that the review matches the desiredState.
1648
type ReviewByUserRequirement struct {
@@ -28,6 +60,23 @@ func (r *ReviewByUserRequirement) IsSatisfied(pr *github.PullRequest, details tr
2860
detail += fmt.Sprintf(" (with state %q)", r.desiredState)
2961
}
3062

63+
// Check if user already approved this PR.
64+
reviews, err := r.gh.ListPRReviews(pr.GetNumber())
65+
if err != nil {
66+
r.gh.Logger.Errorf("unable to check if user %s already approved this PR: %v", r.user, err)
67+
return utils.AddStatusNode(false, detail, details)
68+
}
69+
reviews = deduplicateReviews(reviews)
70+
71+
for _, review := range reviews {
72+
if review.GetUser().GetLogin() == r.user {
73+
r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState())
74+
result := r.desiredState == "" || review.GetState() == r.desiredState
75+
return utils.AddStatusNode(result, detail, details)
76+
}
77+
}
78+
r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber())
79+
3180
// If not a dry run, make the user a reviewer if he's not already.
3281
if !r.gh.DryRun {
3382
requested := false
@@ -62,22 +111,6 @@ func (r *ReviewByUserRequirement) IsSatisfied(pr *github.PullRequest, details tr
62111
}
63112
}
64113

65-
// Check if user already approved this PR.
66-
reviews, err := r.gh.ListPRReviews(pr.GetNumber())
67-
if err != nil {
68-
r.gh.Logger.Errorf("unable to check if user %s already approved this PR: %v", r.user, err)
69-
return utils.AddStatusNode(false, detail, details)
70-
}
71-
72-
for _, review := range reviews {
73-
if review.GetUser().GetLogin() == r.user {
74-
r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState())
75-
result := r.desiredState == "" || review.GetState() == r.desiredState
76-
return utils.AddStatusNode(result, detail, details)
77-
}
78-
}
79-
r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber())
80-
81114
return utils.AddStatusNode(false, detail, details)
82115
}
83116

@@ -123,6 +156,14 @@ func (r *ReviewByTeamMembersRequirement) IsSatisfied(pr *github.PullRequest, det
123156
return utils.AddStatusNode(false, detail, details)
124157
}
125158

159+
reviews, err := r.gh.ListPRReviews(pr.GetNumber())
160+
if err != nil {
161+
r.gh.Logger.Errorf("unable to fetch existing reviews of pr %d: %v", pr.GetNumber(), err)
162+
return utils.AddStatusNode(false, detail, details)
163+
}
164+
165+
reviews = deduplicateReviews(reviews)
166+
126167
// If not a dry run, request a team review if no member has reviewed yet,
127168
// and the team review has not been requested.
128169
if !r.gh.DryRun {
@@ -144,12 +185,18 @@ func (r *ReviewByTeamMembersRequirement) IsSatisfied(pr *github.PullRequest, det
144185

145186
if !teamRequested {
146187
for _, user := range reviewers.Users {
147-
if slices.ContainsFunc(teamMembers, func(memb *github.User) bool {
148-
return memb.GetID() == user.GetID()
149-
}) {
188+
if containsUserWithLogin(teamMembers, user.GetLogin()) {
150189
usersRequested = append(usersRequested, user.GetLogin())
151190
}
152191
}
192+
193+
for _, rev := range reviews {
194+
// if not already requested and user is a team member...
195+
if !slices.Contains(usersRequested, rev.User.GetLogin()) &&
196+
containsUserWithLogin(teamMembers, rev.User.GetLogin()) {
197+
usersRequested = append(usersRequested, rev.User.GetLogin())
198+
}
199+
}
153200
}
154201

155202
switch {
@@ -176,33 +223,29 @@ func (r *ReviewByTeamMembersRequirement) IsSatisfied(pr *github.PullRequest, det
176223

177224
// Check how many members of this team already reviewed this PR.
178225
reviewCount := uint(0)
179-
reviews, err := r.gh.ListPRReviews(pr.GetNumber())
180-
if err != nil {
181-
r.gh.Logger.Errorf("unable to check if a member of team %s already reviewed this PR: %v", r.team, err)
182-
return utils.AddStatusNode(false, detail, details)
183-
}
184226

185-
stateStr := ""
186-
if r.desiredState != "" {
187-
stateStr = fmt.Sprintf("%q ", r.desiredState)
188-
}
189227
for _, review := range reviews {
190-
for _, member := range teamMembers {
191-
if review.GetUser().GetLogin() == member.GetLogin() {
192-
if desired := r.desiredState; desired == "" || desired == review.GetState() {
193-
reviewCount += 1
194-
}
195-
r.gh.Logger.Debugf(
196-
"Member %s from team %s already reviewed PR %d with state %s (%d/%d required %sreview(s))",
197-
member.GetLogin(), r.team, pr.GetNumber(), review.GetState(), reviewCount, r.count, stateStr,
198-
)
228+
login := review.GetUser().GetLogin()
229+
if containsUserWithLogin(teamMembers, login) {
230+
if desired := r.desiredState; desired == "" || desired == review.GetState() {
231+
reviewCount += 1
199232
}
233+
r.gh.Logger.Debugf(
234+
"Member %s from team %s already reviewed PR %d with state %s (%d/%d required review(s) with state %q)",
235+
login, r.team, pr.GetNumber(), review.GetState(), reviewCount, r.count, r.desiredState,
236+
)
200237
}
201238
}
202239

203240
return utils.AddStatusNode(reviewCount >= r.count, detail, details)
204241
}
205242

243+
func containsUserWithLogin(users []*github.User, login string) bool {
244+
return slices.ContainsFunc(users, func(u *github.User) bool {
245+
return u.GetLogin() == login
246+
})
247+
}
248+
206249
// WithCount specifies the number of required reviews.
207250
// By default, this is 1.
208251
func (r *ReviewByTeamMembersRequirement) WithCount(n uint) *ReviewByTeamMembersRequirement {
@@ -262,20 +305,17 @@ func (r *ReviewByOrgMembersRequirement) IsSatisfied(pr *github.PullRequest, deta
262305
r.gh.Logger.Errorf("unable to check number of reviews on this PR: %v", err)
263306
return utils.AddStatusNode(false, detail, details)
264307
}
308+
reviews = deduplicateReviews(reviews)
265309

266-
stateStr := ""
267-
if r.desiredState != "" {
268-
stateStr = fmt.Sprintf("%q ", r.desiredState)
269-
}
270310
for _, review := range reviews {
271311
if review.GetAuthorAssociation() == "MEMBER" {
272312
if r.desiredState == "" || review.GetState() == r.desiredState {
273313
reviewed++
274314
}
275315
r.gh.Logger.Debugf(
276-
"Member %s already reviewed PR %d with state %s (%d/%d required %sreviews)",
316+
"Member %s already reviewed PR %d with state %s (%d/%d required reviews with state %q)",
277317
review.GetUser().GetLogin(), pr.GetNumber(), review.GetState(),
278-
reviewed, r.count, stateStr,
318+
reviewed, r.count, r.desiredState,
279319
)
280320
}
281321
}

0 commit comments

Comments
 (0)