107 lines
3.2 KiB
Python
107 lines
3.2 KiB
Python
"""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 "<unknown>"), 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())
|