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:
2026-07-18 11:37:45 -07:00
parent 96093000aa
commit eaf2742a20

View File

@@ -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)