#!/usr/bin/env python3 """ Gitea Actions Run Detail Inspector v2 AGENTS.md 지침 준수: Gitea API를 하네스로 조회. 직접 서버 접근 금지. formula_id: GITEA_ACTIONS_RUN_DETAIL_V2 토큰 우선순위 (높은 것부터): 1. --token CLI 인자 2. GITEA_TOKEN_BAIK (사용자 지정 별칭) 3. GITEA_TOKEN_TAXBAIK (기존 표준) 4. GITEA_TOKEN (일반 fallback) 5. GITEA_TOKEN_HOME (레거시) """ from __future__ import annotations import argparse import json import os import urllib.error import urllib.request from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_BASE_URL = "https://gitea.taxbaik.com" DEFAULT_OWNER = "kjh2064" DEFAULT_REPO = "QuantEngineByItz" def _get_token(cli_token: str = "") -> str: """토큰 우선순위: CLI → GITEA_TOKEN_BAIK → GITEA_TOKEN_TAXBAIK → GITEA_TOKEN → GITEA_TOKEN_HOME""" if cli_token and cli_token.strip(): return cli_token.strip() for env_key in ("GITEA_TOKEN_BAIK", "GITEA_TOKEN_TAXBAIK", "GITEA_TOKEN", "GITEA_TOKEN_HOME"): val = os.environ.get(env_key, "").strip() if val: return val return "" def _request_json(url: str, token: str = "") -> tuple[int, str, Any]: headers = { "Accept": "application/json", "User-Agent": "QuantEngine-Harness/2.0", } if token: headers["Authorization"] = f"token {token}" req = urllib.request.Request(url, headers=headers, method="GET") try: with urllib.request.urlopen(req, timeout=30) as resp: raw = resp.read().decode("utf-8", errors="replace") payload = json.loads(raw) if raw else None return resp.status, resp.reason, payload except urllib.error.HTTPError as exc: raw = exc.read().decode("utf-8", errors="replace") try: payload = json.loads(raw) except Exception: payload = raw return exc.code, exc.reason or "", payload def _summary_run(run: dict) -> dict: return { "id": run.get("id"), "run_number": run.get("run_number"), "display_title": run.get("display_title"), "workflow_path": run.get("path"), "event": run.get("event"), "status": run.get("status"), "conclusion": run.get("conclusion"), "head_branch": run.get("head_branch"), "head_sha": run.get("head_sha"), "started_at": run.get("started_at"), "completed_at": run.get("completed_at"), "actor": (run.get("actor") or {}).get("login"), } def main() -> int: ap = argparse.ArgumentParser(description="Gitea Actions Run Detail Inspector v2") ap.add_argument("--base-url", default=DEFAULT_BASE_URL) ap.add_argument("--owner", default=DEFAULT_OWNER) ap.add_argument("--repo", default=DEFAULT_REPO) ap.add_argument("--run-id", type=int, default=2545, help="Gitea Actions run ID") ap.add_argument("--token", default="", help="Gitea API Personal Access Token") ap.add_argument("--all-runs", action="store_true", help="List recent N runs") ap.add_argument("--limit", type=int, default=10, help="Number of recent runs to list when --all-runs") args = ap.parse_args() token = _get_token(args.token) base = f"{args.base_url.rstrip('/')}/api/v1/repos/{args.owner}/{args.repo}" errors: list[str] = [] evidence: dict[str, Any] = { "owner": args.owner, "repo": args.repo, "target_run_id": args.run_id, "token_provided": bool(token), } # 1. Target run details run_url = f"{base}/actions/runs/{args.run_id}" s1, _, run_payload = _request_json(run_url, token=token) evidence["target_run_http_status"] = s1 if s1 != 200: errors.append(f"Run #{args.run_id} fetch failed: HTTP {s1}") target_run_summary = None jobs_data = None else: target_run_summary = _summary_run(run_payload) # 2. Fetch jobs for this run jobs_url = f"{base}/actions/runs/{args.run_id}/jobs" s2, _, jobs_payload = _request_json(jobs_url, token=token) evidence["jobs_http_status"] = s2 if s2 == 200 and isinstance(jobs_payload, dict): raw_jobs = jobs_payload.get("workflow_jobs") or [] jobs_data = [] for job in raw_jobs: steps = job.get("steps") or [] failed_steps = [ { "step_number": st.get("number"), "name": st.get("name"), "conclusion": st.get("conclusion"), "started_at": st.get("started_at"), "completed_at": st.get("completed_at"), } for st in steps if st.get("conclusion") not in ("success", "skipped", None) ] jobs_data.append({ "job_id": job.get("id"), "job_name": job.get("name"), "status": job.get("status"), "conclusion": job.get("conclusion"), "started_at": job.get("started_at"), "completed_at": job.get("completed_at"), "runner_name": job.get("runner_name"), "total_steps": len(steps), "failed_steps": failed_steps, }) else: jobs_data = None if s2 != 200: errors.append(f"Jobs fetch failed: HTTP {s2}") # 3. Recent runs list (optional) recent_runs = None if args.all_runs: runs_url = f"{base}/actions/runs?limit={args.limit}" s3, _, runs_payload = _request_json(runs_url, token=token) evidence["runs_list_http_status"] = s3 if s3 == 200 and isinstance(runs_payload, dict): raw_runs = runs_payload.get("workflow_runs") or [] recent_runs = [_summary_run(r) for r in raw_runs] else: errors.append(f"Runs list fetch failed: HTTP {s3}") result = { "formula_id": "GITEA_ACTIONS_RUN_DETAIL_V2", "gate": "PASS" if not errors else "FAIL", "errors": errors, "evidence": evidence, "run_summary": target_run_summary, "jobs": jobs_data, "recent_runs": recent_runs, } out = ROOT / "Temp" / f"gitea_actions_run_{args.run_id}_detail_v2.json" out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 if not errors else 1 if __name__ == "__main__": raise SystemExit(main())