Files
the-nexus/scripts/pr-backlog-analyzer.py
Alexander Whitestone 38ce7e4bc5
Some checks failed
CI / test (pull_request) Failing after 1m14s
CI / validate (pull_request) Failing after 50s
Review Approval Gate / verify-review (pull_request) Failing after 5s
feat: Add PR backlog management process (#1470)
## Summary
Added tools and process for managing PR backlog in timmy-config.

## Problem
timmy-config has 31+ open PRs, the highest in the organization.
This creates confusion, slows down development, and increases
merge conflicts.

## Solution
Created automated tools and process for PR backlog management:

### 1. PR Backlog Analyzer (`scripts/pr-backlog-analyzer.py`)
- Fetches all open PRs from timmy-config
- Analyzes age, review status, labels
- Generates markdown report
- Categorizes PRs: stale, needs review, approved, changes requested

### 2. GitHub Actions Workflow (`.github/workflows/pr-backlog-management.yml`)
- Runs weekly on Monday at 10 AM UTC
- Analyzes PR backlog
- Creates issue if backlog is high (>10 stale PRs)
- Uploads report as artifact

### 3. Documentation (`docs/pr-backlog-process.md`)
- Weekly analysis process
- Review stale PRs procedure
- Merge approved PRs workflow
- Review pending PRs SLA
- Close duplicate PRs process
- Metrics to track
- Escalation procedures

## Usage

### Run Analyzer
```bash
python scripts/pr-backlog-analyzer.py
```

### View Report
```bash
cat reports/pr-backlog-$(date +%Y%m%d).md
```

## Metrics
- **Current**: 32 open PRs in timmy-config
- **Target**: <20 open PRs
- **SLA**: Review within 48 hours, merge within 7 days

Issue: #1470
2026-04-14 21:14:55 -04:00

182 lines
5.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""
PR Backlog Analyzer for timmy-config
Analyzes open PRs and provides recommendations for cleanup.
"""
import json
import subprocess
import sys
from datetime import datetime, timedelta
from pathlib import Path
def get_open_prs(repo: str, token: str) -> list:
"""Get all open PRs from a repository."""
result = subprocess.run([
"curl", "-s", "-H", f"Authorization: token {token}",
f"https://forge.alexanderwhitestone.com/api/v1/repos/{repo}/pulls?state=open&limit=100"
], capture_output=True, text=True)
if result.returncode != 0:
print(f"Error fetching PRs: {result.stderr}")
return []
return json.loads(result.stdout)
def analyze_pr(pr: dict) -> dict:
"""Analyze a single PR."""
created = datetime.fromisoformat(pr['created_at'].replace('Z', '+00:00'))
age_days = (datetime.now(created.tzinfo) - created).days
# Check for reviews
reviews = pr.get('reviews', [])
has_approvals = any(r.get('state') == 'APPROVED' for r in reviews)
has_changes_requested = any(r.get('state') == 'CHANGES_REQUESTED' for r in reviews)
# Check labels
labels = [l['name'] for l in pr.get('labels', [])]
return {
'number': pr['number'],
'title': pr['title'],
'branch': pr['head']['ref'],
'created': pr['created_at'],
'age_days': age_days,
'user': pr['user']['login'],
'has_approvals': has_approvals,
'has_changes_requested': has_changes_requested,
'labels': labels,
'url': pr['html_url'],
}
def categorize_prs(prs: list) -> dict:
"""Categorize PRs by status."""
categories = {
'stale': [], # > 30 days old
'needs_review': [], # No reviews
'approved': [], # Approved but not merged
'changes_requested': [], # Changes requested
'recent': [], # < 7 days old
}
for pr in prs:
if pr['age_days'] > 30:
categories['stale'].append(pr)
elif pr['has_approvals']:
categories['approved'].append(pr)
elif pr['has_changes_requested']:
categories['changes_requested'].append(pr)
elif pr['age_days'] < 7:
categories['recent'].append(pr)
else:
categories['needs_review'].append(pr)
return categories
def generate_report(repo: str, prs: list, categories: dict) -> str:
"""Generate a markdown report."""
report = f"""# PR Backlog Report — {repo}
Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
## Summary
- **Total Open PRs**: {len(prs)}
- **Stale (>30 days)**: {len(categories['stale'])}
- **Needs Review**: {len(categories['needs_review'])}
- **Approved**: {len(categories['approved'])}
- **Changes Requested**: {len(categories['changes_requested'])}
- **Recent (<7 days)**: {len(categories['recent'])}
## Recommendations
### Immediate Actions
1. **Merge approved PRs**: {len(categories['approved'])} PRs are ready to merge
2. **Review stale PRs**: {len(categories['stale'])} PRs are >30 days old
3. **Address changes requested**: {len(categories['changes_requested'])} PRs need updates
### Process Improvements
1. **Assign reviewers**: Ensure each PR has a reviewer within 24 hours
2. **Set SLAs**:
- Review within 48 hours
- Merge within 7 days of approval
- Close stale PRs after 30 days
3. **Automate**: Add CI checks to prevent backlog
## Detailed Analysis
### Stale PRs (>30 days)
"""
for pr in categories['stale']:
report += f"- **#{pr['number']}**: {pr['title']}\n"
report += f" - Age: {pr['age_days']} days\n"
report += f" - Author: {pr['user']}\n"
report += f" - URL: {pr['url']}\n\n"
report += "\n### Approved PRs (Ready to Merge)\n"
for pr in categories['approved']:
report += f"- **#{pr['number']}**: {pr['title']}\n"
report += f" - Age: {pr['age_days']} days\n"
report += f" - Author: {pr['user']}\n"
report += f" - URL: {pr['url']}\n\n"
report += "\n### Needs Review\n"
for pr in categories['needs_review']:
report += f"- **#{pr['number']}**: {pr['title']}\n"
report += f" - Age: {pr['age_days']} days\n"
report += f" - Author: {pr['user']}\n"
report += f" - URL: {pr['url']}\n\n"
return report
def main():
"""Main function."""
token = Path.home() / '.config' / 'gitea' / 'token'
if not token.exists():
print("Error: Gitea token not found")
sys.exit(1)
token_str = token.read_text().strip()
repo = "Timmy_Foundation/timmy-config"
print(f"Fetching PRs for {repo}...")
prs = get_open_prs(repo, token_str)
if not prs:
print("No open PRs found")
return
print(f"Found {len(prs)} open PRs")
# Analyze PRs
analyzed = [analyze_pr(pr) for pr in prs]
categories = categorize_prs(analyzed)
# Generate report
report = generate_report(repo, analyzed, categories)
# Save report
output_dir = Path("reports")
output_dir.mkdir(exist_ok=True)
report_file = output_dir / f"pr-backlog-{datetime.now().strftime('%Y%m%d')}.md"
report_file.write_text(report)
print(f"\nReport saved to: {report_file}")
print(f"\nSummary:")
print(f" Total PRs: {len(prs)}")
print(f" Stale: {len(categories['stale'])}")
print(f" Approved: {len(categories['approved'])}")
print(f" Needs Review: {len(categories['needs_review'])}")
if __name__ == "__main__":
main()