-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlefthook.yml
More file actions
170 lines (156 loc) · 8.08 KB
/
Copy pathlefthook.yml
File metadata and controls
170 lines (156 loc) · 8.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# Git hooks via lefthook, installed by `mise run install` (or `mise run hooks:install`).
#
# Every job goes through `mise exec` / `mise run`, so the tool that runs is the version this
# repo pins: ruff and commitizen from the uv venv reached through the mise-pinned `uv`,
# betterleaks from mise.lock. Never a bare `ruff` or `betterleaks`, whose resolution depends
# on whatever the committer's shell happens to have on PATH — a hook that lints with a
# different ruff than CI is a hook that passes work CI will refuse.
#
# The split:
# commit-msg Conventional Commits, so the history stays machine-readable.
# pre-commit fast and FILE-SCOPED — formatters that rewrite the staged set, plus the one
# scanner that gates rather than reports.
# pre-push the whole-repo gates a per-file pass structurally cannot run.
#
# `mise run test` IS in pre-push, unlike the reference this is ported from. Measured
# 2026-08-28: 816 tests in 131s, one tier, no containers and no network. That is inside the
# window where catching a break locally beats a CI round trip, and the parallel jobs run
# underneath it for free. Revisit if the suite grows an integration tier or passes ~5
# minutes.
#
# No hook reaches Bedrock. `atif-sql analyze` / `embed` / `search` bill per call, so they are
# absent here by construction — a hook that spends money on every push is a hook people
# disable.
#
# Bypass with `--no-verify` in a genuine emergency; CI still refuses.
min_version: 2.1.6
assert_lefthook_installed: true
glob_matcher: doublestar
output:
- meta
- summary
- failure
- execution_info
commit-msg:
jobs:
- name: commitizen
# commitizen already owns this gate and `[tool.commitizen]` already carries the
# escape set (`allowed_prefixes` = Merge / Revert / Pull request / fixup! / squash!),
# so a grep here would be a second, drifting copy of a rule that already has a home.
# Verified 2026-08-28 against this config: `chore(deps): ...` and `ci(deps): ...` pass
# and a bare `deps: ...` is REJECTED — which is why `.github/dependabot.yml` sets its
# `commit-message.prefix` to `chore` and `ci` rather than `deps`. A bot must be able to
# pass the hook its own config guarantees it will hit.
run: "mise exec -- uv run cz check --allow-abort --commit-msg-file {1}"
interactive: false
fail_text: "Commit message is not conventional — see https://www.conventionalcommits.org."
pre-commit:
parallel: true
jobs:
# ORDER IS THE POINT, and a `group` with `piped: true` is what enforces it: `ruff check
# --fix` can rewrite a line the formatter then re-wraps, so running them the other way
# round leaves the second rewrite unstaged. `piped` runs the group's jobs in order and
# stops at the first failure, while the group as a whole still runs alongside the two
# independent jobs below under the `parallel: true` above.
#
# A `priority:` key does NOT do this. Probed 2026-08-28 against lefthook 2.1.11:
# `lefthook validate` rejects `priority` inside `jobs` ("No values are allowed because the
# schema is set to 'false'") while `lefthook run pre-commit` accepts the config and ignores
# the key — so a `priority` here reads as an ordering guarantee and delivers none.
# `mise run lint:hooks` is the gate that catches that, and it exists because this happened.
- name: ruff
group:
piped: true
jobs:
- name: ruff-format
run: "mise exec -- uv run ruff format {staged_files}"
glob: "**/*.py"
stage_fixed: true
fail_text: "ruff format failed — check the error above."
- name: ruff-lint
run: "mise exec -- uv run ruff check --fix {staged_files}"
glob: "**/*.py"
stage_fixed: true
fail_text: "ruff check failed — fix the lint errors above, then re-stage."
- name: leak-staged
# Scans only the staged diff, which is what makes it fast enough for every commit; the
# full-history sweep is `mise run security:leaks` in CI. This is the ONE scanner that
# GATES rather than reports: a staged secret must never become a commit, and unlike the
# CI scans there is no SARIF upload here whose finding detail a non-zero exit would
# discard. `--redact` keeps the secret out of the terminal scrollback and out of any
# log the hook's output lands in.
run: "mise exec -- betterleaks git --staged --no-banner --redact"
fail_text: "A secret is staged. Remove it, rotate it, then re-stage."
- name: uv-lock-sync
# Cheap and file-scoped: a member's dependency edit that never reached uv.lock produces
# a clone that resolves something else entirely, and `mise run lock:check` in CI would
# only find it after the push.
run: "mise exec -- uv lock --check"
glob: "{pyproject.toml,packages/*/pyproject.toml,uv.lock}"
fail_text: "uv.lock is stale — run `uv lock` then re-stage."
pre-push:
parallel: true
jobs:
- name: forbid-local-only
# `local-only/*` branches are private working state and must never reach a remote.
# git hands pre-push one line per ref on stdin (`<local ref> <local oid> <remote ref>
# <remote oid>`); this reads that list and refuses the push if any ref is under
# refs/heads/local-only/. The blanket commands below never look at stdin, so without
# this job a `git push --all` publishes every branch, private or not. Bypassable with
# --no-verify like every hook, so the durable copy of anything private belongs in a
# bundle outside the clone, not in a branch this guard happens to protect.
# The `: {files}` no-op plus `files: echo lefthook.yml` below is what makes this job
# run at all. lefthook skips every pre-push job whose `git diff HEAD @{push}` list is
# empty (build_command.go, "no matching push files", probed 2026-08-30 in v2.1.11) —
# exactly the state of a clean checkout pushing a side branch, the one scenario this
# guard exists for. Watched happen: `git push --all --dry-run` from a clean main ran
# zero pre-push jobs and listed all five private branches. The only escape that check
# leaves is a `run` referencing {files} with a non-empty substitution, and the
# substitution passes through git.FindExistingFiles, so the constant must name a file
# that exists — this one.
run: |
: {files}
while read -r local_ref _rest; do
case "$local_ref" in
refs/heads/local-only/*)
echo "refusing to push private ref: $local_ref" >&2
exit 1
;;
esac
done
use_stdin: true
files: "echo lefthook.yml"
fail_text: "local-only/* refs are private and must never be pushed."
- name: lint
run: mise run lint
- name: typecheck
# ty only. pyright's strict baseline is non-zero on purpose (see
# `mise run typecheck:pyright`), so gating a push on it would gate on work nobody has
# done yet.
run: mise run typecheck
skip:
- merge
- rebase
- name: lint-imports
# The layer DAGs and the converter/corpus/duck/models/embed independence contract. A
# per-file hook cannot see an import graph, which is why this is here and not above.
run: mise run lint:imports
skip:
- merge
- rebase
- name: lint-workflows
# actionlint over .github/workflows. A broken workflow only fails on GitHub, where the
# feedback costs a push and a wait, so it is worth 200ms here.
run: mise run lint:workflows
- name: lint-hooks
# `lefthook validate` over this file. lefthook ignores a key its schema rejects rather
# than refusing to run, so a hook that no longer does what it says keeps passing until
# something asks. This is that something, and it is here rather than in pre-commit
# because a config edit is not file-scoped work.
run: mise run lint:hooks
- name: test
run: mise run test
skip:
- merge
- rebase
fail_text: "pytest failed — run `mise run test` locally before pushing."