@biffo/cli 0.237.0 → 0.238.1

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.
@@ -31,11 +31,14 @@ cd "$root" || exit 0
31
31
  # Why it exists: a force-push can quietly add a file to a branch's change set
32
32
  # and record a stale copy of somebody else's merged work as your own change.
33
33
  # That happened (tabsii-platform#446) and every local gate was green on it.
34
- if [ -f scripts/rewrite-scope-check.sh ]; then
35
- sh scripts/rewrite-scope-check.sh || exit 1
36
- else
37
- echo "pre-push: no scripts/rewrite-scope-check.sh rewrite scope NOT checked" >&2
38
- fi
34
+ # Runs from the version-pinned CLI rather than a per-repo copy (#1109).
35
+ #
36
+ # This used to warn and CONTINUE when the script was absent, which is a
37
+ # fail-open in a gate whose whole job is refusing a push: the one repo missing
38
+ # the file was the one where nothing checked. There is no absent case now --
39
+ # the bridge resolves the script from the package -- and if the bridge itself
40
+ # cannot run, that is a reason to stop, not to wave the push through.
41
+ sh scripts/biffo.sh rewrite-scope-check || exit 1
39
42
 
40
43
  if [ ! -f scripts/verify.sh ]; then
41
44
  echo "pre-push: no scripts/verify.sh — NO checks ran (see local-gates.md)" >&2
@@ -662,8 +662,12 @@ _pg_modules=$(pg_test_modules)
662
662
  # rebuild), so calling it is better than warning about it. Failure is silent
663
663
  # BECAUSE the WARN below is the honest report of it -- no Docker, no server, no
664
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" ] && [ -f scripts/pg-test-db.sh ]; then
666
- PG_TEST_DSN=$(sh scripts/pg-test-db.sh 2>/dev/null | tail -1) || PG_TEST_DSN=""
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=""
667
671
  case "$PG_TEST_DSN" in
668
672
  postgres*) ;;
669
673
  *) PG_TEST_DSN="" ;;
@@ -31,11 +31,14 @@ cd "$root" || exit 0
31
31
  # Why it exists: a force-push can quietly add a file to a branch's change set
32
32
  # and record a stale copy of somebody else's merged work as your own change.
33
33
  # That happened (tabsii-platform#446) and every local gate was green on it.
34
- if [ -f scripts/rewrite-scope-check.sh ]; then
35
- sh scripts/rewrite-scope-check.sh || exit 1
36
- else
37
- echo "pre-push: no scripts/rewrite-scope-check.sh rewrite scope NOT checked" >&2
38
- fi
34
+ # Runs from the version-pinned CLI rather than a per-repo copy (#1109).
35
+ #
36
+ # This used to warn and CONTINUE when the script was absent, which is a
37
+ # fail-open in a gate whose whole job is refusing a push: the one repo missing
38
+ # the file was the one where nothing checked. There is no absent case now --
39
+ # the bridge resolves the script from the package -- and if the bridge itself
40
+ # cannot run, that is a reason to stop, not to wave the push through.
41
+ sh scripts/biffo.sh rewrite-scope-check || exit 1
39
42
 
40
43
  if [ ! -f scripts/verify.sh ]; then
41
44
  echo "pre-push: no scripts/verify.sh — NO checks ran (see local-gates.md)" >&2
@@ -662,8 +662,12 @@ _pg_modules=$(pg_test_modules)
662
662
  # rebuild), so calling it is better than warning about it. Failure is silent
663
663
  # BECAUSE the WARN below is the honest report of it -- no Docker, no server, no
664
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" ] && [ -f scripts/pg-test-db.sh ]; then
666
- PG_TEST_DSN=$(sh scripts/pg-test-db.sh 2>/dev/null | tail -1) || PG_TEST_DSN=""
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=""
667
671
  case "$PG_TEST_DSN" in
668
672
  postgres*) ;;
669
673
  *) PG_TEST_DSN="" ;;
package/dist/index.js CHANGED
@@ -10813,6 +10813,27 @@ function packagedScriptCommand(spec) {
10813
10813
  });
10814
10814
  }
10815
10815
 
10816
+ // src/commands/hook-audit.ts
10817
+ var hookAuditCommand = packagedScriptCommand({
10818
+ name: "hook-audit",
10819
+ script: "scripts/hook-audit.sh",
10820
+ description: "Report whether git hooks will actually fire in every working tree"
10821
+ });
10822
+
10823
+ // src/commands/pg-test-db.ts
10824
+ var pgTestDbCommand = packagedScriptCommand({
10825
+ name: "pg-test-db",
10826
+ script: "scripts/pg-test-db.sh",
10827
+ description: "Provision the local Postgres test database and print its DSN"
10828
+ });
10829
+
10830
+ // src/commands/rewrite-scope-check.ts
10831
+ var rewriteScopeCheckCommand = packagedScriptCommand({
10832
+ name: "rewrite-scope-check",
10833
+ script: "scripts/rewrite-scope-check.sh",
10834
+ description: "Refuse a push whose rewrite scope absorbs merged work from another branch"
10835
+ });
10836
+
10816
10837
  // src/commands/branch-health.ts
10817
10838
  var branchHealthCommand = packagedScriptCommand({
10818
10839
  name: "branch-health",
@@ -10852,6 +10873,9 @@ program.addCommand(teardownCommand);
10852
10873
  program.addCommand(waitForChecksCommand);
10853
10874
  program.addCommand(branchHealthCommand);
10854
10875
  program.addCommand(claimCommand);
10876
+ program.addCommand(hookAuditCommand);
10877
+ program.addCommand(pgTestDbCommand);
10878
+ program.addCommand(rewriteScopeCheckCommand);
10855
10879
  program.addCommand(pluginCommand);
10856
10880
  program.addCommand(dataCommand);
10857
10881
  program.addCommand(coreCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@biffo/cli",
3
- "version": "0.237.0",
3
+ "version": "0.238.1",
4
4
  "description": "Biffo project scaffolding CLI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -26,7 +26,10 @@
26
26
  "_skeletons",
27
27
  "scripts/wait-for-checks.sh",
28
28
  "scripts/branch-health.sh",
29
- "scripts/claim.sh"
29
+ "scripts/claim.sh",
30
+ "scripts/hook-audit.sh",
31
+ "scripts/pg-test-db.sh",
32
+ "scripts/rewrite-scope-check.sh"
30
33
  ],
31
34
  "scripts": {
32
35
  "build": "tsup src/index.ts --format esm --dts --clean",
@@ -1,145 +0,0 @@
1
- #!/usr/bin/env bash
2
- #
3
- # Is a git hook actually going to execute here?
4
- #
5
- # ## Why this exists
6
- #
7
- # A configured hook that does not run is worse than no hook, because it is
8
- # assumed to be protecting you. On 2026-07-29 the estate was in exactly that
9
- # state: `core.hooksPath` pointed at `.husky/_`, a **gitignored** directory
10
- # created only by `prepare: husky` on `pnpm install` — and git resolves that
11
- # relative path against *each worktree's* root. Every fresh worktree therefore
12
- # had no hooks, and git said nothing: no warning, no error, no output.
13
- #
14
- # AGENTS.md §1 mandates a fresh worktree per unit of work, so the required
15
- # workflow disarmed its own gates. 6 of 32 working trees were armed. The pre-push
16
- # pyright, the pre-commit lint-staged and commitlint had all been silently
17
- # skipped in the other 26 for as long as anyone had been using worktrees.
18
- #
19
- # Nothing detected it because nothing looked. This is the thing that looks.
20
- #
21
- # ## Verdicts
22
- #
23
- # ARMED git will execute a hook here.
24
- # DEAD core.hooksPath is set and its target is missing or carries no
25
- # hooks. Git skips silently. **This is the state that lies to you**,
26
- # and the only one that makes this script exit non-zero.
27
- # NO-HOOKS no hooks configured at all. Honest, and visible in this report.
28
- #
29
- # Usage:
30
- # sh scripts/hook-audit.sh # this repo and its worktrees
31
- # sh scripts/hook-audit.sh --estate ~/code # every repo under a directory
32
- # sh scripts/hook-audit.sh --quiet # verdict counts only
33
-
34
- set -uo pipefail
35
-
36
- ESTATE=""
37
- QUIET=""
38
- while [ $# -gt 0 ]; do
39
- case "$1" in
40
- --estate) ESTATE="$2"; shift 2 ;;
41
- --quiet) QUIET=1; shift ;;
42
- *) echo "unknown argument: $1" >&2; exit 2 ;;
43
- esac
44
- done
45
-
46
- armed=0
47
- dead=0
48
- nohooks=0
49
- dead_list=""
50
-
51
- # The three hooks this standard cares about. A hooksPath directory containing
52
- # none of them is not armed for our purposes even if it holds something else.
53
- WANTED='^(pre-commit|pre-push|commit-msg)$'
54
-
55
- report() {
56
- tree="$1"
57
- label="$2"
58
- hp=$(git -C "$tree" config core.hooksPath 2>/dev/null)
59
-
60
- if [ -z "$hp" ]; then
61
- # The default hooks directory is in the **common** git dir, which linked
62
- # worktrees share. Reading "$tree/.git/hooks" is wrong for exactly the trees
63
- # this audit exists to check: in a linked worktree `.git` is a *file*
64
- # containing a gitdir pointer, so that path does not exist and every armed
65
- # worktree was about to be reported NO-HOOKS. Ask git where it actually is.
66
- #
67
- # Git ships .sample files there that never execute, so counting the
68
- # directory as armed merely for being non-empty would be exactly the false
69
- # comfort this script exists to remove.
70
- common=$(git -C "$tree" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
71
- real=$(ls "${common:-$tree/.git}/hooks" 2>/dev/null | grep -vc '\.sample$' || true)
72
- if [ "${real:-0}" -gt 0 ]; then
73
- armed=$((armed + 1))
74
- [ -n "$QUIET" ] || printf '%-56s %-14s \033[32mARMED\033[0m %s\n' "$label" "(default)" "$real hook(s) in .git/hooks"
75
- else
76
- nohooks=$((nohooks + 1))
77
- [ -n "$QUIET" ] || printf '%-56s %-14s \033[33mNO-HOOKS\033[0m %s\n' "$label" "(default)" "no hooks configured"
78
- fi
79
- return
80
- fi
81
-
82
- # Relative hooksPath resolves against the working tree root — the whole bug.
83
- case "$hp" in
84
- /*) dir="$hp" ;;
85
- *) dir="$tree/$hp" ;;
86
- esac
87
-
88
- present=$(ls "$dir" 2>/dev/null | grep -E "$WANTED" | tr '\n' ',' || true)
89
- if [ -z "$present" ]; then
90
- dead=$((dead + 1))
91
- dead_list="$dead_list $label ($hp)
92
- "
93
- [ -n "$QUIET" ] || printf '%-56s %-14s \033[31mDEAD\033[0m %s\n' "$label" "$hp" "$hp missing or holds no hooks — git skips ALL hooks silently"
94
- else
95
- armed=$((armed + 1))
96
- [ -n "$QUIET" ] || printf '%-56s %-14s \033[32mARMED\033[0m %s\n' "$label" "$hp" "${present%,}"
97
- fi
98
- }
99
-
100
- walk_repo() {
101
- root="${1%/}"
102
- name="$2"
103
- # Every working tree, not just the primary — the primary is usually the one
104
- # that IS armed, which is how this went unnoticed for so long.
105
- git -C "$root" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2}' | while read -r t; do
106
- [ -d "$t" ] || continue
107
- if [ "$t" = "$root" ]; then echo "$t|$name"; else echo "$t|$name${t#$root}"; fi
108
- done
109
- }
110
-
111
- [ -n "$QUIET" ] || printf '%-56s %-14s %-8s %s\n' "WORKING TREE" "hooksPath" "VERDICT" "detail"
112
-
113
- if [ -n "$ESTATE" ]; then
114
- targets=$(for d in "$ESTATE"/*/; do
115
- [ -e "$d/.git" ] || continue
116
- walk_repo "$d" "$(basename "${d%/}")"
117
- done)
118
- else
119
- root=$(git rev-parse --show-toplevel 2>/dev/null) || { echo "not a git repo" >&2; exit 2; }
120
- # From inside a worktree, --show-toplevel gives the worktree; walk from the
121
- # common repo so sibling worktrees are audited too.
122
- common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)
123
- root=$(dirname "$common")
124
- targets=$(walk_repo "$root" "$(basename "$root")")
125
- fi
126
-
127
- while IFS='|' read -r tree label; do
128
- [ -n "$tree" ] || continue
129
- report "$tree" "$label"
130
- done <<EOF
131
- $targets
132
- EOF
133
-
134
- total=$((armed + dead + nohooks))
135
- printf '\n%s working trees — \033[32m%s armed\033[0m, \033[31m%s dead\033[0m, %s without hooks' "$total" "$armed" "$dead" "$nohooks"
136
- [ "$total" -gt 0 ] && printf ' (%s%% armed)' "$((100 * armed / total))"
137
- printf '\n'
138
-
139
- if [ "$dead" -gt 0 ]; then
140
- printf '\n\033[31mDEAD working trees — hooks are configured here and are NOT running:\033[0m\n%s' "$dead_list"
141
- printf 'Every commit and push made in these is unguarded, and nothing says so.\n'
142
- printf 'Fix: run `pnpm install` there, or move the repo to tracked .githooks/.\n'
143
- exit 1
144
- fi
145
- exit 0
@@ -1,275 +0,0 @@
1
- #!/usr/bin/env sh
2
- #
3
- # Give the Postgres-dependent test lane a database with a CURRENT schema, and
4
- # print its DSN.
5
- #
6
- # ## Why this exists
7
- #
8
- # `scripts/verify.sh` grew a `pg-test` check (#1089) because on 2026-08-02 nine
9
- # of thirteen locally-catchable failing CI steps across the estate were one
10
- # repo's real-Postgres lane -- a required check with no local counterpart at all.
11
- # But a gate can only run that lane against a database, and no repo documented
12
- # how to get one: no compose file, no script, no DSN written down. The container
13
- # that existed on the workstation had been created ad hoc in some earlier session
14
- # and held a scatter of scratch databases. That undocumented setup WAS the
15
- # fail-open, because a gate nobody can run is not a gate.
16
- #
17
- # ## Why freshness, not rebuild-every-time
18
- #
19
- # The expensive failure is not a slow rebuild, it is a STALE one. Measured on
20
- # tabsii-platform while writing this: a database built about an hour earlier,
21
- # before two PRs merged, produced **23 failures** in a module that had nothing to
22
- # do with the change in hand. Rebuilt from the same tree it passed 336/336, and
23
- # passed 336 again on an immediate re-run -- so the lane was genuinely
24
- # re-runnable and every one of those failures was the old schema.
25
- #
26
- # That is the worst shape a local gate can have. Twenty-three red tests that are
27
- # not your fault teach people the gate is unreliable, and an unreliable gate gets
28
- # bypassed -- which H4 pre-registered as the condition refuting the whole
29
- # local-gate programme. So the schema inputs are fingerprinted and a rebuild
30
- # happens only when they actually changed: reuse ~0.3s, rebuild ~4s.
31
- #
32
- # ## Why it is generic
33
- #
34
- # It adapts to the repo rather than being told about it, for the same reason
35
- # `verify.sh` does: forks drift, and a per-instance copy of this would drift from
36
- # the DDL layout it is meant to build. Everything instance-specific is DERIVED --
37
- # the schema directories from `db/imports/*/`, the engine image from whether the
38
- # DDL asks for PostGIS, and the did-it-build threshold from the number of
39
- # policies the DDL itself declares. Nothing here names a product.
40
- #
41
- # ## Usage
42
- #
43
- # eval "$(sh scripts/pg-test-db.sh --export)" # export BIFFO_TEST_PG_DSN
44
- # sh scripts/pg-test-db.sh # print the DSN on stdout
45
- # sh scripts/pg-test-db.sh --recreate # force a rebuild
46
- #
47
- # Only the DSN reaches stdout, so it is safe to capture; progress goes to stderr.
48
- #
49
- # Overridable: BIFFO_PG_HOST, BIFFO_PG_PORT, BIFFO_PG_USER, BIFFO_PG_PASSWORD,
50
- # BIFFO_PG_DB, BIFFO_PG_CONTAINER, BIFFO_PG_IMAGE.
51
- #
52
- # ## Concurrency: derived, not coordinated
53
- #
54
- # The port, database name, and container name below default to values DERIVED
55
- # from this checkout's own path, not fixed constants. A fixed port
56
- # (`BIFFO_PG_PORT` used to default to `55432` everywhere) meant any two
57
- # checkouts running this script at the same time -- two worktrees of one repo,
58
- # or two entirely different repos on one machine -- attached to the SAME
59
- # Postgres cluster and raced on cluster-wide catalogs like `pg_shdepend`
60
- # (#1114). A fixed database name (`biffo_test`) meant they then shared one
61
- # database on top of that (#1120).
62
- #
63
- # The fix is not a lock: a machine-wide `flock` around this script would make
64
- # concurrent runs correct by serializing them, but that is coordination, and
65
- # this estate's shared-mutable-state defects have twice been solved instead by
66
- # making the value itself unique per user rather than making users take turns.
67
- # Deriving from `$REPO_ROOT` gets that for free -- it is already different for
68
- # every worktree and every repo -- and deterministically, so repeat runs
69
- # against the SAME checkout still land on the same port/db/container and reuse
70
- # the fingerprinted schema (see step 2 below) instead of rebuilding it under a
71
- # fresh identity every time. `BIFFO_PG_PORT`, `BIFFO_PG_DB`, and
72
- # `BIFFO_PG_CONTAINER` remain explicit overrides; only the *default* changed.
73
-
74
- set -eu
75
-
76
- REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
77
-
78
- # sha256sum is already a dependency of this script (see `fingerprint` below),
79
- # so reusing it here for a deterministic, cheap per-checkout key adds nothing
80
- # new to install. 12 hex chars is ample to keep collisions between checkouts
81
- # on one machine practically impossible while staying short enough to read in
82
- # `docker ps` output and a psql prompt.
83
- _checkout_key=$(printf '%s' "$REPO_ROOT" | sha256sum | cut -c1-12)
84
- _checkout_suffix=$(printf '%s' "$_checkout_key" | cut -c1-8)
85
- # IANA's dynamic/private port range (49152-65535, 16384 ports) mapped from the
86
- # next 4 hex chars of the same hash -- an ephemeral port picked deterministically
87
- # rather than asked of the OS, because a freshly-random port on every invocation
88
- # would break the reuse this key is for (a second run against the same checkout
89
- # has to land back on the same container to find its existing database).
90
- _checkout_port=$((49152 + (0x$(printf '%s' "$_checkout_key" | cut -c9-12) % 16384)))
91
-
92
- HOST="${BIFFO_PG_HOST:-localhost}"
93
- PORT="${BIFFO_PG_PORT:-$_checkout_port}"
94
- USER_="${BIFFO_PG_USER:-postgres}"
95
- PASS="${BIFFO_PG_PASSWORD:-postgres}"
96
- DB="${BIFFO_PG_DB:-biffo_test_$_checkout_suffix}"
97
- # Keyed the same way as PORT and for the same reason: the container is where
98
- # the port mapping actually lives (`docker run -p "$PORT:5432"`), so if the
99
- # container name stayed fixed while the port became per-checkout, a second
100
- # checkout would find the first checkout's container already occupying that
101
- # name, "start" it rather than create its own, and then poll forever against a
102
- # port that container was never bound to.
103
- CONTAINER="${BIFFO_PG_CONTAINER:-biffo-pg-test-$_checkout_suffix}"
104
-
105
- RECREATE=0
106
- EXPORT=0
107
- for arg in "$@"; do
108
- case "$arg" in
109
- --recreate) RECREATE=1 ;;
110
- --export) EXPORT=1 ;;
111
- -h | --help)
112
- sed -n '2,72p' "$0" | sed 's/^#\{1,2\} \{0,1\}//'
113
- exit 0
114
- ;;
115
- *)
116
- echo "unknown argument: $arg" >&2
117
- exit 2
118
- ;;
119
- esac
120
- done
121
-
122
- say() { echo "pg-test-db: $*" >&2; }
123
-
124
- cd "$REPO_ROOT"
125
-
126
- # --- what this repo's schema is made of --------------------------------------
127
- #
128
- # `db/imports/<name>/*.sql` is the Biffo DDL-import convention that the API's own
129
- # `ddl_import.list_sql_files` reads at startup, so deriving from it means this
130
- # script and the running app agree by construction rather than by someone
131
- # remembering to update both.
132
- DDL_FILES=$(find db/imports -mindepth 2 -maxdepth 2 -name '*.sql' 2>/dev/null | LC_ALL=C sort || true)
133
- ALEMBIC_DIR=""
134
- for _d in services/api .; do
135
- [ -f "$_d/alembic.ini" ] && ALEMBIC_DIR="$_d" && break
136
- done
137
-
138
- if [ -z "$DDL_FILES" ] && [ -z "$ALEMBIC_DIR" ]; then
139
- say "no db/imports/*/ DDL and no alembic.ini - this repo has no schema to build"
140
- exit 1
141
- fi
142
-
143
- # PostGIS or plain, decided by what the DDL asks for. A plain `postgres` image
144
- # fails on the first `CREATE EXTENSION postgis`, and picking the heavier image
145
- # unconditionally would slow every repo that does not need it.
146
- if [ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs grep -liE 'EXTENSION[[:space:]]+(IF[[:space:]]+NOT[[:space:]]+EXISTS[[:space:]]+)?postgis' >/dev/null 2>&1; then
147
- IMAGE="${BIFFO_PG_IMAGE:-postgis/postgis:16-3.4}"
148
- else
149
- IMAGE="${BIFFO_PG_IMAGE:-postgres:16}"
150
- fi
151
-
152
- export PGPASSWORD="$PASS"
153
- psql_admin() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d postgres "$@"; }
154
- psql_db() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" "$@"; }
155
-
156
- # --- 1. a reachable server ---------------------------------------------------
157
- #
158
- # Started here rather than assumed, because "docker run one yourself" is exactly
159
- # the tribal knowledge this script replaces. An already-running server is reused.
160
- if ! psql_admin -c 'SELECT 1' >/dev/null 2>&1; then
161
- if ! command -v docker >/dev/null 2>&1; then
162
- say "no Postgres at $HOST:$PORT and docker is not installed."
163
- say "Start one and re-run, or set BIFFO_PG_HOST / BIFFO_PG_PORT."
164
- exit 1
165
- fi
166
- if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then
167
- say "starting existing container $CONTAINER"
168
- docker start "$CONTAINER" >/dev/null
169
- else
170
- say "creating container $CONTAINER ($IMAGE) on port $PORT"
171
- docker run -d --name "$CONTAINER" \
172
- -e POSTGRES_PASSWORD="$PASS" -p "$PORT:5432" "$IMAGE" >/dev/null
173
- fi
174
- # Polled, not slept: a cold image pull and a warm restart differ by an order of
175
- # magnitude, and one fixed sleep is wrong for both.
176
- _waited=0
177
- until psql_admin -c 'SELECT 1' >/dev/null 2>&1; do
178
- _waited=$((_waited + 1))
179
- if [ "$_waited" -gt 90 ]; then
180
- say "Postgres did not become ready in 90s"
181
- exit 1
182
- fi
183
- sleep 1
184
- done
185
- say "Postgres ready after ${_waited}s"
186
- fi
187
-
188
- DSN="postgresql+asyncpg://$USER_:$PASS@$HOST:$PORT/$DB"
189
- emit() {
190
- if [ "$EXPORT" -eq 1 ]; then
191
- echo "export BIFFO_TEST_PG_DSN='$DSN'"
192
- else
193
- echo "$DSN"
194
- fi
195
- }
196
-
197
- # --- 2. is the existing schema current? --------------------------------------
198
- #
199
- # By CONTENT, not mtime: a branch switch changes content and leaves mtime
200
- # anywhere. Stored inside the database, so it cannot outlive a drop or describe
201
- # some other database.
202
- fingerprint() {
203
- {
204
- [ -n "$ALEMBIC_DIR" ] && find "$ALEMBIC_DIR" -name '*.py' -path '*alembic*' -type f 2>/dev/null |
205
- LC_ALL=C sort | xargs cat 2>/dev/null
206
- [ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs cat 2>/dev/null
207
- } | sha256sum | cut -d' ' -f1
208
- }
209
-
210
- WANT=$(fingerprint)
211
- HAVE=""
212
- if [ "$RECREATE" -eq 0 ] &&
213
- psql_admin -tAc "SELECT 1 FROM pg_database WHERE datname='$DB'" 2>/dev/null | grep -q 1; then
214
- HAVE=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
215
- -c "SELECT value FROM biffo_pg_test_fingerprint LIMIT 1" 2>/dev/null || true)
216
- fi
217
-
218
- if [ -n "$HAVE" ] && [ "$HAVE" = "$WANT" ]; then
219
- say "schema is current, reusing $DB"
220
- emit
221
- exit 0
222
- fi
223
-
224
- [ -n "$HAVE" ] && say "schema inputs changed - rebuilding rather than serving a stale schema"
225
-
226
- # --- 3. rebuild the way the app and CI do ------------------------------------
227
- say "rebuilding $DB"
228
- psql_admin -c "DROP DATABASE IF EXISTS $DB WITH (FORCE)" >/dev/null
229
- psql_admin -c "CREATE DATABASE $DB" >/dev/null
230
-
231
- if [ -n "$ALEMBIC_DIR" ]; then
232
- BIFFO_DATABASE_URL="$DSN" uv run --directory "$ALEMBIC_DIR" alembic upgrade head >/dev/null
233
- say "alembic upgrade head"
234
- fi
235
-
236
- if [ -n "$DDL_FILES" ]; then
237
- # ONE psql session, sorted by filename, mirroring the API's own DDL import.
238
- # Session state an early module sets -- typically `SET search_path` in the
239
- # first file -- has to survive into later ones, so a per-file connection would
240
- # silently change the meaning of every unqualified name after it. LC_ALL=C
241
- # keeps the shell's sort byte-ordered to match Python's.
242
- # shellcheck disable=SC2046
243
- psql -q -v ON_ERROR_STOP=1 -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
244
- --single-transaction $(echo "$DDL_FILES" | sed 's/^/-f /' | tr '\n' ' ') >/dev/null
245
- say "$(echo "$DDL_FILES" | wc -l | tr -d ' ') DDL modules applied"
246
- fi
247
-
248
- # --- 4. refuse to bless a half-built schema ----------------------------------
249
- #
250
- # The threshold is derived, not guessed: count the policies the DDL declares and
251
- # require the database to hold at least half. Recording a fingerprint against a
252
- # partial schema is worse than failing, because the NEXT run would trust it and
253
- # every failure after that would look like the developer's own change.
254
- if [ -n "$DDL_FILES" ]; then
255
- _declared=$(echo "$DDL_FILES" | xargs grep -ciE '^[[:space:]]*CREATE[[:space:]]+POLICY' 2>/dev/null |
256
- awk -F: '{s+=$NF} END {print s+0}')
257
- if [ "${_declared:-0}" -gt 0 ]; then
258
- _actual=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
259
- -c "SELECT count(*) FROM pg_policies" 2>/dev/null || echo 0)
260
- if [ "${_actual:-0}" -lt $((_declared / 2)) ]; then
261
- say "only ${_actual:-0} policies present against $_declared declared - the schema did not build."
262
- say "Not recording a fingerprint; fix the DDL and re-run."
263
- exit 1
264
- fi
265
- say "$_actual RLS policies ($_declared declared)"
266
- fi
267
- fi
268
-
269
- psql_db \
270
- -c "CREATE TABLE IF NOT EXISTS biffo_pg_test_fingerprint (value text primary key)" \
271
- -c "TRUNCATE biffo_pg_test_fingerprint" \
272
- -c "INSERT INTO biffo_pg_test_fingerprint (value) VALUES ('$WANT')" >/dev/null
273
-
274
- say "ready"
275
- emit
@@ -1,189 +0,0 @@
1
- #!/usr/bin/env sh
2
- # Refuse a force-push that has quietly started touching a file somebody else
3
- # just changed.
4
- #
5
- # ## The incident (tabsii-platform#446, 2026-07-31)
6
- #
7
- # A branch had to be rebuilt to purge a Secret Scan finding from its history.
8
- # The rebuild used:
9
- #
10
- # git reset --soft origin/dev && git add -A
11
- #
12
- # `origin/dev` had advanced by two merged PRs since the branch was cut. So
13
- # `reset --soft` moved HEAD onto the NEWER dev while the working tree still
14
- # held the OLDER copy of a doc those PRs had edited — and `git add -A`
15
- # faithfully staged the difference. The resulting commit, whose subject was
16
- # about RLS policies, silently REVERTED four lines of somebody else's merged
17
- # work.
18
- #
19
- # Nothing local saw it: `verify.sh` passed, 2884 tests passed, the real-Postgres
20
- # lane passed twice. It surfaced only because GitHub reported the PR as
21
- # CONFLICTING and somebody went looking for why. **Had those two PRs touched any
22
- # other file, there would have been no conflict and the revert would have
23
- # merged.** A conflict is not a detector; it is the lucky case.
24
- #
25
- # ## What this checks, and why it is narrow
26
- #
27
- # It fires only on the intersection of two conditions, because either alone is
28
- # far too noisy to be a gate:
29
- #
30
- # 1. The push is a REWRITE — the remote tip is not an ancestor of what you are
31
- # pushing. An ordinary fast-forward push cannot have this problem.
32
- # 2. A file the branch changes would be written back to content the
33
- # integration branch ALREADY HAD earlier in its history. Not new work: an
34
- # older copy.
35
- #
36
- # Condition 2 is the accidental-revert signature exactly, and it is the second
37
- # attempt. The first asked "has the integration branch moved this file since my
38
- # base?", which sounds equivalent and is not — `git reset --soft origin/dev`
39
- # makes your base the integration tip, so the answer is always no. **That
40
- # version scored a clean pass on a faithful reconstruction of the incident
41
- # above.** `cli/src/lib/rewrite-scope-check.test.ts` reinstates it to prove the
42
- # tests can tell the two apart.
43
- #
44
- # What stays silent, deliberately: amending to add a file nobody else has
45
- # touched; rebasing onto a newer integration branch; two branches making
46
- # genuinely different edits to one shared file. Only a STALE copy is a bug.
47
- #
48
- # ## What it deliberately does NOT do
49
- #
50
- # It does not try to decide whether a change is "intended". That needs intent,
51
- # which no gate has. It answers a narrower question answerable from history
52
- # alone: *is the content I am about to push actually older content?*
53
- #
54
- # It also never fails the push because it could not run. A guard that blocks
55
- # work when its own inputs are missing gets disabled, and then it protects
56
- # nothing. When it cannot answer it says so, loudly, and exits 0 — the same
57
- # contract `verify.sh` uses for a check whose tool is absent. Absence is
58
- # reported, never mistaken for a clean result.
59
-
60
- set -u
61
-
62
- ZERO=0000000000000000000000000000000000000000
63
- ESCAPE_HATCH=BIFFO_ALLOW_SCOPE_CHANGE
64
-
65
- note() { printf 'rewrite-scope: %s\n' "$1" >&2; }
66
-
67
- # The integration branch every Biffo repo uses (AGENTS.md §2). Resolved rather
68
- # than assumed: a repo whose remote HEAD says otherwise is a repo this check
69
- # should not guess about.
70
- integration=""
71
- for candidate in dev main master; do
72
- if git rev-parse --verify --quiet "refs/remotes/origin/$candidate" >/dev/null 2>&1; then
73
- integration="origin/$candidate"
74
- break
75
- fi
76
- done
77
-
78
- if [ -z "$integration" ]; then
79
- note "no origin/dev, origin/main or origin/master — check did NOT run"
80
- exit 0
81
- fi
82
-
83
- # The inner filter below runs in a subshell (it is on the right of a pipe), so
84
- # it cannot assign to a variable in this shell. Collect through a file instead.
85
- FINDINGS_FILE=$(mktemp) || {
86
- note "could not create a temp file — check did NOT run"
87
- exit 0
88
- }
89
- trap 'rm -f "$FINDINGS_FILE"' EXIT HUP INT TERM
90
-
91
- # git feeds pre-push one line per ref: <local ref> <local sha> <remote ref> <remote sha>
92
- while read -r _local_ref local_sha _remote_ref remote_sha; do
93
- # Branch deletion, or nothing to push.
94
- [ "$local_sha" = "$ZERO" ] && continue
95
- # Brand-new remote branch: there is no previous scope to compare against.
96
- [ "$remote_sha" = "$ZERO" ] && continue
97
- # The remote may not have the old object locally (shallow clone, pruned).
98
- git cat-file -e "$remote_sha^{commit}" 2>/dev/null || {
99
- note "remote tip $remote_sha not available locally — check did NOT run for this ref"
100
- continue
101
- }
102
- # Fast-forward: not a rewrite, cannot exhibit this defect.
103
- if git merge-base --is-ancestor "$remote_sha" "$local_sha" 2>/dev/null; then
104
- continue
105
- fi
106
-
107
- new_base=$(git merge-base "$local_sha" "$integration" 2>/dev/null) || continue
108
- old_base=$(git merge-base "$remote_sha" "$integration" 2>/dev/null) || continue
109
-
110
- # Every file this branch claims to change, relative to where it forked.
111
- #
112
- # Iterated with `while read`, not `for file in $scope`: word splitting would
113
- # break any path containing a space, and a guard that silently skips the file
114
- # it should have flagged is worse than no guard. Process substitution
115
- # (`grep -f <(...)`) is avoided for the same class of reason — it is a
116
- # bashism, and this runs under `sh` from the pre-push hook.
117
- git diff --name-only "$new_base" "$local_sha" 2>/dev/null | while IFS= read -r file; do
118
- [ -z "$file" ] && continue
119
-
120
- mine=$(git rev-parse --quiet --verify "$local_sha:$file" 2>/dev/null) || continue
121
- theirs=$(git rev-parse --quiet --verify "$integration:$file" 2>/dev/null) || continue
122
- # Agreeing with the integration branch is the normal, safe case.
123
- [ "$mine" = "$theirs" ] && continue
124
-
125
- # The signature of an accidental revert: the content you are about to push
126
- # is not new work, it is a version this file ALREADY HAD earlier in the
127
- # integration branch's history.
128
- #
129
- # This is what makes the check work at all. The first version asked "has
130
- # the integration branch moved this file since my base?", which sounds
131
- # equivalent and is not: `git reset --soft origin/dev` makes your base the
132
- # integration tip, so the answer is always no and the guard never fires.
133
- # Verified against a reconstruction of tabsii-platform#446 — the earlier
134
- # condition scored a clean pass on the exact commit that reverted two
135
- # merged PRs.
136
- #
137
- # Bounded to recent history: an unbounded walk on a long-lived file costs
138
- # more than a pre-push gate should, and a stale copy older than this is not
139
- # the accident this catches.
140
- git log --format=%H -n 200 "$integration" -- "$file" 2>/dev/null | while IFS= read -r commit; do
141
- [ -z "$commit" ] && continue
142
- past=$(git rev-parse --quiet --verify "$commit:$file" 2>/dev/null) || continue
143
- if [ "$past" = "$mine" ]; then
144
- printf '%s\n' "$file"
145
- break
146
- fi
147
- done
148
- done >>"$FINDINGS_FILE"
149
- done
150
-
151
- findings=$(cat "$FINDINGS_FILE" 2>/dev/null)
152
-
153
- [ -z "$findings" ] && exit 0
154
-
155
- if [ -n "${BIFFO_ALLOW_SCOPE_CHANGE:-}" ]; then
156
- note "scope change allowed via $ESCAPE_HATCH:"
157
- printf '%s' "$findings" | sed 's/^/ /' >&2
158
- exit 0
159
- fi
160
-
161
- cat >&2 <<EOF
162
-
163
- This force-push would write a version of these file(s) that $integration
164
- ALREADY HAD earlier in its history — not new work, an older copy:
165
-
166
- $(printf '%s' "$findings" | sed 's/^/ /')
167
-
168
- That is the signature of an accidental revert. A rewrite (rebase, amend,
169
- reset --soft) picked up a stale copy of somebody else's merged work and
170
- recorded it as your change. It merges silently unless it happens to
171
- conflict, and no test can see it — the content is valid, just old.
172
-
173
- Check what you are actually about to write:
174
-
175
- git diff $integration...HEAD -- <file>
176
-
177
- If it is intended, push again with:
178
-
179
- $ESCAPE_HATCH=1 git push ...
180
-
181
- If it is not: restore the file and amend —
182
-
183
- git checkout $integration -- <file> && git commit --amend --no-edit
184
-
185
- Rebuilding a branch? Reset to a FIXED SHA (the branch point), never to a
186
- moving ref like $integration.
187
-
188
- EOF
189
- exit 1