"""Fail CI when changed Python files exceed the configured complexity cap.""" from __future__ import annotations import json import os import subprocess import sys from pathlib import Path DEFAULT_MAX_CC = 25 def _max_cc() -> int: try: raw = os.environ.get("MJR_COMPLEXITY_MAX_CC", "").strip() if raw: return max(1, int(raw)) except Exception: pass return DEFAULT_MAX_CC def _run(cmd: list[str]) -> str: return subprocess.check_output(cmd, text=True, stderr=subprocess.DEVNULL).strip() def _diff_outputs() -> list[str]: diffs: list[str] = [] base_ref = os.environ.get("GITHUB_BASE_REF", "").strip() try: if base_ref: diffs.append(_run(["git", "diff", "--diff-filter=AM", "--name-only", f"origin/{base_ref}...HEAD"])) else: try: diffs.append(_run(["git", "diff", "--diff-filter=AM", "--name-only", "@{upstream}...HEAD"])) except Exception: diffs.append(_run(["git", "diff", "--diff-filter=AM", "--name-only", "HEAD~1...HEAD"])) except Exception: pass for cmd in ( ["git", "diff", "--diff-filter=AM", "--name-only"], ["git", "diff", "--cached", "--diff-filter=AM", "--name-only"], ["git", "ls-files", "--others", "--exclude-standard"], ): try: diffs.append(_run(cmd)) except Exception: continue return diffs def _new_python_files() -> list[Path]: files = [] seen: set[Path] = set() for diff in _diff_outputs(): for raw in diff.splitlines(): p = Path(raw.strip()) if p.suffix == ".py" and p.exists() and p not in seen: seen.add(p) files.append(p) return files def main() -> int: max_cc = _max_cc() files = _new_python_files() if not files: print("No changed Python files; complexity gate skipped.") return 0 cmd = [sys.executable, "-m", "radon", "cc", "--json", *[str(p) for p in files]] try: payload = _run(cmd) data = json.loads(payload) if payload else {} except Exception as exc: print(f"Failed to run radon: {exc}") return 1 violations: list[tuple[str, str, int]] = [] for path, items in data.items(): if not isinstance(items, list): # radon reports {"error": "..."} for files with syntax errors — skip them if isinstance(items, dict) and "error" in items: print(f"Skipping {path}: radon error — {items['error']}") continue for item in items: if not isinstance(item, dict): continue complexity = int(item.get("complexity") or 0) if complexity > max_cc: violations.append((path, str(item.get("name") or ""), complexity)) if violations: print(f"Complexity gate failed: CC must be <= {max_cc} on changed files.") for path, name, complexity in violations: print(f"- {path}: {name} (CC={complexity})") return 1 print(f"Complexity gate passed for {len(files)} changed Python file(s).") return 0 if __name__ == "__main__": raise SystemExit(main())