ci: run hawk on PRs and fail on findings the PR introduces

Closed
RO
1d ago
+396-59

What

Runs the cross-crate dead-code analysis set up in #36184 (hawk, hawk.toml) in GitHub Actions on every PR that touches Rust, so pub items a PR adds that nothing in the shipped binary reaches on any of the 11 targets (or that are more visible than needed) show up on the PR as a failed check with inline annotations, instead of waiting for the next manual sweep.

Why a base/PR comparison

main is not hawk-clean. A full run on 827475e reports 8784 diagnostics: 1222 dead_public, 855 unnecessary_public, 6701 unnecessary_restricted_visibility, plus 6 unknown_item config warnings (#36184 did the narrowing and left the dead-public deletions for later, and the tree has moved since). So -D warnings would be red on every PR. Instead the job analyzes the base commit (HEAD^1 of the merge commit) and the PR, and fails only on findings present in the PR and not in the base.

The comparison keys a finding on lint + crate + item path + item kind (counted, since cfg alternatives of one item produce one entry each). hawk's own identity.id embeds file:line, which would flag every finding below an unrelated edit as new. Moving or renaming an already-flagged item does count as new; that seemed acceptable for an advisory check.

Changes

  • .github/workflows/hawk.yml: clippy.yml's setup, then builds cargo-hawk, analyzes base and PR, and runs the comparison. Not added to merge_group; this is an advisory check, not a required one, so a PR that intentionally adds code ahead of its caller (stacked PRs) still shows red but stays mergeable. For code that is dead on purpose, an [[override]] with level = "expect" in hawk.toml silences it and hawk later reports the override as unfulfilled once the code gains a user.
  • scripts/rust-hawk.ts (bun run rust:hawk): the procedure from tools/hawk/README.md as a script, used locally and by CI. Applies tools/hawk/analysis-root.patch and an empty hawk_root.rs for the duration of the run and reverts them (the extra crate-type cannot live in the tree because it would disable fat LTO on the shipped staticlib). install builds cargo-hawk; diff base.json head.json does the comparison and prints new findings in rustc format. It operates on the current directory so CI can run the PR's copy of the script against the base checkout (which, for this PR, does not have the script yet).
  • The pinned rev is the bun branch of oven-sh/hawk (c9ca301). hawk.toml uses targets, cargo-profile and [[root-marker]], which upstream releases do not have (multi-target is astral-sh/hawk#150, still open), and the fork's one prebuilt release predates the last two. The driver links rustc internals, so the job builds it against the workspace's pinned nightly (rustc-dev component; about 15s). .github/workflows/CLAUDE.md gets the corresponding toolchain-bump step.
  • .github/rust-matcher.json: the first pattern only accepted error[E0xxx]:; hawk's codes look like error[hawk::dead_public]:. Widened to any bracketed code; rustc and clippy lines match exactly as before. The matcher is registered only for the comparison step so annotations never come from compiler output of either analysis.
  • hawk.toml: drops six overrides for bun_platform darwin::Category variants that #36833 deleted; hawk reports them as hawk::unknown_item today, and this job would have flagged the PR that added them.

Cost

One analysis is a cargo check of the workspace per target, 11 targets, production and test surfaces: 21.5 min on a 16-core box at an average of only 2.7 cores busy (the critical path is bun_runtime), so ubuntu-latest should not be dramatically slower. hawk re-instruments every workspace crate on each run, so the PR analysis does not reuse the base one; expect roughly 45 min per job, running alongside the Buildkite pipeline. Target dir is about 4 GB. If that turns out to be too much, caching the base report keyed by base sha would roughly halve it; left out of this PR.

Verification

  • Base run on 827475e (this branch's merge base): 8784 diagnostics, as above.

  • Appended an unused pub fn to src/csrf/lib.rs and re-ran through the script (scaffolding applied and reverted cleanly, JSON on stdout parsed). bun run rust:hawk diff on the two reports:

    error[hawk::dead_public]: `bun_csrf::hawk_negative_test_probe` (function) is pub but nothing reachable from the shipped binary uses it, on any target
      --> src/csrf/lib.rs:267:1
    
    hawk: 1 finding(s) introduced, 6 fixed (base 8784, head 8779)
    

    exit code 1. The "6 fixed" are the stale overrides removed in this PR (the second run used the updated hawk.toml), which also exercises config diagnostics flowing through the comparison. Identical reports, and reports differing only in line numbers, produce 0 introduced and exit 0.

  • The Hawk workflow run on this PR exercises the job itself (hawk.yml is in its own trigger paths); it should finish with 0 finding(s) introduced, 6 fixed.


no test proof · iteration 0 · docs-only change; test-proof not applicable