Skip to content

Commit a913d00

Browse files
committed
fix lint errors, make more reliable
1 parent f108a37 commit a913d00

15 files changed

+412
-136
lines changed

README.md

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ A Go program that intelligently finds and assigns reviewers for GitHub pull requ
44

55
## Features
66

7-
- **Context-based reviewer selection**: Finds reviewers who have previously worked on the lines being changed
8-
- **Activity-based reviewer selection**: Identifies reviewers who have been active on similar files
9-
- **Configurable time constraints**: Only processes PRs within specified age ranges
10-
- **Dry-run mode**: Test the logic without actually assigning reviewers
11-
- **Polling support**: Continuously monitor repositories for new PRs
12-
- **Comprehensive logging**: Detailed logs to understand reviewer selection decisions
7+
- **Smart reviewer selection**: Context-based matching using code blame analysis and activity patterns
8+
- **Workload balancing**: Filters out overloaded reviewers (>9 non-stale open PRs)
9+
- **Stale PR filtering**: Only counts PRs updated within 90 days for accurate workload assessment
10+
- **Resilient API handling**: 25 retry attempts with exponential backoff (5s-20min) and intelligent caching
11+
- **Bot detection**: Comprehensive filtering of bots, service accounts, and organizations
12+
- **Multiple targets**: Single PR, project-wide, or organization-wide monitoring
13+
- **Polling support**: Continuous monitoring with configurable intervals
14+
- **Graceful degradation**: Continues operation even when secondary features fail
15+
- **Comprehensive logging**: Detailed decision tracking and performance insights
16+
- **Dry-run mode**: Test assignments without making changes
1317

1418
## Installation
1519

@@ -20,7 +24,8 @@ go build -o better-reviewers
2024
## Prerequisites
2125

2226
- Go 1.21 or later
23-
- GitHub CLI (`gh`) installed and authenticated
27+
- **For personal use**: GitHub CLI (`gh`) installed and authenticated
28+
- **For GitHub App mode**: `GITHUB_APP_TOKEN` environment variable with valid app token
2429
- GitHub token with appropriate permissions (repo access)
2530

2631
## Usage
@@ -38,12 +43,21 @@ go build -o better-reviewers
3843
./better-reviewers -project "owner/repo"
3944
```
4045

41-
### Organization Monitoring (Coming Soon)
46+
### Organization Monitoring
4247

4348
```bash
4449
./better-reviewers -org "myorg"
4550
```
4651

52+
### GitHub App Mode
53+
54+
Monitor all organizations where your GitHub App is installed:
55+
56+
```bash
57+
export GITHUB_APP_TOKEN="your_app_token_here"
58+
./better-reviewers -app
59+
```
60+
4761
### Polling Mode
4862

4963
```bash
@@ -56,20 +70,37 @@ go build -o better-reviewers
5670
./better-reviewers -pr "owner/repo#123" -dry-run
5771
```
5872

73+
### Advanced Configuration
74+
75+
```bash
76+
# Custom workload limits and caching
77+
./better-reviewers -org "myorg" -max-prs 5 -pr-count-cache 12h
78+
79+
# Tight time constraints with extended polling
80+
./better-reviewers -project "owner/repo" -poll 30m -min-age 30m -max-age 7d
81+
82+
# GitHub App monitoring with polling
83+
export GITHUB_APP_TOKEN="your_app_token_here"
84+
./better-reviewers -app -poll 2h -dry-run
85+
```
86+
5987
## Command Line Options
6088

6189
### Target Flags (Mutually Exclusive)
6290

6391
- `-pr`: Pull request URL or shorthand (e.g., `https://github.com/owner/repo/pull/123` or `owner/repo#123`)
6492
- `-project`: GitHub project to monitor (e.g., `owner/repo`)
65-
- `-org`: GitHub organization to monitor (not yet implemented)
93+
- `-org`: GitHub organization to monitor
94+
- `-app`: Monitor all organizations where this GitHub app is installed
6695

6796
### Behavior Flags
6897

6998
- `-poll`: Polling interval (e.g., `1h`, `30m`). If not set, runs once
7099
- `-dry-run`: Run in dry-run mode (no actual reviewer assignments)
71100
- `-min-age`: Minimum time since last commit or review for PR assignment (default: 1h)
72101
- `-max-age`: Maximum time since last commit or review for PR assignment (default: 180 days)
102+
- `-max-prs`: Maximum non-stale open PRs a candidate can have before being filtered out (default: 9)
103+
- `-pr-count-cache`: Cache duration for PR count queries to optimize performance (default: 6h)
73104

74105
## How It Works
75106

@@ -125,13 +156,14 @@ The secondary reviewer is selected based on review activity, in this priority or
125156
### GitHub API Usage
126157

127158
The program uses:
128-
- GitHub REST API v3 for PR data, file changes, and reviews
129-
- GitHub GraphQL API v4 for blame data and efficient directory/project searches
130-
- **No longer uses the slow Search API** - replaced with GraphQL queries
131-
- Caches blame data to avoid redundant API calls
132-
- 120-second timeout for API calls to handle slow responses
133-
- Retry logic with exponential backoff for GraphQL requests (up to 3 attempts)
134-
- Proper authentication using `gh auth token` for all API calls
159+
- **GitHub REST API v3** for PR data, file changes, and reviews
160+
- **GitHub GraphQL API v4** for blame data and efficient directory/project searches
161+
- **GitHub Search API** for PR count queries with workload balancing
162+
- **Intelligent caching**: 6-hour cache for PR counts, 20-day cache for PR data, failure caching
163+
- **Robust retry logic**: 25 attempts with exponential backoff (5s initial, 20min max delay)
164+
- **Timeout management**: 30-second timeouts for search queries, 120-second for other calls
165+
- **Graceful degradation**: Continues operation when non-critical APIs fail
166+
- **Flexible authentication**: `gh auth token` for personal use or `GITHUB_APP_TOKEN` for app installations
135167

136168
## Architecture
137169

@@ -153,25 +185,19 @@ go test -v
153185

154186
## Example Output
155187

156-
### Successful Primary/Secondary Selection
188+
### Workload Balancing in Action
157189
```
158190
2024/01/15 10:30:45 Processing PR owner/repo#123: Add new feature
159-
2024/01/15 10:30:45 Analyzing 3 changed files for PR 123
160-
2024/01/15 10:30:46 === Finding PRIMARY reviewer (author context) ===
161-
2024/01/15 10:30:46 Checking blame-based authors for primary reviewer
162-
2024/01/15 10:30:46 Found author alice from PR 89 with 25 line overlap
163-
2024/01/15 10:30:46 Found author charlie from PR 76 with 15 line overlap
164-
2024/01/15 10:30:46 Selected blame-based author: alice (score: 25, association: MEMBER)
165-
2024/01/15 10:30:47 === Finding SECONDARY reviewer (active reviewer) ===
166-
2024/01/15 10:30:47 Checking blame-based reviewers for secondary reviewer
167-
2024/01/15 10:30:47 Found reviewer bob from PR 89 with 25 line overlap
168-
2024/01/15 10:30:47 Found reviewer dave from PR 76 with 15 line overlap
169-
2024/01/15 10:30:47 Selected blame-based reviewer: bob (score: 25)
170-
2024/01/15 10:30:47 PRIMARY reviewer selected: alice (method: primary-blame-author)
171-
2024/01/15 10:30:47 SECONDARY reviewer selected: bob (method: secondary-blame-reviewer)
172-
2024/01/15 10:30:47 Found 2 reviewer candidates for PR 123
173-
2024/01/15 10:30:47 Adding reviewers [alice bob] to PR owner/repo#123
174-
2024/01/15 10:30:48 Successfully added reviewers [alice bob] to PR 123
191+
2024/01/15 10:30:45 [CACHE] User type cache hit for alice: User
192+
2024/01/15 10:30:45 📊 User alice has 3 non-stale open PRs in org myorg (2 assigned, 1 for review)
193+
2024/01/15 10:30:45 [CACHE] User type cache hit for bob: User
194+
2024/01/15 10:30:45 📊 User bob has 12 non-stale open PRs in org myorg (8 assigned, 4 for review)
195+
2024/01/15 10:30:45 Filtered (too many open PRs 12 > 9 in org myorg): bob
196+
2024/01/15 10:30:45 [CACHE] User type cache hit for charlie: User
197+
2024/01/15 10:30:45 📊 User charlie has 5 non-stale open PRs in org myorg (3 assigned, 2 for review)
198+
2024/01/15 10:30:45 Found 2 reviewer candidates for PR 123
199+
2024/01/15 10:30:45 Adding reviewers [alice charlie] to PR owner/repo#123
200+
2024/01/15 10:30:46 Successfully added reviewers [alice charlie] to PR 123
175201
```
176202

177203
### Fallback Mechanism in Action

batch_processor.go

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (rf *ReviewerFinder) processPRsBatch(ctx context.Context, prs []*PullReques
8282
}
8383

8484
// groupPRsByRepository groups PRs by their repository.
85-
func (rf *ReviewerFinder) groupPRsByRepository(prs []*PullRequest) []RepoGroup {
85+
func (*ReviewerFinder) groupPRsByRepository(prs []*PullRequest) []RepoGroup {
8686
groupMap := make(map[string]*RepoGroup)
8787

8888
for _, pr := range prs {
@@ -135,16 +135,16 @@ func (rf *ReviewerFinder) prefetchRepositoryData(ctx context.Context, owner, rep
135135
// Placeholder for future repository statistics
136136
go func() {
137137
// This could fetch repository-wide statistics in the future
138-
log.Printf(" ✓ Repository context loaded")
138+
log.Print(" ✓ Repository context loaded")
139139
done <- true
140140
}()
141141

142142
// Wait for all fetches to complete
143-
for i := 0; i < 3; i++ {
143+
for range 3 {
144144
<-done
145145
}
146146

147-
log.Printf(" ✅ Repository data pre-fetched and cached")
147+
log.Print(" ✅ Repository data pre-fetched and cached")
148148
}
149149

150150
// Enhanced prsForOrg that returns PRs without individual fetching.
@@ -153,9 +153,9 @@ func (rf *ReviewerFinder) prsForOrgBatched(ctx context.Context, org string) ([]*
153153

154154
// Try different batch sizes if queries fail
155155
batchSizes := []struct{ repos, prs int }{
156-
{20, 20}, // Default optimized size
157-
{10, 10}, // Smaller if first fails
158-
{5, 5}, // Even smaller
156+
{defaultBatchSize, defaultBatchSize}, // Default optimized size
157+
{smallBatchSize, smallBatchSize}, // Smaller if first fails
158+
{minBatchSize, minBatchSize}, // Even smaller
159159
}
160160

161161
var lastErr error
@@ -240,10 +240,10 @@ func (rf *ReviewerFinder) prsForOrgWithBatchSize(ctx context.Context, org string
240240
}
241241

242242
// Parse the GraphQL response
243-
prs, hasNext, nextCursor := rf.parseOrgPRsFromGraphQL(result)
243+
prs, hasNextPage, nextCursor := rf.parseOrgPRsFromGraphQL(result)
244244
allPRs = append(allPRs, prs...)
245245

246-
if !hasNext {
246+
if !hasNextPage {
247247
break
248248
}
249249
cursor = nextCursor
@@ -254,10 +254,7 @@ func (rf *ReviewerFinder) prsForOrgWithBatchSize(ctx context.Context, org string
254254
}
255255

256256
// parseOrgPRsFromGraphQL parses PRs from GraphQL response.
257-
func (rf *ReviewerFinder) parseOrgPRsFromGraphQL(result map[string]any) ([]*PullRequest, bool, string) {
258-
var prs []*PullRequest
259-
hasNext := false
260-
cursor := ""
257+
func (rf *ReviewerFinder) parseOrgPRsFromGraphQL(result map[string]any) (prs []*PullRequest, hasNextPage bool, cursor string) {
261258

262259
// Navigate through the GraphQL response structure
263260
if data, ok := result["data"].(map[string]any); ok {
@@ -266,7 +263,7 @@ func (rf *ReviewerFinder) parseOrgPRsFromGraphQL(result map[string]any) ([]*Pull
266263
// Get pagination info
267264
if pageInfo, ok := repos["pageInfo"].(map[string]any); ok {
268265
if next, ok := pageInfo["hasNextPage"].(bool); ok {
269-
hasNext = next
266+
hasNextPage = next
270267
}
271268
if endCursor, ok := pageInfo["endCursor"].(string); ok {
272269
cursor = endCursor
@@ -285,7 +282,7 @@ func (rf *ReviewerFinder) parseOrgPRsFromGraphQL(result map[string]any) ([]*Pull
285282
}
286283
}
287284

288-
return prs, hasNext, cursor
285+
return prs, hasNextPage, cursor
289286
}
290287

291288
// parsePRsFromRepo parses PRs from a repository node.

better-reviewers

0 Bytes
Binary file not shown.

constants.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,45 @@ const (
7979

8080
// GraphQL constants.
8181
graphQLNodes = "nodes" // Common GraphQL field name
82+
83+
// HTTP constants.
84+
httpMethodGet = "GET" // HTTP GET method
85+
86+
// Scoring constants.
87+
recentActivityScore = 0.9 // Score for very recent activity (< 3 days)
88+
weekActivityScore = 0.7 // Score for weekly activity (< 7 days)
89+
biweeklyActivityScore = 0.5 // Score for biweekly activity (< 14 days)
90+
monthlyActivityScore = 0.25 // Score for monthly activity (< 30 days)
91+
bimonthlyActivityScore = 0.1 // Score for bimonthly activity (< 60 days)
92+
quarterlyActivityScore = 0.05 // Score for quarterly activity (< 90 days)
93+
defaultExpertiseScore = 0.5 // Default expertise score
94+
reviewerWeightMultiplier = 0.5 // Weight multiplier for reviewers vs authors
95+
96+
// Overlap scoring constants.
97+
contextMatchWeight = 0.7 // Weight for context matches in overlap scoring
98+
minOverlapThreshold = 5.0 // Minimum overlap score threshold
99+
100+
// Analysis limits.
101+
maxRecentCommits = 10 // Maximum recent commits to analyze
102+
maxDirectoryReviewers = 5 // Maximum directory reviewers to return
103+
104+
// Batch processing sizes.
105+
defaultBatchSize = 20 // Default batch size for processing
106+
smallBatchSize = 10 // Small batch size fallback
107+
minBatchSize = 5 // Minimum batch size
108+
109+
// Selection method scoring.
110+
overlapAuthorScore = 30 // Score for author overlap
111+
overlapReviewerScore = 25 // Score for reviewer overlap
112+
fileAuthorScore = 15 // Score for file author
113+
fileReviewerScore = 12 // Score for file reviewer
114+
directoryAuthorScore = 7 // Score for directory author
115+
directoryReviewerScore = 5 // Score for directory reviewer
116+
117+
// Time-based constants.
118+
recentDaysThreshold = 7 // Days threshold for recent activity
119+
biweeklyDaysThreshold = 14 // Days threshold for biweekly activity
120+
monthlyDaysThreshold = 30 // Days threshold for monthly activity
121+
bimonthlyDaysThreshold = 60 // Days threshold for bimonthly activity
122+
quarterlyDaysThreshold = 90 // Days threshold for quarterly activity
82123
)

0 commit comments

Comments
 (0)