Some checks failed
AITBC CI/CD Pipeline / lint-and-test (3.11) (pull_request) Has been cancelled
AITBC CI/CD Pipeline / lint-and-test (3.12) (pull_request) Has been cancelled
AITBC CI/CD Pipeline / lint-and-test (3.13) (pull_request) Has been cancelled
AITBC CLI Level 1 Commands Test / test-cli-level1 (3.11) (pull_request) Has been cancelled
AITBC CLI Level 1 Commands Test / test-cli-level1 (3.12) (pull_request) Has been cancelled
AITBC CLI Level 1 Commands Test / test-cli-level1 (3.13) (pull_request) Has been cancelled
Security Scanning / Bandit Security Scan (apps/coordinator-api/src) (pull_request) Has been cancelled
Security Scanning / Bandit Security Scan (cli/aitbc_cli) (pull_request) Has been cancelled
Security Scanning / Bandit Security Scan (packages/py/aitbc-core/src) (pull_request) Has been cancelled
Security Scanning / Bandit Security Scan (packages/py/aitbc-crypto/src) (pull_request) Has been cancelled
Security Scanning / Bandit Security Scan (packages/py/aitbc-sdk/src) (pull_request) Has been cancelled
Security Scanning / Bandit Security Scan (tests) (pull_request) Has been cancelled
Security Scanning / CodeQL Security Analysis (javascript) (pull_request) Has been cancelled
Security Scanning / CodeQL Security Analysis (python) (pull_request) Has been cancelled
Security Scanning / Dependency Security Scan (pull_request) Has been cancelled
Security Scanning / Container Security Scan (pull_request) Has been cancelled
Security Scanning / OSSF Scorecard (pull_request) Has been cancelled
AITBC CI/CD Pipeline / test-cli (pull_request) Has been cancelled
AITBC CI/CD Pipeline / test-services (pull_request) Has been cancelled
AITBC CI/CD Pipeline / test-production-services (pull_request) Has been cancelled
AITBC CI/CD Pipeline / security-scan (pull_request) Has been cancelled
AITBC CI/CD Pipeline / build (pull_request) Has been cancelled
AITBC CI/CD Pipeline / deploy-staging (pull_request) Has been cancelled
AITBC CI/CD Pipeline / deploy-production (pull_request) Has been cancelled
AITBC CI/CD Pipeline / performance-test (pull_request) Has been cancelled
AITBC CI/CD Pipeline / docs (pull_request) Has been cancelled
AITBC CI/CD Pipeline / release (pull_request) Has been cancelled
AITBC CI/CD Pipeline / notify (pull_request) Has been cancelled
AITBC CLI Level 1 Commands Test / test-summary (pull_request) Has been cancelled
Security Scanning / Security Summary Report (pull_request) Has been cancelled
- CLI commands: replace print with click.echo (ensures proper stdout handling) - Coordinator API services: add logging import and logger; replace print with logger.info - Automation scripts: claim-task.py, monitor-prs.py, qa-cycle.py now use logging and have random jitter at startup - Also includes fix for name shadowing in regulatory.py (aliased service imports) which was pending This addresses issue #23 (print statements) and improves error handling. Note: Many bare except clauses (issue #20) remain; will be addressed separately.
140 lines
5.3 KiB
Python
140 lines
5.3 KiB
Python
#!/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()
|