Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codeowners
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# For now, Hans reviews everything
* @BakerNet
codeowners.toml @zbedforrest
2 changes: 1 addition & 1 deletion .github/workflows/codeowners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ concurrency:
on:
pull_request:
branches: [main]
types: [opened, reopened, synchronize, ready_for_review]
types: [opened, reopened, synchronize, ready_for_review, labeled, unlabeled]

jobs:
codeowners:
Expand Down
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ concurrency:

on:
pull_request:
types: [opened, reopened, synchronize, ready_for_review]
types: [opened, reopened, synchronize, ready_for_review, labeled, unlabeled]

jobs:
codeowners:
Expand Down Expand Up @@ -244,6 +244,23 @@ To prevent ownership rules from being checked or applied for certain directories
ignore = ["test_project"]
```

### High Priority Labels

You can configure labels that indicate a high priority PR. When a PR has any of these labels, the comment will include a high priority indicator:

```toml
high_priority_labels = ["high-priority", "urgent"]
```

When a PR has any of these labels, the comment will look like this:
```
❗High Prio❗

Codeowners approval required for this PR:
- @user1
- @user2
```

## CLI Tool

A CLI tool is available which provides some utilities for working with `.codeowners` files.
Expand Down
2 changes: 2 additions & 0 deletions codeowners.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ max_reviews = 2
unskippable_reviewers = ["@BakerNet"]
# `ignore` allows you to specify directories that should be ignored by the codeowners check
ignore = ["test_project"]
# `high_priority_lables` allows you to specify labels that should be considered high priority
high_priority_labels = ["P0"]

# `enforcement` allows you to specify how the codeowners check should be enforced
[enforcement]
Expand Down
2 changes: 2 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Config struct {
UnskippableReviewers []string `toml:"unskippable_reviewers"`
Ignore []string `toml:"ignore"`
Enforcement *Enforcement `toml:"enforcement"`
HighPriorityLabels []string `toml:"high_priority_labels"`
}

type Enforcement struct {
Expand All @@ -32,6 +33,7 @@ func ReadConfig(path string) (*Config, error) {
UnskippableReviewers: []string{},
Ignore: []string{},
Enforcement: &Enforcement{Approval: false, FailCheck: true},
HighPriorityLabels: []string{},
}

fileName := path + "codeowners.toml"
Expand Down
3 changes: 3 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ ignore = ["ignored/"]
[enforcement]
approval = true
fail_check = false
high_priority_labels = ["high-priority", "urgent"]
`,
path: "testdata/",
expected: &Config{
Expand All @@ -44,6 +45,7 @@ fail_check = false
UnskippableReviewers: []string{"@user1", "@user2"},
Ignore: []string{"ignored/"},
Enforcement: &Enforcement{Approval: true, FailCheck: false},
HighPriorityLabels: []string{"high-priority", "urgent"},
},
expectedErr: false,
},
Expand All @@ -60,6 +62,7 @@ unskippable_reviewers = ["@user1"]
UnskippableReviewers: []string{"@user1"},
Ignore: []string{},
Enforcement: &Enforcement{Approval: false, FailCheck: true},
HighPriorityLabels: []string{},
},
expectedErr: false,
},
Expand Down
2 changes: 1 addition & 1 deletion internal/github/approvals.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

"github.com/multimediallc/codeowners-plus/internal/git"
"github.com/multimediallc/codeowners-plus/pkg/codeowners"
"github.com/multimediallc/codeowners-plus/pkg/functional"
f "github.com/multimediallc/codeowners-plus/pkg/functional"
)

type approvalWithDiff struct {
Expand Down
21 changes: 20 additions & 1 deletion internal/github/gh.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

"github.com/google/go-github/v63/github"
"github.com/multimediallc/codeowners-plus/internal/git"
"github.com/multimediallc/codeowners-plus/pkg/functional"
f "github.com/multimediallc/codeowners-plus/pkg/functional"
)

type NoPRError struct{}
Expand Down Expand Up @@ -47,6 +47,7 @@ type Client interface {
IsInComments(comment string, since *time.Time) (bool, error)
IsSubstringInComments(substring string, since *time.Time) (bool, error)
CheckApprovals(fileReviewerMap map[string][]string, approvals []*CurrentApproval, originalDiff git.Diff) (approvers []string, staleApprovals []*CurrentApproval)
IsInLabels(labels []string) (bool, error)
}

type GHClient struct {
Expand Down Expand Up @@ -414,6 +415,24 @@ func (gh *GHClient) IsSubstringInComments(substring string, since *time.Time) (b
return false, nil
}

// IsInLabels checks if the PR has any of the given labels
func (gh *GHClient) IsInLabels(labels []string) (bool, error) {
if gh.pr == nil {
return false, &NoPRError{}
}
if len(labels) == 0 {
return false, nil
}
for _, label := range gh.pr.Labels {
for _, targetLabel := range labels {
if label.GetName() == targetLabel {
return true, nil
}
}
}
return false, nil
}

// Apply approver satisfaction to the owners map, and return the approvals which should be invalidated
func (gh *GHClient) CheckApprovals(
fileReviewerMap map[string][]string,
Expand Down
100 changes: 99 additions & 1 deletion internal/github/gh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"time"

"github.com/google/go-github/v63/github"
"github.com/multimediallc/codeowners-plus/pkg/functional"
f "github.com/multimediallc/codeowners-plus/pkg/functional"
)

func setupReviews() *GHClient {
Expand Down Expand Up @@ -424,6 +424,13 @@ func TestNilPRErr(t *testing.T) {
return err
},
},
{
name: "IsInLabels",
testFn: func() error {
_, err := gh.IsInLabels([]string{"label"})
return err
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -1019,3 +1026,94 @@ func TestInitUserReviewerMap(t *testing.T) {
}
}
}

func TestIsInLabels(t *testing.T) {
tt := []struct {
name string
pr *github.PullRequest
labels []string
expected bool
expectError bool
failMessage string
}{
{
name: "has matching label",
pr: &github.PullRequest{
Labels: []*github.Label{
{Name: github.String("high-priority")},
},
},
labels: []string{"high-priority"},
expected: true,
expectError: false,
failMessage: "Should detect matching label",
},
{
name: "has multiple labels but no match",
pr: &github.PullRequest{
Labels: []*github.Label{
{Name: github.String("bug")},
{Name: github.String("enhancement")},
},
},
labels: []string{"high-priority"},
expected: false,
expectError: false,
failMessage: "Should not detect label when not present",
},
{
name: "empty labels list",
pr: &github.PullRequest{
Labels: []*github.Label{
{Name: github.String("high-priority")},
},
},
labels: []string{},
expected: false,
expectError: false,
failMessage: "Should return false for empty labels list",
},
{
name: "multiple target labels",
pr: &github.PullRequest{
Labels: []*github.Label{
{Name: github.String("urgent")},
},
},
labels: []string{"high-priority", "urgent"},
expected: true,
expectError: false,
failMessage: "Should detect any of the target labels",
},
{
name: "nil PR",
pr: nil,
labels: []string{"high-priority"},
expected: false,
expectError: true,
failMessage: "Should return error for nil PR",
},
}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
client := &GHClient{pr: tc.pr}
hasLabel, err := client.IsInLabels(tc.labels)
if tc.expectError {
if err == nil {
t.Error("Expected error but got none")
}
if _, ok := err.(*NoPRError); !ok {
t.Errorf("Expected NoPRError, got %T", err)
}
return
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if hasLabel != tc.expected {
t.Error(tc.failMessage)
}
})
}
}
12 changes: 9 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import (
"testing"
"time"

"github.com/multimediallc/codeowners-plus/internal/config"
owners "github.com/multimediallc/codeowners-plus/internal/config"
"github.com/multimediallc/codeowners-plus/internal/git"
"github.com/multimediallc/codeowners-plus/internal/github"
gh "github.com/multimediallc/codeowners-plus/internal/github"
"github.com/multimediallc/codeowners-plus/pkg/codeowners"
"github.com/multimediallc/codeowners-plus/pkg/functional"
f "github.com/multimediallc/codeowners-plus/pkg/functional"
)

// AppConfig holds the application configuration
Expand Down Expand Up @@ -214,6 +214,12 @@ func (a *App) processApprovalsAndReviewers() (bool, string, error) {
if len(unapprovedOwners) > 0 {
// Comment on the PR with the codeowner teams that have not approved the PR
comment := allRequiredOwners.ToCommentString()
hasHighPriority, err := a.client.IsInLabels(a.conf.HighPriorityLabels)
if err != nil {
fmt.Fprintf(WarningBuffer, "WARNING: Error checking high priority labels: %v\n", err)
} else if hasHighPriority {
comment = "❗High Prio❗\n\n" + comment
}
if maxReviewsMet {
comment += "\n\n"
comment += "The PR has received the max number of required reviews. No further action is required."
Expand Down
21 changes: 19 additions & 2 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import (
"time"

"github.com/google/go-github/v63/github"
"github.com/multimediallc/codeowners-plus/internal/config"
owners "github.com/multimediallc/codeowners-plus/internal/config"
"github.com/multimediallc/codeowners-plus/internal/git"
"github.com/multimediallc/codeowners-plus/internal/github"
gh "github.com/multimediallc/codeowners-plus/internal/github"
"github.com/multimediallc/codeowners-plus/pkg/codeowners"
f "github.com/multimediallc/codeowners-plus/pkg/functional"
)
Expand Down Expand Up @@ -262,6 +262,23 @@ func (m *mockGitHubClient) IsSubstringInComments(substring string, since *time.T
return false, nil
}

func (m *mockGitHubClient) IsInLabels(labels []string) (bool, error) {
if m.pr == nil {
return false, &gh.NoPRError{}
}
if len(labels) == 0 {
return false, nil
}
for _, label := range m.pr.Labels {
for _, targetLabel := range labels {
if label.GetName() == targetLabel {
return true, nil
}
}
}
return false, nil
}

func init() {
// Initialize test flags with default values
flags = &Flags{
Expand Down
2 changes: 1 addition & 1 deletion pkg/codeowners/codeowners_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"reflect"
"testing"

"github.com/multimediallc/codeowners-plus/pkg/functional"
f "github.com/multimediallc/codeowners-plus/pkg/functional"
)

func TestInitOwnerTree(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/codeowners/reviewers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"strings"

"github.com/bmatcuk/doublestar/v4"
"github.com/multimediallc/codeowners-plus/pkg/functional"
f "github.com/multimediallc/codeowners-plus/pkg/functional"
)

var commentPrefix = "Codeowners approval required for this PR:\n"
Expand Down
2 changes: 1 addition & 1 deletion pkg/codeowners/reviewers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"sort"
"testing"

"github.com/multimediallc/codeowners-plus/pkg/functional"
f "github.com/multimediallc/codeowners-plus/pkg/functional"
)

func TestToReviewers(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion tools/cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

"github.com/boyter/gocodewalker"
"github.com/multimediallc/codeowners-plus/pkg/codeowners"
"github.com/multimediallc/codeowners-plus/pkg/functional"
f "github.com/multimediallc/codeowners-plus/pkg/functional"
"github.com/urfave/cli/v2"
)

Expand Down