diff --git a/CLAUDE.md b/CLAUDE.md index 209d9aaf326..1bc4d108da7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,6 +41,8 @@ Python max line length is 120, not 88 When you fix violations gated by `ruff-strict-budget.json`, `type-discipline-budget.json`, or `basedpyright-code-budget.json`, run `make lint-budget-update` and commit the lowered limits so the ceilings ratchet down instead of leaving stale headroom. It measures the working tree, so it must contain exactly the fixes you're committing +`make pre-commit` always saves its complete output to a per-worktree log file and prints that path as its first and last output lines. To inspect a run, read or grep that log instead of re-running the multi-minute checks just to see a different slice, and re-run only after the working tree actually changed + If you're trying to create a new function that relies on untyped stuff, instead of adding more Any's and pushing `reportAny` / `reportExplicitAny` closer to their basedpyright ceilings, just validate it in the caller with Pydantic (a model or `TypeAdapter` that returns the typed thing or raises will do) and then pass the now typed variable in If you get an LIT001 or LIT002 fail, refactor the code to follow functional programming best practices rather than introducing mutable data structures. For example, build values in one shot with comprehensions or generators wrapped in `tuple()` / `frozenset()` instead of seeding an empty `list`/`dict`/`set` and mutating it over time. Ideally, `# mutable-ok` is never used; reach for it only as a genuine last resort when an immutable rewrite is truly impossible, and always pair it with a real reason diff --git a/scripts/pre_commit_lint.sh b/scripts/pre_commit_lint.sh index d2cf7cd307f..af8335e0e84 100755 --- a/scripts/pre_commit_lint.sh +++ b/scripts/pre_commit_lint.sh @@ -17,6 +17,23 @@ set -eu +if [ -z "${PRE_COMMIT_LINT_INNER:-}" ]; then + log_file=$(git rev-parse --path-format=absolute --git-path pre_commit_lint.log) + if : > "$log_file" 2>/dev/null; then + echo "pre-commit: logging full output to $log_file" + PRE_COMMIT_LINT_INNER=1 "$0" "$@" 2>&1 | tee "$log_file" + pipe_status=("${PIPESTATUS[@]}") + if [ "${pipe_status[1]}" -eq 0 ]; then + echo "pre-commit: full log: $log_file" + else + echo "pre-commit: WARNING - writing $log_file failed; the log may be incomplete" >&2 + fi + exit "${pipe_status[0]}" + fi + echo "pre-commit: WARNING - cannot write $log_file; output will not be saved" >&2 + PRE_COMMIT_LINT_INNER=1 exec "$0" "$@" +fi + repo_root=$(git rev-parse --show-toplevel) cd "$repo_root" diff --git a/tests/test_litellm/test_pre_commit_lint.py b/tests/test_litellm/test_pre_commit_lint.py index 92de89a3cd5..274d3c517f6 100644 --- a/tests/test_litellm/test_pre_commit_lint.py +++ b/tests/test_litellm/test_pre_commit_lint.py @@ -148,6 +148,41 @@ def test_all_blocks_passing_exits_zero(tmp_path: Path) -> None: assert proc.returncode == 0, proc.stdout + proc.stderr +def test_full_output_is_saved_to_a_log_file_in_the_git_dir(tmp_path: Path) -> None: + repo, bin_dir = _sandbox(tmp_path) + (repo / "scratch.txt").write_text("") + proc = _run(repo, bin_dir, {}) + assert proc.returncode == 0, proc.stdout + proc.stderr + log_file = repo / ".git" / "pre_commit_lint.log" + log = log_file.read_text() + assert "linting Python" in log + assert "linting dashboard" in log + assert "API types" in log + assert "unstaged/untracked changes" in log + assert f"pre-commit: full log: {log_file}" in proc.stdout + assert "pre-commit: full log:" not in log + + +def test_unwritable_log_warns_and_falls_back_to_running_without_one(tmp_path: Path) -> None: + repo, bin_dir = _sandbox(tmp_path) + (repo / ".git" / "pre_commit_lint.log").mkdir() + proc = _run(repo, bin_dir, {}) + assert proc.returncode == 0, proc.stdout + proc.stderr + assert "linting Python" in proc.stdout + assert "output will not be saved" in proc.stderr + assert "pre-commit: full log:" not in proc.stdout + failing = _run(repo, bin_dir, {"STUB_FAIL": "make-lint"}) + assert failing.returncode == 1 + + +def test_failing_run_exit_code_survives_the_log_pipeline(tmp_path: Path) -> None: + repo, bin_dir = _sandbox(tmp_path) + proc = _run(repo, bin_dir, {"STUB_FAIL": "make-lint"}) + assert proc.returncode == 1 + log = (repo / ".git" / "pre_commit_lint.log").read_text() + assert "Python lint failed" in log + + def _wait_until(predicate: Callable[[], bool], timeout_seconds: float) -> bool: deadline = time.monotonic() + timeout_seconds while time.monotonic() < deadline: