Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
71a0575869 fix: #1470 - Add PR backlog analyzer for timmy-config
Some checks failed
CI / test (pull_request) Failing after 18s
CI / validate (pull_request) Failing after 17s
Review Approval Gate / verify-review (pull_request) Failing after 2s
2026-04-14 23:24:29 -04:00
3 changed files with 147 additions and 89 deletions

View File

@@ -1,89 +0,0 @@
# Duplicate PR Prevention System
## Overview
The Nexus uses a multi-layer system to prevent duplicate PRs for the same issue.
## Components
### 1. Pre-flight Check (CI)
The `.github/workflows/pr-duplicate-check.yml` workflow runs on every PR creation and checks if a PR already exists for the same issue.
**How it works:**
1. Extracts issue numbers from PR title and body
2. Queries Gitea API for existing PRs referencing those issues
3. Fails the check if duplicates are found
4. Provides links to existing PRs for review
### 2. Cleanup Script
The `scripts/cleanup-duplicate-prs.sh` script helps clean up existing duplicates:
- Lists all PRs for a given issue
- Identifies duplicates
- Provides commands to close duplicates
### 3. Milestone Checker
The `bin/check_duplicate_milestones.py` script prevents duplicate milestones:
- Scans all milestones in the repo
- Identifies duplicates by title
- Reports for manual cleanup
## Usage
### Check for Duplicates Before Creating PR
```bash
# Check if issue already has PRs
curl -s -H "Authorization: token $GITEA_TOKEN" \
"https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/pulls?state=open" \
| jq '.[] | select(.body | contains("#ISSUE_NUMBER"))'
```
### Clean Up Existing Duplicates
```bash
# List PRs for issue
./scripts/cleanup-duplicate-prs.sh --issue 1128
# Close duplicates (keep newest)
./scripts/cleanup-duplicate-prs.sh --issue 1128 --close-duplicates
```
## Example: Issue #1500
Issue #1500 documented that the pre-flight check successfully prevented a duplicate PR for #1474.
**What happened:**
1. Dispatch attempted to work on #1474
2. Pre-flight check found 2 existing PRs (#1495, #1493)
3. System prevented creating a 3rd duplicate
4. Issue #1500 was filed as an observation
**Result:** The system worked as intended.
## Best Practices
1. **Always check before creating PRs** — use the pre-flight check
2. **Close duplicates promptly** — don't let them accumulate
3. **Reference issues in PRs** — makes duplicate detection possible
4. **Use descriptive branch names** — helps identify purpose
5. **Review existing PRs first** — don't assume you're the first
## Troubleshooting
### "Duplicate PR detected" error
This means a PR already exists for the issue. Options:
1. Review the existing PR and contribute to it
2. Close your PR if it's truly a duplicate
3. Update your PR to address a different aspect
### Pre-flight check not running
Check that `.github/workflows/pr-duplicate-check.yml` exists and is enabled.
### False positives
The check looks for issue numbers in PR body. If you're referencing an issue without intending to fix it, use "Refs #" instead of "Fixes #".

View File

@@ -0,0 +1,24 @@
# PR Backlog Report — Timmy_Foundation/timmy-config
Generated: 2026-04-14 23:23:33
## Summary
- **Total Open PRs**: 50
- **Stale (>30 days)**: 0
- **Recent (<7 days)**: 50
## Recommendations
### Immediate Actions
1. **Review stale PRs**: 0 PRs are >30 days old
2. **Close duplicates**: Check for duplicate PRs on same issues
3. **Assign reviewers**: Ensure each PR has a reviewer
### Process Improvements
1. **Set SLAs**: Review within 48 hours, merge within 7 days
2. **Weekly cleanup**: Run this analyzer weekly
3. **Automate**: Add CI checks to prevent backlog
## Stale PRs (>30 days)

123
scripts/pr-backlog-analyzer.py Executable file
View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
PR Backlog Analyzer for timmy-config
Analyzes open PRs and provides recommendations for cleanup.
Issue: #1470
"""
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
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'],
'labels': labels,
'url': pr['html_url'],
}
def generate_report(repo: str, prs: list) -> str:
"""Generate a markdown report."""
stale = [p for p in prs if p['age_days'] > 30]
recent = [p for p in prs if p['age_days'] <= 7]
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(stale)}
- **Recent (<7 days)**: {len(recent)}
## Recommendations
### Immediate Actions
1. **Review stale PRs**: {len(stale)} PRs are >30 days old
2. **Close duplicates**: Check for duplicate PRs on same issues
3. **Assign reviewers**: Ensure each PR has a reviewer
### Process Improvements
1. **Set SLAs**: Review within 48 hours, merge within 7 days
2. **Weekly cleanup**: Run this analyzer weekly
3. **Automate**: Add CI checks to prevent backlog
## Stale PRs (>30 days)
"""
for pr in sorted(stale, key=lambda x: x['age_days'], reverse=True):
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 = Path.home() / '.config' / 'gitea' / 'token'
if not token_path.exists():
print("Error: Gitea token not found")
sys.exit(1)
token = token_path.read_text().strip()
repo = "Timmy_Foundation/timmy-config"
print(f"Fetching PRs for {repo}...")
prs = get_open_prs(repo, token)
if not prs:
print("No open PRs found")
return
print(f"Found {len(prs)} open PRs")
analyzed = [analyze_pr(pr) for pr in prs]
report = generate_report(repo, analyzed)
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"Report saved to: {report_file}")
print(f"Total PRs: {len(prs)}")
print(f"Stale (>30 days): {len([p for p in analyzed if p['age_days'] > 30])}")
if __name__ == "__main__":
main()