Repository: gnolang/gno
Pull request #4145: fix: github-bot multiple fixes on ReviewBy requirements
Author: login: aeddi
Created: 2025-04-14T22:17:28Z | Merged: 2025-04-15T06:23:26Z

This PR:
- https://github.com/gnolang/gno/commit/f99df53babbb496691df264b05d2fadb7dcb673b Improves the `gnoweb` review rule to make it fit with [this description](https://github.com/gnolang/gno/issues/3238#issuecomment-2793635486):
> a. If Alexis or Guilhem is the author of a PR related to gnoweb, the other is assigned as a reviewer.
b. If someone else is the author of a PR related to gnoweb, both are assigned as reviewers, but only one review from one of them is required.
- https://github.com/gnolang/gno/commit/7088bffb4e5a06c4e0b6ccf8350cd90b8f70edc3 Prevents [an error](https://github.com/gnolang/gno/issues/3238#issuecomment-2783717659) when the bot try to request a review from the PR author.
- https://github.com/gnolang/gno/commit/24b473848776f9c63322718868ef67e43e3b3e3e Adds a `RequestAction` mechanism similar to the `LabelAction` to the `ReviewByUser` and `ReviewByTeam` requirements.


Diff:
diff --git a/contribs/github-bot/internal/config/config.go b/contribs/github-bot/internal/config/config.go
index 6fdc1a1cfc8..b40ac43761f 100644
--- a/contribs/github-bot/internal/config/config.go
+++ b/contribs/github-bot/internal/config/config.go
@@ -48,11 +48,11 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) {
 			Then: r.And(
 				r.Or(
 					r.AuthorInTeam(gh, "tech-staff"),
-					r.ReviewByTeamMembers(gh, "tech-staff").WithDesiredState(utils.ReviewStateApproved),
+					r.ReviewByTeamMembers(gh, "tech-staff", r.RequestApply).WithDesiredState(utils.ReviewStateApproved),
 				),
 				r.Or(
 					r.AuthorInTeam(gh, "devrels"),
-					r.ReviewByTeamMembers(gh, "devrels").WithDesiredState(utils.ReviewStateApproved),
+					r.ReviewByTeamMembers(gh, "devrels", r.RequestApply).WithDesiredState(utils.ReviewStateApproved),
 				),
 			),
 		},
@@ -63,8 +63,26 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) {
 				c.FileChanged(gh, "^gno.land/pkg/gnoweb/"),
 			),
 			Then: r.Or(
-				r.ReviewByUser(gh, "alexiscolin"),
-				r.ReviewByUser(gh, "gfanton"),
+				// If alexiscolin or gfanton is the author of the PR, the other must review it.
+				r.Or(
+					r.And(
+						r.Author("alexiscolin"),
+						r.ReviewByUser(gh, "gfanton", r.RequestApply).WithDesiredState(utils.ReviewStateApproved),
+					),
+					r.And(
+						r.Author("gfanton"),
+						r.ReviewByUser(gh, "alexiscolin", r.RequestApply).WithDesiredState(utils.ReviewStateApproved),
+					),
+				),
+				// If neither of them is the author of the PR, at least one of them must review it.
+				r.And(
+					r.Not(r.Author("alexiscolin")),
+					r.Not(r.Author("gfanton")),
+					r.Or(
+						r.ReviewByUser(gh, "alexiscolin", r.RequestApply).WithDesiredState(utils.ReviewStateApproved),
+						r.ReviewByUser(gh, "gfanton", r.RequestApply).WithDesiredState(utils.ReviewStateApproved),
+					),
+				),
 			),
 		},
 		{
@@ -81,16 +99,29 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) {
 			Then: r.
 				If(r.Or(
 					r.ReviewByOrgMembers(gh).WithDesiredState(utils.ReviewStateApproved),
-					r.ReviewByTeamMembers(gh, "tech-staff"),
+					r.ReviewByTeamMembers(gh, "tech-staff", r.RequestIgnore),
 					r.Draft(),
 				)).
 				// Either there was a first approval from a member, and we
-				// assert that the label for triage-pending is removed...
-				Then(r.Not(r.Label(gh, "review/triage-pending", r.LabelRemove))).
-				// Or there was not, and we apply the triage pending label.
+				// assert that the label for triage-pending is removed and we
+				// request a review from the tech-staff team...
+				Then(
+					r.And(
+						r.Not(r.Label(gh, "review/triage-pending", r.LabelRemove)),
+						r.ReviewByTeamMembers(gh, "tech-staff", r.RequestApply),
+					),
+				).
+				// Or there was not, and we apply the triage-pending label
+				// and remove the request for review from the tech-staff team.
 				// The requirement should always fail, to mark the PR is not
 				// ready to be merged.
-				Else(r.And(r.Label(gh, "review/triage-pending", r.LabelApply), r.Never())),
+				Else(
+					r.And(
+						r.Label(gh, "review/triage-pending", r.LabelApply),
+						r.ReviewByTeamMembers(gh, "tech-staff", r.RequestRemove),
+						r.Never(), // Always fail the requirement.
+					),
+				),
 		},
 	}
 
diff --git a/contribs/github-bot/internal/requirements/reviewer.go b/contribs/github-bot/internal/requirements/reviewer.go
index 6562c30df69..5ecd5978874 100644
--- a/contribs/github-bot/internal/requirements/reviewer.go
+++ b/contribs/github-bot/internal/requirements/reviewer.go
@@ -11,6 +11,18 @@ import (
 	"github.com/xlab/treeprint"
 )
 
+// RequestAction controls what to do about the review request.
+type RequestAction byte
+
+const (
+	// RequestApply will request a review from the user/team if not already requested.
+	RequestApply = iota
+	// RequestRemove will remove the review request from the user/team.
+	RequestRemove
+	// RequestIgnore always leaves the review request as it is.
+	RequestIgnore
+)
+
 // deduplicateReviews returns a list of reviews with at most 1 review per
 // author, where approval/changes requested reviews are preferred over comments
 // and later reviews are preferred over earlier ones.
@@ -60,6 +72,7 @@ type ReviewByUserRequirement struct {
 	gh           *client.GitHub
 	user         string
 	desiredState string
+	action       RequestAction
 }
 
 var _ Requirement = &ReviewByUserRequirement{}
@@ -88,8 +101,9 @@ func (r *ReviewByUserRequirement) IsSatisfied(pr *github.PullRequest, details tr
 	}
 	r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber())
 
-	// If not a dry run, make the user a reviewer if he's not already.
-	if !r.gh.DryRun {
+	// If not a dry run, change the review request according to the action.
+	if !r.gh.DryRun && r.action != RequestIgnore {
+		// Check if this user is already requested for review.
 		requested := false
 		reviewers, err := r.gh.ListPRReviewers(pr.GetNumber())
 		if err != nil {
@@ -104,20 +118,44 @@ func (r *ReviewByUserRequirement) IsSatisfied(pr *github.PullRequest, details tr
 			}
 		}
 
-		if requested {
-			r.gh.Logger.Debugf("Review of user %s already requested on PR %d", r.user, pr.GetNumber())
-		} else {
-			r.gh.Logger.Debugf("Requesting review from user %s on PR %d", r.user, pr.GetNumber())
-			if _, _, err := r.gh.Client.PullRequests.RequestReviewers(
-				r.gh.Ctx,
-				r.gh.Owner,
-				r.gh.Repo,
-				pr.GetNumber(),
-				github.ReviewersRequest{
-					Reviewers: []string{r.user},
-				},
-			); err != nil {
-				r.gh.Logger.Errorf("Unable to request review from user %s on PR %d: %v", r.user, pr.GetNumber(), err)
+		switch r.action {
+		case RequestApply:
+			switch {
+			case requested:
+				r.gh.Logger.Debugf("Review of user %s already requested on PR %d", r.user, pr.GetNumber())
+			case r.user == pr.GetUser().GetLogin():
+				r.gh.Logger.Debugf("Review of user %s is not requested on PR %d because he's the author", r.user, pr.GetNumber())
+			default:
+				r.gh.Logger.Debugf("Requesting review from user %s on PR %d", r.user, pr.GetNumber())
+				if _, _, err := r.gh.Client.PullRequests.RequestReviewers(
+					r.gh.Ctx,
+					r.gh.Owner,
+					r.gh.Repo,
+					pr.GetNumber(),
+					github.ReviewersRequest{
+						Reviewers: []string{r.user},
+					},
+				); err != nil {
+					r.gh.Logger.Errorf("Unable to request review from user %s on PR %d: %v", r.user, pr.GetNumber(), err)
+				}
+			}
+		case RequestRemove:
+			switch {
+			case !requested:
+				r.gh.Logger.Debugf("Review of user %s already not requested on PR %d", r.user, pr.GetNumber())
+			default:
+				r.gh.Logger.Debugf("Removing review request from user %s on PR %d", r.user, pr.GetNumber())
+				if _, err := r.gh.Client.PullRequests.RemoveReviewers(
+					r.gh.Ctx,
+					r.gh.Owner,
+					r.gh.Repo,
+					pr.GetNumber(),
+					github.ReviewersRequest{
+						Reviewers: []string{r.user},
+					},
+				); err != nil {
+					r.gh.Logger.Errorf("Unable to remove review request from user %s on PR %d: %v", r.user, pr.GetNumber(), err)
+				}
 			}
 		}
 	}
@@ -138,8 +176,12 @@ func (r *ReviewByUserRequirement) WithDesiredState(s utils.ReviewState) *ReviewB
 }
 
 // ReviewByUser asserts that the PR has been reviewed by the given user.
-func ReviewByUser(gh *client.GitHub, user string) *ReviewByUserRequirement {
-	return &ReviewByUserRequirement{gh: gh, user: user}
+func ReviewByUser(gh *client.GitHub, user string, action RequestAction) *ReviewByUserRequirement {
+	return &ReviewByUserRequirement{
+		gh:     gh,
+		user:   user,
+		action: action,
+	}
 }
 
 // ReviewByTeamMembersRequirement asserts that count members of the given team
@@ -150,6 +192,7 @@ type ReviewByTeamMembersRequirement struct {
 	team         string
 	count        uint
 	desiredState string
+	action       RequestAction
 }
 
 var _ Requirement = &ReviewByTeamMembersRequirement{}
@@ -177,7 +220,8 @@ func (r *ReviewByTeamMembersRequirement) IsSatisfied(pr *github.PullRequest, det
 
 	// If not a dry run, request a team review if no member has reviewed yet,
 	// and the team review has not been requested.
-	if !r.gh.DryRun {
+	if !r.gh.DryRun && r.action != RequestIgnore {
+		// Check if the team or any of its members are already requested for review.
 		var teamRequested bool
 		var usersRequested []string
 
@@ -210,24 +254,45 @@ func (r *ReviewByTeamMembersRequirement) IsSatisfied(pr *github.PullRequest, det
 			}
 		}
 
-		switch {
-		case teamRequested:
-			r.gh.Logger.Debugf("Review of team %s already requested on PR %d", r.team, pr.GetNumber())
-		case len(usersRequested) > 0:
-			r.gh.Logger.Debugf("Members %v of team %s already requested on (or reviewed) PR %d",
-				usersRequested, r.team, pr.GetNumber())
-		default:
-			r.gh.Logger.Debugf("Requesting review from team %s on PR %d", r.team, pr.GetNumber())
-			if _, _, err := r.gh.Client.PullRequests.RequestReviewers(
-				r.gh.Ctx,
-				r.gh.Owner,
-				r.gh.Repo,
-				pr.GetNumber(),
-				github.ReviewersRequest{
-					TeamReviewers: []string{r.team},
-				},
-			); err != nil {
-				r.gh.Logger.Errorf("Unable to request review from team %s on PR %d: %v", r.team, pr.GetNumber(), err)
+		switch r.action {
+		case RequestApply:
+			switch {
+			case teamRequested:
+				r.gh.Logger.Debugf("Review of team %s already requested on PR %d", r.team, pr.GetNumber())
+			case len(usersRequested) > 0:
+				r.gh.Logger.Debugf("Members %v of team %s already requested on (or reviewed) PR %d",
+					usersRequested, r.team, pr.GetNumber())
+			default:
+				r.gh.Logger.Debugf("Requesting review from team %s on PR %d", r.team, pr.GetNumber())
+				if _, _, err := r.gh.Client.PullRequests.RequestReviewers(
+					r.gh.Ctx,
+					r.gh.Owner,
+					r.gh.Repo,
+					pr.GetNumber(),
+					github.ReviewersRequest{
+						TeamReviewers: []string{r.team},
+					},
+				); err != nil {
+					r.gh.Logger.Errorf("Unable to request review from team %s on PR %d: %v", r.team, pr.GetNumber(), err)
+				}
+			}
+		case RequestRemove:
+			switch {
+			case !teamRequested:
+				r.gh.Logger.Debugf("Review of team %s already not requested on PR %d", r.team, pr.GetNumber())
+			default:
+				r.gh.Logger.Debugf("Removing review request from team %s on PR %d", r.team, pr.GetNumber())
+				if _, err := r.gh.Client.PullRequests.RemoveReviewers(
+					r.gh.Ctx,
+					r.gh.Owner,
+					r.gh.Repo,
+					pr.GetNumber(),
+					github.ReviewersRequest{
+						TeamReviewers: []string{r.team},
+					},
+				); err != nil {
+					r.gh.Logger.Errorf("Unable to remove review request from team %s on PR %d: %v", r.team, pr.GetNumber(), err)
+				}
 			}
 		}
 	}
@@ -283,11 +348,12 @@ func (r *ReviewByTeamMembersRequirement) WithDesiredState(s utils.ReviewState) *
 //
 // The number of required reviews, or the state of the reviews (e.g., to filter
 // only for approval reviews) can be modified using WithCount and WithDesiredState.
-func ReviewByTeamMembers(gh *client.GitHub, team string) *ReviewByTeamMembersRequirement {
+func ReviewByTeamMembers(gh *client.GitHub, team string, action RequestAction) *ReviewByTeamMembersRequirement {
 	return &ReviewByTeamMembersRequirement{
-		gh:    gh,
-		team:  team,
-		count: 1,
+		gh:     gh,
+		team:   team,
+		count:  1,
+		action: action,
 	}
 }
 
diff --git a/contribs/github-bot/internal/requirements/reviewer_test.go b/contribs/github-bot/internal/requirements/reviewer_test.go
index dac285b6b9f..e391e9e43d4 100644
--- a/contribs/github-bot/internal/requirements/reviewer_test.go
+++ b/contribs/github-bot/internal/requirements/reviewer_test.go
@@ -91,61 +91,80 @@ func Test_deduplicateReviews(t *testing.T) {
 func TestReviewByUser(t *testing.T) {
 	t.Parallel()
 
-	reviewers := github.Reviewers{
-		Users: []*github.User{
-			{Login: github.String("notTheRightOne")},
-			{Login: github.String("user")},
-			{Login: github.String("anotherOne")},
-		},
-	}
+	var (
+		user1    = &github.User{Login: github.String("user1")}
+		user2    = &github.User{Login: github.String("user2")}
+		user3    = &github.User{Login: github.String("user3")}
+		user4    = &github.User{Login: github.String("user4")}
+		prAuthor = &github.User{Login: github.String("prAuthor")}
 
-	reviews := []*github.PullRequestReview{
-		{
-			User:  &github.User{Login: github.String("notTheRightOne")},
-			State: github.String("APPROVED"),
-		}, {
-			User:  &github.User{Login: github.String("user")},
-			State: github.String("APPROVED"),
-		}, {
+		reviewers = github.Reviewers{
+			Users: []*github.User{user1, user2, user3},
+		}
+
+		reviews = []*github.PullRequestReview{
+			{User: user1, State: github.String("APPROVED")},
 			// Should be ignored in favour of the following one
-			User:  &github.User{Login: github.String("anotherOne")},
-			State: github.String("APPROVED"),
-		}, {
-			User:  &github.User{Login: github.String("anotherOne")},
-			State: github.String("CHANGES_REQUESTED"),
-		},
-	}
+			{User: user2, State: github.String("APPROVED")},
+			{User: user2, State: github.String("CHANGES_REQUESTED")},
+		}
+	)
 
 	for _, testCase := range []struct {
 		name        string
 		user        string
+		action      RequestAction
 		isSatisfied bool
-		create      bool
+		isRequested bool
 	}{
-		{"reviewer matches", "user", true, false},
-		{"reviewer matches without approval", "anotherOne", false, false},
-		{"reviewer doesn't match", "user2", false, true},
+		{"reviewer approved with RequestApply", user1.GetLogin(), RequestApply, true, false},
+		{"reviewer approved with RequestIgnore", user1.GetLogin(), RequestIgnore, true, false},
+		{"reviewer approved with RequestRemove", user1.GetLogin(), RequestRemove, true, false},
+
+		{"reviewer requested changes with RequestApply", user2.GetLogin(), RequestApply, false, false},
+		{"reviewer requested changes with RequestIgnore", user2.GetLogin(), RequestIgnore, false, false},
+		{"reviewer requested changes with RequestRemove", user2.GetLogin(), RequestIgnore, false, false},
+
+		{"reviewer not reviewed with RequestApply", user3.GetLogin(), RequestApply, false, false},
+		{"reviewer not reviewed with RequestIgnore", user3.GetLogin(), RequestIgnore, false, false},
+		{"reviewer not reviewed with RequestRemove", user3.GetLogin(), RequestRemove, false, true},
+
+		{"not a reviewer with RequestApply", user4.GetLogin(), RequestApply, false, true},
+		{"not a reviewer with RequestIgnore", user4.GetLogin(), RequestIgnore, false, false},
+		{"not a reviewer with RequestRemove", user4.GetLogin(), RequestRemove, false, false},
+
+		{"reviewer is the PR author with RequestApply", prAuthor.GetLogin(), RequestApply, false, false},
+		{"reviewer is the PR author with RequestIgnore", prAuthor.GetLogin(), RequestIgnore, false, false},
+		{"reviewer is the PR author with RequestRemove", prAuthor.GetLogin(), RequestRemove, false, false},
 	} {
 		t.Run(testCase.name, func(t *testing.T) {
 			t.Parallel()
 
 			firstRequest := true
 			requested := false
+
+			// This is a mock HTTP client that simulates the behavior of the GitHub API.
 			mockedHTTPClient := mock.NewMockedHTTPClient(
+				// This handler simulates the request reviewers endpoint used to
+				// get the reviewers list or request a review.
 				mock.WithRequestMatchHandler(
 					mock.EndpointPattern{
 						Pattern: "/repos/pulls/0/requested_reviewers",
 						Method:  "GET",
 					},
-					http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+					http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+						// The first request is just used to get the reviewers.
 						if firstRequest {
 							w.Write(mock.MustMarshal(reviewers))
 							firstRequest = false
 						} else {
+							// A subsequent request indicates that a review was requested
+							// or a request was removed.
 							requested = true
 						}
 					}),
 				),
+				// This handler simulates the reviews endpoint.
 				mock.WithRequestMatchPages(
 					mock.EndpointPattern{
 						Pattern: "/repos/pulls/0/reviews",
@@ -155,19 +174,23 @@ func TestReviewByUser(t *testing.T) {
 				),
 			)
 
+			// Create a new GitHub client with the mocked HTTP client.
 			gh := &client.GitHub{
 				Client: github.NewClient(mockedHTTPClient),
 				Ctx:    context.Background(),
 				Logger: logger.NewNoopLogger(),
 			}
 
-			pr := &github.PullRequest{}
+			// Create a new PullRequest object with an author
+			pr := &github.PullRequest{User: prAuthor}
+
+			// Run the requirement and check if it is satisfied and if a review was requested.
 			details := treeprint.New()
-			requirement := ReviewByUser(gh, testCase.user).WithDesiredState(utils.ReviewStateApproved)
+			requirement := ReviewByUser(gh, testCase.user, testCase.action).WithDesiredState(utils.ReviewStateApproved)
 
 			assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied))
 			assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied))
-			assert.Equal(t, testCase.create, requested, fmt.Sprintf("requirement should have requested to create item: %t", testCase.create))
+			assert.Equal(t, testCase.isRequested, requested, fmt.Sprintf("requirement should have requested a review: %t", testCase.isRequested))
 		})
 	}
 }
@@ -176,68 +199,42 @@ func TestReviewByTeamMembers(t *testing.T) {
 	t.Parallel()
 
 	var (
-		reviewers = github.Reviewers{
-			Teams: []*github.Team{
-				{Slug: github.String("team1")},
-				{Slug: github.String("team2")},
-				{Slug: github.String("team3")},
-			},
-		}
+		user1 = &github.User{Login: github.String("user1")}
+		user2 = &github.User{Login: github.String("user2")}
+		user3 = &github.User{Login: github.String("user3")}
+		user4 = &github.User{Login: github.String("user4")}
+		user5 = &github.User{Login: github.String("user5")}
+
+		team1 = &github.Team{Slug: github.String("team1")}
+		team2 = &github.Team{Slug: github.String("team2")}
+		team3 = &github.Team{Slug: github.String("team3")}
+
 		noReviewers   = github.Reviewers{}
+		teamReviewers = github.Reviewers{
+			Teams: []*github.Team{team1, team2, team3},
+		}
 		userReviewers = github.Reviewers{
-			Users: []*github.User{
-				{Login: github.String("user1")},
-				{Login: github.String("user2")},
-				{Login: github.String("user3")},
-				{Login: github.String("user4")},
-				{Login: github.String("user5")},
-			},
+			Users: []*github.User{user1, user2, user3, user4, user5},
 		}
-	)
 
-	members := map[string][]*github.User{
-		"team1": {
-			{Login: github.String("user1")},
-			{Login: github.String("user2")},
-			{Login: github.String("user3")},
-		},
-		"team2": {
-			{Login: github.String("user3")},
-			{Login: github.String("user4")},
-			{Login: github.String("user5")},
-		},
-		"team3": {
-			{Login: github.String("user4")},
-			{Login: github.String("user5")},
-		},
-	}
+		members = map[string][]*github.User{
+			team1.GetSlug(): {user1, user2, user3},
+			team2.GetSlug(): {user3, user4, user5},
+			team3.GetSlug(): {user4, user5},
+		}
 
-	reviews := []*github.PullRequestReview{
-		{
-			// only later review should be counted.
-			User:  &github.User{Login: github.String("user1")},
-			State: github.String("CHANGES_REQUESTED"),
-		}, {
-			User:  &github.User{Login: github.String("user1")},
-			State: github.String("APPROVED"),
-		}, {
-			User:  &github.User{Login: github.String("user2")},
-			State: github.String("APPROVED"),
-		}, {
-			User:  &github.User{Login: github.String("user3")},
-			State: github.String("APPROVED"),
-		}, {
-			User:  &github.User{Login: github.String("user4")},
-			State: github.String("CHANGES_REQUESTED"),
-		}, {
-			// only later review should be counted.
-			User:  &github.User{Login: github.String("user5")},
-			State: github.String("APPROVED"),
-		}, {
-			User:  &github.User{Login: github.String("user5")},
-			State: github.String("CHANGES_REQUESTED"),
-		},
-	}
+		reviews = []*github.PullRequestReview{
+			// Only later review should be counted (user1).
+			{User: user1, State: github.String("CHANGES_REQUESTED")},
+			{User: user1, State: github.String("APPROVED")},
+			{User: user2, State: github.String("APPROVED")},
+			{User: user3, State: github.String("APPROVED")},
+			{User: user4, State: github.String("CHANGES_REQUESTED")},
+			// Only later review should be counted (user5).
+			{User: user5, State: github.String("APPROVED")},
+			{User: user5, State: github.String("CHANGES_REQUESTED")},
+		}
+	)
 
 	const (
 		notSatisfied = 0
@@ -253,17 +250,35 @@ func TestReviewByTeamMembers(t *testing.T) {
 		expectedResult byte
 	}{
 		{
-			name: "3/3 team members approved",
-			req: ReviewByTeamMembers(nil, "team1").
+			name: "3/3 team members approved with RequestApply",
+			req: ReviewByTeamMembers(nil, "team1", RequestApply).
+				WithCount(3).
+				WithDesiredState(utils.ReviewStateApproved),
+			reviews:        reviews,
+			reviewers:      teamReviewers,
+			expectedResult: satisfied,
+		},
+		{
+			name: "3/3 team members approved with RequestIgnore",
+			req: ReviewByTeamMembers(nil, "team1", RequestIgnore).
 				WithCount(3).
 				WithDesiredState(utils.ReviewStateApproved),
 			reviews:        reviews,
-			reviewers:      reviewers,
+			reviewers:      teamReviewers,
 			expectedResult: satisfied,
 		},
+		{
+			name: "3/3 team members approved with RequestRemove",
+			req: ReviewByTeamMembers(nil, "team1", RequestRemove).
+				WithCount(3).
+				WithDesiredState(utils.ReviewStateApproved),
+			reviews:        reviews,
+			reviewers:      teamReviewers,
+			expectedResult: satisfied | withRequest,
+		},
 		{
 			name: "3/3 team members approved (with user reviewers)",
-			req: ReviewByTeamMembers(nil, "team1").
+			req: ReviewByTeamMembers(nil, "team1", RequestApply).
 				WithCount(3).
 				WithDesiredState(utils.ReviewStateApproved),
 			reviews:        reviews,
@@ -271,40 +286,94 @@ func TestReviewByTeamMembers(t *testing.T) {
 			expectedResult: satisfied,
 		},
 		{
-			name: "1/1 team member approved",
-			req: ReviewByTeamMembers(nil, "team2").
+			name: "1/1 team member approved with RequestApply",
+			req: ReviewByTeamMembers(nil, "team2", RequestApply).
 				WithDesiredState(utils.ReviewStateApproved),
 			reviews:        reviews,
-			reviewers:      reviewers,
+			reviewers:      teamReviewers,
 			expectedResult: satisfied,
 		},
 		{
-			name: "1/2 team member approved",
-			req: ReviewByTeamMembers(nil, "team2").
+			name: "1/1 team member approved with RequestIgnore",
+			req: ReviewByTeamMembers(nil, "team2", RequestRemove).
+				WithDesiredState(utils.ReviewStateApproved),
+			reviews:        reviews,
+			reviewers:      teamReviewers,
+			expectedResult: satisfied | withRequest,
+		},
+		{
+			name: "1/2 team member approved with RequestApply",
+			req: ReviewByTeamMembers(nil, "team2", RequestApply).
+				WithCount(2).
+				WithDesiredState(utils.ReviewStateApproved),
+			reviews:        reviews,
+			reviewers:      teamReviewers,
+			expectedResult: notSatisfied,
+		},
+		{
+			name: "1/2 team member approved with RequestIgnore",
+			req: ReviewByTeamMembers(nil, "team2", RequestIgnore).
 				WithCount(2).
 				WithDesiredState(utils.ReviewStateApproved),
 			reviews:        reviews,
-			reviewers:      reviewers,
+			reviewers:      teamReviewers,
+			expectedResult: notSatisfied,
+		},
+		{
+			name: "1/2 team member approved with RequestRemove",
+			req: ReviewByTeamMembers(nil, "team2", RequestRemove).
+				WithCount(2).
+				WithDesiredState(utils.ReviewStateApproved),
+			reviews:        reviews,
+			reviewers:      teamReviewers,
+			expectedResult: notSatisfied | withRequest,
+		},
+		{
+			name: "0/1 team member approved with RequestApply",
+			req: ReviewByTeamMembers(nil, "team3", RequestApply).
+				WithDesiredState(utils.ReviewStateApproved),
+			reviews:        reviews,
+			reviewers:      teamReviewers,
 			expectedResult: notSatisfied,
 		},
 		{
-			name: "0/1 team member approved",
-			req: ReviewByTeamMembers(nil, "team3").
+			name: "0/1 team member approved with RequestIgnore",
+			req: ReviewByTeamMembers(nil, "team3", RequestIgnore).
 				WithDesiredState(utils.ReviewStateApproved),
 			reviews:        reviews,
-			reviewers:      reviewers,
+			reviewers:      teamReviewers,
 			expectedResult: notSatisfied,
 		},
 		{
-			name: "0/1 team member reviewed with request",
-			req:  ReviewByTeamMembers(nil, "team3"),
+			name: "0/1 team member approved with RequestRemove",
+			req: ReviewByTeamMembers(nil, "team3", RequestRemove).
+				WithDesiredState(utils.ReviewStateApproved),
+			reviews:        reviews,
+			reviewers:      teamReviewers,
+			expectedResult: notSatisfied | withRequest,
+		},
+		{
+			name: "0/1 team member reviewed with request with RequestApply",
+			req:  ReviewByTeamMembers(nil, "team3", RequestApply),
 			// Show there are no current reviews, so we actually perform the request.
 			reviewers:      noReviewers,
 			expectedResult: notSatisfied | withRequest,
 		},
+		{
+			name:           "0/1 team member reviewed with request with RequestIgnore",
+			req:            ReviewByTeamMembers(nil, "team3", RequestIgnore),
+			reviewers:      noReviewers,
+			expectedResult: notSatisfied,
+		},
+		{
+			name:           "0/1 team member reviewed with request with RequestRemove",
+			req:            ReviewByTeamMembers(nil, "team3", RequestRemove),
+			reviewers:      noReviewers,
+			expectedResult: notSatisfied,
+		},
 		{
 			name: "3/3 team member approved from review list",
-			req: ReviewByTeamMembers(nil, "team1").
+			req: ReviewByTeamMembers(nil, "team1", RequestApply).
 				WithDesiredState(utils.ReviewStateApproved).
 				WithCount(3),
 			reviews:        reviews,
@@ -313,7 +382,7 @@ func TestReviewByTeamMembers(t *testing.T) {
 		},
 		{
 			name: "1/2 team member approved from review list",
-			req: ReviewByTeamMembers(nil, "team3").
+			req: ReviewByTeamMembers(nil, "team3", RequestApply).
 				WithDesiredState(utils.ReviewStateApproved).
 				WithCount(2),
 			reviews:        reviews,
@@ -322,7 +391,7 @@ func TestReviewByTeamMembers(t *testing.T) {
 		},
 		{
 			name: "team doesn't exist with request",
-			req: ReviewByTeamMembers(nil, "team4").
+			req: ReviewByTeamMembers(nil, "team4", RequestApply).
 				WithDesiredState(utils.ReviewStateApproved),
 			reviews:        reviews,
 			reviewers:      noReviewers,
@@ -330,28 +399,46 @@ func TestReviewByTeamMembers(t *testing.T) {
 		},
 		{
 			name: "3/3 team members reviewed",
-			req: ReviewByTeamMembers(nil, "team2").
+			req: ReviewByTeamMembers(nil, "team2", RequestApply).
 				WithCount(3),
 			reviews:        reviews,
-			reviewers:      reviewers,
+			reviewers:      teamReviewers,
 			expectedResult: satisfied,
 		},
 		{
-			name: "2/2 team members rejected",
-			req: ReviewByTeamMembers(nil, "team2").
+			name: "2/2 team members rejected with RequestApply",
+			req: ReviewByTeamMembers(nil, "team2", RequestApply).
 				WithCount(2).
 				WithDesiredState(utils.ReviewStateChangesRequested),
 			reviews:        reviews,
-			reviewers:      reviewers,
+			reviewers:      teamReviewers,
 			expectedResult: satisfied,
 		},
+		{
+			name: "2/2 team members rejected with RequestIgnore",
+			req: ReviewByTeamMembers(nil, "team2", RequestIgnore).
+				WithCount(2).
+				WithDesiredState(utils.ReviewStateChangesRequested),
+			reviews:        reviews,
+			reviewers:      teamReviewers,
+			expectedResult: satisfied,
+		},
+		{
+			name: "2/2 team members rejected with RequestRemove",
+			req: ReviewByTeamMembers(nil, "team2", RequestRemove).
+				WithCount(2).
+				WithDesiredState(utils.ReviewStateChangesRequested),
+			reviews:        reviews,
+			reviewers:      teamReviewers,
+			expectedResult: satisfied | withRequest,
+		},
 		{
 			name: "1/3 team members approved",
-			req: ReviewByTeamMembers(nil, "team2").
+			req: ReviewByTeamMembers(nil, "team2", RequestApply).
 				WithCount(3).
 				WithDesiredState(utils.ReviewStateApproved),
 			reviews:        reviews,
-			reviewers:      reviewers,
+			reviewers:      teamReviewers,
 			expectedResult: notSatisfied,
 		},
 	} {
@@ -410,7 +497,7 @@ func TestReviewByTeamMembers(t *testing.T) {
 			assert.True(t, utils.TestLastNodeStatus(t, expSatisfied, details),
 				"requirement details should have a status: %t", expSatisfied)
 			assert.Equal(t, expRequested, requested,
-				"requirement should have requested to create item: %t", expRequested)
+				"requirement should have requested a review: %t", expRequested)
 		})
 	}
 }


Reviews:
user: login: thehowl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T22:27:24Z
user: login: thehowl
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-14T22:29:22Z
user: login: aeddi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T22:57:46Z
user: login: thehowl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T23:22:16Z
user: login: aeddi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T05:42:10Z
user: login: moul
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-15T06:23:07Z

Review comments:
diff_hunk: @@ -63,8 +63,26 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) {
 				c.FileChanged(gh, "^gno.land/pkg/gnoweb/"),
 			),
 			Then: r.Or(
-				r.ReviewByUser(gh, "alexiscolin"),
-				r.ReviewByUser(gh, "gfanton"),
+				// If alexiscolin or gfanton is the author of the PR, the other must review it.
path: contribs/github-bot/internal/config/config.go
user: login: thehowl
body: Isn't this the same in functionality as the previous one, but longer?
created_at: 2025-04-14T22:27:24Z
author_association: MEMBER
diff_hunk: @@ -63,8 +63,26 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) {
 				c.FileChanged(gh, "^gno.land/pkg/gnoweb/"),
 			),
 			Then: r.Or(
-				r.ReviewByUser(gh, "alexiscolin"),
-				r.ReviewByUser(gh, "gfanton"),
+				// If alexiscolin or gfanton is the author of the PR, the other must review it.
path: contribs/github-bot/internal/config/config.go
user: login: aeddi
body: Yes it's exactly the same thing, but I felt that this part needed to be made more explicit even if it made it longer, I can roll back this change without any problem.
created_at: 2025-04-14T22:57:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,8 +63,26 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) {
 				c.FileChanged(gh, "^gno.land/pkg/gnoweb/"),
 			),
 			Then: r.Or(
-				r.ReviewByUser(gh, "alexiscolin"),
-				r.ReviewByUser(gh, "gfanton"),
+				// If alexiscolin or gfanton is the author of the PR, the other must review it.
path: contribs/github-bot/internal/config/config.go
user: login: thehowl
body: >a. If Alexis or Guilhem is the author of a PR related to gnoweb, the other is assigned as a reviewer.

Doesn't this imply that approval from the other one is not necessary?
created_at: 2025-04-14T23:22:16Z
author_association: MEMBER
diff_hunk: @@ -63,8 +63,26 @@ func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) {
 				c.FileChanged(gh, "^gno.land/pkg/gnoweb/"),
 			),
 			Then: r.Or(
-				r.ReviewByUser(gh, "alexiscolin"),
-				r.ReviewByUser(gh, "gfanton"),
+				// If alexiscolin or gfanton is the author of the PR, the other must review it.
path: contribs/github-bot/internal/config/config.go
user: login: aeddi
body: I'm not sure, my idea was that since they want to review each other's work, we could make their reviews 'mandatory.'
And if one of them is OOO or their review isn't relevant in a particular case, they could just click the bot's skip button. 

I can modify it to make it completely optional.
created_at: 2025-04-15T05:42:10Z
author_association: CONTRIBUTOR

PR comments:
user: login: Gno2D2
created_at: 2025-04-14T22:18:33Z
body: #### 🛠 PR Checks Summary
All **Automated Checks** passed. ✅

##### Manual Checks (for Reviewers):
- [ ] **IGNORE** the bot requirements for this PR (force green CI check)


<details><summary>Read More</summary>

🤖 This bot helps streamline PR reviews by verifying automated checks and providing guidance for contributors and reviewers.

##### ✅ Automated Checks (for Contributors):
 🟢 Maintainers must be able to edit this pull request ([more info](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork))


##### ☑️ Contributor Actions:
1. Fix any issues flagged by automated checks.
2. Follow the Contributor Checklist to ensure your PR is ready for review.
	- Add new tests, or document why they are unnecessary.
	- Provide clear examples/screenshots, if necessary.
	- Update documentation, if required.
	- Ensure no breaking changes, or include `BREAKING CHANGE` notes.
	- Link related issues/PRs, where applicable.

##### ☑️ Reviewer Actions:
1. Complete manual checks for the PR, including the guidelines and additional checks if applicable.

##### 📚 Resources:
- [Report a bug with the bot](https://www.github.com/gnolang/gno/issues/3238).
- [View the bot’s configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go).

<details><summary><b>Debug</b></summary><blockquote>
<details><summary><b>Automated Checks</b></summary><blockquote>

<details><summary>Maintainers must be able to edit this pull request (more info)</summary><blockquote>

### If
```
🟢 Condition met
└── 🟢 And
    ├── 🟢 The base branch matches this pattern: ^master$
    └── 🟢 The pull request was created from a fork (head branch repo: aeddi/gno)

```
### Then
```
🟢 Requirement satisfied
└── 🟢 Maintainer can modify this pull request

```
</blockquote></details>

</blockquote></details>


<details><summary><b>Manual Checks</b></summary><blockquote>

<details><summary>**IGNORE** the bot requirements for this PR (force green CI check)</summary><blockquote>

### If
```
🟢 Condition met
└── 🟢 On every pull request

```
### Can be checked by

- Any user with comment edit permission

</blockquote></details>

</blockquote></details>

</blockquote></details>

</details>

author_association: COLLABORATOR
user: login: codecov[bot]
created_at: 2025-04-14T22:19:15Z
body: ## [Codecov](https://app.codecov.io/gh/gnolang/gno/pull/4145?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gnolang) Report
Attention: Patch coverage is `60.30534%` with `52 lines` in your changes missing coverage. Please review.
| [Files with missing lines](https://app.codecov.io/gh/gnolang/gno/pull/4145?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gnolang) | Patch % | Lines |
|---|---|---|
| [contribs/github-bot/internal/config/config.go](https://app.codecov.io/gh/gnolang/gno/pull/4145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gnolang#diff-Y29udHJpYnMvZ2l0aHViLWJvdC9pbnRlcm5hbC9jb25maWcvY29uZmlnLmdv) | 0.00% | [40 Missing :warning: ](https://app.codecov.io/gh/gnolang/gno/pull/4145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gnolang) |
| [...tribs/github-bot/internal/requirements/reviewer.go](https://app.codecov.io/gh/gnolang/gno/pull/4145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gnolang#diff-Y29udHJpYnMvZ2l0aHViLWJvdC9pbnRlcm5hbC9yZXF1aXJlbWVudHMvcmV2aWV3ZXIuZ28=) | 86.81% | [8 Missing and 4 partials :warning: ](https://app.codecov.io/gh/gnolang/gno/pull/4145?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gnolang) |

:loudspeaker: Thoughts on this report? [Let us know!](https://github.com/codecov/feedback/issues/255)
author_association: NONE