Compare commits
1 Commits
mimo/code/
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38ce7e4bc5 |
70
.github/workflows/pr-backlog-management.yml
vendored
Normal file
70
.github/workflows/pr-backlog-management.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
name: PR Backlog Management
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
# Run weekly on Monday at 10 AM UTC
|
||||||
|
- cron: '0 10 * * 1'
|
||||||
|
workflow_dispatch: # Allow manual trigger
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze-backlog:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install requests
|
||||||
|
|
||||||
|
- name: Analyze PR backlog
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
python scripts/pr-backlog-analyzer.py
|
||||||
|
|
||||||
|
- name: Upload report
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: pr-backlog-report
|
||||||
|
path: reports/
|
||||||
|
|
||||||
|
- name: Create issue if backlog is high
|
||||||
|
if: failure()
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
const report = fs.readFileSync('reports/pr-backlog-' + new Date().toISOString().split('T')[0] + '.md', 'utf8');
|
||||||
|
|
||||||
|
// Check if backlog is high (more than 10 stale PRs)
|
||||||
|
const staleMatch = report.match(/Stale \(>30 days\): (\d+)/);
|
||||||
|
const staleCount = staleMatch ? parseInt(staleMatch[1]) : 0;
|
||||||
|
|
||||||
|
if (staleCount > 10) {
|
||||||
|
const title = 'PR Backlog Alert: ' + staleCount + ' stale PRs';
|
||||||
|
const body = `## PR Backlog Alert
|
||||||
|
|
||||||
|
The PR backlog analysis found ${staleCount} stale PRs (>30 days old).
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
Review and close stale PRs to reduce backlog.
|
||||||
|
|
||||||
|
### Report
|
||||||
|
See attached artifact for full analysis.
|
||||||
|
|
||||||
|
This issue was automatically created by the PR backlog management workflow.`;
|
||||||
|
|
||||||
|
await github.rest.issues.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
labels: ['process-improvement', 'p2-backlog']
|
||||||
|
});
|
||||||
|
}
|
||||||
126
docs/pr-backlog-process.md
Normal file
126
docs/pr-backlog-process.md
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
# PR Backlog Management Process
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This document outlines the process for managing PR backlog in the Timmy Foundation repositories, specifically addressing the high PR backlog in timmy-config.
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
As of the latest analysis:
|
||||||
|
- **timmy-config**: 31 open PRs (highest in org)
|
||||||
|
- **the-nexus**: Multiple PRs for same issues
|
||||||
|
- **hermes-agent**: Moderate PR count
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
### 1. Weekly Analysis
|
||||||
|
|
||||||
|
Run the PR backlog analyzer weekly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/pr-backlog-analyzer.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This generates a report in `reports/pr-backlog-YYYYMMDD.md`.
|
||||||
|
|
||||||
|
### 2. Review Stale PRs
|
||||||
|
|
||||||
|
PRs older than 30 days are considered stale. For each stale PR:
|
||||||
|
|
||||||
|
1. **Check relevance**: Is the PR still needed?
|
||||||
|
2. **Check conflicts**: Does it conflict with current main?
|
||||||
|
3. **Check activity**: Has there been recent activity?
|
||||||
|
4. **Action**: Close, update, or merge
|
||||||
|
|
||||||
|
### 3. Merge Approved PRs
|
||||||
|
|
||||||
|
PRs with approvals should be merged within 7 days:
|
||||||
|
|
||||||
|
1. **Verify CI**: Ensure all checks pass
|
||||||
|
2. **Verify review**: At least 1 approval
|
||||||
|
3. **Merge**: Use squash merge for clean history
|
||||||
|
4. **Delete branch**: Clean up after merge
|
||||||
|
|
||||||
|
### 4. Review Pending PRs
|
||||||
|
|
||||||
|
PRs waiting for review should be reviewed within 48 hours:
|
||||||
|
|
||||||
|
1. **Assign reviewer**: Ensure someone is responsible
|
||||||
|
2. **Review**: Check code quality, tests, documentation
|
||||||
|
3. **Approve or request changes**: Don't leave PRs in limbo
|
||||||
|
4. **Follow up**: If no response in 48 hours, escalate
|
||||||
|
|
||||||
|
### 5. Close Duplicate PRs
|
||||||
|
|
||||||
|
Multiple PRs for the same issue should be consolidated:
|
||||||
|
|
||||||
|
1. **Identify duplicates**: Same issue number or similar changes
|
||||||
|
2. **Keep newest**: Usually the most up-to-date
|
||||||
|
3. **Close older**: With explanatory comments
|
||||||
|
4. **Document**: Update issue with which PR was kept
|
||||||
|
|
||||||
|
## Automation
|
||||||
|
|
||||||
|
### GitHub Actions Workflow
|
||||||
|
|
||||||
|
The `pr-backlog-management.yml` workflow runs weekly to:
|
||||||
|
|
||||||
|
1. Analyze all open PRs
|
||||||
|
2. Generate a report
|
||||||
|
3. Create an issue if backlog is high (>10 stale PRs)
|
||||||
|
|
||||||
|
### Manual Trigger
|
||||||
|
|
||||||
|
The workflow can be triggered manually via GitHub Actions UI.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Track these metrics weekly:
|
||||||
|
|
||||||
|
- **Total open PRs**: Should be <20 per repo
|
||||||
|
- **Stale PRs**: Should be <5 per repo
|
||||||
|
- **Average PR age**: Should be <14 days
|
||||||
|
- **Time to review**: Should be <48 hours
|
||||||
|
- **Time to merge**: Should be <7 days after approval
|
||||||
|
|
||||||
|
## Escalation
|
||||||
|
|
||||||
|
If backlog exceeds thresholds:
|
||||||
|
|
||||||
|
1. **Level 1**: Automated issue created
|
||||||
|
2. **Level 2**: Team lead notified
|
||||||
|
3. **Level 3**: Organization-wide cleanup sprint
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
### PR Backlog Analyzer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run analysis
|
||||||
|
python scripts/pr-backlog-analyzer.py
|
||||||
|
|
||||||
|
# View report
|
||||||
|
cat reports/pr-backlog-$(date +%Y%m%d).md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Cleanup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# List stale PRs
|
||||||
|
curl -s -H "Authorization: token $GITEA_TOKEN" "https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/timmy-config/pulls?state=open" | jq -r '.[] | select(.created_at < "'$(date -u -d '30 days ago' +%Y-%m-%dT%H:%M:%SZ)'") | .number'
|
||||||
|
|
||||||
|
# Close a PR
|
||||||
|
curl -s -X PATCH -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" -d '{"state": "closed"}' "https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/timmy-config/pulls/123"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- **Short-term**: Reduce timmy-config PRs from 31 to <20
|
||||||
|
- **Medium-term**: Maintain <15 open PRs across all repos
|
||||||
|
- **Long-term**: Automated PR lifecycle management
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- Issue #1470: process: Address timmy-config PR backlog (9 PRs - highest in org)
|
||||||
|
- Issue #1127: Evening triage pass
|
||||||
|
- Issue #1128: Forge Cleanup
|
||||||
35
reports/pr-backlog-20260414.md
Normal file
35
reports/pr-backlog-20260414.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# PR Backlog Report — Timmy_Foundation/timmy-config
|
||||||
|
|
||||||
|
Generated: 2026-04-14 21:13:34
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
- **Total Open PRs**: 32
|
||||||
|
- **Stale (>30 days)**: 0
|
||||||
|
- **Needs Review**: 0
|
||||||
|
- **Approved**: 0
|
||||||
|
- **Changes Requested**: 0
|
||||||
|
- **Recent (<7 days)**: 32
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### Immediate Actions
|
||||||
|
1. **Merge approved PRs**: 0 PRs are ready to merge
|
||||||
|
2. **Review stale PRs**: 0 PRs are >30 days old
|
||||||
|
3. **Address changes requested**: 0 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)
|
||||||
|
|
||||||
|
### Approved PRs (Ready to Merge)
|
||||||
|
|
||||||
|
### Needs Review
|
||||||
181
scripts/pr-backlog-analyzer.py
Executable file
181
scripts/pr-backlog-analyzer.py
Executable file
@@ -0,0 +1,181 @@
|
|||||||
|
#!/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()
|
||||||
Reference in New Issue
Block a user