Skip to content

Commit a728958

Browse files
committed
Make it a proper GitHub app
1 parent 2626277 commit a728958

23 files changed

+1447
-419
lines changed

.claude/prompt-reviewers.txt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Logic for finding reviewer candidates should be as follows:
2+
- We are lookin for two pairs of people: the primary - with the most author context in the changed lines, and the secondary - who is the most active reviewer other than the primary
3+
4+
To find the primary reviewer, we need to come up with a list of candidates who know this code the best, and track how we decided to make them a candidate. This is the fallback/priority order of who should be considered a great primary:
5+
* Examining the same Github blame history for the previously existing files, consider the top 5 PR's in the size of their overlap with the lines editing this file. The authors of these most recent PR's that overlap are the next priority for the primary reviewer, but only if the author_association shows they still have write access to the repository.
6+
* The next best reviewer is the most recent author for a merged PR that impacted a file in this directory.
7+
* The next best reviewer is the most recent author for a merged PR in this project.
8+
9+
You'll need to look at the author_association for each author candidate to understand if they have write access and are thus a valid reviewer.
10+
11+
To find the secondary reviewer, we need to come up with a list of candidates who know actively review code, and track how we decided to make them a candidate. This is the fallback/priority order of who should be considered a great secondary:
12+
13+
* Examining the same Github blame history for the previously existing files, consider the top 5 PR's in the size of their overlap with the lines editing this file. The reviewer of these most recent PR's that overlap are the highest priority for the primary reviewer.
14+
* The next best reviewer is the most recent reviewer for a merged PR that impacted a file in this directory.
15+
* The next best reviewer is the most recent reviewer for a merged PR in this project.
16+
17+
Every PR should have at least two reviewers. The author of the current PR cannot be a reviewer. The primary and secondary must be different people.
18+
19+
When proposing a reviewer for a PR, be sure to log which of these selection mechanisms the reviewer was selected by.

.claude/prompt.txt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
Create a brand new Go program that finds reviewers for GitHub pull requests.
2+
3+
- Go code should be written with best practires in a way that emulates what the Go project itself uses; prioritizing advice mentioned on go.dev, including https://go.dev/wiki/CodeReviewComments and https://go.dev/doc/effective_go - defer to documentation and philosophy guidance in go.dev above all other reference material.
4+
5+
- Go code should incorporate thoughts from Google's style guide: https://google.github.io/styleguide/go/
6+
7+
- Code should be written with tests and unit testing in mind
8+
9+
- Code should use the token stored by the gh command line utility, probably by using `gh auth token`
10+
- Code should have as few external dependencies as possible
11+
- Logic for finding reviewer candidates should be as follows:
12+
- We are lookin for two pairs of people: the one with the most context in the changed lines, and the one who most recently approved a PR to this file who is more likely to be active.
13+
- To find the reviewers with the most context on the changed lines:
14+
- For the top 3 files with the largest number of changes within this PR:
15+
- Use the GitHub API v4 to fetch blame data for those files
16+
- For the lines being changed in each of thone files, look for the top 3 PR's that last changed the lines this PR alters
17+
- Look at who approved those PR's
18+
- Rank these approvers in terms of how many lines of this new PR they previously reviewed
19+
- To find the most active reviewers for these files, look at most recent PR for the top 3 files with the largest number of changes this within this PR. This will be the same fileset as the context list
20+
- Rank these in order of how big the PR is that they reviewed
21+
- If the reviewer with the most context is already a reviewer on this PR, and it's been more than 2 days, add the reviewer with the second most context.
22+
- If the most active reviewer for these files is already a reviewer on this PR, and it's been more than 2 days, add the reviewer who is the second most active.
23+
24+
- Some example flags this program should support:
25+
26+
// Target flags (mutually exclusive)
27+
prURL = flag.String("pr", "", "Pull request URL (e.g., https://github.com/owner/repo/pull/123 or owner/repo#123)")
28+
project = flag.String("project", "", "GitHub project to monitor (e.g., owner/repo)")
29+
org = flag.String("org", "", "GitHub organization to monitor")
30+
31+
// Behavior flags
32+
poll = flag.Duration("poll", 0, "Polling interval (e.g., 1h, 30m). If not set, runs once")
33+
dryRun = flag.Bool("dry-run", false, "Run in dry-run mode (no actual approvals)")
34+
minOpenTime = flag.Duration("min-age", 1*time.Hour, "Minimum time PR since last commit or review for PR assignment")
35+
maxOpenTime = flag.Duration("max-age", 180*24*time.Hour, "Maximum time PR since last commit or review for PR assignment")
36+
37+
- Code should have great logging to better understand the decisions it's making.

Makefile

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# BEGIN: lint-install .
2+
# http://github.com/codeGROOVE-dev/lint-install
3+
4+
.PHONY: lint
5+
lint: _lint
6+
7+
LINT_ARCH := $(shell uname -m)
8+
LINT_OS := $(shell uname)
9+
LINT_OS_LOWER := $(shell echo $(LINT_OS) | tr '[:upper:]' '[:lower:]')
10+
LINT_ROOT := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
11+
12+
# shellcheck and hadolint lack arm64 native binaries: rely on x86-64 emulation
13+
ifeq ($(LINT_OS),Darwin)
14+
ifeq ($(LINT_ARCH),arm64)
15+
LINT_ARCH=x86_64
16+
endif
17+
endif
18+
19+
LINTERS :=
20+
FIXERS :=
21+
22+
GOLANGCI_LINT_CONFIG := $(LINT_ROOT)/.golangci.yml
23+
GOLANGCI_LINT_VERSION ?= v2.3.1
24+
GOLANGCI_LINT_BIN := $(LINT_ROOT)/out/linters/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(LINT_ARCH)
25+
$(GOLANGCI_LINT_BIN):
26+
mkdir -p $(LINT_ROOT)/out/linters
27+
rm -rf $(LINT_ROOT)/out/linters/golangci-lint-*
28+
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(LINT_ROOT)/out/linters $(GOLANGCI_LINT_VERSION)
29+
mv $(LINT_ROOT)/out/linters/golangci-lint $@
30+
31+
LINTERS += golangci-lint-lint
32+
golangci-lint-lint: $(GOLANGCI_LINT_BIN)
33+
find . -name go.mod -execdir "$(GOLANGCI_LINT_BIN)" run -c "$(GOLANGCI_LINT_CONFIG)" \;
34+
35+
FIXERS += golangci-lint-fix
36+
golangci-lint-fix: $(GOLANGCI_LINT_BIN)
37+
find . -name go.mod -execdir "$(GOLANGCI_LINT_BIN)" run -c "$(GOLANGCI_LINT_CONFIG)" --fix \;
38+
39+
.PHONY: _lint $(LINTERS)
40+
_lint: $(LINTERS)
41+
42+
.PHONY: fix $(FIXERS)
43+
fix: $(FIXERS)
44+
45+
# END: lint-install .

README.md

Lines changed: 56 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ A Go program that intelligently finds and assigns reviewers for GitHub pull requ
55
## Features
66

77
- **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)
8+
- **Workload balancing**: Filters out overloaded reviewers (>9 non-stale open PRs)
99
- **Stale PR filtering**: Only counts PRs updated within 90 days for accurate workload assessment
1010
- **Resilient API handling**: 25 retry attempts with exponential backoff (5s-20min) and intelligent caching
1111
- **Bot detection**: Comprehensive filtering of bots, service accounts, and organizations
@@ -25,7 +25,9 @@ go build -o better-reviewers
2525

2626
- Go 1.21 or later
2727
- **For personal use**: GitHub CLI (`gh`) installed and authenticated
28-
- **For GitHub App mode**: `GITHUB_APP_TOKEN` environment variable with valid app token
28+
- **For GitHub App mode**:
29+
- GitHub App ID (found in your app settings)
30+
- GitHub App private key file (.pem file downloaded when creating the app)
2931
- GitHub token with appropriate permissions (repo access)
3032

3133
## Usage
@@ -54,8 +56,13 @@ go build -o better-reviewers
5456
Monitor all organizations where your GitHub App is installed:
5557

5658
```bash
57-
export GITHUB_APP_TOKEN="your_app_token_here"
58-
./better-reviewers -app
59+
# Using command-line flags
60+
./better-reviewers --app-id "123456" --app-key "/path/to/private-key.pem"
61+
62+
# Using environment variables
63+
export GITHUB_APP_ID="123456"
64+
export GITHUB_APP_KEY="/path/to/private-key.pem"
65+
./better-reviewers --app-id "" --app-key "" # Flags can be empty to use env vars
5966
```
6067

6168
### Polling Mode
@@ -70,191 +77,65 @@ export GITHUB_APP_TOKEN="your_app_token_here"
7077
./better-reviewers -pr "owner/repo#123" -dry-run
7178
```
7279

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-
```
80+
## Configuration Options
8681

87-
## Command Line Options
82+
### Command-Line Flags
8883

89-
### Target Flags (Mutually Exclusive)
90-
91-
- `-pr`: Pull request URL or shorthand (e.g., `https://github.com/owner/repo/pull/123` or `owner/repo#123`)
92-
- `-project`: GitHub project to monitor (e.g., `owner/repo`)
84+
- `-pr`: Pull request URL or reference
85+
- `-project`: GitHub project to monitor
9386
- `-org`: GitHub organization to monitor
94-
- `-app`: Monitor all organizations where this GitHub app is installed
95-
96-
### Behavior Flags
97-
98-
- `-poll`: Polling interval (e.g., `1h`, `30m`). If not set, runs once
99-
- `-dry-run`: Run in dry-run mode (no actual reviewer assignments)
100-
- `-min-age`: Minimum time since last commit or review for PR assignment (default: 1h)
101-
- `-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)
87+
- `--app-id`: GitHub App ID for authentication
88+
- `--app-key`: Path to GitHub App private key file
89+
- `-poll`: Polling interval (e.g., 1h, 30m)
90+
- `-dry-run`: Run without making changes
91+
- `-min-age`: Minimum time since last activity (default: 1h)
92+
- `-max-age`: Maximum time since last activity (default: 180d)
93+
- `-max-prs`: Maximum open PRs per reviewer (default: 9)
94+
- `-pr-count-cache`: Cache duration for PR counts (default: 6h)
95+
96+
### Environment Variables
97+
98+
For GitHub App authentication:
99+
- `GITHUB_APP_ID`: Your GitHub App's ID
100+
- `GITHUB_APP_KEY`: Path to your app's private key file
101+
- `GITHUB_APP_PRIVATE_KEY_PATH`: (Legacy) Alternative to GITHUB_APP_KEY
102+
103+
## GitHub App Setup
104+
105+
1. Create a GitHub App in your organization settings
106+
2. Required permissions:
107+
- Repository: Read & Write (for PR assignments)
108+
- Pull requests: Read & Write
109+
- Organization members: Read
110+
3. Download the private key when prompted
111+
4. Note your App ID from the app settings page
112+
5. Install the app on your organization(s)
104113

105114
## How It Works
106115

107-
### Reviewer Selection Algorithm
108-
109-
The program finds exactly two reviewers for each PR: a **primary reviewer** (with author context) and a **secondary reviewer** (who actively reviews code).
110-
111-
#### Primary Reviewer (Author Context)
112-
113-
The primary reviewer is selected based on who knows the code best, in this priority order:
114-
115-
1. **Blame-based Authors**:
116-
- Examines GitHub blame history for changed files
117-
- Considers the top 5 PRs by overlap with edited lines
118-
- Selects authors of these PRs who still have write access
119-
- Verifies write access via author_association
120-
121-
2. **Directory Author** (fallback):
122-
- Most recent author of a merged PR in the same directory
123-
124-
3. **Project Author** (fallback):
125-
- Most recent author of a merged PR in the project
126-
127-
#### Secondary Reviewer (Active Reviewer)
128-
129-
The secondary reviewer is selected based on review activity, in this priority order:
130-
131-
1. **Blame-based Reviewers**:
132-
- Examines the same GitHub blame history
133-
- Considers the top 5 PRs by overlap with edited lines
134-
- Selects reviewers/approvers of these PRs
135-
136-
2. **Directory Reviewer** (fallback):
137-
- Most recent reviewer of a merged PR in the same directory
138-
139-
3. **Project Reviewer** (fallback):
140-
- Most recent reviewer of a merged PR in the project
141-
142-
### Assignment Rules
116+
1. **Analysis**: Examines PR changes, file history, and contributor patterns
117+
2. **Scoring**: Rates candidates based on:
118+
- Code overlap with changed files
119+
- Recent activity and expertise
120+
- Current workload (open PRs)
121+
3. **Selection**: Chooses optimal reviewers avoiding overloaded contributors
122+
4. **Assignment**: Adds reviewers to PRs (unless in dry-run mode)
143123

144-
- Always attempts to assign exactly 2 reviewers: primary and secondary
145-
- The PR author cannot be a reviewer
146-
- Primary and secondary must be different people
147-
- Each reviewer selection logs the mechanism used (e.g., "primary-blame-author", "secondary-directory-reviewer")
148-
- **Draft PRs**: Skips reviewer assignment for draft PRs but logs who would have been assigned
124+
## Security Notes
149125

150-
### Error Handling
151-
152-
- If after exhausting all fallback mechanisms, the only candidate found is the PR author, the program will error with a clear message
153-
- This ensures that the PR author is never assigned as their own reviewer
154-
- The error message will indicate that all candidates have been exhausted
155-
156-
### GitHub API Usage
157-
158-
The program uses:
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
167-
168-
## Architecture
169-
170-
The codebase is organized into several key files:
171-
172-
- `main.go`: Command-line interface and main program logic
173-
- `github.go`: GitHub API client implementation
174-
- `reviewer.go`: Core reviewer finding and assignment logic
175-
- `analysis.go`: Blame data analysis and PR relationship detection
176-
- `main_test.go`: Unit tests for core functionality
126+
- Private keys should have restricted permissions (not world-readable)
127+
- JWT tokens are automatically refreshed before expiry
128+
- All API responses are sanitized in logs
129+
- Token validation ensures only valid GitHub tokens are accepted
177130

178131
## Testing
179132

180-
Run the test suite:
133+
Run in dry-run mode to preview reviewer assignments:
181134

182135
```bash
183-
go test -v
136+
./better-reviewers --dry-run --pr https://github.com/owner/repo/pull/123
184137
```
185138

186-
## Example Output
187-
188-
### Workload Balancing in Action
189-
```
190-
2024/01/15 10:30:45 Processing PR owner/repo#123: Add new feature
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
201-
```
202-
203-
### Fallback Mechanism in Action
204-
```
205-
2024/01/15 10:31:00 Processing PR owner/repo#124: Fix minor bug
206-
2024/01/15 10:31:00 Analyzing 3 changed files for PR 124
207-
2024/01/15 10:31:01 === Finding PRIMARY reviewer (author context) ===
208-
2024/01/15 10:31:01 Checking blame-based authors for primary reviewer
209-
2024/01/15 10:31:01 No blame-based authors found, checking directory authors
210-
2024/01/15 10:31:02 PRIMARY reviewer selected: charlie (method: primary-directory-author)
211-
2024/01/15 10:31:02 === Finding SECONDARY reviewer (active reviewer) ===
212-
2024/01/15 10:31:02 Checking blame-based reviewers for secondary reviewer
213-
2024/01/15 10:31:02 No blame-based reviewers found, checking directory reviewers
214-
2024/01/15 10:31:03 No directory reviewers found, checking project reviewers
215-
2024/01/15 10:31:03 SECONDARY reviewer selected: dave (method: secondary-project-reviewer)
216-
2024/01/15 10:31:03 Found 2 reviewer candidates for PR 124
217-
2024/01/15 10:31:03 Adding reviewers [charlie dave] to PR owner/repo#124
218-
2024/01/15 10:31:03 Successfully added reviewers [charlie dave] to PR 124
219-
```
220-
221-
### Error Case: Only PR Author Found
222-
```
223-
2024/01/15 10:32:00 Processing PR owner/repo#125: Initial commit
224-
2024/01/15 10:32:00 Top changed files for PR 125: [main.go, go.mod, README.md]
225-
2024/01/15 10:32:01 Finding context reviewers using blame data for 3 files
226-
2024/01/15 10:32:01 Finding activity reviewers for 3 files
227-
2024/01/15 10:32:01 Found only 0 candidates, trying fallback to line authors
228-
2024/01/15 10:32:01 Finding line authors with write access for fallback
229-
2024/01/15 10:32:01 Line author fallback candidate: john-doe (lines: 50, association: OWNER, method: fallback-line-author)
230-
2024/01/15 10:32:02 Found only 1 candidates, trying fallback to recent file authors
231-
2024/01/15 10:32:02 Found only 1 candidates, trying directory-based fallbacks
232-
2024/01/15 10:32:02 Found only 1 candidates, trying project-wide fallbacks
233-
2024/01/15 10:32:03 Project author fallback candidate: john-doe (method: fallback-project-author)
234-
2024/01/15 10:32:03 Failed to find reviewer candidates: exhausted all reviewer candidates: the only candidate found was the PR author (john-doe)
235-
```
236-
237-
### Draft PR Handling
238-
```
239-
2024/01/15 10:33:00 Processing PR owner/repo#126 [DRAFT]: WIP: New feature
240-
2024/01/15 10:33:00 Top changed files for PR 126: [feature.go, feature_test.go]
241-
2024/01/15 10:33:01 Finding context reviewers using blame data for 2 files
242-
2024/01/15 10:33:01 Context reviewer candidate: alice (score: 20, method: context-blame-approver)
243-
2024/01/15 10:33:01 Activity reviewer candidate: bob (PR size: 120, method: activity-recent-approver)
244-
2024/01/15 10:33:01 Found 2 reviewer candidates for PR 126
245-
2024/01/15 10:33:01 Selected reviewer: alice (method: context-blame-approver, context score: 20, activity score: 0)
246-
2024/01/15 10:33:01 Selected reviewer: bob (method: activity-recent-approver, context score: 0, activity score: 120)
247-
2024/01/15 10:33:01 PR 126 is a draft - skipping reviewer assignment
248-
2024/01/15 10:33:01 Would have assigned reviewers [alice bob] to PR 126 if it wasn't a draft
249-
```
250-
251-
## Contributing
252-
253-
1. Follow Go best practices and code review guidelines from go.dev
254-
2. Add tests for new functionality
255-
3. Ensure comprehensive logging for debugging
256-
4. Use minimal external dependencies
257-
258139
## License
259140

260-
This project is provided as-is for educational and productivity purposes.
141+
[License details here]

0 commit comments

Comments
 (0)