Skip to content

feat: no-production-newtonsoft hook, on shared YAML/reporting helpers - #5

Merged
blairham merged 2 commits into
mainfrom
feat/no-production-newtonsoft
Aug 13, 2026
Merged

feat: no-production-newtonsoft hook, on shared YAML/reporting helpers#5
blairham merged 2 commits into
mainfrom
feat/no-production-newtonsoft

Conversation

@blairham

Copy link
Copy Markdown
Contributor

Second half of the trading#3362 guard migration, stacked on #4. Consumer side is trading's chore/newtonsoft-static-scan-to-shared-hook, which pins rev: v0.4.0 — so this needs merging and tagging v0.4.0 before that branch can even resolve its hooks.

The split

The Newtonsoft policy has two halves with completely different costs:

Half Needs Where it goes
Static scan of tracked sources and build files reads files here, runs in ms on every commit
Transitive package-graph resolution dotnet restore + private feed creds stays in the consumer's CI

Only the static half is portable, and only the static half encodes an org convention — "production code uses System.Text.Json" is true everywhere, not just in trading. The graph half stays in scripts/ci/check_no_production_newtonsoft.py as a CI job.

no-production-newtonsoft

Case-insensitive scan for the token, exempt only under the prefixes named by --allow-prefix and on the single central PackageVersion line named by --central-version-file. --token and --source-glob are configurable, so this is not actually Newtonsoft-specific — it's a banned-dependency scanner with a default.

It enumerates via git ls-files and sets always_run: true / pass_filenames: false deliberately: a violation in a file the current commit did not touch still fails the tree. A staged-set-only scan would let a bad reference ride in on an unrelated commit and never be seen again.

Shared helpers

_common.py (private, not a console script) now holds the two behaviors every hook has to get identically right:

  • MISSING — distinguishes "file absent" from "file parsed to None". A hook must treat an absent surface as a clean no-op but an empty one as a real, checkable document; collapsing both to None loses that distinction.
  • load_yaml — a malformed document is fatal, not a skipped check. Silently passing on unparseable YAML is how a guard stops guarding without anyone noticing.
  • emit — GHA annotations under Actions, plain prefixes in a terminal.

stevedore_release_scope is refactored onto them, which is why this PR touches it. Its public functions keep their signatures, so the existing tests cover the refactor unchanged.

Verification

  • 26 tests green. The sharp cases are covered: a lookalike prefix is not exempt, the central-version exemption is scoped to the named file and still fails on a non-PackageVersion reference there, an untouched file still fails the whole-tree scan, and no --allow-prefix permits nothing.
  • Full dogfooded pre-commit run --all-files green.
  • hook-install job extended — the try-repo fixture now contains a tracked, clean production source, because with zero matching files the hook would pass without ever exercising the git enumeration path.
  • Run against trading's real tree with trading's exact args: clean across 1,289 files. The consumer handoff is proven before tagging, not after.

`stevedore-release-scope` grew a MISSING sentinel, a `load_yaml` that treats an
absent file as a clean no-op but a malformed one as fatal, and a GHA-aware
`emit`. All three are behavior every Python hook in this repo has to get
identically right, and the next hook needs them, so move them into a private
`pinpredict_hooks._common` rather than copy them.

`load_yaml` and `emit` take the caller's prog/label so their messages stay
attributed to the hook that failed. No behavior change; the existing
stevedore-release-scope tests cover the refactor.
Production .NET code should use System.Text.Json; a Newtonsoft.Json reference
belongs only in explicitly approved test and benchmark projects. trading
enforced this with a `repo: local` hook plus a second local hook running the
guard's own unit tests — duplicated policy that no other .NET repo could pick
up, which is exactly what this repo exists to fix.

Only the static half moves here. The transitive half resolves each project's
package graph with `dotnet list package --include-transitive`, which needs
`dotnet restore` and the solution's private feed credentials — too slow and too
credential-bound for a commit hook — so consumers keep that guard in their own
CI beside the SDK install. Direct references are the ones a commit introduces,
and they are now caught before it lands.

Nothing is exempt by default: every permitted path is named via --allow-prefix,
and the one file allowed to declare the package's central PackageVersion via
--central-version-file. The exemption is scoped to that named file, so a
PackageVersion element copied into an ordinary project file is still a
violation and cannot be used to smuggle a reference in.

Files come from `git ls-files` with pass_filenames: false, not from the staged
set. The policy is a property of the whole repository, so a violation sitting
in a file the commit never touched still fails; scoping to changed files would
let a pre-existing reference stay invisible forever.
@blairham
blairham merged commit 65f5cc3 into main Aug 13, 2026
3 checks passed
@blairham
blairham deleted the feat/no-production-newtonsoft branch August 13, 2026 22:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant