ENG-4687 pre-commit hook: cargo fmt --check + hygiene#8
Draft
jimbofreedman wants to merge 26 commits into
Draft
Conversation
Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
…ene) Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
…consumer
The tests/downstream_consumer/ sub-package is its own [package] (not a
workspace member), so `cargo fmt --check` from the root does not
traverse into it. Without an explicit --manifest-path invocation the
pre-commit gate would silently miss fmt drift in that crate — failing
AC3 ("committing a file with bad formatting fails the hook") for any
edit under tests/downstream_consumer/.
Extend the cargo-fmt-check hook entry to run both invocations:
cargo fmt --check && \
cargo fmt --manifest-path tests/downstream_consumer/Cargo.toml --check
Lock the new invariant in tests/pre_commit_config.rs and one-time-format
the existing drift in tests/downstream_consumer/src/lib.rs.
Found by peer review (Codex) on the initial implementation.
Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
Shells out to the real `pre-commit` binary and asserts the runtime acceptance criteria that the dev-phase string-assertion test in `tests/pre_commit_config.rs` cannot cover: - AC2 `pre-commit run --all-files` exits 0 on the worktree - AC3 staging a fmt-bad `.rs` file fails `cargo-fmt-check` with a rustfmt diff - AC4 `pre-commit run --hook-stage manual cargo-clippy` invokes clippy - AC4 corollary: clippy is filtered out on the default stage (proves `stages: [manual]` is doing real work) Tests are skip-clean when pre-commit/git are absent (Rust-only contributors), so `cargo test` stays runnable without `pip install pre-commit`. CI exercises the suite for real because pre-commit is installed in the workflow. AC3/AC4 isolate against a throwaway git repo built in `tempfile::TempDir` with the real `.pre-commit-config.yaml` copied in; AC2 runs against the actual tree and restores any auto-fixer modifications it triggers. Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
end-of-file-fixer requires a single trailing LF; the file shipped with two from the initial repo skeleton, causing `pre-commit run --all-files` to fail (ac2 e2e test). Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
…rgo fmt` The AC3 fallback assertion in `tests/pre_commit_e2e.rs` accepted either `Diff in` (the rustfmt --check signature) OR `cargo fmt` in pre-commit output. The second alternative was too broad: pre-commit's own hook banner prints "cargo fmt --check", so the assertion would pass even if rustfmt stopped emitting diff output and the hook failed for some other reason — degrading the contributor signal to "the hook failed but I don't see why". Narrow the fallback to `rustfmt` so the assertion only succeeds when the output genuinely looks like rustfmt diagnostics, not just the hook chrome. Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
The "Before opening a pull request, run:" sentence used to lead directly into the `cargo fmt --check / cargo check / ...` block. After the pre-commit stanza was inserted above that block, the sentence read as a lead-in to an install instruction, not a command list — disjointed UX. Drop the stranded sentence from the section opener and move it down to immediately precede the actual command block. No content changes, just reordering so the sentence introduces the commands it's describing. Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
The e2e tests in `tests/pre_commit_e2e.rs` shell out to the system
`pre-commit` binary and silently skip when it is not on PATH. The
existing CI workflow did not install pre-commit anywhere, so every CI
run was returning success without exercising a single hook — AC2
("pre-commit run --all-files passes"), AC3 (fmt-bad commit fails the
hook), and AC4 (manual-stage clippy) were effectively self-attested.
Add a dedicated `pre-commit` job that:
- installs the stable Rust toolchain with rustfmt + clippy components
- sets up Python 3.x and `pip install pre-commit`
- runs `pre-commit run --all-files --show-diff-on-failure` (AC2 direct)
- runs `cargo test --test pre_commit_e2e` (AC2/AC3/AC4 via harness)
Also update the module doc-comment in `tests/pre_commit_e2e.rs` to
match reality — the previous wording falsely claimed CI exercised the
tests; it now points at the new pre-commit job and is explicit that
other CI jobs intentionally skip.
Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
Drop the bare `output.contains("clippy")` alternative from the
ac4_manual_stage_invokes_clippy probe. That substring matches
pre-commit's hook-name banner, which is printed regardless of whether
the hook entry actually executed — making the probe firable on the
banner alone. Keep `Checking ` (cargo's compile banner) and the
literal entry `cargo clippy --no-deps` as the two valid evidence
substrings.
Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
Replace the substring `cargo clippy --no-deps` in the ac4_clippy_does_not_run_on_default_stage filter with `Checking e2e_root`. The previous substring overlaps the hook NAME in .pre-commit-config.yaml, which can appear in pre-commit's output regardless of whether clippy actually launched. `Checking e2e_root` is cargo's compile banner for the seeded test crate and only appears when clippy genuinely runs against it, making the negative assertion robust against pre-commit upgrades. Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
Lowercase the .pre-commit-config.yaml contents before substring matching in python_specific_backend_hooks_are_absent. The previous test forbade the exact substring `djLint` (upstream repo casing), which a future contributor using the lowercase hook id `djlint` would silently bypass. Lowercasing the haystack and forbidding `djlint` catches both casings with no enumeration. Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
Pre-existing drift introduced when tightening the AC3 fallback assertion; cargo fmt --check (the form pre-commit runs) flagged it. Without this fix, the new AC2 e2e test fails against the worktree itself. Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
jimbofreedman
force-pushed
the
worktree-ENG-4687
branch
from
May 20, 2026 18:48
d8ccaba to
d1b3030
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces a repository-wide pre-commit gate to enforce cargo fmt --check plus hygiene hooks on every commit, with cargo clippy exposed as an opt-in manual-stage hook. It also adds invariant + end-to-end tests and a CI job to ensure the hook contract can’t drift unnoticed.
Changes:
- Add
.pre-commit-config.yamlwith hygiene hooks,cargo fmt --check(default stage), andcargo clippy(manual stage). - Add invariant tests (
tests/pre_commit_config.rs) and e2e tests (tests/pre_commit_e2e.rs) to lock configuration and behavior. - Update contributor documentation and CI to install/run
pre-commitand execute the e2e suite.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
tests/pre_commit_e2e.rs |
Adds end-to-end tests that shell out to pre-commit to validate AC2/AC3/AC4. |
tests/pre_commit_config.rs |
Adds invariant tests that lock the .pre-commit-config.yaml shape via string assertions. |
tests/downstream_consumer/src/lib.rs |
Applies formatting changes so cargo fmt --check passes. |
CONTRIBUTING.md |
Documents pre-commit usage in the local validation workflow. |
CODE_OF_CONDUCT.md |
Removes trailing blank line at EOF (hygiene). |
.pre-commit-config.yaml |
Introduces the pre-commit hook configuration (hygiene + fmt + manual clippy). |
.github/workflows/ci.yaml |
Adds a dedicated CI job to install/run pre-commit and execute e2e hook tests. |
Comments suppressed due to low confidence (2)
tests/pre_commit_e2e.rs:342
augmented_pathis built with a hard-coded:separator here as well, which is not portable to Windows PATH semantics. Usestd::env::join_pathsto construct the PATH value in an OS-appropriate way.
let cargo = cargo_bin();
let path = std::env::var("PATH").unwrap_or_default();
let cargo_dir = Path::new(&cargo)
.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let augmented_path = if cargo_dir.is_empty() {
path.clone()
} else {
format!("{cargo_dir}:{path}")
};
tests/pre_commit_e2e.rs:412
- Same PATH concatenation issue here: using
format!("{cargo_dir}:{path}")is not portable on Windows. Preferstd::env::join_paths/split_pathsso this test behaves consistently across platforms.
let cargo = cargo_bin();
let path = std::env::var("PATH").unwrap_or_default();
let cargo_dir = Path::new(&cargo)
.parent()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_default();
let augmented_path = if cargo_dir.is_empty() {
path.clone()
} else {
format!("{cargo_dir}:{path}")
};
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…pport The chained `bash -c '... && ...'` entry required bash on PATH, breaking the Windows runners in the CI test matrix. Splitting into two separate local hooks (`cargo-fmt-check` for the root crate, `cargo-fmt-check-downstream` for the nested sub-package) keeps coverage identical without needing a shell. Invariant test updated to assert both hooks exist and neither uses a shell entry. Reported by Copilot on PR #8. Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
- AC2: assert the worktree stays hook-clean after `pre-commit run` even
if pre-commit exited 0. A silent auto-fixer rewriting files violates
the "main-equivalent tree is hook-clean" guarantee just as much as a
non-zero exit would. Capture the post-run diff for the error message
before restoring the tree.
- AC3: drop the `output.contains("rustfmt")` fallback in the diff-visible
assertion. The bare "rustfmt" substring would match unrelated error
messages (e.g. "rustfmt component not installed"), letting AC3 pass
for the wrong reason. Require `Diff in` only.
- AC4 corollary: tighten the negative check to require pre-commit's
explicit "No hook with id" diagnostic and a non-zero exit. The prior
absence-of-banner fallback could be true if clippy errored before
printing — false positive risk.
- Extract `cargo_augmented_path()` helper using `std::env::join_paths`
so PATH manipulation uses the platform separator (Windows uses `;`,
Unix `:`). The old `format!("{cargo_dir}:{path}")` only worked on
Unix. Removes ~30 lines of duplication across 3 call sites.
Reported by Copilot on PR #8.
Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
The previous stanza jumped straight to `pre-commit install` without explaining how to obtain the `pre-commit` tool itself. Add pipx/pip/brew install options before the install line so the setup steps stand alone. Reported by Copilot on PR #8. Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
- ci: pin `pre-commit==4.6.0` in the CI job. The e2e tests assert on
specific pre-commit diagnostic wording ("No hook with id …"); an
unpinned upstream release could change that wording and cause
nondeterministic CI failures.
- docs: extend the CONTRIBUTING.md manual-stage comment to include
`-- -D warnings` so the example matches the actual hook entry.
- test(infra): tighten AC2's worktree-clean check to use
`git status --porcelain` (empty == truly clean: no unstaged, no
staged, no untracked) instead of `git diff --quiet`. The previous
check only saw unstaged drift, so a contributor with staged work or
untracked files could trigger the drift-recovery `git checkout -- .`
and lose their in-progress changes.
- test(infra): collapse the duplicate `pre_commit_on_path` /
`git_on_path` helpers into a single `bin_on_path(&str)`.
Reported by Copilot on PR #8.
Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
- security(infra): drop `args: ['--unsafe']` from check-yaml. Verified
via `yaml.safe_load(...)` that every YAML in the repo (workflows,
config, .pre-commit-config.yaml itself) loads safely — there's no
custom-tag YAML that needs the unsafe loader, so the flag was pure
attack surface against running pre-commit on untrusted PR branches.
- test(infra): AC2 drift-recovery now also runs `git clean -fd` after
`git checkout -- .` so an untracked artifact written by a hook
doesn't leave the worktree dirty. Safe because the recovery path
only fires when the pre-check confirmed the tree was truly clean.
- test(infra): tighten the cargo-fmt-check invariant test to match
the full `entry:` line (post-trim) instead of a substring. The
prior `cfg.contains("entry: cargo fmt --check")` would have happily
passed `entry: cargo fmt --check --all` and silently expanded
coverage. Now compares against the exact expected line for both
hooks.
- docs(test): update the e2e module-level doc-comment from
`std::env::temp_dir()` to `tempfile::tempdir()` (auto-cleans on
drop) to match the implementation.
Reported by Copilot on PR #8.
Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
Three nits flagged on commit fd46d11: - AC4 `invoked` probe: drop the `cargo clippy --no-deps` substring alternative. It also matches pre-commit's `name:` banner, which is printed regardless of whether the entry actually executed (e.g. when the clippy toolchain component is missing). Anchor solely on cargo's own `Checking ...` output. - Skip-semantics module doc: the wording "returns Ok(())" was wrong; these are `#[test] fn ... ()` tests that `return;` early. Updated the doc-comment to describe the actual mechanism. - `cargo_fmt_check_hook_contract`: previously asserted that the four contract attributes appeared *somewhere* in the file. That would let the downstream fmt hook silently lose `pass_filenames: false` or `types: [rust]` without detection. Switched to a count-based assertion (`>= 2 occurrences` per attribute), so dropping one from either hook now trips the invariant. Tests: 11/11 (config) + 4/4 (e2e) pass. Reported by Copilot on PR #8.
- test(infra): AC4 manual-stage check now uses `pre-commit run --verbose` and asserts only on cargo's own `Checking <crate>` banner. Pre-commit swallows command stdout by default when a hook exits 0, so the prior version had to fall back to matching `cargo clippy --no-deps` — which is also the hook's `name:` in `.pre-commit-config.yaml` and would have appeared even if clippy never ran (missing toolchain component, etc.). With --verbose, cargo's banner is reliably visible whenever clippy actually executes; the substring fallback is removed. - test(infra): tighten `cargo_fmt_check_hook_contract` to require `language: system`, `pass_filenames: false`, and `types: [rust]` appear AT LEAST twice (once per fmt hook). A bare `contains` would silently accept a downstream hook that lost one of those keys, which would break that hook in ways the existing e2e (which only exercises `cargo-fmt-check`) wouldn't catch. - docs(test): clarify the skip-semantics doc-comment — the tests are plain `#[test] fn ... -> ()` that `return;` early when pre-commit is missing, not Result-returning functions. Cargo treats no-panic as pass, so the skip is invisible to the suite count. Reported by Copilot on PR #8. Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
The CI run of `cargo test --test pre_commit_e2e` on Linux failed AC4
because cargo's `Checking` banner came back with ANSI escape codes
glued to it (`Checking\x1b[0m e2e_root`), so the substring assertion
`output.contains("Checking ")` (with a trailing space) did not match.
`pre-commit run --color never` only controls pre-commit's own
colors, not what the underlying cargo subprocess emits. Set
`CARGO_TERM_COLOR=never` on the pre-commit env so cargo strips its
ANSI output, and tighten the substring to `Checking e2e_root` (the
seeded crate name) so the assertion fails clearly if anything other
than that specific compile happened.
Local cargo test --test pre_commit_e2e: 4/4 PASS.
Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
Two AC2 nits flagged on commit 8d19074: - AC2 dirty-tree behaviour: previously, the test ran `pre-commit run --all-files` even when `pre_clean` was false, and intentionally disabled the drift-recovery path in that case so a contributor's in-progress work wouldn't be `git checkout`-ed over. The side effect was that auto-fixer hooks (end-of-file-fixer, trailing-whitespace, mixed-line-ending) could still silently rewrite those in-progress changes — worse than failing. Skip with a clear stderr message instead; AC2 is about a main-equivalent CLEAN tree, not whatever transient state is in someone's worktree mid-edit. The post-run drift logic is now unconditional (no `if pre_clean { ... } else { None }`) since we early-return on dirty trees. - AC2 module doc-comment: said the test snapshots `git diff --quiet` before/after, but the implementation uses `git status --porcelain` + `git diff`. Update the wording to match the actual mechanism so future readers don't chase the wrong command when debugging drift. Tests: 11/11 (config) + 4/4 (e2e) pass; rustfmt clean. Reported by Copilot on PR #8.
Two nits flagged on commit bfb40f6: - `cargo_fmt_check_hook_contract` previously used global occurrence counts (`>= 2`) for `language: system` / `pass_filenames: false` / `types: [rust]`. That could be satisfied by the cargo-clippy hook (which also has `language: system` and `pass_filenames: false`), masking a silently dropped attribute on cargo-fmt-check-downstream. Now scopes the check per-hook: locate each `- id: cargo-fmt-check*` header, read up to the next `- id:`, assert each attribute appears inside the block. Drops the cargo-clippy false positive entirely. - CI workflow was running `pre-commit run --all-files --show-diff-on-failure` and then `cargo test --test pre_commit_e2e` immediately after — AC2 in that test calls `pre-commit run --all-files` on the same root, so the hook suite ran twice. AC2 also asserts the tree stays drift-free, which the standalone step did not, so AC2 strictly supersedes it. Removed the standalone step; updated the comment to explain the dedup. Tests: 11/11 (config) + 4/4 (e2e) pass; rustfmt clean. Reported by Copilot on PR #8.
Two of three nits flagged on commit 5a4e172: - AC3/AC4 toolchain-component probes: `prereqs_available()` only checked for `pre-commit` and `git`, but AC3 needs rustfmt (via `cargo fmt`) and AC4 needs clippy-driver (via `cargo clippy`). A contributor with `pre-commit` installed but missing rustfmt or clippy would hit a misleading hook failure. Added a `cargo_subcommand_available(name)` helper that probes `cargo <name> --version` with the cargo-augmented PATH, and gates AC3 / AC4 on it so they skip cleanly with a `rustup component add ...` hint. - `pre_commit_config.rs` module-doc invariant list: said `cargo fmt --check` had four contract attributes, but the test now checks three per-block (`language: system`, `pass_filenames: false`, `types: [rust]`) — `entry:` is the fourth attribute, asserted separately as an exact match. Reworded the bullet to call out both fmt hooks and to distinguish the per-block attributes from the separately-asserted entry line. The third nit (Copilot suggested making the e2e tests opt-in via `#[ignore]` or an env var to avoid pre-commit auto-downloading hook envs on first cargo-test run) is a design call. The reply on PR #8 explains why current skip-on-PATH-missing semantics is the right tradeoff for this project. Tests: 11/11 (config) + 4/4 (e2e) pass; rustfmt clean. Reported by Copilot on PR #8.
Clippy 1.93 added the `manual_contains` lint, which fires on the `entries.iter().any(|line| *line == "...")` pattern in `cargo_fmt_check_hook_contract`. Both `entries.contains(&"...")` and the `iter().any(|line| *line == ...)` form compile to the same loop, but `contains` is more idiomatic. No behaviour change; cargo test --test pre_commit_config still 11/11 green. Reported by clippy CI on PR #8. Signed-off-by: Jimbo Freedman <jimbo@spot-ship.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a pre-commit configuration that runs
cargo fmt --checkplus standard hygiene hooks on every commit, and exposescargo clippyon the manual stage. Includes contributor docs and machine-verified invariant + e2e tests so the hook contract cannot drift silently..pre-commit-config.yaml— twocargo fmt --checkhooks (root crate viacargo-fmt-check, nestedtests/downstream_consumersub-package viacargo-fmt-check-downstream, both on the default stage), a manual-stagecargo-clippyhook, and thepre-commit-hooksv5.0.0 hygiene baseline. Bothcargo fmtinvocations are direct entries (no shell), so the config works on Windows runners.CONTRIBUTING.mdupdated withpipx install pre-commit && pre-commit installinstructions (plus pip/brew alternatives) and a "before opening a PR" reminder.pre-commitinstallspre-commit==4.6.0and runscargo test --test pre_commit_e2e— AC2 inside that test invokespre-commit run --all-fileson the repo root and asserts the tree stays drift-free, so the standalonepre-commit runstep was unnecessary.tests/pre_commit_config.rs, 11 cases) lock the YAML shape including the no-shell requirement, the dual-hook structure, and per-hook attribute presence; e2e tests (tests/pre_commit_e2e.rs, 4 cases) exercise the hooks against ephemeral git repos and skip cleanly whenpre-commit,cargo fmt, orcargo clippyis absent.Acceptance criteria
pre-commit installsucceeds on a fresh clone. Verified by thepre-commitCI job, which performspip install pre-commit==4.6.0followed bycargo test --test pre_commit_e2e(whose AC2 case shells out topre-commitagainst the repo root).pre-commit run --all-filespasses on the current tree. Verified bytests/pre_commit_e2e.rs::ac2_pre_commit_run_all_files_passes_on_current_tree, which also fails if any hook silently rewrites files (auto-fixer with a zero exit), and skips cleanly when the worktree has uncommitted local changes.tests/pre_commit_e2e.rs::ac3_cargo_fmt_check_fails_on_misformatted_rust, which requires rustfmt'sDiff insignature in the output (no broad fallback) and skips with arustup component add rustfmthint if the toolchain component is absent.pre-commit run --hook-stage manualruns clippy (and clippy does NOT run on default stage). Verified bytests/pre_commit_e2e.rs::ac4_manual_stage_invokes_clippy(anchored on cargo's ownChecking ...banner — not pre-commit's hook-name banner, which would fire even if clippy never ran) andac4_clippy_does_not_run_on_default_stage(requires pre-commit's explicitNo hook with iddiagnostic plus a non-zero exit, and assertsChecking e2e_rootis absent).cargo test --test pre_commit_config(11/11 passing). The fmt-hook contract is now asserted per-hook (locate each- id: cargo-fmt-check*block, then check each attribute inside it) so a silently dropped attribute on either fmt hook trips the invariant.Test plan
cargo fmt --check— cleancargo clippy --no-deps -- -D warnings— cleancargo test --test pre_commit_config— 11/11 passpipx install pre-commit(orpip install pre-commit) thencargo test --test pre_commit_e2e— 4/4 passpre-commit run --all-files— all hooks greenClickUp
ENG-4687: https://app.clickup.com/t/86c9umm8t