154 lines
4.5 KiB
Python
Executable File
154 lines
4.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
backlog_triage.py — Weekly backlog health check for timmy-home.
|
|
|
|
Queries Gitea API for open issues and reports:
|
|
- Unassigned issues
|
|
- Issues with no labels
|
|
- Batch-pipeline issues (triaged with comments)
|
|
|
|
Usage:
|
|
python scripts/backlog_triage.py [--token TOKEN] [--repo OWNER/REPO]
|
|
|
|
Exit codes:
|
|
0 = backlog healthy (no action needed)
|
|
1 = issues found requiring attention
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from urllib.request import Request, urlopen
|
|
from urllib.error import URLError
|
|
|
|
GITEA_BASE = os.environ.get("GITEA_BASE_URL", "https://forge.alexanderwhitestone.com/api/v1")
|
|
|
|
|
|
def fetch_issues(owner: str, repo: str, token: str, state: str = "open") -> list:
|
|
"""Fetch all open issues from Gitea."""
|
|
issues = []
|
|
page = 1
|
|
per_page = 50
|
|
|
|
while True:
|
|
url = f"{GITEA_BASE}/repos/{owner}/{repo}/issues?state={state}&page={page}&per_page={per_page}&type=issues"
|
|
req = Request(url)
|
|
req.add_header("Authorization", f"token {token}")
|
|
|
|
try:
|
|
with urlopen(req) as resp:
|
|
batch = json.loads(resp.read())
|
|
except URLError as e:
|
|
print(f"ERROR: Failed to fetch issues: {e}", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
if not batch:
|
|
break
|
|
|
|
issues.extend(batch)
|
|
page += 1
|
|
|
|
return issues
|
|
|
|
|
|
def categorize_issues(issues: list) -> dict:
|
|
"""Categorize issues into triage buckets."""
|
|
unassigned = []
|
|
no_labels = []
|
|
batch_pipeline = []
|
|
|
|
for issue in issues:
|
|
# Skip pull requests (Gitea includes them in issues endpoint)
|
|
if "pull_request" in issue:
|
|
continue
|
|
|
|
number = issue["number"]
|
|
title = issue["title"]
|
|
assignee = issue.get("assignee")
|
|
labels = issue.get("labels", [])
|
|
|
|
if not assignee:
|
|
unassigned.append({"number": number, "title": title})
|
|
|
|
if not labels:
|
|
no_labels.append({"number": number, "title": title})
|
|
|
|
if "batch-pipeline" in title.lower() or any(
|
|
lbl.get("name", "").lower() == "batch-pipeline" for lbl in labels
|
|
):
|
|
batch_pipeline.append({"number": number, "title": title})
|
|
|
|
return {
|
|
"unassigned": unassigned,
|
|
"no_labels": no_labels,
|
|
"batch_pipeline": batch_pipeline,
|
|
}
|
|
|
|
|
|
def print_report(owner: str, repo: str, categories: dict) -> int:
|
|
"""Print triage report and return count of issues needing attention."""
|
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
print(f"# Backlog Triage Report — {owner}/{repo}")
|
|
print(f"Generated: {now}\n")
|
|
|
|
total_attention = 0
|
|
|
|
# Unassigned
|
|
print(f"## Unassigned Issues ({len(categories['unassigned'])})")
|
|
if categories["unassigned"]:
|
|
total_attention += len(categories["unassigned"])
|
|
for item in categories["unassigned"]:
|
|
print(f" - #{item['number']}: {item['title']}")
|
|
else:
|
|
print(" ✓ None")
|
|
print()
|
|
|
|
# No labels
|
|
print(f"## Issues with No Labels ({len(categories['no_labels'])})")
|
|
if categories["no_labels"]:
|
|
total_attention += len(categories["no_labels"])
|
|
for item in categories["no_labels"]:
|
|
print(f" - #{item['number']}: {item['title']}")
|
|
else:
|
|
print(" ✓ None")
|
|
print()
|
|
|
|
# Batch-pipeline
|
|
print(f"## Batch-Pipeline Issues ({len(categories['batch_pipeline'])})")
|
|
if categories["batch_pipeline"]:
|
|
for item in categories["batch_pipeline"]:
|
|
print(f" - #{item['number']}: {item['title']}")
|
|
else:
|
|
print(" ✓ None")
|
|
print()
|
|
|
|
print(f"---\nTotal issues requiring attention: {total_attention}")
|
|
return total_attention
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Weekly backlog triage for timmy-home")
|
|
parser.add_argument("--token", default=os.environ.get("GITEA_TOKEN", ""),
|
|
help="Gitea API token (or set GITEA_TOKEN env)")
|
|
parser.add_argument("--repo", default="Timmy_Foundation/timmy-home",
|
|
help="Repository in OWNER/REPO format")
|
|
args = parser.parse_args()
|
|
|
|
if not args.token:
|
|
print("ERROR: No Gitea token provided. Set GITEA_TOKEN or use --token.", file=sys.stderr)
|
|
sys.exit(2)
|
|
|
|
owner, repo = args.repo.split("/", 1)
|
|
|
|
issues = fetch_issues(owner, repo, args.token)
|
|
categories = categorize_issues(issues)
|
|
needs_attention = print_report(owner, repo, categories)
|
|
|
|
sys.exit(1 if needs_attention > 0 else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|