Some checks failed
Smoke Test / smoke (pull_request) Failing after 28s
249 lines
8.3 KiB
Python
249 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
cross-repo-qa.py — Foundation-wide QA checks across all repos.
|
|
|
|
Runs automated checks that would have caught the issues in #691:
|
|
- Duplicate PR detection across repos
|
|
- Port drift detection in fleet configs
|
|
- PR count per repo vs capacity limits
|
|
- Health endpoint reachability
|
|
|
|
Usage:
|
|
python3 scripts/cross-repo-qa.py --report # Full QA report
|
|
python3 scripts/cross-repo-qa.py --duplicates # Find duplicate PRs
|
|
python3 scripts/cross-repo-qa.py --capacity # Check PR capacity
|
|
python3 scripts/cross-repo-qa.py --port-drift # Check fleet config consistency
|
|
python3 scripts/cross-repo-qa.py --json # Machine-readable output
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
import urllib.request
|
|
from collections import defaultdict
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
import re
|
|
|
|
GITEA_URL = "https://forge.alexanderwhitestone.com"
|
|
GITEA_TOKEN_PATH = Path.home() / ".config" / "gitea" / "token"
|
|
ORG = "Timmy_Foundation"
|
|
|
|
REPOS = [
|
|
"hermes-agent", "timmy-home", "timmy-config", "the-nexus", "fleet-ops",
|
|
"the-playground", "the-beacon", "wolf", "turboquant", "timmy-academy",
|
|
"compounding-intelligence", "the-testament", "second-son-of-timmy",
|
|
"ai-safety-review", "the-echo-pattern", "burn-fleet", "timmy-dispatch",
|
|
"the-door",
|
|
]
|
|
|
|
|
|
def load_token() -> str:
|
|
if GITEA_TOKEN_PATH.exists():
|
|
return GITEA_TOKEN_PATH.read_text().strip()
|
|
return os.environ.get("GITEA_TOKEN", "")
|
|
|
|
|
|
def api_get(path: str, token: str) -> list | dict:
|
|
req = urllib.request.Request(
|
|
f"{GITEA_URL}/api/v1{path}",
|
|
headers={"Authorization": f"token {token}"}
|
|
)
|
|
try:
|
|
return json.loads(urllib.request.urlopen(req, timeout=20).read())
|
|
except Exception:
|
|
return []
|
|
|
|
|
|
def extract_issue_refs(text: str) -> set[int]:
|
|
return set(int(m) for m in re.findall(r'#(\d{2,5})', text or ""))
|
|
|
|
|
|
def check_duplicate_prs(token: str) -> dict:
|
|
"""Find duplicate PRs across all repos (same issue referenced)."""
|
|
issue_to_prs = defaultdict(list)
|
|
|
|
for repo in REPOS:
|
|
prs = api_get(f"/repos/{ORG}/{repo}/pulls?state=open&limit=100", token)
|
|
if not isinstance(prs, list):
|
|
continue
|
|
for pr in prs:
|
|
refs = extract_issue_refs(f"{pr['title']} {pr.get('body', '')}")
|
|
for ref in refs:
|
|
issue_to_prs[ref].append({
|
|
"repo": repo,
|
|
"number": pr["number"],
|
|
"title": pr["title"][:70],
|
|
"branch": pr.get("head", {}).get("ref", ""),
|
|
})
|
|
|
|
duplicates = {k: v for k, v in issue_to_prs.items() if len(v) > 1}
|
|
return duplicates
|
|
|
|
|
|
def check_pr_capacity(token: str) -> list[dict]:
|
|
"""Check PR counts vs limits."""
|
|
capacity_path = Path(__file__).parent / "pr-capacity.json"
|
|
if capacity_path.exists():
|
|
config = json.loads(capacity_path.read_text())
|
|
limits = {k: v.get("limit", 10) for k, v in config.get("repos", {}).items()}
|
|
default_limit = config.get("default_limit", 10)
|
|
else:
|
|
limits = {}
|
|
default_limit = 10
|
|
|
|
results = []
|
|
for repo in REPOS:
|
|
prs = api_get(f"/repos/{ORG}/{repo}/pulls?state=open&limit=100", token)
|
|
count = len(prs) if isinstance(prs, list) else 0
|
|
limit = limits.get(repo, default_limit)
|
|
if count > limit:
|
|
results.append({"repo": repo, "count": count, "limit": limit, "over": count - limit})
|
|
|
|
return sorted(results, key=lambda x: -x["over"])
|
|
|
|
|
|
def check_wrong_repo_prs(token: str) -> list[dict]:
|
|
"""Find PRs filed in the wrong repo (title mentions different repo)."""
|
|
wrong = []
|
|
for repo in REPOS:
|
|
prs = api_get(f"/repos/{ORG}/{repo}/pulls?state=open&limit=100", token)
|
|
if not isinstance(prs, list):
|
|
continue
|
|
for pr in prs:
|
|
title = pr["title"].lower()
|
|
# Check if title references a different repo
|
|
for other_repo in REPOS:
|
|
if other_repo == repo:
|
|
continue
|
|
# Check for repo name in title (with common separators)
|
|
patterns = [
|
|
f"{other_repo} ",
|
|
f"{other_repo}:",
|
|
f"{other_repo} backlog",
|
|
f"{other_repo} report",
|
|
f"{other_repo} triage",
|
|
]
|
|
if any(p in title for p in patterns):
|
|
wrong.append({
|
|
"pr_repo": repo,
|
|
"pr_number": pr["number"],
|
|
"pr_title": pr["title"][:70],
|
|
"should_be_in": other_repo,
|
|
})
|
|
return wrong
|
|
|
|
|
|
def cmd_report(token: str, as_json: bool = False):
|
|
"""Full QA report."""
|
|
report = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"repos_scanned": len(REPOS),
|
|
}
|
|
|
|
# Duplicates
|
|
print("Checking duplicate PRs...", file=sys.stderr)
|
|
dupes = check_duplicate_prs(token)
|
|
report["duplicate_prs"] = {
|
|
"issues_with_duplicates": len(dupes),
|
|
"total_duplicate_prs": sum(len(v) - 1 for v in dupes.values()),
|
|
"details": {str(k): v for k, v in sorted(dupes.items())},
|
|
}
|
|
|
|
# Capacity
|
|
print("Checking PR capacity...", file=sys.stderr)
|
|
over_capacity = check_pr_capacity(token)
|
|
report["over_capacity"] = over_capacity
|
|
|
|
# Wrong repo
|
|
print("Checking wrong-repo PRs...", file=sys.stderr)
|
|
wrong_repo = check_wrong_repo_prs(token)
|
|
report["wrong_repo_prs"] = wrong_repo
|
|
|
|
if as_json:
|
|
print(json.dumps(report, indent=2))
|
|
return
|
|
|
|
# Human-readable
|
|
print(f"\n{'='*60}")
|
|
print(f"CROSS-REPO QA REPORT — {report['timestamp'][:19]}")
|
|
print(f"{'='*60}")
|
|
|
|
print(f"\nDuplicate PRs: {report['duplicate_prs']['issues_with_duplicates']} issues, "
|
|
f"{report['duplicate_prs']['total_duplicate_prs']} duplicates")
|
|
for issue_num, pr_list in sorted(dupes.items(), key=lambda x: -len(x[1]))[:10]:
|
|
print(f" Issue #{issue_num}: {len(pr_list)} PRs")
|
|
for pr in pr_list:
|
|
print(f" {pr['repo']}#{pr['number']}: {pr['title'][:60]}")
|
|
|
|
print(f"\nOver Capacity: {len(over_capacity)} repos")
|
|
for r in over_capacity:
|
|
print(f" {r['repo']}: {r['count']}/{r['limit']} ({r['over']} over)")
|
|
|
|
if wrong_repo:
|
|
print(f"\nWrong Repo PRs: {len(wrong_repo)}")
|
|
for r in wrong_repo:
|
|
print(f" {r['pr_repo']}#{r['pr_number']}: should be in {r['should_be_in']}")
|
|
print(f" {r['pr_title']}")
|
|
|
|
# Severity
|
|
p0 = len(over_capacity)
|
|
p1 = report['duplicate_prs']['total_duplicate_prs']
|
|
print(f"\n{'='*60}")
|
|
print(f"Severity: {p0} capacity violations, {p1} duplicate PRs")
|
|
if p0 > 3 or p1 > 10:
|
|
print("Status: NEEDS ATTENTION")
|
|
else:
|
|
print("Status: OK")
|
|
|
|
|
|
def cmd_duplicates(token: str):
|
|
dupes = check_duplicate_prs(token)
|
|
if not dupes:
|
|
print("No duplicate PRs found.")
|
|
return
|
|
print(f"Found {len(dupes)} issues with duplicate PRs:\n")
|
|
for issue_num, pr_list in sorted(dupes.items(), key=lambda x: -len(x[1])):
|
|
print(f"Issue #{issue_num}: {len(pr_list)} PRs")
|
|
for pr in pr_list:
|
|
print(f" {pr['repo']}#{pr['number']}: {pr['title'][:60]}")
|
|
|
|
|
|
def cmd_capacity(token: str):
|
|
over = check_pr_capacity(token)
|
|
if not over:
|
|
print("All repos within capacity.")
|
|
return
|
|
print(f"{len(over)} repos over capacity:\n")
|
|
for r in over:
|
|
print(f" {r['repo']}: {r['count']}/{r['limit']} ({r['over']} over)")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Cross-repo QA automation")
|
|
parser.add_argument("--report", action="store_true")
|
|
parser.add_argument("--duplicates", action="store_true")
|
|
parser.add_argument("--capacity", action="store_true")
|
|
parser.add_argument("--port-drift", action="store_true")
|
|
parser.add_argument("--json", action="store_true", dest="as_json")
|
|
args = parser.parse_args()
|
|
|
|
token = load_token()
|
|
if not token:
|
|
print("No Gitea token found", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
if args.duplicates:
|
|
cmd_duplicates(token)
|
|
elif args.capacity:
|
|
cmd_capacity(token)
|
|
elif args.port_drift:
|
|
print("Port drift check: see fleet-ops registry.yaml comparison")
|
|
else:
|
|
cmd_report(token, args.as_json)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|