Skip to content

Commit 0d4ac38

Browse files
committed
Change the way docs approvals work
Docs PRs are consistently delayed due to the the fact that we currently require two approvals to come from a small team of people who are all very busy. To mitigate this, we will treat PRs that touch docs similarly to PRs that touch code. The new behavior is: 1. PRs continue to require two approvals, whether they touch code, docs, or both. 2. At least one of these two approvals must come from a code "owner" (otherwise known as a "group 1 reviewer"). This becomes the case whether the PR touches docs or not. 3. If the PR does not touch docs, then approvals from the docs reviewers don't count towards the 2 approval limit. 4. If the PR does touch docs, then the docs reviewers are added to group 2 reviewers. Their reviews count towards the two review limit, but their reviews are not required in order to merge the PR.
1 parent 4bd2c6c commit 0d4ac38

File tree

7 files changed

+48
-66
lines changed

7 files changed

+48
-66
lines changed

bot/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/gravitational/shared-workflows/bot
22

3-
go 1.21
3+
go 1.23
44

55
require (
66
github.com/google/go-github/v37 v37.0.0

bot/internal/bot/backport_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func TestFindBranches(t *testing.T) {
4444
func TestBackport(t *testing.T) {
4545
buildTestBot := func(github Client) (*Bot, context.Context) {
4646
r, _ := review.New(&review.Config{
47-
CoreReviewers: map[string]review.Reviewer{"dev": review.Reviewer{}},
47+
CoreReviewers: map[string]review.Reviewer{"dev": {}},
4848
CloudReviewers: map[string]review.Reviewer{},
4949
CodeReviewersOmit: map[string]bool{},
5050
DocsReviewers: map[string]review.Reviewer{},

bot/internal/bot/bloat.go

+2-16
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"unicode/utf8"
1515

1616
"github.com/gravitational/trace"
17+
"golang.org/x/exp/slices"
1718
)
1819

1920
const (
@@ -89,14 +90,6 @@ func (b *Bot) BloatCheck(ctx context.Context, baseStats, current string, artifac
8990
default:
9091
}
9192

92-
skipped := false
93-
for _, s := range skip {
94-
if s == artifact {
95-
skipped = true
96-
break
97-
}
98-
}
99-
10093
stats, err := calculateChange(baseLookup, current, artifact)
10194
if err != nil {
10295
return err
@@ -105,7 +98,7 @@ func (b *Bot) BloatCheck(ctx context.Context, baseStats, current string, artifac
10598
log.Printf("artifact %s has a current size of %d", artifact, stats.currentSize)
10699

107100
status := "✅"
108-
if skipped {
101+
if skipped := slices.Contains(skip, artifact); skipped {
109102
status += " skipped by admin"
110103
} else {
111104
if stats.diff > int64(warnThreshold) {
@@ -150,13 +143,6 @@ func renderMarkdownTable(w io.Writer, data map[string]result) error {
150143
padding[v] = len(v)
151144
}
152145

153-
max := func(a, b int) int {
154-
if a > b {
155-
return a
156-
}
157-
return b
158-
}
159-
160146
// get the largest item from the title or items in the column to determine
161147
// the actual padding
162148
for k, column := range data {

bot/internal/bot/check.go

+7-18
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import (
2020
"context"
2121
"fmt"
2222
"log"
23+
"slices"
2324
"strings"
2425

26+
"github.com/gravitational/shared-workflows/bot/internal/github"
2527
"github.com/gravitational/shared-workflows/bot/internal/review"
2628
"github.com/gravitational/trace"
2729
)
@@ -87,13 +89,9 @@ func (b *Bot) Check(ctx context.Context) error {
8789
b.c.Environment.Repository,
8890
b.c.Environment.Number,
8991
)
90-
commentExists := false
91-
for _, c := range comments {
92-
if c.Body == comment {
93-
commentExists = true
94-
break
95-
}
96-
}
92+
commentExists := slices.ContainsFunc(comments, func(c github.Comment) bool {
93+
return c.Body == comment
94+
})
9795
if !commentExists {
9896
if err := b.c.GitHub.CreateComment(ctx,
9997
b.c.Environment.Organization,
@@ -119,15 +117,6 @@ func (b *Bot) Check(ctx context.Context) error {
119117
return nil
120118
}
121119

122-
func contains(ss []string, s string) bool {
123-
for i := range ss {
124-
if ss[i] == s {
125-
return true
126-
}
127-
}
128-
return false
129-
}
130-
131120
// checkDoNotMerge checks if the PR has "do-not-merge" label on it.
132121
func (b *Bot) checkDoNotMerge(ctx context.Context) error {
133122
pull, err := b.c.GitHub.GetPullRequest(ctx,
@@ -138,7 +127,7 @@ func (b *Bot) checkDoNotMerge(ctx context.Context) error {
138127
return trace.Wrap(err)
139128
}
140129

141-
if contains(pull.UnsafeLabels, doNotMergeLabel) {
130+
if slices.Contains(pull.UnsafeLabels, doNotMergeLabel) {
142131
return trace.BadParameter("the pull request is marked as %v", doNotMergeLabel)
143132
}
144133

@@ -225,7 +214,7 @@ func (b *Bot) reviewersToDismiss(ctx context.Context) ([]string, error) {
225214
}
226215

227216
const (
228-
// doNotMergeLabel is the name of the Github label that is put on PRs
217+
// doNotMergeLabel is the name of the GitHub label that is put on PRs
229218
// to prevent them from merging.
230219
doNotMergeLabel = "do-not-merge"
231220
)

bot/internal/bot/skip.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/gravitational/trace"
9+
"golang.org/x/exp/slices"
910
)
1011

1112
// skipItems finds any comments from an admin with the skipPrefix
@@ -30,7 +31,7 @@ func (b *Bot) skipItems(ctx context.Context, skipPrefix string) ([]string, error
3031

3132
admins := b.c.Review.GetAdminCheckers(b.c.Environment.Author)
3233
for _, c := range comments {
33-
if !contains(admins, c.Author) {
34+
if !slices.Contains(admins, c.Author) {
3435
log.Printf("ignoring comment from non-admin %v", c.Author)
3536
continue
3637
}

bot/internal/review/review.go

+15-23
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ var (
5252
// singleApproverPaths defines paths in cloud or core repos that only require a single approver.
5353
// The map key is the repo (cloud|teleport|teleport.e) and the value is a list of paths.
5454
singleApproverPaths = map[string][]string{
55-
"cloud": []string{
55+
"cloud": {
5656
"deploy/fluxcd/config/values.yaml",
5757
"deploy/fluxcd/config/*/values.yaml",
5858
"deploy/fluxcd/config/*/*/values.yaml",
@@ -68,7 +68,7 @@ var (
6868
// require a single approver. The map key is the repo (cloud|teleport|teleport.e) and the
6969
// value is a list of authors. BOTS ONLY - DO NOT INCLUDE EMPLOYEE GITHUB HANDLES.
7070
singleApproverAuthors = map[string][]string{
71-
"cloud": []string{Dependabot, DependabotBatcher, RenovateBotPrivate, RenovateBotPublic},
71+
"cloud": {Dependabot, DependabotBatcher, RenovateBotPrivate, RenovateBotPublic},
7272
}
7373
)
7474

@@ -255,12 +255,12 @@ func (r *Assignments) getDocsReviewers(e *env.Environment, files []github.PullRe
255255
// the changed docs files. If so, add them as docs reviewers.
256256
repoReviewers := r.repoReviewers(e)
257257
a, b := getReviewerSets(e.Author, repoReviewers, r.c.CodeReviewersOmit)
258-
prefCodeReviewers := r.getAllPreferredReviewers(repoReviewers, append(a, b...), files)
258+
preferredCodeReviewers := r.getAllPreferredReviewers(repoReviewers, append(a, b...), files)
259259

260260
// Get the docs reviewer pool, which does not depend on the files
261261
// changed by a pull request.
262262
docsA, docsB := getReviewerSets(e.Author, r.c.DocsReviewers, r.c.DocsReviewersOmit)
263-
reviewers := append(prefCodeReviewers, append(docsA, docsB...)...)
263+
reviewers := append(preferredCodeReviewers, append(docsA, docsB...)...)
264264

265265
// If no docs reviewers were assigned, assign admin reviews.
266266
if len(reviewers) == 0 {
@@ -438,20 +438,17 @@ func (r *Assignments) CheckInternal(e *env.Environment, reviews []github.Review,
438438
switch {
439439
case changes.Docs && changes.Code:
440440
log.Printf("Check: Found docs and code changes.")
441-
if err := r.checkInternalDocsReviews(e, reviews, files); err != nil {
442-
return trace.Wrap(err)
443-
}
444-
if err := r.checkInternalCodeReviews(e, changes, reviews); err != nil {
441+
if err := r.checkInternalReviews(e, changes, reviews, files); err != nil {
445442
return trace.Wrap(err)
446443
}
447444
case !changes.Docs && changes.Code:
448445
log.Printf("Check: Found code changes.")
449-
if err := r.checkInternalCodeReviews(e, changes, reviews); err != nil {
446+
if err := r.checkInternalReviews(e, changes, reviews, files); err != nil {
450447
return trace.Wrap(err)
451448
}
452449
case changes.Docs && !changes.Code:
453450
log.Printf("Check: Found docs changes.")
454-
if err := r.checkInternalDocsReviews(e, reviews, files); err != nil {
451+
if err := r.checkInternalReviews(e, changes, reviews, files); err != nil {
455452
return trace.Wrap(err)
456453
}
457454
// Strange state, an empty commit? Check admins.
@@ -478,23 +475,18 @@ func (r *Assignments) checkInternalReleaseReviews(reviews []github.Review) error
478475
return trace.BadParameter("requires at least one approval from %v", reviewers)
479476
}
480477

481-
// checkInternalDocsReviews checks whether docs review requirements are satisfied
478+
// checkInternalReviews checks whether review requirements are satisfied
482479
// for a PR authored by an internal employee
483-
func (r *Assignments) checkInternalDocsReviews(e *env.Environment, reviews []github.Review, files []github.PullRequestFile) error {
484-
reviewers := r.getDocsReviewers(e, files)
480+
func (r *Assignments) checkInternalReviews(e *env.Environment, changes env.Changes, reviews []github.Review, files []github.PullRequestFile) error {
481+
setA, setB := getReviewerSets(e.Author, r.repoReviewers(e), r.c.CodeReviewersOmit)
485482

486-
if check(reviewers, reviews) {
487-
return nil
483+
// If this PR touches docs, then approvals from docs reviewers also count.
484+
// Add them to set B, as docs reviewers are not required so long as we get
485+
// the appropriate number of approvals.
486+
if changes.Docs {
487+
setB = append(setB, r.getDocsReviewers(e, files)...)
488488
}
489489

490-
return trace.BadParameter("requires at least one approval from %v", reviewers)
491-
}
492-
493-
// checkInternalCodeReviews checks whether code review requirements are satisfied
494-
// for a PR authored by an internal employee
495-
func (r *Assignments) checkInternalCodeReviews(e *env.Environment, changes env.Changes, reviews []github.Review) error {
496-
setA, setB := getReviewerSets(e.Author, r.repoReviewers(e), r.c.CodeReviewersOmit)
497-
498490
// PRs can be approved if you either have multiple code owners that approve
499491
// or code owner and code reviewer. An exception is for PRs that
500492
// only modify paths that require a single approver.

bot/internal/review/review_test.go

+20-6
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ func TestGetCodeReviewerSets(t *testing.T) {
434434
Author: test.author,
435435
}
436436
changes := env.Changes{ApproverCount: env.DefaultApproverCount}
437-
require.ErrorContains(t, test.assignments.checkInternalCodeReviews(e, changes, nil),
437+
require.ErrorContains(t, test.assignments.checkInternalReviews(e, changes, nil, nil),
438438
"at least one approval required from each set")
439439

440440
setA, setB := test.assignments.getCodeReviewerSets(e)
@@ -683,6 +683,7 @@ func TestCheckInternal(t *testing.T) {
683683
// Docs.
684684
DocsReviewers: map[string]Reviewer{
685685
"7": {Owner: true},
686+
"8": {Owner: false},
686687
},
687688
DocsReviewersOmit: map[string]bool{},
688689
CodeReviewersOmit: map[string]bool{},
@@ -738,16 +739,28 @@ func TestCheckInternal(t *testing.T) {
738739
result: false,
739740
},
740741
{
741-
desc: "docs-only-docs-approval-success",
742+
desc: "docs-only-two-approval-success",
742743
repository: "teleport",
743744
author: "4",
744745
reviews: []github.Review{
745-
{Author: "7", State: Approved},
746+
{Author: "1", State: Approved},
747+
{Author: "8", State: Approved},
746748
},
747749
docs: true,
748750
code: false,
749751
result: true,
750752
},
753+
{
754+
desc: "docs-only-docs-approval-fail",
755+
repository: "teleport",
756+
author: "4",
757+
reviews: []github.Review{
758+
{Author: "7", State: Approved},
759+
},
760+
docs: true,
761+
code: false,
762+
result: false,
763+
},
751764
{
752765
desc: "code-only-no-reviews-fail",
753766
repository: "teleport",
@@ -850,9 +863,10 @@ func TestCheckInternal(t *testing.T) {
850863
{Author: "3", State: Approved},
851864
{Author: "4", State: Approved},
852865
},
853-
docs: true,
854-
code: true,
855-
result: false,
866+
docs: true,
867+
code: true,
868+
// docs reviewers count towards required approvals, but are not required
869+
result: true,
856870
},
857871
{
858872
desc: "docs-and-code-docs-and-code-approval-success",

0 commit comments

Comments
 (0)