vet

Mirror of Vet, an AI code review tool
git clone git://git.laack.co/vet.git
Log | Files | Refs | README | LICENSE

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()