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.
139 lines
5.0 KiB
Python
139 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
import os, sys, json, subprocess, random, time, logging
|
|
from datetime import datetime
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
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")
|
|
|
|
def log(msg):
|
|
logger.info(msg)
|
|
|
|
def die(msg):
|
|
log(f"FATAL: {msg}")
|
|
sys.exit(1)
|
|
|
|
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 compute_utility(issue):
|
|
score = 0
|
|
labels = [l['name'] for l in issue['labels']]
|
|
if 'security' in labels:
|
|
score += 100
|
|
elif 'bug' in labels:
|
|
score += 50
|
|
elif 'feature' in labels:
|
|
score += 30
|
|
elif 'refactor' in labels:
|
|
score += 10
|
|
if 'good-first-task-for-agent' in labels:
|
|
score += 20
|
|
if any(tag in labels for tag in ['needs-design', 'blocked', 'needs-reproduction']):
|
|
score -= 1000
|
|
score += issue['comments'] * 5
|
|
return score
|
|
|
|
def get_open_unassigned_issues():
|
|
items = api_get(f"repos/{REPO}/issues?state=open") or []
|
|
candidates = []
|
|
for i in items:
|
|
# Skip PRs: pull_request field is non-null for PRs
|
|
if i.get('pull_request') is not None:
|
|
continue
|
|
if i['assignee'] is not None:
|
|
continue
|
|
labels = [l['name'] for l in i['labels']]
|
|
if any(tag in labels for tag in ['needs-design', 'blocked', 'needs-reproduction']):
|
|
continue
|
|
candidates.append(i)
|
|
return candidates
|
|
|
|
def create_claim_branch(issue_number):
|
|
branch = f"claim/{issue_number}"
|
|
subprocess.run(["git", "fetch", "origin"], capture_output=True, cwd=REPO_DIR)
|
|
subprocess.run(["git", "checkout", "-B", branch, "origin/main"], capture_output=True, cwd=REPO_DIR)
|
|
subprocess.run(["git", "commit", "--allow-empty", "-m", f"Claim issue #{issue_number} for {AGENT}"], capture_output=True, cwd=REPO_DIR)
|
|
r = subprocess.run(["git", "push", "-u", "origin", branch], capture_output=True, cwd=REPO_DIR)
|
|
return r.returncode == 0
|
|
|
|
def assign_issue(issue_number):
|
|
payload = {"assignee": AGENT}
|
|
result = api_post(f"repos/{REPO}/issues/{issue_number}/assignees", payload)
|
|
return result is not None
|
|
|
|
def add_comment(issue_number, body):
|
|
payload = {"body": body}
|
|
return api_post(f"repos/{REPO}/issues/{issue_number}/comments", payload) is not None
|
|
|
|
def main():
|
|
# Jitter 0-60s
|
|
time.sleep(random.randint(0, 60))
|
|
log("Claim task cycle starting...")
|
|
state_file = os.path.join(REPO_DIR, ".claim-state.json")
|
|
try:
|
|
with open(state_file) as f:
|
|
state = json.load(f)
|
|
except:
|
|
state = {}
|
|
if state.get('current_claim'):
|
|
log(f"Already working on issue #{state['current_claim']} (branch {state.get('work_branch')})")
|
|
return
|
|
issues = get_open_unassigned_issues()
|
|
if not issues:
|
|
log("No unassigned issues available.")
|
|
return
|
|
issues.sort(key=lambda i: compute_utility(i), reverse=True)
|
|
for issue in issues:
|
|
num = issue['number']
|
|
title = issue['title']
|
|
labels = [lbl['name'] for lbl in issue.get('labels', [])]
|
|
log(f"Attempting to claim issue #{num}: {title} (labels={labels})")
|
|
if create_claim_branch(num):
|
|
assign_issue(num)
|
|
slug = ''.join(c if c.isalnum() else '-' for c in title.lower())[:40].strip('-')
|
|
work_branch = f"{AGENT}/{num}-{slug}"
|
|
subprocess.run(["git", "checkout", "-b", work_branch, "main"], cwd=REPO_DIR, capture_output=True)
|
|
state = {
|
|
'current_claim': num,
|
|
'claim_branch': f'claim/{num}',
|
|
'work_branch': work_branch,
|
|
'claimed_at': datetime.utcnow().isoformat() + 'Z',
|
|
'issue_title': title,
|
|
'labels': labels
|
|
}
|
|
with open(state_file, 'w') as f:
|
|
json.dump(state, f, indent=2)
|
|
add_comment(num, f"Agent `{AGENT}` claiming this task. (automated)")
|
|
log(f"✅ Claimed issue #{num}. Work branch: {work_branch}")
|
|
return
|
|
else:
|
|
log(f"Claim failed for #{num} (branch exists). Trying next...")
|
|
log("Could not claim any issue; all taken or unavailable.")
|
|
|
|
if __name__ == "__main__":
|
|
REPO_DIR = '/opt/aitbc'
|
|
main()
|