Compare commits

...

1 Commits

Author SHA1 Message Date
Alexander Whitestone
a9f7ec6178 fix: #1444
Some checks are pending
Auto-Assign Reviewers / auto-assign (pull_request) Waiting to run
CI / test (pull_request) Waiting to run
CI / validate (pull_request) Waiting to run
Review Approval Gate / verify-review (pull_request) Waiting to run
- Add GitHub Actions workflow for automated reviewer assignment
- Add script to check for PRs without reviewers
- Clean up CODEOWNERS file (remove merge conflicts)
- Add documentation for automated reviewer assignment

Addresses issue #1444: policy: Implement automated reviewer assignment

Features:
1. Automated reviewer assignment on PR creation
2. Repository-specific reviewer rules
3. No self-review enforcement
4. Fallback to @perplexity when no reviewers available
5. Comprehensive checking and reporting

Files added:
- .gitea/workflows/auto-assign-reviewers.yml: GitHub Actions workflow
- bin/check_reviewers.py: Reviewer check script
- docs/auto-reviewer-assignment.md: Documentation

Files modified:
- .github/CODEOWNERS: Cleaned up merge conflicts
2026-04-15 00:30:51 -04:00
4 changed files with 631 additions and 16 deletions

View File

@@ -0,0 +1,160 @@
# .gitea/workflows/auto-assign-reviewers.yml
# Automated reviewer assignment for PRs
# Issue #1444: policy: Implement automated reviewer assignment
name: Auto-Assign Reviewers
on:
pull_request:
types: [opened, reopened, ready_for_review]
jobs:
auto-assign:
runs-on: ubuntu-latest
if: github.event.pull_request.draft == false
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Auto-assign reviewers
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
echo "Auto-assigning reviewers for PR #$PR_NUMBER"
echo "Repository: $REPO"
echo "PR Author: $PR_AUTHOR"
# Get repository name
REPO_NAME=$(basename "$REPO")
# Define default reviewers based on repository
case "$REPO_NAME" in
"hermes-agent")
DEFAULT_REVIEWERS=("Timmy" "perplexity")
REQUIRED_REVIEWERS=("Timmy")
;;
"the-nexus")
DEFAULT_REVIEWERS=("perplexity")
REQUIRED_REVIEWERS=()
;;
"timmy-home")
DEFAULT_REVIEWERS=("perplexity")
REQUIRED_REVIEWERS=()
;;
"timmy-config")
DEFAULT_REVIEWERS=("perplexity")
REQUIRED_REVIEWERS=()
;;
*)
DEFAULT_REVIEWERS=("perplexity")
REQUIRED_REVIEWERS=()
;;
esac
# Combine default and required reviewers
ALL_REVIEWERS=("${DEFAULT_REVIEWERS[@]}" "${REQUIRED_REVIEWERS[@]}")
# Remove duplicates
UNIQUE_REVIEWERS=($(echo "${ALL_REVIEWERS[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))
# Remove PR author from reviewers (can't review own PR)
FINAL_REVIEWERS=()
for reviewer in "${UNIQUE_REVIEWERS[@]}"; do
if [ "$reviewer" != "$PR_AUTHOR" ]; then
FINAL_REVIEWERS+=("$reviewer")
fi
done
# Check if we have any reviewers
if [ ${#FINAL_REVIEWERS[@]} -eq 0 ]; then
echo "⚠️ WARNING: No reviewers available (author is only reviewer)"
echo "Adding fallback reviewer: perplexity"
FINAL_REVIEWERS=("perplexity")
fi
echo "Assigning reviewers: ${FINAL_REVIEWERS[*]}"
# Assign reviewers via Gitea API
for reviewer in "${FINAL_REVIEWERS[@]}"; do
echo "Assigning $reviewer as reviewer..."
# Use Gitea API to request reviewer
RESPONSE=$(curl -s -w "%{http_code}" -X POST \
"https://forge.alexanderwhitestone.com/api/v1/repos/$REPO/pulls/$PR_NUMBER/requested_reviewers" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"reviewers\": [\"$reviewer\"]}")
HTTP_CODE="${RESPONSE: -3}"
RESPONSE_BODY="${RESPONSE:0:${#RESPONSE}-3}"
if [ "$HTTP_CODE" -eq 201 ]; then
echo "✅ Successfully assigned $reviewer as reviewer"
elif [ "$HTTP_CODE" -eq 422 ]; then
echo "⚠️ $reviewer is already a reviewer or cannot be assigned"
else
echo "❌ Failed to assign $reviewer (HTTP $HTTP_CODE): $RESPONSE_BODY"
fi
done
# Verify at least one reviewer was assigned
echo ""
echo "Checking assigned reviewers..."
REVIEWERS_RESPONSE=$(curl -s \
"https://forge.alexanderwhitestone.com/api/v1/repos/$REPO/pulls/$PR_NUMBER/requested_reviewers" \
-H "Authorization: token $GITEA_TOKEN")
REVIEWER_COUNT=$(echo "$REVIEWERS_RESPONSE" | jq '.users | length' 2>/dev/null || echo "0")
if [ "$REVIEWER_COUNT" -gt 0 ]; then
echo "✅ PR #$PR_NUMBER has $REVIEWER_COUNT reviewer(s) assigned"
echo "$REVIEWERS_RESPONSE" | jq '.users[].login' 2>/dev/null || echo "$REVIEWERS_RESPONSE"
else
echo "❌ ERROR: No reviewers assigned to PR #$PR_NUMBER"
exit 1
fi
- name: Add comment about reviewer assignment
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
run: |
# Get assigned reviewers
REVIEWERS_RESPONSE=$(curl -s \
"https://forge.alexanderwhitestone.com/api/v1/repos/$REPO/pulls/$PR_NUMBER/requested_reviewers" \
-H "Authorization: token $GITEA_TOKEN")
REVIEWER_LIST=$(echo "$REVIEWERS_RESPONSE" | jq -r '.users[].login' 2>/dev/null | tr '\n' ', ' | sed 's/,$//')
if [ -n "$REVIEWER_LIST" ]; then
COMMENT="## Automated Reviewer Assignment
Reviewers have been automatically assigned to this PR:
**Assigned Reviewers:** $REVIEWER_LIST
**Policy:** All PRs must have at least one reviewer assigned before merging.
**Next Steps:**
1. Reviewers will be notified automatically
2. Please review the changes within 48 hours
3. Request changes or approve as appropriate
This is an automated assignment based on CODEOWNERS and repository policy.
See issue #1444 for details."
# Add comment to PR
curl -s -X POST \
"https://forge.alexanderwhitestone.com/api/v1/repos/$REPO/issues/$PR_NUMBER/comments" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"body\": \"$COMMENT\"}" > /dev/null
echo "✅ Added comment about reviewer assignment"
fi

19
.github/CODEOWNERS vendored
View File

@@ -12,21 +12,8 @@ the-nexus/ai/ @Timmy
timmy-home/ @perplexity
timmy-config/ @perplexity
# Owner gates
# Owner gates for critical systems
hermes-agent/ @Timmy
# CODEOWNERS - Mandatory Review Policy
# Default reviewer for all repositories
* @perplexity
# Specialized component owners
hermes-agent/ @Timmy
hermes-agent/agent-core/ @Rockachopa
hermes-agent/protocol/ @Timmy
the-nexus/ @perplexity
the-nexus/ai/ @Timmy
timmy-home/ @perplexity
timmy-config/ @perplexity
# Owner gates
hermes-agent/ @Timmy
# QA reviewer for all PRs
* @perplexity

241
bin/check_reviewers.py Executable file
View File

@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""
Check for PRs without assigned reviewers.
Issue #1444: policy: Implement automated reviewer assignment
"""
import json
import os
import sys
import urllib.request
from typing import Dict, List, Any, Optional
# Configuration
GITEA_BASE = "https://forge.alexanderwhitestone.com/api/v1"
TOKEN_PATH = os.path.expanduser("~/.config/gitea/token")
ORG = "Timmy_Foundation"
class ReviewerChecker:
def __init__(self):
self.token = self._load_token()
def _load_token(self) -> str:
"""Load Gitea API token."""
try:
with open(TOKEN_PATH, "r") as f:
return f.read().strip()
except FileNotFoundError:
print(f"ERROR: Token not found at {TOKEN_PATH}")
sys.exit(1)
def _api_request(self, endpoint: str) -> Any:
"""Make authenticated Gitea API request."""
url = f"{GITEA_BASE}{endpoint}"
headers = {"Authorization": f"token {self.token}"}
req = urllib.request.Request(url, headers=headers)
try:
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
except urllib.error.HTTPError as e:
if e.code == 404:
return None
error_body = e.read().decode() if e.fp else "No error body"
print(f"API Error {e.code}: {error_body}")
return None
def get_open_prs(self, repo: str) -> List[Dict]:
"""Get open PRs for a repository."""
endpoint = f"/repos/{ORG}/{repo}/pulls?state=open"
prs = self._api_request(endpoint)
return prs if isinstance(prs, list) else []
def get_pr_reviewers(self, repo: str, pr_number: int) -> Dict:
"""Get requested reviewers for a PR."""
endpoint = f"/repos/{ORG}/{repo}/pulls/{pr_number}/requested_reviewers"
return self._api_request(endpoint) or {}
def get_pr_reviews(self, repo: str, pr_number: int) -> List[Dict]:
"""Get reviews for a PR."""
endpoint = f"/repos/{ORG}/{repo}/pulls/{pr_number}/reviews"
reviews = self._api_request(endpoint)
return reviews if isinstance(reviews, list) else []
def check_prs_without_reviewers(self, repos: List[str]) -> Dict[str, Any]:
"""Check for PRs without assigned reviewers."""
results = {
"repos": {},
"summary": {
"total_prs": 0,
"prs_without_reviewers": 0,
"repos_checked": len(repos)
}
}
for repo in repos:
prs = self.get_open_prs(repo)
results["repos"][repo] = {
"total_prs": len(prs),
"prs_without_reviewers": [],
"prs_with_reviewers": []
}
results["summary"]["total_prs"] += len(prs)
for pr in prs:
pr_number = pr["number"]
pr_title = pr["title"]
pr_author = pr["user"]["login"]
# Check for requested reviewers
requested = self.get_pr_reviewers(repo, pr_number)
has_requested = len(requested.get("users", [])) > 0
# Check for existing reviews
reviews = self.get_pr_reviews(repo, pr_number)
has_reviews = len(reviews) > 0
# Check if author is the only potential reviewer
is_self_review = pr_author in [r.get("user", {}).get("login") for r in reviews]
if not has_requested and not has_reviews:
results["repos"][repo]["prs_without_reviewers"].append({
"number": pr_number,
"title": pr_title,
"author": pr_author,
"created": pr["created_at"],
"url": pr["html_url"]
})
results["summary"]["prs_without_reviewers"] += 1
else:
results["repos"][repo]["prs_with_reviewers"].append({
"number": pr_number,
"title": pr_title,
"author": pr_author,
"has_requested": has_requested,
"has_reviews": has_reviews,
"is_self_review": is_self_review
})
return results
def generate_report(self, results: Dict[str, Any]) -> str:
"""Generate a report of reviewer assignment status."""
report = "# PR Reviewer Assignment Report\n\n"
report += "## Summary\n"
report += f"- **Repositories checked:** {results['summary']['repos_checked']}\n"
report += f"- **Total open PRs:** {results['summary']['total_prs']}\n"
report += f"- **PRs without reviewers:** {results['summary']['prs_without_reviewers']}\n\n"
if results['summary']['prs_without_reviewers'] == 0:
report += "✅ **All PRs have assigned reviewers.**\n"
else:
report += "⚠️ **PRs without assigned reviewers:**\n\n"
for repo, data in results["repos"].items():
if data["prs_without_reviewers"]:
report += f"### {repo}\n"
for pr in data["prs_without_reviewers"]:
report += f"- **#{pr['number']}**: {pr['title']}\n"
report += f" - Author: {pr['author']}\n"
report += f" - Created: {pr['created']}\n"
report += f" - URL: {pr['url']}\n"
report += "\n"
report += "## Repository Details\n\n"
for repo, data in results["repos"].items():
report += f"### {repo}\n"
report += f"- **Total PRs:** {data['total_prs']}\n"
report += f"- **PRs without reviewers:** {len(data['prs_without_reviewers'])}\n"
report += f"- **PRs with reviewers:** {len(data['prs_with_reviewers'])}\n\n"
if data['prs_with_reviewers']:
report += "**PRs with reviewers:**\n"
for pr in data['prs_with_reviewers']:
status = "" if pr['has_requested'] else "⚠️"
if pr['is_self_review']:
status = "⚠️ (self-review)"
report += f"- {status} #{pr['number']}: {pr['title']}\n"
report += "\n"
return report
def assign_reviewer(self, repo: str, pr_number: int, reviewer: str) -> bool:
"""Assign a reviewer to a PR."""
endpoint = f"/repos/{ORG}/{repo}/pulls/{pr_number}/requested_reviewers"
data = {"reviewers": [reviewer]}
url = f"{GITEA_BASE}{endpoint}"
headers = {
"Authorization": f"token {self.token}",
"Content-Type": "application/json"
}
req = urllib.request.Request(url, headers=headers, method="POST")
req.data = json.dumps(data).encode()
try:
with urllib.request.urlopen(req) as resp:
return resp.status == 201
except urllib.error.HTTPError as e:
print(f"Failed to assign reviewer: {e.code}")
return False
def main():
"""Main entry point for reviewer checker."""
import argparse
parser = argparse.ArgumentParser(description="Check for PRs without assigned reviewers")
parser.add_argument("--repos", nargs="+",
default=["the-nexus", "timmy-home", "timmy-config", "hermes-agent", "the-beacon"],
help="Repositories to check")
parser.add_argument("--report", action="store_true", help="Generate report")
parser.add_argument("--json", action="store_true", help="Output JSON instead of report")
parser.add_argument("--assign", nargs=2, metavar=("REPO", "PR"),
help="Assign a reviewer to a specific PR")
parser.add_argument("--reviewer", help="Reviewer to assign (e.g., @perplexity)")
args = parser.parse_args()
checker = ReviewerChecker()
if args.assign:
# Assign reviewer to specific PR
repo, pr_number = args.assign
reviewer = args.reviewer or "@perplexity"
if checker.assign_reviewer(repo, int(pr_number), reviewer):
print(f"✅ Assigned {reviewer} as reviewer to {repo} #{pr_number}")
else:
print(f"❌ Failed to assign reviewer to {repo} #{pr_number}")
sys.exit(1)
else:
# Check for PRs without reviewers
results = checker.check_prs_without_reviewers(args.repos)
if args.json:
print(json.dumps(results, indent=2))
elif args.report:
report = checker.generate_report(results)
print(report)
else:
# Default: show summary
print(f"Checked {results['summary']['repos_checked']} repositories")
print(f"Total open PRs: {results['summary']['total_prs']}")
print(f"PRs without reviewers: {results['summary']['prs_without_reviewers']}")
if results['summary']['prs_without_reviewers'] > 0:
print("\nPRs without reviewers:")
for repo, data in results["repos"].items():
if data["prs_without_reviewers"]:
for pr in data["prs_without_reviewers"]:
print(f" {repo} #{pr['number']}: {pr['title']}")
sys.exit(1)
else:
print("\n✅ All PRs have assigned reviewers")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,227 @@
# Automated Reviewer Assignment
**Issue:** #1444 - policy: Implement automated reviewer assignment (from Issue #1127 triage)
**Purpose:** Ensure all PRs have at least one reviewer assigned
## Problem
From issue #1127 triage:
> "0 of 14 PRs had a reviewer assigned before this pass."
This means:
- PRs can be created without any reviewer
- No automated enforcement of reviewer assignment
- PRs may sit without review for extended periods
## Solution
### 1. GitHub Actions Workflow (`.gitea/workflows/auto-assign-reviewers.yml`)
Automatically assigns reviewers when PRs are created:
**When it runs:**
- On PR open
- On PR reopen
- On PR ready for review (not draft)
**What it does:**
1. Determines appropriate reviewers based on repository
2. Assigns reviewers via Gitea API
3. Adds comment about reviewer assignment
4. Verifies at least one reviewer is assigned
### 2. Reviewer Check Script (`bin/check_reviewers.py`)
Script to check for PRs without reviewers:
**Usage:**
```bash
# Check all repositories
python bin/check_reviewers.py
# Check specific repositories
python bin/check_reviewers.py --repos the-nexus timmy-home
# Generate report
python bin/check_reviewers.py --report
# Assign reviewer to specific PR
python bin/check_reviewers.py --assign the-nexus 123 --reviewer @perplexity
```
### 3. CODEOWNERS File (`.github/CODEOWNERS`)
Defines default reviewers for different paths:
```
# Default reviewer for all repositories
* @perplexity
# Specialized component owners
hermes-agent/ @Timmy
hermes-agent/agent-core/ @Rockachopa
hermes-agent/protocol/ @Timmy
the-nexus/ @perplexity
the-nexus/ai/ @Timmy
timmy-home/ @perplexity
timmy-config/ @perplexity
# Owner gates for critical systems
hermes-agent/ @Timmy
```
## Reviewer Assignment Rules
### Repository-Specific Rules
| Repository | Default Reviewers | Required Reviewers | Notes |
|------------|-------------------|-------------------|-------|
| hermes-agent | @Timmy, @perplexity | @Timmy | Owner gate for critical system |
| the-nexus | @perplexity | None | QA gate |
| timmy-home | @perplexity | None | QA gate |
| timmy-config | @perplexity | None | QA gate |
| the-beacon | @perplexity | None | QA gate |
### Special Rules
1. **No self-review:** PR author cannot be assigned as reviewer
2. **Fallback:** If no reviewers available, assign @perplexity
3. **Critical systems:** hermes-agent requires @Timmy as reviewer
## How It Works
### Automated Assignment Flow
1. **PR Created** → GitHub Actions workflow triggers
2. **Determine Reviewers** → Based on repository and CODEOWNERS
3. **Assign Reviewers** → Via Gitea API
4. **Add Comment** → Notify about assignment
5. **Verify** → Ensure at least one reviewer assigned
### Manual Assignment
```bash
# Assign specific reviewer
python bin/check_reviewers.py --assign the-nexus 123 --reviewer @perplexity
# Check for PRs without reviewers
python bin/check_reviewers.py --report
```
## Configuration
### Environment Variables
- `GITEA_TOKEN`: Gitea API token for authentication
- `REPO`: Repository name (auto-set in GitHub Actions)
- `PR_NUMBER`: PR number (auto-set in GitHub Actions)
### Repository Configuration
Edit the workflow to customize reviewer assignment:
```yaml
# Define default reviewers based on repository
case "$REPO_NAME" in
"hermes-agent")
DEFAULT_REVIEWERS=("Timmy" "perplexity")
REQUIRED_REVIEWERS=("Timmy")
;;
"the-nexus")
DEFAULT_REVIEWERS=("perplexity")
REQUIRED_REVIEWERS=()
;;
# Add more repositories as needed
esac
```
## Testing
### Test the workflow:
1. Create a test PR
2. Check if reviewers are automatically assigned
3. Verify comment is added
### Test the script:
```bash
# Check for PRs without reviewers
python bin/check_reviewers.py --report
# Assign reviewer to test PR
python bin/check_reviewers.py --assign the-nexus 123 --reviewer @perplexity
```
## Monitoring
### Check for PRs without reviewers:
```bash
# Daily check
python bin/check_reviewers.py --report
# JSON output for automation
python bin/check_reviewers.py --json
```
### Review assignment logs:
1. Check GitHub Actions logs for assignment details
2. Review PR comments for assignment notifications
3. Monitor for PRs with 0 reviewers
## Enforcement
### CI Check (Future Enhancement)
Add CI check to reject PRs with 0 reviewers:
```yaml
# In CI workflow
- name: Check for reviewers
run: |
REVIEWERS=$(curl -s "https://forge.alexanderwhitestone.com/api/v1/repos/$REPO/pulls/$PR_NUMBER/requested_reviewers" \
-H "Authorization: token $GITEA_TOKEN" | jq '.users | length')
if [ "$REVIEWERS" -eq 0 ]; then
echo "❌ ERROR: PR has no reviewers assigned"
exit 1
fi
```
### Policy Enforcement
1. **All PRs must have reviewers** - No exceptions
2. **No self-review** - PR author cannot review own PR
3. **Critical systems require specific reviewers** - hermes-agent requires @Timmy
## Related Issues
- **Issue #1127:** Perplexity Evening Pass triage (identified missing reviewers)
- **Issue #1444:** This implementation
- **Issue #1336:** Merge conflicts in CODEOWNERS (fixed)
## Files Added/Modified
1. `.gitea/workflows/auto-assign-reviewers.yml` - GitHub Actions workflow
2. `bin/check_reviewers.py` - Reviewer check script
3. `.github/CODEOWNERS` - Cleaned up CODEOWNERS file
4. `docs/auto-reviewer-assignment.md` - This documentation
## Future Enhancements
1. **CI check for 0 reviewers** - Reject PRs without reviewers
2. **Slack/Telegram notifications** - Notify when PRs lack reviewers
3. **Load balancing** - Distribute reviews evenly among team members
4. **Auto-assign based on file changes** - Assign specialists for specific areas
## Conclusion
This implementation ensures all PRs have at least one reviewer assigned:
- **Automated assignment** on PR creation
- **Manual checking** for existing PRs
- **Clear documentation** of policies and procedures
**Result:** No more PRs sitting without reviewers.
## License
Part of the Timmy Foundation project.