feat(pre-commit): save full lint output to a per-worktree log file (#36004)

* feat(pre-commit): save full lint output to a per-worktree log file

* docs(claude): point agents at the pre-commit log instead of rerunning

* fix(pre-commit): warn when the log cannot be created or fully written
This commit is contained in:
Mateo Wang 2026-08-05 15:16:52 -07:00 • committed by GitHub
parent f54cd287a2
commit f047124b5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 54 additions and 0 deletions

View file

@ -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

View file

@ -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"

View file

@ -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: