sync: auto-sync from HOWARD-HOME at 2026-07-18 11:37:15
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-07-18 11:37:15
This commit is contained in:
@@ -25,6 +25,8 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
|
||||
API = "https://git.azcomputerguru.com/api/v1"
|
||||
@@ -33,9 +35,8 @@ TRACKED_REPOS = ["gururmm", "guru-rmm", "guru-connect", "claudetools"]
|
||||
PRIORITY_LABELS = ["P1-critical", "P2-high", "P3-normal", "P4-low"]
|
||||
ASSIGN_LABELS = ["assigned:howard", "assigned:mike"]
|
||||
|
||||
# Resolve repo root for vault.sh
|
||||
SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
REPO_ROOT = SCRIPT_DIR.parent.parent.parent.parent # .claude/skills/bug-tracker/scripts -> repo root
|
||||
REPO_ROOT = SCRIPT_DIR.parent.parent.parent.parent
|
||||
|
||||
|
||||
def get_token():
|
||||
@@ -60,9 +61,6 @@ def get_token():
|
||||
|
||||
def api(method, path, token, data=None):
|
||||
"""Make a Gitea API call using urllib (no external deps)."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
url = f"{API}{path}"
|
||||
headers = {"Authorization": f"token {token}", "Accept": "application/json"}
|
||||
body = None
|
||||
@@ -85,6 +83,23 @@ def api(method, path, token, data=None):
|
||||
return 0, {"error": str(e)}
|
||||
|
||||
|
||||
def fetch_all_issues(token, repo, state):
|
||||
"""Fetch all issues for a repo+state with pagination."""
|
||||
results = []
|
||||
page = 1
|
||||
while True:
|
||||
code, issues = api("GET",
|
||||
f"/repos/{OWNER}/{repo}/issues?state={state}&type=issues&limit=50&page={page}",
|
||||
token)
|
||||
if code != 200 or not issues:
|
||||
break
|
||||
results.extend(issues)
|
||||
page += 1
|
||||
if len(issues) < 50:
|
||||
break
|
||||
return results
|
||||
|
||||
|
||||
def get_label_ids(token, repo):
|
||||
"""Fetch label name->id map for a repo."""
|
||||
code, labels = api("GET", f"/repos/{OWNER}/{repo}/labels?limit=200", token)
|
||||
@@ -100,12 +115,10 @@ def cmd_status(args, token):
|
||||
total_open = 0
|
||||
total_closed = 0
|
||||
for repo in TRACKED_REPOS:
|
||||
code, issues = api("GET", f"/repos/{OWNER}/{repo}/issues?state=open&type=issues&limit=1", token)
|
||||
# Gitea doesn't return total count easily; fetch up to 50
|
||||
code, open_issues = api("GET", f"/repos/{OWNER}/{repo}/issues?state=open&type=issues&limit=50", token)
|
||||
code2, closed_issues = api("GET", f"/repos/{OWNER}/{repo}/issues?state=closed&type=issues&limit=50", token)
|
||||
o = len(open_issues) if code == 200 else 0
|
||||
c = len(closed_issues) if code2 == 200 else 0
|
||||
open_issues = fetch_all_issues(token, repo, "open")
|
||||
closed_issues = fetch_all_issues(token, repo, "closed")
|
||||
o = len(open_issues)
|
||||
c = len(closed_issues)
|
||||
total_open += o
|
||||
total_closed += c
|
||||
print(f"{repo:<20} {o:>6} {c:>8}")
|
||||
@@ -116,32 +129,24 @@ def cmd_status(args, token):
|
||||
def cmd_list(args, token):
|
||||
"""List issues with optional filters."""
|
||||
repos = [args.repo] if args.repo else TRACKED_REPOS
|
||||
state = args.state or "open"
|
||||
state_filter = args.state or "open"
|
||||
states = ["open", "closed"] if state_filter == "all" else [state_filter]
|
||||
all_issues = []
|
||||
|
||||
for repo in repos:
|
||||
page = 1
|
||||
while True:
|
||||
code, issues = api("GET",
|
||||
f"/repos/{OWNER}/{repo}/issues?state={state}&type=issues&limit=50&page={page}",
|
||||
token)
|
||||
if code != 200 or not issues:
|
||||
break
|
||||
for i in issues:
|
||||
i["_repo"] = repo
|
||||
all_issues.extend(issues)
|
||||
page += 1
|
||||
if len(issues) < 50:
|
||||
break
|
||||
for state in states:
|
||||
for issue in fetch_all_issues(token, repo, state):
|
||||
issue["_repo"] = repo
|
||||
all_issues.append(issue)
|
||||
|
||||
# Filter by type
|
||||
if args.type:
|
||||
all_issues = [i for i in all_issues if any(l["name"] == args.type for l in i.get("labels", []))]
|
||||
all_issues = [i for i in all_issues
|
||||
if any(l["name"] == args.type for l in i.get("labels", []))]
|
||||
|
||||
# Filter by client
|
||||
if args.client:
|
||||
tag = args.client if args.client.startswith("client:") else f"client:{args.client}"
|
||||
all_issues = [i for i in all_issues if any(l["name"] == tag for l in i.get("labels", []))]
|
||||
all_issues = [i for i in all_issues
|
||||
if any(l["name"] == tag for l in i.get("labels", []))]
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(all_issues, indent=2, default=str))
|
||||
@@ -160,37 +165,37 @@ def cmd_list(args, token):
|
||||
|
||||
def cmd_create(args, token):
|
||||
"""Create a new issue."""
|
||||
# Validate client-project requires --client (fail fast)
|
||||
if args.type == "client-project" and not args.client:
|
||||
print("[ERROR] --client is required for client-project type", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
repo = args.repo
|
||||
lbl_map = get_label_ids(token, repo)
|
||||
label_ids = []
|
||||
|
||||
# Type label
|
||||
if args.type in lbl_map:
|
||||
label_ids.append(lbl_map[args.type])
|
||||
else:
|
||||
if args.type not in lbl_map:
|
||||
print(f"[ERROR] Type label '{args.type}' not found in {repo}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
label_ids.append(lbl_map[args.type])
|
||||
|
||||
# Priority label
|
||||
if args.priority in lbl_map:
|
||||
label_ids.append(lbl_map[args.priority])
|
||||
else:
|
||||
if args.priority not in lbl_map:
|
||||
print(f"[ERROR] Priority label '{args.priority}' not found in {repo}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
label_ids.append(lbl_map[args.priority])
|
||||
|
||||
# Optional component
|
||||
if args.component and args.component in lbl_map:
|
||||
label_ids.append(lbl_map[args.component])
|
||||
|
||||
# Optional client
|
||||
if args.client:
|
||||
tag = args.client if args.client.startswith("client:") else f"client:{args.client}"
|
||||
if tag in lbl_map:
|
||||
label_ids.append(lbl_map[tag])
|
||||
else:
|
||||
print(f"[WARNING] Client label '{tag}' not found in {repo} — issue created without it", file=sys.stderr)
|
||||
|
||||
# Optional assignee
|
||||
if args.assign:
|
||||
atag = args.assign if args.assign.startswith("assigned:") else f"assigned:{args.assign}"
|
||||
atag = f"assigned:{args.assign}" if not args.assign.startswith("assigned:") else args.assign
|
||||
if atag in lbl_map:
|
||||
label_ids.append(lbl_map[atag])
|
||||
|
||||
@@ -228,7 +233,8 @@ def cmd_show(args, token):
|
||||
print(f"\n--- {len(comments)} comment(s) ---")
|
||||
for c in comments:
|
||||
print(f"\n {c['user']['login']} ({c['created_at'][:10]}):")
|
||||
print(f" {c['body']}")
|
||||
for line in (c.get("body") or "").split("\n"):
|
||||
print(f" {line}")
|
||||
|
||||
|
||||
def cmd_comment(args, token):
|
||||
@@ -245,12 +251,10 @@ def cmd_comment(args, token):
|
||||
def cmd_priority(args, token):
|
||||
"""Change issue priority (swap labels)."""
|
||||
lbl_map = get_label_ids(token, args.repo)
|
||||
# Remove existing priority labels
|
||||
for p in PRIORITY_LABELS:
|
||||
pid = lbl_map.get(p)
|
||||
if pid:
|
||||
api("DELETE", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels/{pid}", token)
|
||||
# Add new
|
||||
new_id = lbl_map.get(args.set)
|
||||
if new_id:
|
||||
api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels", token, {"labels": [new_id]})
|
||||
@@ -263,13 +267,11 @@ def cmd_priority(args, token):
|
||||
def cmd_assign(args, token):
|
||||
"""Change issue assignee (swap labels)."""
|
||||
lbl_map = get_label_ids(token, args.repo)
|
||||
# Remove existing assign labels
|
||||
for a in ASSIGN_LABELS:
|
||||
aid = lbl_map.get(a)
|
||||
if aid:
|
||||
api("DELETE", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels/{aid}", token)
|
||||
# Add new
|
||||
atag = args.to if args.to.startswith("assigned:") else f"assigned:{args.to}"
|
||||
atag = f"assigned:{args.to}" if not args.to.startswith("assigned:") else args.to
|
||||
new_id = lbl_map.get(atag)
|
||||
if new_id:
|
||||
api("POST", f"/repos/{OWNER}/{args.repo}/issues/{args.number}/labels", token, {"labels": [new_id]})
|
||||
@@ -286,20 +288,22 @@ def cmd_close(args, token):
|
||||
token, {"body": args.comment})
|
||||
code, result = api("PATCH", f"/repos/{OWNER}/{args.repo}/issues/{args.number}",
|
||||
token, {"state": "closed"})
|
||||
if code == 201:
|
||||
if code in (200, 201):
|
||||
print(f"[OK] Closed {args.repo} #{args.number}")
|
||||
else:
|
||||
print(f"[ERROR] HTTP {code}: {json.dumps(result)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_reopen(args, token):
|
||||
"""Reopen an issue."""
|
||||
code, result = api("PATCH", f"/repos/{OWNER}/{args.repo}/issues/{args.number}",
|
||||
token, {"state": "open"})
|
||||
if code == 201:
|
||||
if code in (200, 201):
|
||||
print(f"[OK] Reopened {args.repo} #{args.number}")
|
||||
else:
|
||||
print(f"[ERROR] HTTP {code}: {json.dumps(result)}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def cmd_label(args, token):
|
||||
@@ -325,10 +329,8 @@ def main():
|
||||
parser = argparse.ArgumentParser(description="ACG Bug & Project Tracker")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
# status
|
||||
sub.add_parser("status", help="Show issue counts")
|
||||
|
||||
# list
|
||||
p_list = sub.add_parser("list", help="List issues")
|
||||
p_list.add_argument("--repo", choices=TRACKED_REPOS)
|
||||
p_list.add_argument("--type", choices=["bug", "feature", "skill", "project", "security", "client-project"])
|
||||
@@ -336,53 +338,46 @@ def main():
|
||||
p_list.add_argument("--client")
|
||||
p_list.add_argument("--json", action="store_true")
|
||||
|
||||
# create
|
||||
p_create = sub.add_parser("create", help="Create an issue")
|
||||
p_create.add_argument("--repo", required=True, choices=TRACKED_REPOS)
|
||||
p_create.add_argument("--title", required=True)
|
||||
p_create.add_argument("--body", default="")
|
||||
p_create.add_argument("--type", required=True, choices=["bug", "feature", "skill", "project", "security", "client-project"])
|
||||
p_create.add_argument("--type", required=True,
|
||||
choices=["bug", "feature", "skill", "project", "security", "client-project"])
|
||||
p_create.add_argument("--priority", required=True, choices=PRIORITY_LABELS)
|
||||
p_create.add_argument("--component")
|
||||
p_create.add_argument("--client")
|
||||
p_create.add_argument("--assign", choices=["howard", "mike"])
|
||||
p_create.add_argument("--discovered")
|
||||
|
||||
# show
|
||||
p_show = sub.add_parser("show", help="Show issue detail")
|
||||
p_show.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_show.add_argument("number", type=int)
|
||||
|
||||
# comment
|
||||
p_comment = sub.add_parser("comment", help="Add a comment")
|
||||
p_comment.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_comment.add_argument("number", type=int)
|
||||
p_comment.add_argument("--body", required=True)
|
||||
|
||||
# priority
|
||||
p_prio = sub.add_parser("priority", help="Change priority")
|
||||
p_prio.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_prio.add_argument("number", type=int)
|
||||
p_prio.add_argument("--set", required=True, choices=PRIORITY_LABELS)
|
||||
|
||||
# assign
|
||||
p_assign = sub.add_parser("assign", help="Change assignee")
|
||||
p_assign.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_assign.add_argument("number", type=int)
|
||||
p_assign.add_argument("--to", required=True, choices=["howard", "mike"])
|
||||
|
||||
# close
|
||||
p_close = sub.add_parser("close", help="Close an issue")
|
||||
p_close.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_close.add_argument("number", type=int)
|
||||
p_close.add_argument("--comment")
|
||||
|
||||
# reopen
|
||||
p_reopen = sub.add_parser("reopen", help="Reopen an issue")
|
||||
p_reopen.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_reopen.add_argument("number", type=int)
|
||||
|
||||
# label
|
||||
p_label = sub.add_parser("label", help="Add/remove a label")
|
||||
p_label.add_argument("repo", choices=TRACKED_REPOS)
|
||||
p_label.add_argument("number", type=int)
|
||||
|
||||
Reference in New Issue
Block a user