mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
* feat: add CI-parity mode and truncation-proof summary to strict ruff gate * refactor: tolerant worktree cleanup and concrete GateInputs types * fix: clean up temp dir when git worktree add fails * fix: align lint-gate with CI by dropping unused --ci-parity path The lint-gate Makefile target invoked ruff_strict_gate.py with --ci-parity, which counted violations on a throwaway merge of base into HEAD against base counts at the base tip. CI in test-linting.yml runs the same script without --ci-parity on a PR-head checkout, taking the gather_fast path that counts on the live tree against base counts at the merge-base. A local pass could therefore disagree with CI. Drop --ci-parity from the Makefile and remove the now-unused gather_ci_parity branch and flag so there is one code path that both local and CI exercise. The docstring claim that CI runs against the synthetic merge ref was also wrong; the workflow checks out github.event.pull_request.head.sha. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
194 lines
6.1 KiB
Python
194 lines
6.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Total-count gate for the strict ruff rules in ruff-strict.toml.
|
|
|
|
Each rule has a hard ceiling (baseline + slack) in ruff-strict-budget.json. The
|
|
gate counts each rule across the whole tree and fails when a rule is both over
|
|
its ceiling and higher than the base it merges into, so a change is blamed for
|
|
the violations it adds, never for drift that already exists in the base.
|
|
|
|
The base is the merge-base of the current branch with --base; this matches CI,
|
|
which checks out the PR head sha and runs the gate against the PR's base sha.
|
|
"""
|
|
|
|
import argparse
|
|
import contextlib
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from collections import Counter
|
|
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
from typing import NamedTuple
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
STRICT_CONFIG = REPO_ROOT / "ruff-strict.toml"
|
|
BUDGET_PATH = REPO_ROOT / "ruff-strict-budget.json"
|
|
TARGET = "litellm"
|
|
DEFAULT_BASE = "origin/litellm_internal_staging"
|
|
|
|
_HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
|
|
|
|
|
|
class Violation(NamedTuple):
|
|
file: str
|
|
line: int
|
|
code: str
|
|
|
|
|
|
class Breach(NamedTuple):
|
|
rule: str
|
|
total: int
|
|
cap: int
|
|
added: int
|
|
|
|
|
|
class GateInputs(NamedTuple):
|
|
head: list[Violation]
|
|
base: dict[str, int]
|
|
changed: dict[str, set[int]]
|
|
|
|
|
|
def _run(cmd: list, cwd: Path = REPO_ROOT) -> str:
|
|
proc = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
|
|
if proc.returncode not in (0, 1):
|
|
sys.stderr.write(proc.stderr)
|
|
raise SystemExit(f"{cmd[0]} exited {proc.returncode}")
|
|
return proc.stdout
|
|
|
|
|
|
def _ruff_json(cwd: Path, config: Path) -> list:
|
|
raw = _run(
|
|
["ruff", "check", TARGET, "--config", str(config), "--output-format", "json"],
|
|
cwd=cwd,
|
|
)
|
|
return json.loads(raw or "[]")
|
|
|
|
|
|
def collect_violations(root: Path, config: Path) -> list:
|
|
out = []
|
|
for item in _ruff_json(root, config):
|
|
name = Path(item["filename"])
|
|
rel = (
|
|
(name if name.is_absolute() else root / name)
|
|
.resolve()
|
|
.relative_to(root)
|
|
.as_posix()
|
|
)
|
|
out.append(Violation(rel, item["location"]["row"], item["code"]))
|
|
return out
|
|
|
|
|
|
def count_by_rule(violations: list) -> dict:
|
|
return dict(Counter(v.code for v in violations))
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _temp_worktree(ref: str) -> Iterator[Path]:
|
|
parent = Path(tempfile.mkdtemp(prefix="ruff_wt_"))
|
|
worktree = parent / "wt"
|
|
try:
|
|
_run(["git", "worktree", "add", "--detach", str(worktree), ref])
|
|
yield worktree
|
|
finally:
|
|
subprocess.run(
|
|
["git", "worktree", "remove", "--force", str(worktree)],
|
|
cwd=REPO_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
shutil.rmtree(parent, ignore_errors=True)
|
|
|
|
|
|
def base_counts(ref: str) -> dict:
|
|
with _temp_worktree(ref) as worktree:
|
|
shutil.copy(STRICT_CONFIG, worktree / "ruff-strict.toml")
|
|
return count_by_rule(
|
|
collect_violations(worktree, worktree / "ruff-strict.toml")
|
|
)
|
|
|
|
|
|
def parse_changed_lines(diff_text: str) -> dict:
|
|
changed: dict = {}
|
|
path = None
|
|
for line in diff_text.splitlines():
|
|
if line.startswith("+++ b/"):
|
|
path = line[6:]
|
|
elif path and (match := _HUNK.match(line)):
|
|
start = int(match.group(1))
|
|
count = int(match.group(2)) if match.group(2) is not None else 1
|
|
changed.setdefault(path, set()).update(range(start, start + count))
|
|
return changed
|
|
|
|
|
|
def evaluate(head: dict, base: dict, budget: dict) -> list:
|
|
breaches = []
|
|
for rule, spec in budget.items():
|
|
cap = spec["baseline"] + spec["slack"]
|
|
total = head.get(rule, 0)
|
|
if total > cap and total > base.get(rule, 0):
|
|
breaches.append(Breach(rule, total, cap, total - base.get(rule, 0)))
|
|
return sorted(breaches)
|
|
|
|
|
|
def introduced(violations: list, changed: dict) -> list:
|
|
return [v for v in violations if v.line in changed.get(v.file, set())]
|
|
|
|
|
|
def gather(base: str) -> GateInputs:
|
|
base_point = _run(["git", "merge-base", base, "HEAD"]).strip() or base
|
|
diff = _run(["git", "diff", base_point, "--unified=0", "--no-color", "--", TARGET])
|
|
return GateInputs(
|
|
collect_violations(REPO_ROOT, STRICT_CONFIG),
|
|
base_counts(base_point),
|
|
parse_changed_lines(diff),
|
|
)
|
|
|
|
|
|
def report(breaches: list, new: list, base: str) -> None:
|
|
print(f"FAIL: strict-rule totals exceed their ceiling (base {base}):")
|
|
for breach in breaches:
|
|
print(
|
|
f" {breach.rule}: total {breach.total} over cap {breach.cap} (this change added {breach.added})"
|
|
)
|
|
for violation in sorted(v for v in new if v.code == breach.rule):
|
|
print(f" {violation.file}:{violation.line}")
|
|
print(
|
|
"Reduce the new violations or remove an equal number elsewhere; the ceiling is baseline + slack in ruff-strict-budget.json."
|
|
)
|
|
summary = "; ".join(f"{b.rule} {b.total}/{b.cap} (+{b.added})" for b in breaches)
|
|
print(f"BREACHED RULES: {summary}")
|
|
|
|
|
|
def cmd_check(base: str) -> None:
|
|
budget = json.loads(BUDGET_PATH.read_text())
|
|
inputs = gather(base)
|
|
breaches = evaluate(count_by_rule(inputs.head), inputs.base, budget)
|
|
if not breaches:
|
|
print(f"OK: every strict rule is within its codebase ceiling (base {base})")
|
|
return
|
|
report(breaches, introduced(inputs.head, inputs.changed), base)
|
|
raise SystemExit(1)
|
|
|
|
|
|
def cmd_update() -> None:
|
|
budget = json.loads(BUDGET_PATH.read_text())
|
|
head = count_by_rule(collect_violations(REPO_ROOT, STRICT_CONFIG))
|
|
for rule in budget:
|
|
budget[rule]["baseline"] = head.get(rule, 0)
|
|
BUDGET_PATH.write_text(json.dumps(budget, indent=2, sort_keys=True) + "\n")
|
|
print("Re-captured per-rule baselines from the current tree")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--base", default=DEFAULT_BASE)
|
|
parser.add_argument("--update", action="store_true")
|
|
args = parser.parse_args()
|
|
cmd_update() if args.update else cmd_check(args.base)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|