#!/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()