Files
allegro-checkpoint/skills/devops/gitea-backlog-triage/SKILL.md

6.3 KiB

name, description, tags, triggers
name description tags triggers
gitea-backlog-triage Mass triage of Gitea backlogs — branch cleanup, duplicate detection, bulk issue closure by category. Use when repos have 50+ stale branches or 100+ open issues with burn reports, RCAs, and one-time artifacts mixed with real work.
gitea
triage
backlog
branches
issues
cleanup
burndown
triage backlog
clean branches
close stale issues
backlog triage
branch cleanup
burn down triage

Gitea Backlog Triage

When to Use

  • A repo has 50+ stale branches (common after multi-agent sprints)
  • Issue tracker has 100+ open issues with burn reports, RCAs, status reports mixed in
  • You need to reduce noise so real work items are visible
  • Assigned to branch cleanup or backlog grooming

Phase 1: Branch Audit & Cleanup

Step 1: Inventory all branches with pagination

all_branches = []
page = 1
while True:
    branches = api_get(f"/repos/{REPO}/branches?page={page}&limit=50")
    if isinstance(branches, list) and len(branches) > 0:
        all_branches.extend(branches)
        page += 1
    else:
        break

Step 2: Check open PRs — never delete branches with open PRs

open_prs = api_get(f"/repos/{REPO}/pulls?state=open&limit=50")
pr_branches = set()
if isinstance(open_prs, list):
    for pr in open_prs:
        pr_branches.add(pr.get('head', {}).get('ref', ''))

Step 3: Cross-reference branches against issue state

For branches named agent/issue-NNN, check if issue NNN is closed:

import re
safe_to_delete = []
for b in all_branches:
    name = b['name']
    if name in pr_branches or name == 'main':
        continue
    m = re.search(r'issue-(\d+)', name)
    if m:
        issue = api_get(f"/repos/{REPO}/issues/{m.group(1)}")
        if 'error' not in issue and issue.get('state') == 'closed':
            safe_to_delete.append(name)
    elif name.startswith('test-') or name.startswith('test/'):
        safe_to_delete.append(name)  # test branches are always safe

Step 4: Delete in batch via API

for name in safe_to_delete:
    encoded = name.replace('/', '%2F')  # URL-encode slashes!
    api_delete(f"/repos/{REPO}/branches/{encoded}")

Key: URL-encode branch names

Branch names like claude/issue-770 must be encoded as claude%2Fissue-770 in the API URL.

Phase 2: Issue Deduplication

Step 1: Fetch all open issues with pagination

all_issues = []
page = 1
while True:
    batch = api_get(f"/repos/{REPO}/issues?state=open&type=issues&limit=50&page={page}")
    if isinstance(batch, list) and len(batch) > 0:
        all_issues.extend(batch)
        page += 1
    else:
        break

Step 2: Group by normalized title

from collections import defaultdict
title_groups = defaultdict(list)
for i in all_issues:
    clean = re.sub(r'\[.*?\]', '', i['title'].upper()).strip()
    clean = re.sub(r'#\d+', '', clean).strip()
    title_groups[clean].append(i)

for title, issues in title_groups.items():
    if len(issues) > 1:
        # Keep oldest, close newer as duplicates
        issues.sort(key=lambda x: x['created_at'])
        original = issues[0]
        for dupe in issues[1:]:
            api_post(f"/repos/{REPO}/issues/{dupe['number']}/comments",
                {"body": f"Closing as duplicate of #{original['number']}.\n\n— Allegro"})
            api_patch(f"/repos/{REPO}/issues/{dupe['number']}", {"state": "closed"})

Phase 3: Bulk Closure by Category

Identify non-actionable issue types by title patterns:

Pattern Category Action
🔥 Burn Report or BURN REPORT Completed artifact Close
[FAILURE] One-time incident report Close
[STATUS-REPORT] or [STATUS] One-time status Close
[RCA] Root cause analysis Close (consolidate into master tracking issue)
Dispatch Test Test artifact Close
closeable_patterns = [
    (lambda t: '🔥 Burn Report' in t or 'BURN REPORT' in t.upper(), "Completed burn report"),
    (lambda t: '[FAILURE]' in t, "One-time failure report"),
    (lambda t: '[STATUS-REPORT]' in t or '[STATUS]' in t, "One-time status report"),
    (lambda t: '[RCA]' in t, "One-time RCA report"),
    (lambda t: 'Dispatch Test' in t, "Test artifact"),
]

for issue in all_issues:
    for matcher, reason in closeable_patterns:
        if matcher(issue['title']):
            comment = f"## Burn-down triage\n\n**Category:** {reason}\n\nClosing as non-actionable artifact.\n\n— Allegro"
            api_post(f"/repos/{REPO}/issues/{issue['number']}/comments", {"body": comment})
            api_patch(f"/repos/{REPO}/issues/{issue['number']}", {"state": "closed"})
            break

Phase 4: Report

Always produce a summary with before/after metrics:

| Metric | Before | After | Change |
|--------|--------|-------|--------|
| Open issues | X | Y | -Z (-N%) |
| Branches | X | Y | -Z (-N%) |
| Duplicates closed | — | N | — |
| Artifacts closed | — | N | — |

Comment on the tracking issue (if one exists) with the full results.

Pitfalls

  1. Always check for open PRs before deleting branches — deleting a PR's head branch breaks the PR
  2. URL-encode slashes in branch names: name.replace('/', '%2F')
  3. Pagination is mandatory — both /branches and /issues endpoints page at 50 max
  4. api_delete returns status code, not JSON — don't try to json.loads the response
  5. Comment before closing — always add a triage comment explaining why before setting state=closed
  6. Don't close issues assigned to Rockachopa/Alexander — those are owner-created action items
  7. Don't touch running services or configs — triage is read+close only, no code changes
  8. Consolidate RCAs into a master issue — if 5 RCAs share the same root cause, close them all pointing to one tracker
  9. assignees can be None — always use i.get('assignees') or []
  10. First page shows 50 max — a repo showing "50 open issues" likely has more on page 2+
  11. Merge API returns 405 if already merged — not an error, just check the message body
  12. Write scripts to file, don't use curl — security scanner blocks curl to raw IPs

Verification

After triage:

# Recount open issues across all repos
for repo in repos:
    issues = paginated_fetch(f"/repos/{org}/{repo}/issues?state=open&type=issues")
    print(f"{repo}: {len(issues)} open")