#!/usr/bin/env python3 import os, sys, json, subprocess, tempfile, shutil, re, time, random from datetime import datetime TOKEN = os.getenv("GITEA_TOKEN", "ffce3b62d583b761238ae00839dce7718acaad85") API_BASE = os.getenv("GITEA_API_BASE", "http://gitea.bubuit.net:3000/api/v1") REPO = "oib/aitbc" AGENT = os.getenv("AGENT_NAME", "aitbc") OTHER = "aitbc1" if AGENT == "aitbc" else "aitbc" RING_PREFIXES = [ (0, ["packages/py/aitbc-core", "packages/py/aitbc-sdk"]), (1, ["apps/"]), (2, ["cli/", "analytics/", "tools/"]), ] RING_THRESHOLD = {0: 0.9, 1: 0.8, 2: 0.7, 3: 0.5} logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") logger = logging.getLogger(__name__) def log(msg): logger.info(msg) def api_get(path): cmd = ["curl", "-s", "-H", f"Authorization: token {TOKEN}", f"{API_BASE}/{path}"] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: return None try: return json.loads(result.stdout) except: return None def api_post(path, payload): cmd = ["curl", "-s", "-X", "POST", "-H", f"Authorization: token {TOKEN}", "-H", "Content-Type: application/json", f"{API_BASE}/{path}", "-d", json.dumps(payload)] result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: return None try: return json.loads(result.stdout) except: return None def get_open_prs(): return api_get(f"repos/{REPO}/pulls?state=open") or [] def get_my_reviews(pr_number): return api_get(f"repos/{REPO}/pulls/{pr_number}/reviews") or [] def is_test_file(path): return '/tests/' in path or path.startswith('tests/') or path.endswith('_test.py') def detect_ring(base_sha, head_sha): try: output = subprocess.run( ["git", "diff", "--name-only", base_sha, head_sha], capture_output=True, text=True, check=True, cwd='/opt/aitbc' ).stdout files = [f.strip() for f in output.splitlines() if f.strip()] except subprocess.CalledProcessError: files = [] if files and all(is_test_file(f) for f in files): return 3 for ring, prefixes in sorted(RING_PREFIXES, key=lambda x: x[0]): for p in files: if any(p.startswith(prefix) for prefix in prefixes): return ring return 3 def syntax_check(worktree): py_files = [] for root, dirs, files in os.walk(worktree): for f in files: if f.endswith('.py'): py_files.append(os.path.join(root, f)) for f in py_files: r = subprocess.run([sys.executable, '-m', 'py_compile', f], capture_output=True) if r.returncode != 0: return False, f"Syntax error in {os.path.relpath(f, worktree)}" return True, "" def post_review(pr_number, event, body): payload = {"event": event, "body": body} return api_post(f"repos/{REPO}/pulls/{pr_number}/reviews", payload) is not None def request_review(pr_number, reviewer): payload = {"reviewers": [reviewer]} return api_post(f"repos/{REPO}/pulls/{pr_number}/requested-reviewers", payload) is not None def main(): # Jitter 0-60s time.sleep(random.randint(0, 60)) log("Fetching open PRs...") prs = get_open_prs() if not prs: log("No open PRs") return # Process sibling PRs for pr in prs: if pr['user']['login'] != OTHER: continue number = pr['number'] title = pr['title'][:50] log(f"Reviewing sibling PR #{number}: {title}") # Checkout and validate tmp = tempfile.mkdtemp(prefix="aitbc_monitor_") try: subprocess.run(["git", "clone", "--no-checkout", "origin", tmp], capture_output=True, check=True) wt = os.path.join(tmp, "wt") os.makedirs(wt) subprocess.run(["git", "--git-dir", os.path.join(tmp, ".git"), "--work-tree", wt, "fetch", "origin", pr['head']['ref']], capture_output=True, check=True) subprocess.run(["git", "--git-dir", os.path.join(tmp, ".git"), "--work-tree", wt, "checkout", "FETCH_HEAD"], capture_output=True, check=True) ok, err = syntax_check(wt) if not ok: post_review(number, "REQUEST_CHANGES", f"❌ Syntax error: {err}") log(f"PR #{number} failed syntax: {err}") continue ring = detect_ring(pr['base']['sha'], pr['head']['sha']) threshold = RING_THRESHOLD.get(ring, 0.5) if ring == 0: post_review(number, "REQUEST_CHANGES", f"Ring 0 (core): needs manual review. Threshold: >{threshold}") log(f"PR #{number} is Ring0; manual review required") else: post_review(number, "APPROVE", f"✅ Auto‑approved (ring {ring}, threshold >{threshold})") log(f"PR #{number} approved") finally: shutil.rmtree(tmp, ignore_errors=True) # Our own PRs: request review from OTHER if not yet our_prs = [pr for pr in prs if pr['user']['login'] == AGENT] for pr in our_prs: number = pr['number'] reviews = get_my_reviews(number) if not any(r['user']['login'] == OTHER for r in reviews): log(f"Requesting review from {OTHER} on our PR #{number}") request_review(number, OTHER) if __name__ == "__main__": main()