@biffo/cli 0.239.0 → 0.241.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,8 +40,13 @@ cd "$root" || exit 0
40
40
  # cannot run, that is a reason to stop, not to wave the push through.
41
41
  sh scripts/biffo.sh rewrite-scope-check || exit 1
42
42
 
43
- if [ ! -f scripts/verify.sh ]; then
44
- echo "pre-push: no scripts/verify.sh NO checks ran (see local-gates.md)" >&2
45
- exit 0
46
- fi
47
- exec sh scripts/verify.sh
43
+ # Through the version-pinned CLI since #1109, so a repo cannot run a gate two
44
+ # versions old -- eight of them did, and tabsii-crm checked ONE thing in eight
45
+ # on a 700-line change and printed `verify passed` (#855). There is one copy of
46
+ # verify.sh now, and which version a repo runs is its .biffo-shared-version.
47
+ #
48
+ # The branch this replaces printed "NO checks ran" and **exited 0**: a repo
49
+ # without the file passed its pre-push having checked nothing, which is the
50
+ # same defect one level up from the one #855 recorded. A gate that cannot run
51
+ # is not a gate that passed.
52
+ exec sh scripts/biffo.sh verify
@@ -40,8 +40,13 @@ cd "$root" || exit 0
40
40
  # cannot run, that is a reason to stop, not to wave the push through.
41
41
  sh scripts/biffo.sh rewrite-scope-check || exit 1
42
42
 
43
- if [ ! -f scripts/verify.sh ]; then
44
- echo "pre-push: no scripts/verify.sh NO checks ran (see local-gates.md)" >&2
45
- exit 0
46
- fi
47
- exec sh scripts/verify.sh
43
+ # Through the version-pinned CLI since #1109, so a repo cannot run a gate two
44
+ # versions old -- eight of them did, and tabsii-crm checked ONE thing in eight
45
+ # on a 700-line change and printed `verify passed` (#855). There is one copy of
46
+ # verify.sh now, and which version a repo runs is its .biffo-shared-version.
47
+ #
48
+ # The branch this replaces printed "NO checks ran" and **exited 0**: a repo
49
+ # without the file passed its pre-push having checked nothing, which is the
50
+ # same defect one level up from the one #855 recorded. A gate that cannot run
51
+ # is not a gate that passed.
52
+ exec sh scripts/biffo.sh verify
package/dist/index.js CHANGED
@@ -10842,6 +10842,13 @@ var gateCoverageCommand = packagedScriptCommand({
10842
10842
  description: "Report how much of CI the local gate actually mirrors"
10843
10843
  });
10844
10844
 
10845
+ // src/commands/verify.ts
10846
+ var verifyCommand = packagedScriptCommand({
10847
+ name: "verify",
10848
+ script: "scripts/verify.sh",
10849
+ description: "Run the checks CI runs, before the push"
10850
+ });
10851
+
10845
10852
  // src/commands/branch-health.ts
10846
10853
  var branchHealthCommand = packagedScriptCommand({
10847
10854
  name: "branch-health",
@@ -10881,6 +10888,7 @@ program.addCommand(teardownCommand);
10881
10888
  program.addCommand(waitForChecksCommand);
10882
10889
  program.addCommand(branchHealthCommand);
10883
10890
  program.addCommand(claimCommand);
10891
+ program.addCommand(verifyCommand);
10884
10892
  program.addCommand(gateCoverageCommand);
10885
10893
  program.addCommand(hookAuditCommand);
10886
10894
  program.addCommand(pgTestDbCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.239.0",
3
+ "version": "0.241.0",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -30,7 +30,8 @@
30
30
  "scripts/hook-audit.sh",
31
31
  "scripts/pg-test-db.sh",
32
32
  "scripts/rewrite-scope-check.sh",
33
- "scripts/gate-coverage.sh"
33
+ "scripts/gate-coverage.sh",
34
+ "scripts/verify.sh"
34
35
  ],
35
36
  "scripts": {
36
37
  "build": "tsup src/index.ts --format esm --dts --clean",
@@ -1,978 +0,0 @@
1
- #!/usr/bin/env sh
2
- #
3
- # Run the checks CI runs, here, before the push that would have found them.
4
- #
5
- # ## Why this exists
6
- #
7
- # Until 2026-07-29 the only local gate was a whole-project `pyright` in a
8
- # pre-push hook -- in the three repos that had hooks at all. Everything else
9
- # (eslint, prettier, tsc, vitest, ruff, terraform fmt, the plugin guards) ran for
10
- # the first time on a GitHub runner, after a push, after a PR, after the merge
11
- # race.
12
- #
13
- # Over the 30 days to 2026-07-29, across the twelve repos in the estate that run
14
- # CI: 373 failed runs, and 211 of 342 failing steps (62%) were locally
15
- # catchable -- deterministic, offline, no credentials. By kind: tests 49,
16
- # format 53, typecheck 20, lint 16, terraform fmt 12, and the core ownership
17
- # guard 11 -- a check already wired as a commit hook, being discovered in the
18
- # pipeline because the hook was not running.
19
- #
20
- # The same file failed the same check across consecutive runs -- e.g.
21
- # services/api/src/api/routing/crud_handlers.py failing `ruff format --check` on
22
- # four separate runs. That is the signature of a round trip being used as the
23
- # check: push, wait for CI, read the failure, fix, push again.
24
- #
25
- # ## Why it adapts instead of being tailored
26
- #
27
- # This one file runs in the template, in instances, in sibling apps and in
28
- # plugin repos, whose CI check sets differ. It could have been forked per repo;
29
- # forks drift, and a gate that has drifted from CI reports a green CI will not
30
- # honour.
31
- #
32
- # So every check is conditional on the repo actually having it, and an
33
- # inapplicable check prints `n/a` rather than being silently absent. Absence and
34
- # inapplicability look identical in a summary that omits both, and telling them
35
- # apart is the entire point (docs/practices/standards/local-gates.md).
36
- #
37
- # ## What is deliberately excluded
38
- #
39
- # - pytest -- 56s in the template, more than the rest of the gate combined,
40
- # and it failed once there in 30 days. Opt in per repo with
41
- # BIFFO_VERIFY_PYTEST=1 where the suite is fast.
42
- # - app/portal build -- a full Next build.
43
- # - dependency audits, pip-audit, pnpm audit -- network.
44
- # - gitleaks HISTORY pass -- genuinely scans git history, which a pre-push gate
45
- # cannot usefully anticipate.
46
- #
47
- # The gitleaks WORKING-TREE pass is no longer excluded (#897). The old reason
48
- # here read "gitleaks -- scans history, not the working tree", which was false:
49
- # ci.yml runs two passes, and the second is `gitleaks detect --no-git`, i.e.
50
- # exactly the working tree, which is what a pre-push gate is for. An exclusion
51
- # must describe what the CI step actually DOES -- the same defect as the bandit
52
- # exclusion that claimed "the finding gate is the upload step" (#855).
53
- #
54
- # cli/src/lib/verify-parity.test.ts fails if the template's CI grows a check
55
- # that is neither here nor in that written exclusion list.
56
- #
57
- # Usage:
58
- # sh scripts/verify.sh # everything applicable to this repo
59
- # sh scripts/verify.sh --list # print the checks it WOULD run, and stop
60
- # pnpm run verify # same
61
- # BIFFO_SKIP_VERIFY=1 git push # escape hatch, for when you mean it
62
- #
63
- # `--list` exists so parity with CI can be tested against what this script
64
- # actually does, rather than against its source text. The checks are assembled
65
- # at runtime from what the repo has, so grepping the file for `pnpm run lint`
66
- # proves nothing -- and a parity test that can be satisfied by a comment is not
67
- # a parity test.
68
- #
69
- # `--list` reports what THIS REPO requires, deliberately ignoring whether the
70
- # tooling happens to be installed here. Parity with CI is a property of the
71
- # repository; "can this machine run it" is a property of the machine. Conflating
72
- # them made the parity test pass locally and fail on a CI runner that has no
73
- # `uv` or `terraform` -- the gate-green/CI-red split this whole exercise exists
74
- # to remove, reproduced inside its own guard.
75
-
76
- set -u
77
-
78
- LIST=""
79
- [ "${1:-}" = "--list" ] && LIST=1
80
-
81
- # Checkout health -- is the tree a command is about to trust stale or dirty in
82
- # the ONE place that matters: the PRIMARY checkout. (#1196)
83
- #
84
- # AGENTS.md SS1/SS2 already say the primary must stay on the integration
85
- # branch, no more than a `git fetch` behind, and that real work happens in
86
- # worktrees instead -- but nothing checked it, and it happened again. An agent
87
- # asked to migrate tabsii-crm's `auth.ts` read an 80-line file exporting five
88
- # extra functions and built a whole, internally consistent, WRONG analysis on
89
- # it: `origin/dev` held 36 lines and one export. The primary checkout it read
90
- # was 16 commits behind, 1 ahead, dirty. The agent's reasoning was sound; its
91
- # input was not, and nothing said so.
92
- #
93
- # This does not naturally belong to a PRE-PUSH file -- the incident above
94
- # never touched git at all, it was a pure read, so a gate that only runs at
95
- # push time could never have caught it by running later. It is here anyway,
96
- # not in `scripts/hook-audit.sh` as the issue itself proposed, for a
97
- # mechanical reason recorded rather than argued away: #1194 (gitleaks scope)
98
- # and this were required to land in the same PR, in this file. Two things
99
- # follow from that:
100
- #
101
- # `--checkout-health` is the part meant to answer #1196's actual question --
102
- # run it BEFORE trusting a read, standalone, any time:
103
- # sh scripts/verify.sh --checkout-health
104
- #
105
- # The WARN folded into the ordinary run further down is defence in depth for
106
- # the one case an ordinary PUSH-TIME run can still see: a push attempted
107
- # FROM the primary, which SS1 forbids outright regardless of staleness.
108
- # It does not, and cannot, cover the read-only case the issue was filed for.
109
- #
110
- # Scope is deliberately narrow. A WORKTREE being behind `origin/dev` is normal
111
- # mid-work -- that is what branching for a unit of work looks like -- so this
112
- # never evaluates one. It only evaluates the ONE checkout AGENTS.md says must
113
- # always mirror the integration branch: a non-worktree working tree whose
114
- # CURRENT branch IS that branch. A worktree, a detached HEAD, or some other
115
- # branch checked out at a repo root are all out of scope, and reported as
116
- # such (`n/a`) rather than silently read as healthy for a question they were
117
- # never asked.
118
- CHECKOUT_HEALTH_BRANCH="${BIFFO_INTEGRATION_BRANCH:-dev}"
119
-
120
- # A linked worktree's git-dir lives under the primary's `.git/worktrees/`; the
121
- # primary's own git-dir does not. That is the one property distinguishing them
122
- # regardless of where either happens to be checked out on disk.
123
- _is_linked_worktree() {
124
- case "$(git rev-parse --git-dir 2>/dev/null)" in
125
- */worktrees/*) return 0 ;;
126
- *) return 1 ;;
127
- esac
128
- }
129
-
130
- # Verdict in CHECKOUT_HEALTH_VERDICT: healthy | stale | n/a | detached | unknown.
131
- # Detail (the "why", for stale/unknown) in CHECKOUT_HEALTH_DETAIL.
132
- #
133
- # Sets globals rather than echoing the verdict for `$(...)` capture on
134
- # purpose: `_v=$(checkout_health)` runs the function in a SUBSHELL, and any
135
- # variable it assigns -- CHECKOUT_HEALTH_DETAIL included -- dies with that
136
- # subshell. The first version did exactly that and every non-trivial verdict
137
- # crashed the caller under `set -u` reading a detail that command substitution
138
- # had already thrown away. Caught by the fail-first rehearsal below, not by
139
- # the happy-path `healthy` case, which never touches DETAIL and looked fine.
140
- checkout_health() {
141
- CHECKOUT_HEALTH_VERDICT=""
142
- CHECKOUT_HEALTH_DETAIL=""
143
- if _is_linked_worktree; then
144
- CHECKOUT_HEALTH_VERDICT="n/a"
145
- return 0
146
- fi
147
- _ch_branch=$(git symbolic-ref --short -q HEAD) || {
148
- CHECKOUT_HEALTH_VERDICT="detached"
149
- return 0
150
- }
151
- if [ "$_ch_branch" != "$CHECKOUT_HEALTH_BRANCH" ]; then
152
- CHECKOUT_HEALTH_VERDICT="n/a"
153
- return 0
154
- fi
155
- _ch_dirty=""
156
- [ -n "$(git status --porcelain 2>/dev/null)" ] && _ch_dirty=1
157
-
158
- if ! git remote get-url origin >/dev/null 2>&1; then
159
- CHECKOUT_HEALTH_VERDICT="unknown"
160
- CHECKOUT_HEALTH_DETAIL="no 'origin' remote configured"
161
- return 0
162
- fi
163
-
164
- # A fetch may be the only way to know staleness -- do not hard-fail an
165
- # offline machine over a question it structurally cannot answer. Bounded so
166
- # a dead network cannot hang the gate the way an unbounded fetch could; a
167
- # stale-but-cached remote-tracking ref is still evidence, just older.
168
- _ch_fetched=1
169
- timeout 10 git fetch --quiet origin "$CHECKOUT_HEALTH_BRANCH" 2>/dev/null || _ch_fetched=""
170
- if [ -z "$_ch_fetched" ] && ! git rev-parse -q --verify "origin/$CHECKOUT_HEALTH_BRANCH" >/dev/null 2>&1; then
171
- CHECKOUT_HEALTH_VERDICT="unknown"
172
- CHECKOUT_HEALTH_DETAIL="could not reach origin, and no cached origin/$CHECKOUT_HEALTH_BRANCH to fall back on"
173
- return 0
174
- fi
175
-
176
- _ch_behind=$(git rev-list --count "HEAD..origin/$CHECKOUT_HEALTH_BRANCH" 2>/dev/null || echo 0)
177
- _ch_ahead=$(git rev-list --count "origin/$CHECKOUT_HEALTH_BRANCH..HEAD" 2>/dev/null || echo 0)
178
-
179
- if [ -n "$_ch_dirty" ] || [ "${_ch_behind:-0}" -gt 0 ] || [ "${_ch_ahead:-0}" -gt 0 ]; then
180
- CHECKOUT_HEALTH_VERDICT="stale"
181
- _ch_bits=""
182
- [ "${_ch_behind:-0}" -gt 0 ] && _ch_bits="$_ch_bits ${_ch_behind} behind"
183
- [ "${_ch_ahead:-0}" -gt 0 ] && _ch_bits="$_ch_bits ${_ch_ahead} ahead"
184
- [ -n "$_ch_dirty" ] && _ch_bits="$_ch_bits dirty"
185
- CHECKOUT_HEALTH_DETAIL="${_ch_bits# }"
186
- return 0
187
- fi
188
-
189
- CHECKOUT_HEALTH_VERDICT="healthy"
190
- return 0
191
- }
192
-
193
- # Standalone entry point. Exits before any of the slower checks below run --
194
- # answering "can I trust this tree" is the whole job, not a side effect of a
195
- # full gate run. FAILS CLOSED on a confirmed-stale tree (exit 1) and on a tree
196
- # it could not evaluate (exit 2) -- "cannot tell" is never a pass, the same
197
- # convention `wait-for-checks.sh` and `branch-health.sh` already use for
198
- # exactly this reason. It does not fail merely for BEING the primary: a clean,
199
- # up-to-date primary on the integration branch is the correct, expected state,
200
- # not a violation -- only staleness and dirt are.
201
- if [ "${1:-}" = "--checkout-health" ]; then
202
- checkout_health
203
- case "$CHECKOUT_HEALTH_VERDICT" in
204
- healthy)
205
- printf 'checkout-health: OK - on %s, clean, matches origin/%s\n' \
206
- "$CHECKOUT_HEALTH_BRANCH" "$CHECKOUT_HEALTH_BRANCH"
207
- exit 0
208
- ;;
209
- n/a)
210
- printf 'checkout-health: n/a - not the primary checkout parked on %s (worktree, detached HEAD, or another branch)\n' \
211
- "$CHECKOUT_HEALTH_BRANCH"
212
- exit 0
213
- ;;
214
- detached)
215
- printf 'checkout-health: n/a - detached HEAD, not evaluated\n'
216
- exit 0
217
- ;;
218
- stale)
219
- printf 'checkout-health: STALE - this checkout is %s\n' "$CHECKOUT_HEALTH_DETAIL"
220
- printf 'AGENTS.md SS1/SS2: keep the primary on %s, no more than a git fetch\n' "$CHECKOUT_HEALTH_BRANCH"
221
- printf 'behind, and do real work in a worktree instead. Do not trust anything\n'
222
- printf 'read from this tree until: git fetch origin && git status\n'
223
- exit 1
224
- ;;
225
- unknown)
226
- printf 'checkout-health: CANNOT TELL - %s\n' "$CHECKOUT_HEALTH_DETAIL"
227
- printf 'Not a pass -- reconnect and re-run before trusting this tree.\n'
228
- exit 2
229
- ;;
230
- *)
231
- printf 'checkout-health: CANNOT TELL - unexpected verdict "%s"\n' "$CHECKOUT_HEALTH_VERDICT"
232
- exit 2
233
- ;;
234
- esac
235
- fi
236
-
237
- FAILED=""
238
- PASSED=""
239
- SKIPPED=""
240
- # Checks that were APPLICABLE and did not run. Kept apart from SKIPPED because
241
- # the summary must not print "not applicable here: pg-test" about a lane this
242
- # repo demonstrably has -- absence and blindness reading identically is the
243
- # defect, not a formatting nit.
244
- NOT_RUN=""
245
- # Defined up here, not inside run_check. `run_check` returns EARLY in --list
246
- # mode, before it would set this -- so `pytest_record "$d" "$LAST_CHECK_SECONDS"`
247
- # read an unset variable and `set -u` killed the script silently, mid-list.
248
- #
249
- # The damage was invisible and downstream: gate-coverage.sh reads --list, so a
250
- # truncated list looked like MISSING COVERAGE. tabsii-geo dropped from 8/8 to
251
- # 4/8 -- and only in repos where a pytest measurement already existed, i.e. only
252
- # after the gate had run there once. A defect that appears on second use is the
253
- # hardest kind to attribute.
254
- LAST_CHECK_SECONDS=""
255
-
256
- # pytest is included where the suite is FAST ENOUGH TO PAY, measured rather than
257
- # opted into (#869, H5 gap 4).
258
- #
259
- # The old rule was a blanket exclusion with a manual opt-in nobody ever issued,
260
- # so the fastest suites in the estate were the ones not being run:
261
- #
262
- # tabsii-marketplace 1.7s tabsii-geo 2.1s tabsii-intake 2.5s tabsii-crm 2.7s
263
- # biffo-template 51.2s biffo-platform 57.4s tabsii-platform 85.6s
264
- #
265
- # The exclusion was right for the three repos it was written against and wrong
266
- # for the four it was applied to. The arithmetic: ~2.5s on every push against a
267
- # ~14 min sibling CI round trip to discover and confirm a Python test failure --
268
- # break-even at one catch per 336 pushes, against an observed rate of roughly
269
- # one per 165.
270
- #
271
- # The command is plain `pytest -q`, matching CI. `--no-cov` looked like a free
272
- # speed-up and is a **pytest-cov flag**: repos without that plugin -- e.g.
273
- # biffo-plugin-ideation, whose CI runs `uv run pytest -q` -- reject it outright
274
- # with `unrecognized arguments`. That is the gate failing where CI passes, which
275
- # H5 pre-registered as a condition that refutes it, and it was caught during
276
- # rollout rather than by review.
277
- #
278
- # BIFFO_VERIFY_PYTEST overrides in BOTH directions: 1 forces it in, 0 forces it
279
- # out. An override that only forces on would leave no way to escape a suite that
280
- # has quietly grown past the threshold.
281
- PYTEST_BUDGET_SECONDS="${BIFFO_VERIFY_PYTEST_BUDGET:-15}"
282
- # How long a measurement is trusted before being re-taken. Only ever matters for
283
- # a `slow` verdict; a `fast` one is re-measured by every run that uses it.
284
- PYTEST_MAX_AGE_DAYS="${BIFFO_VERIFY_PYTEST_MAX_AGE_DAYS:-7}"
285
- PYTEST="${BIFFO_VERIFY_PYTEST:-}"
286
-
287
- # Cached per directory, because timing the suite to decide whether to run the
288
- # suite would cost exactly what it is trying to save. The cache lives with the
289
- # repo, not in $HOME, so it cannot leak a fast verdict from one repo to another.
290
- # Where the measurement lives.
291
- #
292
- # NOT in the working tree. The first version wrote `$_d/.pytest-duration` and
293
- # was gitignored in biffo-template only -- .gitignore is not a synced file, so
294
- # every other repo in the estate grew an untracked `?? services/api/.pytest-duration`
295
- # the moment the gate ran. A cache that dirties `git status` in fifteen repos is
296
- # a defect regardless of what it caches.
297
- #
298
- # The git common dir is outside every working tree, shared by all worktrees of a
299
- # clone (they run the same suite), and cannot be committed by accident. Keyed by
300
- # the directory measured, so a root suite and services/api do not collide.
301
- pytest_cache_file() {
302
- _cd=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)/biffo-verify
303
- mkdir -p "$_cd" 2>/dev/null || true
304
- printf '%s/pytest-%s' "$_cd" "$(printf '%s' "$1" | tr '/.' '__')"
305
- }
306
-
307
- # Record what a real run actually took. The gate runs pytest whenever it believes
308
- # the suite is fast, so it observes the true duration every single time -- and
309
- # throwing that away was the whole bug. A suite that grows past the budget now
310
- # excludes itself on the next push, for free and exactly.
311
- pytest_record() {
312
- [ -n "${2:-}" ] || return 0
313
- printf '%s\n' "$2" > "$(pytest_cache_file "$1")" 2>/dev/null || true
314
- }
315
-
316
- pytest_is_fast() {
317
- _d="$1"
318
- _cache=$(pytest_cache_file "$_d")
319
- # Age matters in ONE direction. A `fast` verdict is re-confirmed by every run
320
- # (pytest_record), so it cannot go stale. A `slow` verdict is never re-tested,
321
- # because the whole point of it is that the suite does not run -- so a suite
322
- # that has since been split or sped up stays excluded for ever. Expiry is what
323
- # gives it a way back in.
324
- if [ -f "$_cache" ] && [ -n "$(find "$_cache" -mtime "-$PYTEST_MAX_AGE_DAYS" 2>/dev/null)" ]; then
325
- _secs=$(cat "$_cache" 2>/dev/null)
326
- elif [ -f "$_cache" ] && [ -n "$LIST" ]; then
327
- # Expired, and --list must not run a suite to answer a question. Use the
328
- # stale value rather than guessing: it is evidence, just old.
329
- _secs=$(cat "$_cache" 2>/dev/null)
330
- elif [ -n "$LIST" ]; then
331
- # --list must not run a test suite to answer a question about the repo, so
332
- # with no cached measurement it has to guess -- and the direction of the
333
- # guess is the whole decision.
334
- #
335
- # Guessing "fast" makes --list CLAIM a check the gate may not run, which is
336
- # the fail-open direction and exactly what this tooling exists to eliminate.
337
- # Guessing "slow" makes it under-report a check the gate does run: visible,
338
- # conservative, and self-correcting, because the first real run writes the
339
- # measurement and every --list after that is exact.
340
- #
341
- # Both directions were tried. Under-reporting is the one that cannot lie
342
- # about coverage.
343
- _secs=99999
344
- else
345
- # First run in a repo: time it once, then decide from then on. A timeout
346
- # means "too slow", which is the correct verdict rather than a hang.
347
- _start=$(date +%s)
348
- if [ "$_d" = "." ]; then
349
- timeout "$((PYTEST_BUDGET_SECONDS * 4))" uv run pytest -q >/dev/null 2>&1 || true
350
- else
351
- timeout "$((PYTEST_BUDGET_SECONDS * 4))" uv run --directory "$_d" pytest -q >/dev/null 2>&1 || true
352
- fi
353
- _secs=$(($(date +%s) - _start))
354
- printf '%s\n' "$_secs" > "$_cache" 2>/dev/null || true
355
- fi
356
- [ "${_secs:-9999}" -le "$PYTEST_BUDGET_SECONDS" ]
357
- }
358
-
359
- # Does THIS repo's CI run a check of this kind?
360
- #
361
- # ## Why every check is gated on this (#861)
362
- #
363
- # The standard has said "derived per repo from that repo's ci.yml, not decreed"
364
- # since it was written. The gate did not do that: it ran a fixed list, and the
365
- # list was tuned against biffo-template. Every consequence was the same shape,
366
- # three times in one afternoon:
367
- #
368
- # - terraform-fmt over infra/ where CI checks only modules/
369
- # - bandit over -r services where CI scans only template-owned paths
370
- # - bandit at all in the plugin repos, whose CI has no bandit step and where
371
- # the tool is not even installed
372
- #
373
- # A gate STRICTER than CI is not a safer gate. It blocks correct work, sends
374
- # people to read failures CI would never raise, and is exactly what drives
375
- # BIFFO_SKIP_VERIFY -- a counter-metric H4 pre-registered as refuting itself.
376
- #
377
- # With no ci.yml there is nothing to mirror, so everything applicable runs:
378
- # best-effort beats silence in a repo that has no pipeline to disagree with.
379
- # NO_CI is set once, up front, so the two states this predicate conflates stay
380
- # distinguishable to the reader even though it answers the same for both (#942).
381
- # "Yes, CI runs this" and "there is no CI to ask" are not the same claim, and a
382
- # repo that LOST its ci.yml must not read as maximally covered. The summary
383
- # below says which one produced the run.
384
- [ -f .github/workflows/ci.yml ] || NO_CI=1
385
-
386
- ci_has() {
387
- [ -n "${NO_CI:-}" ] && return 0
388
- grep -qE "$1" .github/workflows/ci.yml
389
- }
390
-
391
- have_script() {
392
- [ -f "$2/package.json" ] || return 1
393
- # grep rather than node: --list must work on a machine with no toolchain at
394
- # all, because what it reports is a property of the repo, not of the machine.
395
- # Deliberately NOT anchored to line start: that only matches a pretty-printed
396
- # package.json, and a minified one would silently report "no lint script" --
397
- # a skip that looks like a considered decision. A false positive here costs a
398
- # loud `pnpm run` failure; a false negative costs an unchecked push.
399
- grep -qE "\"$1\"[[:space:]]*:" "$2/package.json"
400
- }
401
-
402
- # Every directory holding a JS package this repo owns.
403
- #
404
- # A repo with a root package.json is a workspace: `turbo run lint` fans out and
405
- # running per-package as well would double the work. A repo WITHOUT one keeps
406
- # its JS in subdirectories -- web/ and web-admin/ in the plugin repos,
407
- # apps/frontend/ in the siblings -- and their CI runs the same scripts there
408
- # with `working-directory:`.
409
- #
410
- # ## Why this exists (#852)
411
- #
412
- # The gate used to check the repo root and nothing else. In the ten repos with
413
- # no root package.json -- every plugin, every sibling, both runner repos -- it
414
- # printed `javascript n/a - no package.json in this repo` and then
415
- # `verify passed`, on repos whose entire frontend is JS. A 100% TypeScript
416
- # change pushed green with zero JavaScript verification.
417
- #
418
- # That is worse than the missing hooks this gate was built to fix. A repo with
419
- # no hooks makes no claim; this one claimed to have checked. And the standard
420
- # it was written to enforce says exactly that inapplicable and absent must not
421
- # look the same -- while reporting "not applicable" for the language the change
422
- # was written in.
423
- # Every directory holding a Python project this repo owns.
424
- #
425
- # ## Why this exists (#855)
426
- #
427
- # #853 fixed this for JavaScript and left Python with the identical bug. The
428
- # check was `[ -f pyproject.toml ]` — root only. Every sibling keeps its API at
429
- # `services/api/pyproject.toml`, so ruff, ruff-format and pyright were skipped
430
- # entirely, and #853's own rationale applies verbatim: a change pushed green
431
- # with zero verification of the language it was written in.
432
- #
433
- # Found by an agent whose 700-line TypeScript-and-Python change to tabsii-crm
434
- # ran exactly one check — terraform-fmt — and printed `verify passed`.
435
- py_dirs() {
436
- if [ -f pyproject.toml ]; then
437
- echo "."
438
- return
439
- fi
440
- find . -name pyproject.toml \
441
- -not -path "*/node_modules/*" -not -path "*/.venv/*" -not -path "*/dist/*" \
442
- -not -path "*/.worktrees/*" -not -path "*/.terraform/*" -not -path "*/vendor/*" \
443
- -not -path "*/site-packages/*" 2>/dev/null |
444
- sed 's|/pyproject.toml$||' | sort
445
- }
446
-
447
- js_dirs() {
448
- if [ -f package.json ]; then
449
- echo "."
450
- return
451
- fi
452
- # `.terraform/` is a DOWNLOAD CACHE of third-party modules, and the two runner
453
- # repos carry eight vendored lambda packages in it -- each declaring lint and
454
- # test scripts. Linting someone else's vendored code is slow, always red, and
455
- # not this repo's business. It is gitignored, so a fresh worktree never has
456
- # it and the omission was invisible until a primary checkout was audited.
457
- find . -name package.json \
458
- -not -path "*/node_modules/*" -not -path "*/dist/*" -not -path "*/.next/*" \
459
- -not -path "*/.turbo/*" -not -path "*/.worktrees/*" -not -path "*/out/*" \
460
- -not -path "*/coverage/*" -not -path "*/.venv/*" \
461
- -not -path "*/.terraform/*" -not -path "*/vendor/*" 2>/dev/null |
462
- sed 's|/package.json$||' | sort
463
- }
464
-
465
- run_check() {
466
- name="$1"
467
- shift
468
- if [ -n "$LIST" ]; then
469
- echo "$*"
470
- return 0
471
- fi
472
- start=$(date +%s)
473
- if "$@" >"/tmp/biffo-verify.$$" 2>&1; then
474
- PASSED="$PASSED $name"
475
- LAST_CHECK_SECONDS=$(($(date +%s) - start))
476
- printf ' \033[32mOK\033[0m %-16s %ss\n' "$name" "$LAST_CHECK_SECONDS"
477
- else
478
- FAILED="$FAILED $name"
479
- printf ' \033[31mFAIL\033[0m %-16s %ss\n' "$name" "$(($(date +%s) - start))"
480
- sed 's/^/ /' "/tmp/biffo-verify.$$" | tail -25
481
- fi
482
- rm -f "/tmp/biffo-verify.$$"
483
- }
484
-
485
- skip() {
486
- [ -n "$LIST" ] && return 0
487
- SKIPPED="$SKIPPED $1"
488
- printf ' \033[90m-- %-16s n/a - %s\033[0m\n' "$1" "$2"
489
- }
490
-
491
- if [ -z "$LIST" ]; then
492
- # State which template this gate came from. A gate two versions old is the
493
- # condition that let tabsii-crm print `verify passed` on a 700-line change
494
- # while running one check, and nothing in the repo said so (#869, H5 gap 1).
495
- _stamp=""
496
- [ -f .biffo-shared-version ] && _stamp=" (template $(cat .biffo-shared-version))"
497
- printf '\nverify - the checks CI runs, before the push%s\n\n' "$_stamp"
498
-
499
- # Defence in depth (#1196): the ordinary run only reaches the ONE case it
500
- # can, a push attempted straight from the primary checkout -- see the long
501
- # comment above `checkout_health` for why this is a WARN, not a FAIL, and
502
- # why it does not (cannot) cover the read-only case the issue was filed for.
503
- # A worktree -- the normal place this script runs -- returns `n/a` here
504
- # before any git command runs, so this costs nothing on the common path.
505
- checkout_health
506
- case "$CHECKOUT_HEALTH_VERDICT" in
507
- stale)
508
- printf ' \033[33mWARN\033[0m %-16s this is the primary checkout on %s and it is %s\n' \
509
- "checkout-health" "$CHECKOUT_HEALTH_BRANCH" "$CHECKOUT_HEALTH_DETAIL"
510
- printf ' \033[33mAGENTS.md SS1/SS2: never edit or push from the primary -- do the work in a worktree instead.\033[0m\n'
511
- NOT_RUN="$NOT_RUN checkout-health"
512
- ;;
513
- unknown)
514
- printf ' \033[33mWARN\033[0m %-16s could not verify - %s\n' "checkout-health" "$CHECKOUT_HEALTH_DETAIL"
515
- ;;
516
- esac
517
- fi
518
-
519
- # Python first: ruff is near-instant, so the cheapest feedback on the largest
520
- # single class of failure comes back immediately.
521
- PY_DIRS=$(py_dirs)
522
- if [ -n "$PY_DIRS" ]; then
523
- if [ -n "$LIST" ] || command -v uv >/dev/null 2>&1; then
524
- for d in $PY_DIRS; do
525
- suffix=""
526
- [ "$d" != "." ] && suffix="(${d#./})"
527
- if [ "$d" = "." ]; then
528
- ci_has "ruff check" && run_check "ruff-check$suffix" uv run ruff check .
529
- ci_has "ruff format" && run_check "ruff-format$suffix" uv run ruff format --check .
530
- ci_has "pyright" && run_check "pyright$suffix" uv run pyright
531
- # bandit is NOT excluded: it exits non-zero on findings and it is the
532
- # RUN step that fails in CI, not the artefact upload. See the exclusion
533
- # audit in verify-parity.test.ts (#855).
534
- #
535
- # Scoped to the SAME paths CI scans, never wider. CI runs
536
- # `-r services/api services/_plugins` -- template-owned code only --
537
- # because a template-shipped check asserting over paths the template
538
- # does not own reds an instance on content it neither wrote nor can
539
- # repair (#325). Running `-r services` here found three B310s in
540
- # biffo-platform's user-owned services/idea-scout/ and refused a push CI
541
- # would have passed: the gate being stricter than CI is its own defect,
542
- # and it is what drives people to BIFFO_SKIP_VERIFY.
543
- bandit_paths=""
544
- [ -d services/api ] && bandit_paths="$bandit_paths services/api"
545
- [ -d services/_plugins ] && bandit_paths="$bandit_paths services/_plugins"
546
- [ -z "$bandit_paths" ] && [ -d src ] && bandit_paths="src"
547
- # shellcheck disable=SC2086
548
- [ -n "$bandit_paths" ] && ci_has "bandit" && run_check "bandit$suffix" uv run bandit -r $bandit_paths -ll -q
549
- if [ "$PYTEST" = "0" ]; then
550
- skip "pytest$suffix" "excluded by BIFFO_VERIFY_PYTEST=0"
551
- elif [ -n "$PYTEST" ] || pytest_is_fast "."; then
552
- if ci_has "pytest"; then
553
- run_check "pytest$suffix" uv run pytest -q
554
- # Re-confirm the verdict from what the run actually took. This is the
555
- # invalidation that costs nothing: the gate has just measured the
556
- # suite, so a suite that has grown past the budget excludes itself on
557
- # the next push rather than slowing every push for ever.
558
- pytest_record "." "$LAST_CHECK_SECONDS"
559
- fi
560
- else
561
- skip "pytest$suffix" "suite is slower than ${PYTEST_BUDGET_SECONDS}s - CI keeps it"
562
- fi
563
- else
564
- ci_has "ruff check" && run_check "ruff-check$suffix" uv run --directory "$d" ruff check .
565
- ci_has "ruff format" && run_check "ruff-format$suffix" uv run --directory "$d" ruff format --check .
566
- ci_has "pyright" && run_check "pyright$suffix" uv run --directory "$d" pyright
567
- ci_has "bandit" && run_check "bandit$suffix" uv run --directory "$d" bandit -r src -ll -q
568
- if [ "$PYTEST" = "0" ]; then
569
- skip "pytest$suffix" "excluded by BIFFO_VERIFY_PYTEST=0"
570
- elif [ -n "$PYTEST" ] || pytest_is_fast "$d"; then
571
- if ci_has "pytest"; then
572
- run_check "pytest$suffix" uv run --directory "$d" pytest -q
573
- pytest_record "$d" "$LAST_CHECK_SECONDS"
574
- fi
575
- else
576
- skip "pytest$suffix" "suite is slower than ${PYTEST_BUDGET_SECONDS}s - CI keeps it"
577
- fi
578
- fi
579
- done
580
- else
581
- skip python "uv not installed"
582
- fi
583
- else
584
- skip python "no pyproject.toml anywhere in this repo"
585
- fi
586
-
587
- # Postgres-dependent tests -- the lane a SQLite suite cannot stand in for.
588
- #
589
- # Tests that assert on row-level security, real DDL, or anything the app leaves
590
- # to Postgres only mean something against Postgres. They are selected by the
591
- # same CONVENTION their CI lane uses -- a module needing real Postgres is named
592
- # `test_*_pg.py` -- rather than a hand-maintained list, which is a fail-open
593
- # waiting to happen: add a Postgres test, forget the list, and it skips locally
594
- # and runs nowhere.
595
- #
596
- # Why this is here at all. On 2026-08-02 **9 of 13** locally-catchable failing
597
- # CI steps across the estate were this lane, every one of them a genuine
598
- # assertion failure on a feature branch that a local run would have caught. The
599
- # gate simply did not run it: `verify.sh` had no reference to Postgres in any
600
- # form, so a required check that costs a full CI round trip had no local
601
- # counterpart. Measured on tabsii-platform: schema build ~2s, 310 tests ~28s.
602
- #
603
- # The budget is deliberately its own, and larger than pytest's. `pytest_is_fast`
604
- # excludes a suite over 15s because a slow unit suite slows every push for a
605
- # class of failure the fast checks mostly catch first; this lane is the opposite
606
- # trade -- it is the ONLY local sight of a required check, and 30s against a
607
- # ~7-minute CI round trip pays for itself the first time it fires.
608
- PG_TEST_BUDGET_SECONDS="${BIFFO_VERIFY_PG_BUDGET:-120}"
609
- PG_TEST_DSN="${BIFFO_TEST_PG_DSN:-${TABSII_TEST_PG_DSN:-}}"
610
-
611
- # `.claude/worktrees` is excluded alongside `.worktrees`, and finding out why
612
- # cost a wrong answer: tabsii-platform reported **66** modules where its CI lane
613
- # runs 40, because an agent tool keeps its worktrees INSIDE the repo under
614
- # `.claude/`. A gate that runs a stale nested checkout's copy of a test would
615
- # fail a push over code that is not being pushed -- and the first such false
616
- # positive is what teaches people to reach for BIFFO_SKIP_VERIFY.
617
- pg_test_modules() {
618
- find . -name 'test_*_pg.py' \
619
- -not -path "*/node_modules/*" -not -path "*/.venv/*" \
620
- -not -path "*/.worktrees/*" -not -path "*/.claude/*" -not -path "*/.git/*" 2>/dev/null | sort
621
- }
622
-
623
- # Assert the lane EXERCISED something, not merely that pytest exited 0.
624
- #
625
- # Both inputs degrade to empty silently: a DSN pointing at a database whose
626
- # schema never built makes every module skip, and pytest reports "0 passed" as
627
- # success. A green gate that ran nothing is the exact shape this whole lane
628
- # exists to end, so the summary line is asserted rather than trusted -- the same
629
- # assertions its CI workflow makes, for the same reason.
630
- pg_test_run() {
631
- _out="/tmp/biffo-verify-pg.$$"
632
- if ! TABSII_TEST_PG_DSN="$PG_TEST_DSN" BIFFO_TEST_PG_DSN="$PG_TEST_DSN" \
633
- timeout "$PG_TEST_BUDGET_SECONDS" uv run --directory "$1" pytest -q $2 >"$_out" 2>&1; then
634
- cat "$_out"
635
- rm -f "$_out"
636
- return 1
637
- fi
638
- if grep -qiE '[0-9]+ skipped' "$_out"; then
639
- echo "A Postgres module reported skips -- the lane exercised nothing."
640
- echo "The DSN is set but the database is probably missing its schema."
641
- tail -5 "$_out"
642
- rm -f "$_out"
643
- return 1
644
- fi
645
- if ! grep -qE '[0-9]+ passed' "$_out"; then
646
- echo "No tests passed -- the lane did not run."
647
- tail -5 "$_out"
648
- rm -f "$_out"
649
- return 1
650
- fi
651
- rm -f "$_out"
652
- return 0
653
- }
654
-
655
- _pg_modules=$(pg_test_modules)
656
-
657
- # Provision the database rather than requiring the operator to remember.
658
- #
659
- # A gate that only runs when you exported the right variable is a gate that runs
660
- # on the days you did not need it. `scripts/pg-test-db.sh` is idempotent and
661
- # cheap when the schema is unchanged (~0.3s; ~4s when it genuinely has to
662
- # rebuild), so calling it is better than warning about it. Failure is silent
663
- # BECAUSE the WARN below is the honest report of it -- no Docker, no server, no
664
- # schema all end in the same place: the lane did not run, and the gate says so.
665
- if [ -z "$PG_TEST_DSN" ] && [ -n "$_pg_modules" ] && [ -z "$LIST" ]; then
666
- # Through the bridge since #1109; the `-f scripts/pg-test-db.sh` guard went
667
- # with the copy. Failure stays silent here BECAUSE the WARN below is the
668
- # honest report of it -- no Docker, no server, no schema and no CLI all end
669
- # in the same place: the lane did not run, and the gate says so.
670
- PG_TEST_DSN=$(sh scripts/biffo.sh pg-test-db 2>/dev/null | tail -1) || PG_TEST_DSN=""
671
- case "$PG_TEST_DSN" in
672
- postgres*) ;;
673
- *) PG_TEST_DSN="" ;;
674
- esac
675
- fi
676
-
677
- # Order matters, and getting it wrong made these very tests machine-dependent:
678
- # with `uv not installed` checked FIRST, a runner without uv skipped quietly and
679
- # the gap warning never printed -- green on a workstation, red on CI, for a
680
- # reason unconnected to the change. "This repo has a lane and nothing local is
681
- # checking it" is true whether or not uv is installed, and it is the more
682
- # actionable of the two, so it is reported first. `uv` is only required to
683
- # actually RUN the lane.
684
- if [ -z "$_pg_modules" ]; then
685
- skip pg-test "no Postgres-dependent tests (test_*_pg.py) in this repo"
686
- elif [ -z "$PG_TEST_DSN" ]; then
687
- # NOT a quiet `--`. Every other skip in this file means "this repo does not
688
- # have the thing"; this one means "this repo HAS the thing and the gate is
689
- # blind to it", which is the fail-open shape, and printing the two the same
690
- # way is how a gap gets read as coverage. It stays a skip rather than a
691
- # failure because a push must not be blocked by a database being down -- but
692
- # it says so where it cannot be missed, and names the command that fixes it.
693
- if [ -z "$LIST" ]; then
694
- NOT_RUN="$NOT_RUN pg-test"
695
- printf ' \033[33mWARN\033[0m %-16s NOT RUN - %s Postgres module(s) present, no DSN set\n' \
696
- "pg-test" "$(echo "$_pg_modules" | wc -l | tr -d ' ')"
697
- printf ' \033[33m%s\033[0m\n' \
698
- "CI runs these as a required check; nothing local is checking them."
699
- printf ' \033[90m%s\033[0m\n' \
700
- "set BIFFO_TEST_PG_DSN, or run scripts/pg-test-db.sh if this repo ships one"
701
- fi
702
- elif ! command -v uv >/dev/null 2>&1; then
703
- skip pg-test "uv not installed"
704
- else
705
- # Run from the uv project that owns the modules, with paths relative to it, so
706
- # this works wherever a repo keeps its API (root here, services/api in every
707
- # instance and sibling).
708
- _pg_dir=$(echo "$_pg_modules" | head -1)
709
- while [ "$_pg_dir" != "." ] && [ "$_pg_dir" != "/" ]; do
710
- _pg_dir=$(dirname "$_pg_dir")
711
- [ -f "$_pg_dir/pyproject.toml" ] && break
712
- done
713
- if [ ! -f "$_pg_dir/pyproject.toml" ]; then
714
- skip pg-test "no pyproject.toml above the Postgres modules"
715
- else
716
- _pg_rel=$(echo "$_pg_modules" | sed "s|^$_pg_dir/||" | tr '\n' ' ')
717
- # shellcheck disable=SC2086
718
- run_check pg-test pg_test_run "$_pg_dir" "$_pg_rel"
719
- fi
720
- fi
721
-
722
- # Terraform, wherever this repo keeps it: modules/ in the template and
723
- # instances, infra/ and modules/ in siblings.
724
- if [ -n "$LIST" ] || command -v terraform >/dev/null 2>&1; then
725
- # Scope must match this repo's CI, not exceed it. The template and instances
726
- # deliberately fmt-check modules/ ONLY: infra/environments/ is user-owned, and
727
- # a template-shipped check asserting over paths the template does not own is
728
- # the #325 trap -- it reds an instance on content it neither wrote nor can
729
- # repair. Siblings own their whole infra/ and their CI checks it, so they get
730
- # both. biffo.sibling.json is what tells them apart.
731
- tf_dirs=""
732
- [ -d modules ] && tf_dirs="$tf_dirs modules/"
733
- [ -f biffo.sibling.json ] && [ -d infra ] && tf_dirs="$tf_dirs infra/"
734
- if [ -n "$tf_dirs" ]; then
735
- # shellcheck disable=SC2086
736
- ci_has "terraform fmt" && run_check terraform-fmt terraform fmt -check -recursive $tf_dirs
737
- else
738
- skip terraform-fmt "no terraform in this repo"
739
- fi
740
- else
741
- skip terraform-fmt "terraform not installed"
742
- fi
743
-
744
- # The Biffo guards, where the dispatcher exists. Cheap, and two of them
745
- # (ownership, plugin-terraform) were being caught in CI.
746
- if [ -f scripts/biffo.sh ]; then
747
- run_check plugin-tf sh scripts/biffo.sh check plugin-terraform
748
- run_check plugin-names sh scripts/biffo.sh check plugin-collisions
749
- run_check adr-numbering sh scripts/biffo.sh check adr-numbering
750
- else
751
- skip biffo-guards "no scripts/biffo.sh in this repo"
752
- fi
753
-
754
- # The append-only corpus guard (#778). CI runs it in Release Guards, and it was
755
- # invisible to the parity test until #897 widened the harvester -- it is neither
756
- # `pnpm`, `uv`, `terraform` nor `sh scripts/`, so the guard whose property is
757
- # "every CI check is in the gate or explicitly excluded" could not see it at all.
758
- # Measured 0.06s here, which is cheaper than every other check in this file.
759
- if [ -f scripts/practices-monotonic.mjs ]; then
760
- ci_has "practices-monotonic" && run_check corpus-append-only node scripts/practices-monotonic.mjs
761
- fi
762
-
763
- # Terraform plan artefacts, refused by CONTENT (biffo-runners#1).
764
- #
765
- # A saved plan is a zip. `strings`/`grep` over it is a false-negative machine —
766
- # a pre-commit check on `terraform/tfplan2` reported it clean and it carried a
767
- # live private key. gitleaks cannot see inside it either, so the working-tree
768
- # pass below is no protection: the bytes are compressed.
769
- #
770
- # So the answer is not a better scanner, it is refusing to track the artefact at
771
- # all. Name-based ignoring already failed: `.gitignore` carried `tfplan`, and the
772
- # file that nearly leaked was `tfplan2`.
773
- #
774
- # Detection is content-first: a zip magic (`PK\003\004`) whose central
775
- # directory names a `tfplan` member. Filenames are stored uncompressed in a zip,
776
- # so this needs no `unzip` and works on any machine.
777
- # Skipped for --list, which must answer a question about the repo without doing
778
- # work. Measured 4.2s over 907 tracked files before this gate existed, which
779
- # timed out three parity tests that only wanted the check NAMES.
780
- #
781
- # Candidates are narrowed by extension FIRST, then detected by content. The
782
- # narrowing is not a weakening: the files it skips are text, and text is exactly
783
- # what gitleaks can already scan. The whole reason a plan needs its own guard is
784
- # that its bytes are compressed and no scanner can read them.
785
- #
786
- # `read -r -d ""` is a bashism and this file runs under `sh`. Using it here made
787
- # the loop error and the guard report nothing — a fail-open inside the guard
788
- # written to close one. It passed `sh -n`, because the syntax is valid; only
789
- # running it revealed the failure.
790
- plan_artefacts=""
791
- if [ -z "$LIST" ]; then
792
- plan_artefacts=$(
793
- git ls-files | while IFS= read -r f; do
794
- case "$f" in
795
- *.ts|*.tsx|*.js|*.jsx|*.mjs|*.cjs|*.py|*.md|*.json|*.yml|*.yaml|*.tf|\
796
- *.tfvars|*.sh|*.toml|*.txt|*.css|*.html|*.svg|*.lock|*.snap|*.sql) continue ;;
797
- esac
798
- [ -f "$f" ] || continue
799
- case $(head -c 4 "$f" 2>/dev/null | od -An -c 2>/dev/null | tr -d " ") in
800
- # -a because the file is binary and grep would otherwise decline to report.
801
- PK003004) LC_ALL=C grep -aq tfplan "$f" 2>/dev/null && printf "%s\n" "$f" ;;
802
- esac
803
- done
804
- )
805
- fi
806
- if [ -n "$plan_artefacts" ]; then
807
- printf "\033[31mFAIL\033[0m terraform plan artefact is tracked:\n"
808
- printf " %s\n" $plan_artefacts
809
- printf "A saved plan is a zip and routinely contains credentials. It cannot be\n"
810
- printf "scanned by gitleaks or by grep. Remove it from the index:\n"
811
- printf " git rm --cached <file>\n\n"
812
- exit 1
813
- fi
814
-
815
- # gitleaks, WORKING-TREE pass only (#897). `--no-git` is what CI's second pass
816
- # runs, and it is the half a pre-push gate can meaningfully do.
817
- #
818
- # Not installed is reported, never assumed clean. A secret scanner that silently
819
- # does nothing and lets the gate print `verify passed` is the precise failure this
820
- # whole file exists to prevent, and it would be worse here than elsewhere: the
821
- # thing not being checked is credentials.
822
- #
823
- # Scoped to TRACKED files only (#1194). `--no-git` walks the filesystem, not the
824
- # index, and does not honour `.gitignore` -- so anything a build leaves behind
825
- # gets scanned too. An agent in tabsii-crm ran `pnpm run build`, then hit this
826
- # gate scanning 218MB of `.next/`/`out/` and got 30 phantom leaks, none in a
827
- # tracked file, none of them committable. A scanner that cries wolf is worse
828
- # than a slow one: the second time 30 leaks turn out to be bundle noise, people
829
- # stop reading gitleaks output, which is exactly the day a real one hides in it.
830
- #
831
- # The fix is not a broader `.gitleaks.toml` allowlist -- that has to be kept in
832
- # step with `.gitignore` by hand, in every repo this file runs in, forever, and
833
- # it is the wrong shape besides: AGENTS.md SS7 says never fix a scan failure by
834
- # editing the allowlist, and a path-list that grows to cover every build tool's
835
- # output directory is that same fix wearing a different hat. It is narrowed to
836
- # what the gate is actually FOR instead: nothing untracked can reach the remote
837
- # a push sends to, so nothing untracked needs to be able to fail a push.
838
- # `gitleaks_tracked_only` (below) mirrors `git ls-files` into a scratch
839
- # directory -- current on-disk content, not HEAD, so a secret staged into an
840
- # already-tracked file is still caught before the commit that would push it --
841
- # and scans that copy. Relative paths inside the copy match the repo, so the
842
- # existing path-based `.gitleaks.toml` allowlist entries keep working unchanged.
843
- gitleaks_tracked_only() {
844
- _gl_dir=$(mktemp -d "${TMPDIR:-/tmp}/biffo-gitleaks.XXXXXX") || return 1
845
- _gl_root=$(git rev-parse --show-toplevel) || {
846
- rm -rf "$_gl_dir"
847
- return 1
848
- }
849
- # Newline-delimited, not `git ls-files -z` + `read -d ''`: `-d` is a bashism
850
- # dash does not implement, and this file runs under `sh`
851
- # (shell-portability.test.ts enforces it). The `plan_artefacts` loop above
852
- # already made this exact call for the same reason.
853
- ( cd "$_gl_root" && git ls-files ) | while IFS= read -r _f; do
854
- [ -f "$_gl_root/$_f" ] || continue
855
- mkdir -p "$_gl_dir/$(dirname "$_f")"
856
- cp -p "$_gl_root/$_f" "$_gl_dir/$_f" 2>/dev/null
857
- done
858
- # No explicit `--config`, deliberately. gitleaks' own default resolution
859
- # looks for `.gitleaks.toml` at "(target path)", which is `--source` -- i.e.
860
- # the mirrored copy, where the file already sits at its usual relative path
861
- # if this repo tracks one. A first cut passed `--config "$_gl_root/.gitleaks.toml"`
862
- # to remove any doubt about that resolution, and it was a regression:
863
- # against a repo with NO `.gitleaks.toml` at all -- a real, valid state,
864
- # since gitleaks otherwise falls back to its built-in default ruleset --
865
- # a literal `--config` path that does not exist is FATAL ("unable to load
866
- # gitleaks config"), where the flagless form degrades gracefully to
867
- # defaults, identical to what this repo ran before #1194. Verified by
868
- # running both forms against a `--source` with no config file present.
869
- gitleaks detect --no-git --redact --exit-code=2 --source "$_gl_dir"
870
- _gl_status=$?
871
- rm -rf "$_gl_dir"
872
- return $_gl_status
873
- }
874
-
875
- if ci_has "gitleaks"; then
876
- # The installation check gates EXECUTION only, never `--list`.
877
- #
878
- # `--list` reports what THIS REPO requires, deliberately independent of what the
879
- # machine happens to have (see the --list contract at the top of this file), and
880
- # verify-parity.test.ts reads `--list`. A first cut wrapped the whole branch in
881
- # `command -v gitleaks`, which made the listed check set machine-dependent: on a
882
- # machine without gitleaks the parity test then reported the gate as missing a
883
- # check it does declare. The parity test caught it.
884
- #
885
- # `terraform-fmt` above has the same shape and is only unexposed because
886
- # terraform happens to be installed here. Recorded, not fixed in this change.
887
- if [ -n "$LIST" ] || command -v gitleaks >/dev/null 2>&1; then
888
- run_check gitleaks gitleaks_tracked_only
889
- else
890
- # Say how to close it, pinned to the version ci.yml installs. A skip that
891
- # only reports its own absence stays skipped: this one sat `n/a` long
892
- # enough for the `\b\d{12}\b` account-id rule to reach CI three times,
893
- # most recently on two test UUIDs whose last segment happened to be
894
- # twelve digits (tabsii-platform#446). Thirty seconds of install would
895
- # have caught it before the push, and version parity matters — an older
896
- # gitleaks disagreeing with CI reintroduces exactly the local/CI
897
- # divergence this gate exists to remove.
898
- skip gitleaks "not installed - CI still runs both passes. Install the version ci.yml pins:
899
- curl -sSfL -o /tmp/gl.tgz https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz \\
900
- && tar -xzf /tmp/gl.tgz -C \"\$HOME/.local/bin\" gitleaks"
901
- fi
902
- fi
903
-
904
- # JS, cheapest first; `test` last because it is slowest and the most likely to
905
- # be interrupted by an impatient reader.
906
- JS_DIRS=$(js_dirs)
907
- if [ -n "$JS_DIRS" ]; then
908
- skip build "excluded - a full app build is too slow for a push gate"
909
- for d in $JS_DIRS; do
910
- # Name the package in the label when there is more than one, so a failure
911
- # says WHERE. A single unlabelled "lint" across three packages is how you
912
- # end up fixing the wrong one.
913
- suffix=""
914
- [ "$d" != "." ] && suffix="(${d#./})"
915
- for s in lint typecheck format:check test; do
916
- label="$(printf '%s' "$s" | tr -d ':')$suffix"
917
- if have_script "$s" "$d"; then
918
- if [ "$d" = "." ]; then
919
- run_check "$label" pnpm run "$s"
920
- else
921
- run_check "$label" pnpm --dir "$d" run "$s"
922
- fi
923
- else
924
- skip "$label" "no \"$s\" script"
925
- fi
926
- done
927
- done
928
- else
929
- skip javascript "no package.json anywhere in this repo"
930
- fi
931
-
932
- [ -n "$LIST" ] && exit 0
933
-
934
- printf '\n'
935
- if [ -n "$FAILED" ]; then
936
- printf '\033[31mverify failed:\033[0m%s\n' "$FAILED"
937
- printf 'Fix these here - CI will find them anyway, three minutes and a merge race later.\n'
938
- printf 'Most format failures are one command: pnpm run format\n\n'
939
- exit 1
940
- fi
941
- if [ -z "$PASSED" ]; then
942
- # "Nothing applicable ran" is a different outcome from "checks passed", and
943
- # conflating them is the exact failure this gate exists to remove -- the
944
- # standard's own principle, applied to the gate itself. tabsii-crm ran ONE
945
- # check on a 700-line change and printed a pass (#855).
946
- #
947
- # Whether that BLOCKS depends on one thing: does this repo have CI the gate
948
- # should have mirrored?
949
- #
950
- # - CI exists and the gate ran nothing -> that is the #855 bug. Block.
951
- # - No CI at all -> the repo has no shift-left obligation, and blocking
952
- # every push there is friction with no benefit. Friction is what drives
953
- # people to BIFFO_SKIP_VERIFY, which is a counter-metric H4 pre-registered
954
- # as refuting itself. Say it loudly, exit 0.
955
- #
956
- # Found immediately: the first run of this rule refused the push in the three
957
- # repos that have no CI (tabsii-runners, biffo-runners,
958
- # tabsii-data-model-design) -- blocking the very sync PR that was installing
959
- # the gate.
960
- printf '\033[31mverify ran NOTHING - this is not a pass\033[0m\n'
961
- if [ -f .github/workflows/ci.yml ]; then
962
- printf 'This repo HAS CI, and the gate mirrored none of it. That is the #855 bug:\n'
963
- printf 'a gate that reports on work it never checked. Run scripts/gate-coverage.sh\n'
964
- printf 'to see which of its CI checks are missing.\n\n'
965
- exit 1
966
- fi
967
- printf 'This repo has no CI for the gate to mirror, so there is nothing to shift\n'
968
- printf 'left. Not blocking -- but nothing was verified here.\n'
969
- printf 'See docs/practices/standards/local-gates.md\n\n'
970
- exit 0
971
- fi
972
- printf '\033[32mverify passed\033[0m -%s\n' "$PASSED"
973
- [ -n "$SKIPPED" ] && printf '\033[90mnot applicable here:%s\033[0m\n' "$SKIPPED"
974
- [ -n "$NOT_RUN" ] && printf '\033[33mAPPLICABLE BUT NOT RUN:%s - CI checks this and the gate did not\033[0m\n' "$NOT_RUN"
975
- # Say which question was answered. Without this, a repo whose ci.yml was deleted
976
- # prints exactly what a fully-mirrored repo prints (#942).
977
- [ -n "${NO_CI:-}" ] && printf '\033[33mno ci.yml - nothing to mirror, so every applicable check ran as\nbest-effort. This is NOT evidence that CI requires them. If this repo is\nmeant to have CI, its workflow is missing.\033[0m\n'
978
- printf '\n'