@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.
- package/_skeletons/plugin-template/.githooks/pre-push +10 -5
- package/_skeletons/sibling-template/.githooks/pre-push +10 -5
- package/dist/index.js +8 -0
- package/package.json +3 -2
- package/_skeletons/sibling-template/scripts/verify.sh +0 -978
- /package/{_skeletons/plugin-template/scripts → scripts}/verify.sh +0 -0
|
@@ -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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|
|
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'
|
|
File without changes
|