run.py (5194B)
1 import json 2 import os 3 import subprocess 4 import sys 5 from pathlib import Path 6 from typing import Tuple 7 8 import httpx 9 10 # reusing SyncLocalGitRepo from vet.git to compute merge base 11 from vet.git import SyncLocalGitRepo 12 13 14 def get_env(name: str, required: bool = True) -> str: 15 value = os.environ.get(name) 16 if required and not value: 17 print(f"::error::{name} environment variable not set") 18 sys.exit(1) 19 return value or "" 20 21 22 def compute_merge_base(base_ref: str, head_sha: str) -> str: 23 repo = SyncLocalGitRepo(Path(".")) 24 try: 25 return repo.get_merge_base(f"origin/{base_ref}", head_sha) 26 except Exception: 27 print(f"::error::Failed to compute merge base between origin/{base_ref} and {head_sha}") 28 sys.exit(1) 29 30 31 def build_vet_args(goal: str, merge_base: str) -> list[str]: 32 args = [ 33 goal, 34 "--quiet", 35 "--output-format", 36 "github", 37 "--base-commit", 38 merge_base, 39 ] 40 41 # Multi-value flags (space-splitting like bash) 42 multi_value_envs = { 43 "INPUT_ENABLED_ISSUE_CODES": "--enabled-issue-codes", 44 "INPUT_DISABLED_ISSUE_CODES": "--disabled-issue-codes", 45 "INPUT_EXTRA_CONTEXT": "--extra-context", 46 } 47 48 # Single-value flags 49 single_value_envs = { 50 "INPUT_MODEL": "--model", 51 "INPUT_CONFIDENCE_THRESHOLD": "--confidence-threshold", 52 "INPUT_MAX_WORKERS": "--max-workers", 53 "INPUT_MAX_SPEND": "--max-spend", 54 "INPUT_TEMPERATURE": "--temperature", 55 "INPUT_CONFIG": "--config", 56 } 57 58 # Agentic flag 59 if os.environ.get("INPUT_AGENTIC") == "true": 60 args.append("--agentic") 61 62 # Handle single-value flags 63 for env_key, flag in single_value_envs.items(): 64 value = os.environ.get(env_key) 65 if value: 66 args.extend([flag, value]) 67 68 # Handle multi-value flags (space-splitting like bash) 69 for env_key, flag in multi_value_envs.items(): 70 value = os.environ.get(env_key) 71 if value: 72 args.append(flag) 73 args.extend(value.split()) 74 75 return args 76 77 78 def run_vet(args: list[str]) -> Tuple[dict, int]: 79 result = subprocess.run( 80 ["vet"] + args, 81 stdout=subprocess.PIPE, 82 stderr=None, 83 text=True, 84 ) 85 86 status = result.returncode 87 88 if status not in (0, 10): 89 print(f"::error::Vet failed with exit code {status}") 90 sys.exit(status) 91 92 try: 93 review_json = json.loads(result.stdout) 94 except json.JSONDecodeError: 95 print("::error::Failed to parse vet JSON output") 96 print(result.stdout) 97 sys.exit(1) 98 99 return review_json, status 100 101 102 def post_review(review_json: dict, repo: str, pr_number: str, token: str): 103 headers = { 104 "Authorization": f"Bearer {token}", 105 "Accept": "application/vnd.github+json", 106 } 107 108 review_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}/reviews" 109 comment_url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments" 110 111 with httpx.Client(timeout=10.0) as client: 112 # Try review post 113 try: 114 response = client.post(review_url, json=review_json, headers=headers) 115 if response.status_code in (200, 201): 116 return 117 print(f"::warning::Review POST failed: {response.status_code} {response.text}") 118 except httpx.HTTPError as e: 119 print(f"::warning::Review POST failed: {e}") 120 121 # Fallback to comment 122 body_parts = [review_json.get("body", "")] 123 for comment in review_json.get("comments", []): 124 path = comment.get("path") 125 line = comment.get("line") 126 text = comment.get("body", "") 127 body_parts.append(f"**{path}:{line}**\n\n{text}") 128 129 comment_body = "\n\n---\n\n".join(body_parts) 130 131 try: 132 fallback_response = client.post( 133 comment_url, 134 json={"body": comment_body}, 135 headers=headers, 136 ) 137 if fallback_response.status_code in (200, 201): 138 return 139 print(f"::warning::Fallback comment POST failed: {fallback_response.status_code} {fallback_response.text}") 140 except httpx.HTTPError as e: 141 print(f"::warning::Fallback comment POST failed: {e}") 142 143 # both failing results in workflow failure 144 print("::error::Failed to post GitHub review and fallback comment") 145 sys.exit(1) 146 147 148 def main(): 149 goal = get_env("INPUT_GOAL") 150 base_ref = get_env("INPUT_BASE_REF") 151 head_sha = get_env("INPUT_HEAD_SHA") 152 pr_number = get_env("INPUT_PR_NUMBER") 153 repo = get_env("GITHUB_REPOSITORY") 154 token = get_env("GH_TOKEN") 155 156 fail_on_issues = os.environ.get("INPUT_FAIL_ON_ISSUES") == "true" 157 158 merge_base = compute_merge_base(base_ref, head_sha) 159 160 args = build_vet_args(goal, merge_base) 161 162 review_json, status = run_vet(args) 163 164 # Inject commit_id (replaces jq logic) 165 review_json["commit_id"] = head_sha 166 167 post_review(review_json, repo, pr_number, token) 168 169 # Replicate fail-on-issues behavior 170 if fail_on_issues and status == 10: 171 sys.exit(1) 172 173 sys.exit(0) 174 175 176 if __name__ == "__main__": 177 main()