CI Pipeline / lint (push) Failing after 45s
CI Pipeline / unit-tests (push) Failing after 17s
CI Pipeline / canary (push) Successful in 26s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 3s
CI Pipeline / type-check (push) Successful in 21s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / required-checks (push) Skipped
CI Pipeline / lint (pull_request) Failing after 23s
CI Pipeline / unit-tests (pull_request) Failing after 16s
CI Pipeline / canary (pull_request) Successful in 25s
CI Pipeline / best-practices (pull_request) Successful in 2s
CI Pipeline / documentation (pull_request) Successful in 3s
CI Pipeline / type-check (pull_request) Successful in 55s
CI Pipeline / gitea-offline (pull_request) Successful in 13s
CI Pipeline / required-checks (pull_request) Skipped
Spec gate (headless) / spec-gate (pull_request) Successful in 5s
81 lines
2.7 KiB
Python
81 lines
2.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Silent-handler ratchet (#126): fail if S110/BLE001 counts exceed the baseline.
|
|
|
|
S110 = try-except-pass (the truly silent handlers), BLE001 = blind
|
|
`except Exception`. The baseline (lint-baseline.json) is committed and can
|
|
only shrink: new silent handlers fail CI, fixes lower the ledger. BLE001's
|
|
built-in exemptions (re-raise, log with `.exception()`/`exc_info=`) are the
|
|
compliant-handler spec; intentional silence carries `# noqa: BLE001 — <reason>`.
|
|
|
|
Same philosophy as the pyright ratchet (scripts/type_check_ratchet.py, #148).
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
BASELINE_FILE = Path(os.environ.get("LINT_BASELINE", ROOT / "lint-baseline.json"))
|
|
SCOPE = ["src/", "shared/", "scripts/"]
|
|
RULES = ("BLE001", "S110")
|
|
|
|
|
|
def _ruff_bin() -> str:
|
|
"""Prefer the venv sibling of this interpreter, then PATH."""
|
|
sibling = Path(sys.executable).parent / "ruff"
|
|
if sibling.is_file():
|
|
return str(sibling)
|
|
return "ruff"
|
|
|
|
|
|
def current_counts() -> dict[str, int]:
|
|
result = subprocess.run(
|
|
[_ruff_bin(), "check", "--select", ",".join(RULES), "--output-format", "json", *SCOPE],
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=str(ROOT),
|
|
timeout=300,
|
|
)
|
|
if result.returncode not in (0, 1): # 1 = violations found (expected)
|
|
sys.stderr.write(result.stderr)
|
|
raise RuntimeError(f"ruff failed with exit {result.returncode}")
|
|
counts = {rule: 0 for rule in RULES}
|
|
for v in json.loads(result.stdout): # ruff emits one JSON array
|
|
code = v.get("code")
|
|
code = code.get("name") if isinstance(code, dict) else code
|
|
if code in counts:
|
|
counts[code] += 1
|
|
return counts
|
|
|
|
|
|
def main() -> int:
|
|
baseline = json.loads(BASELINE_FILE.read_text())
|
|
missing = [rule for rule in RULES if rule not in baseline]
|
|
if missing:
|
|
print(f"::error::lint-baseline.json is missing rules {missing} — ratchet cannot run")
|
|
return 1
|
|
counts = current_counts()
|
|
print(
|
|
"silent-handler ratchet: " + ", ".join(f"{rule} {counts[rule]} (baseline {baseline[rule]})" for rule in RULES)
|
|
)
|
|
failed = False
|
|
for rule in RULES:
|
|
if counts[rule] > baseline[rule]:
|
|
print(
|
|
f"::error::Silent-handler count increased for {rule}: "
|
|
f"{baseline[rule]} -> {counts[rule]} (max {baseline[rule]}). "
|
|
"Fix the handler (log with exc_info or re-raise) or add "
|
|
"`# noqa: BLE001 — <reason>` only when silence is intentional."
|
|
)
|
|
failed = True
|
|
if failed:
|
|
return 1
|
|
print("Silent-handler ratchet: PASS")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|