web lint hook: run no check code a push reaches through a symlink or an earlier push

Merged
LA
3d ago
+1032-31

Summary

The pre-push web lint must run only check code that the push did not change (docs/contributor-verification.md, trust boundary). In some cases a push could get pushed code run by the hook. This PR closes them in scripts/web_lint_prepush.py:

  • Which changes count. The hook now measures changes to check code from where the pushed commit meets the reviewed branches (<remote>/feat-cmux-next, else the remote's HEAD). Before, it measured them from the branch's last pushed head. Now a change to check code that an earlier push of the branch made also keeps later pushes of that branch out. Deletes and type changes count, and so does a change to a parent folder of check code.

  • Symlinks. A pushed commit may hold no symlink at check code, inside it (outside the package source folders), or at a parent folder of it. The hook reads this from git before it plans anything.

  • Resolution before run. Before anything runs, every executed folder, and every file a package's check names, must resolve (symlinks followed) to that same path in the tree being linted. This also covers untracked files when the hook lints in place.

  • Reviewed remotes only. The base comes only from a reviewed remote: one whose URL is manaflow-ai/cmux, or one that the clone's own config names (cmux.reviewedRemote). It never comes from the remote being pushed to. With no reviewed remote, the hook fails closed.

  • Imported source files are check code. A file in src/ that check code loads (the check, fix and install scripts, the package's *.config.* files and everything they import) counts as check code. If the hook cannot tell what check code loads, it refuses the package.

  • Paths are compared as a Mac file system sees them, ignoring case and Unicode normalization.

  • Files in folders above a package are check code. Tools search upward from a package for their config, so every file directly in a folder above a package or its local build, and any node_modules there, is check code. In the last 410 web pushes, 2 changed such a file. A symlink at check code is allowed only when it points at another file that is itself check code (for example AGENTS.md to CLAUDE.md).

  • node_modules is check code. A path with a node_modules folder in it is check code for every package. A commit that tracks a node_modules in or above a package folder is never linted with that package. The real tree tracks none.

  • More config files count as check code. In web/, every ESLint and Babel config file name, plus .npmrc and bunfig.toml. In the TypeScript bindings, .npmrc and bunfig.toml.

A refused package is named in the push output. CI's sandboxed web lint job checks it, as before. Nothing changes for a normal push. Planning a push takes up to about 1.5 s on this tree, and about 2 s for a Swift-only push, which runs the full trust check for webviews before its four Swift-reading source checks (#34046).

No CI job, step or gate is added.

Testing

Each fix has its own red test commit before it. The tests run red on the commit before them and green on the head:

(cd tests && python3 -m unittest -k symlink -k later_push -k TrustBoundary test_web_lint_prepush)   # d5792039507, red on d7fe55f8612
(cd tests && python3 -m unittest -k fork -k no_reviewed_remote -k reviewed_remote test_web_lint_prepush)   # 7b53690c692, red on c67abc2c325
(cd tests && python3 -m unittest -k imports_is_check_code -k only_in_case -k tools_find_first -k install_config -k sees_every_case test_web_lint_prepush)   # 4d233dd2b4d, red on 54014ed3b66
(cd tests && python3 -m unittest -k folder_above -k folders_above -k regex_that -k built_up -k package_json_main -k fails_closed_when_git -k mac_file_system -k reviewed_remote_by test_web_lint_prepush)   # red on 643ef7ec491
(cd tests && python3 -m unittest -k pushed_node_modules -k node_modules_anywhere -k never_links_over test_web_lint_prepush)   # red on 559c0eb4152
  • Base: 7 failures and 6 errors. A marker script that only pushed content can reach ran, on a build host and on a host with CMUX_LOCAL_HEAVY_CHECKS=0.
  • Head: all pass, and the marker never runs.
  • test_the_real_check_code_has_no_symlink_and_resolves_to_itself shows that the real tree is not refused.

Observed / Not run

Observed:

  • python3 tests/test_web_lint_prepush.py: 89 tests OK.
  • Merged with feat-cmux-next, which holds #34046: its lone source-check pieces now pass the same trust checks as their package.
  • The last 12 feat-cmux-next commits and this head plan with no refusal.
  • python3 -m unittest test_cmux_next_fast_gates: 77 tests OK.
  • python3 scripts/cmux-next/prepush-gate.py: no new violations.
  • On this tree, check code loads 35 webviews modules (8 under src/), with no symlink and no import that the hook cannot follow.

Not run:

  • A real push through an installed hook on a Mac (the tests use the real git push and the installed hook in a throwaway repository).
  • A case-insensitive file system: the tests check the folded comparison, but this host's file system is case-sensitive.
  • The stacked runner PRs (#34046, #34060, #34114) are separate. This PR is based on feat-cmux-next.

Changelog

none

Proof

No capture: hook tooling only.