Shared pre-commit hooks used across PinPredict
repositories. Hosted here so a fix or addition lands once and is picked up by
every consumer via a rev: bump rather than copying the same script into N
repos.
| id | What | Triggers on |
|---|---|---|
csharpier-worktree-guard |
Run dotnet csharpier format . (or check . via CSHARPIER_MODE=check) but fail loudly if csharpier reports "0 files" while the repo actually contains tracked .cs files. Catches the silent no-op observed when csharpier runs from inside a git worktree. |
*.cs |
service-yaml-check |
Static checks for new/changed .platform/services/<svc>.yaml files: chart path resolves, ECR repo / PIA / GHA push-role exist in TF, networkPolicy ingress ports match the chart's declared health port. Catches the post-merge failure modes from platform-gitops#544. |
.platform/services/*.yaml |
stevedore-release-scope |
Assert that onboarding or retiring a service does not widen the shared image build contract: named services pair an .stevedore.yaml image id with a name-matching sibling chart, docker-release and chart-release receive the same only: selector, and change_detection.shared_paths carries the all-image signal without listing paths every onboarding touches. |
.stevedore.yaml, .github/workflows/ci.yml, charts/*/Chart.yaml |
no-production-newtonsoft |
Reject Newtonsoft.Json references in production .NET sources: a case-insensitive scan of every tracked source and build file, permitted only under the --allow-prefix paths (the approved test/benchmark projects) and on the one central PackageVersion line. Static half only — the transitive package-graph half needs dotnet restore and stays in the consumer's CI. |
every commit (whole-tree scan) |
check-go-version-sync |
Fails when a go.mod go directive and the governing .tool-versions golang pin drift apart. |
go.mod, .tool-versions |
Reference this repo from a consumer's .pre-commit-config.yaml:
repos:
- repo: https://github.com/pinpredict/pre-commit-hooks
rev: v0.3.0 # bump to upgrade
hooks:
- id: csharpier-worktree-guardEach hook's parameters (files, args, etc.) can be overridden in the
consumer's config the same way as for any third-party hook repo.
Defaults to format mode. Switch to check (CI-friendly, no in-place
changes) via the CSHARPIER_MODE env var:
- id: csharpier-worktree-guard
args: []
# set in CI via env: CSHARPIER_MODE=checkRequires dotnet csharpier on PATH in the environment running pre-commit
(same requirement as the plain csharpier hook).
Stub. Currently the only fully-wired check is chart-path (resolves
repositories.chart against the consumer repo's tree). The remaining
checks emit a ! warning citing platform-gitops#544 and will be filled
in incrementally:
image-repo— verify the ECR repo exists in TF (or is added by a paired PR).pod-identity— verify a Crossplane PIA / TF entry exists for the SA.push-role— verifyxp-<svc>-gha-pushis declared in TF.netpol-ports— cross-check ingress ports against the chart's defaults.
Optional env vars (used by the not-yet-implemented checks):
| var | purpose |
|---|---|
PLATFORM_GITOPS_DIR |
path to a platform-gitops checkout, for PIA / Crossplane lookups |
PLATFORM_INFRA_DIR |
path to a platform-infrastructure checkout, for TF role / ECR lookups |
Wire into CI by adding to the consumer repo's .pre-commit-config.yaml
and running pre-commit run service-yaml-check --all-files in CI, or by
calling hooks/service-yaml-check.py <paths> directly.
Cut a tag when a hook changes:
git tag v0.2.0
git push origin v0.2.0Consumers don't move until they bump their rev: — keeps changes explicit
and revertable. Tags follow semver:
- patch — internal script change, behaviour unchanged
- minor — new hook added, or new optional flag on an existing hook
- major — breaking change to a hook's contract (entry, default mode, required env, etc.)
| Path | What |
|---|---|
pinpredict_hooks/ |
Python hooks, shipped as console scripts via pyproject.toml |
hooks/ |
Shell hooks (language: script), run directly from the repo |
tests/ |
Unit tests for the Python hooks — python -m unittest discover -s tests |
Python hooks must be console scripts. pre-commit's language: python
pip-installs this repo and then runs the hook's entry as a command, so a
repo-relative path entry (hooks/foo.py) cannot work. Every Python hook needs
an entry in [project.scripts] and an entry: matching that script name.
Getting this wrong fails at install time for every consumer with
Directory '.' is not installable — service-yaml-check shipped that way and
was never runnable anywhere until it was fixed. The hook-install CI job now
exercises the install path for exactly this reason.
Shell hooks stay in hooks/ with language: script; they need no packaging.
- Python: add a module under
pinpredict_hooks/with amain(argv=None) -> intand register it in[project.scripts]. Shell: drop the script inhooks/<hook-id>.shand uselanguage: script. - Add an entry to
.pre-commit-hooks.yamlwithid,name,description,entry,language,files, and any other relevant keys. See the pre-commit docs for the full schema. - Add tests under
tests/. Keep them file-based and free ofgit/subprocess work so the suite stays fast enough to run as a hook itself. - Update this README's "Available hooks" table.
- Open a PR. After merge, cut a tag.
Every assertion is opt-in via args, so the hook fits repos that have only some
of the surfaces — a repo with no chart-release job, or no
change_detection.shared_paths key, skips those checks instead of failing.
A repo with no .stevedore.yaml at all is a clean no-op.
- repo: https://github.com/pinpredict/pre-commit-hooks
rev: v0.3.0
hooks:
- id: stevedore-release-scope
args:
- --service=nadex-rfqgw
- --require-shared-path=Directory.Build.props
- --forbid-shared-path=Dockerfile
- --forbid-shared-path=.dockerignore
- --forbid-shared-path=*.sln| Flag | Purpose |
|---|---|
--service ID (repeatable) |
ID is an image id in .stevedore.yaml and charts/ID/Chart.yaml declares name: ID |
--require-shared-path P (repeatable) |
change_detection.shared_paths must contain P |
--forbid-shared-path P (repeatable) |
change_detection.shared_paths must not contain P |
--expect-selector EXPR |
both release jobs pass exactly this only: expression |
--docker-job / --chart-job |
job names to compare (default docker-release / chart-release) |
--catalog / --workflow / --charts-dir |
override the default paths |
Note that the manual-dispatch selector expression is not uniform across the
org — most repos map services: all to an empty selector, while trading maps
it to all on purpose. Pin --expect-selector only when a repo wants its own
variant frozen; the docker/chart consistency check runs either way.
All violations are collected and reported in one run rather than failing on the
first, so a single pre-commit run shows the whole picture.
Production .NET code should use System.Text.Json; a Newtonsoft.Json
reference is allowed only in explicitly approved test and benchmark projects.
Nothing is exempt by default — name every permitted prefix:
- repo: https://github.com/pinpredict/pre-commit-hooks
rev: v0.4.0
hooks:
- id: no-production-newtonsoft
args:
- --allow-prefix=PinPredict.Tests/
- --allow-prefix=PinPredict.ParlayManager.Tests/
- --allow-prefix=PinPredict.Benchmarks/
- --central-version-file=Directory.Packages.props| Flag | Purpose |
|---|---|
--allow-prefix PREFIX (repeatable) |
path prefix where a reference is permitted; nothing is allowed by default |
--central-version-file PATH |
the one file permitted to declare the package's central PackageVersion |
--token TOKEN |
case-insensitive token that marks a violation (default newtonsoft) |
--package ID |
package id for the central-version exemption (default <token>.Json) |
--source-glob GLOB (repeatable) |
git pathspecs to scan (default *.cs *.csproj *.props *.targets) |
--root PATH |
repository root to scan (default the working directory) |
This is the static half of the policy only. Catching Newtonsoft that arrives
transitively needs dotnet list package --include-transitive, which needs
dotnet restore and the solution's private feed credentials — far too slow and
too credential-bound for a commit hook. Keep that guard in your own CI beside
the SDK install; this hook covers the direct references, which are the ones a
commit actually introduces.
Two contract details worth knowing before you tune it:
- The scan is whole-tree, not staged-files. Files come from
git ls-filesand the hook runspass_filenames: false/always_run: true. The policy is a property of the repository, so a violation sitting in a file your commit never touched must still fail — scoping to changed files would let a pre-existing reference stay invisible forever. - The central-version exemption is scoped to one named file. A
PackageVersionelement copied into an ordinary project file is still a violation, so the exemption can't be used to smuggle a reference in.
Pre-commit hooks defined as repo: local in a .pre-commit-config.yaml are
quick to add but duplicate across repos, drift over time, and don't have a
clear ownership story. Once a hook fits more than one repo — or once it
encodes platform-wide policy (commit-message format, no raw-terraform,
account-id hardcoding) — it belongs here so a fix flows everywhere via a
version bump.
Fails when a go.mod go directive and the governing .tool-versions
golang pin drift apart — they must match so local (asdf) and CI build the
same toolchain. Each module's governing pin is the nearest ancestor
.tool-versions with a golang line, so nested modules
(apps/<svc>/go.mod) work; modules without a governing pin are skipped.
- repo: https://github.com/pinpredict/pre-commit-hooks
rev: v0.2.0
hooks:
- id: check-go-version-sync