@biffo/cli 0.212.1 → 0.214.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/scripts/pg-test-db.sh +232 -0
- package/_skeletons/plugin-template/scripts/verify.sh +18 -0
- package/_skeletons/sibling-template/scripts/pg-test-db.sh +232 -0
- package/_skeletons/sibling-template/scripts/verify.sh +18 -0
- package/dist/index.js +106 -49
- package/package.json +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
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
|
+
set -eu
|
|
53
|
+
|
|
54
|
+
HOST="${BIFFO_PG_HOST:-localhost}"
|
|
55
|
+
PORT="${BIFFO_PG_PORT:-55432}"
|
|
56
|
+
USER_="${BIFFO_PG_USER:-postgres}"
|
|
57
|
+
PASS="${BIFFO_PG_PASSWORD:-postgres}"
|
|
58
|
+
DB="${BIFFO_PG_DB:-biffo_test}"
|
|
59
|
+
CONTAINER="${BIFFO_PG_CONTAINER:-biffo-pg-test}"
|
|
60
|
+
|
|
61
|
+
RECREATE=0
|
|
62
|
+
EXPORT=0
|
|
63
|
+
for arg in "$@"; do
|
|
64
|
+
case "$arg" in
|
|
65
|
+
--recreate) RECREATE=1 ;;
|
|
66
|
+
--export) EXPORT=1 ;;
|
|
67
|
+
-h | --help)
|
|
68
|
+
sed -n '2,48p' "$0" | sed 's/^#\{1,2\} \{0,1\}//'
|
|
69
|
+
exit 0
|
|
70
|
+
;;
|
|
71
|
+
*)
|
|
72
|
+
echo "unknown argument: $arg" >&2
|
|
73
|
+
exit 2
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
say() { echo "pg-test-db: $*" >&2; }
|
|
79
|
+
|
|
80
|
+
REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
|
81
|
+
cd "$REPO_ROOT"
|
|
82
|
+
|
|
83
|
+
# --- what this repo's schema is made of --------------------------------------
|
|
84
|
+
#
|
|
85
|
+
# `db/imports/<name>/*.sql` is the Biffo DDL-import convention that the API's own
|
|
86
|
+
# `ddl_import.list_sql_files` reads at startup, so deriving from it means this
|
|
87
|
+
# script and the running app agree by construction rather than by someone
|
|
88
|
+
# remembering to update both.
|
|
89
|
+
DDL_FILES=$(find db/imports -mindepth 2 -maxdepth 2 -name '*.sql' 2>/dev/null | LC_ALL=C sort || true)
|
|
90
|
+
ALEMBIC_DIR=""
|
|
91
|
+
for _d in services/api .; do
|
|
92
|
+
[ -f "$_d/alembic.ini" ] && ALEMBIC_DIR="$_d" && break
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
if [ -z "$DDL_FILES" ] && [ -z "$ALEMBIC_DIR" ]; then
|
|
96
|
+
say "no db/imports/*/ DDL and no alembic.ini - this repo has no schema to build"
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# PostGIS or plain, decided by what the DDL asks for. A plain `postgres` image
|
|
101
|
+
# fails on the first `CREATE EXTENSION postgis`, and picking the heavier image
|
|
102
|
+
# unconditionally would slow every repo that does not need it.
|
|
103
|
+
if [ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs grep -liE 'EXTENSION[[:space:]]+(IF[[:space:]]+NOT[[:space:]]+EXISTS[[:space:]]+)?postgis' >/dev/null 2>&1; then
|
|
104
|
+
IMAGE="${BIFFO_PG_IMAGE:-postgis/postgis:16-3.4}"
|
|
105
|
+
else
|
|
106
|
+
IMAGE="${BIFFO_PG_IMAGE:-postgres:16}"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
export PGPASSWORD="$PASS"
|
|
110
|
+
psql_admin() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d postgres "$@"; }
|
|
111
|
+
psql_db() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" "$@"; }
|
|
112
|
+
|
|
113
|
+
# --- 1. a reachable server ---------------------------------------------------
|
|
114
|
+
#
|
|
115
|
+
# Started here rather than assumed, because "docker run one yourself" is exactly
|
|
116
|
+
# the tribal knowledge this script replaces. An already-running server is reused.
|
|
117
|
+
if ! psql_admin -c 'SELECT 1' >/dev/null 2>&1; then
|
|
118
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
119
|
+
say "no Postgres at $HOST:$PORT and docker is not installed."
|
|
120
|
+
say "Start one and re-run, or set BIFFO_PG_HOST / BIFFO_PG_PORT."
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then
|
|
124
|
+
say "starting existing container $CONTAINER"
|
|
125
|
+
docker start "$CONTAINER" >/dev/null
|
|
126
|
+
else
|
|
127
|
+
say "creating container $CONTAINER ($IMAGE) on port $PORT"
|
|
128
|
+
docker run -d --name "$CONTAINER" \
|
|
129
|
+
-e POSTGRES_PASSWORD="$PASS" -p "$PORT:5432" "$IMAGE" >/dev/null
|
|
130
|
+
fi
|
|
131
|
+
# Polled, not slept: a cold image pull and a warm restart differ by an order of
|
|
132
|
+
# magnitude, and one fixed sleep is wrong for both.
|
|
133
|
+
_waited=0
|
|
134
|
+
until psql_admin -c 'SELECT 1' >/dev/null 2>&1; do
|
|
135
|
+
_waited=$((_waited + 1))
|
|
136
|
+
if [ "$_waited" -gt 90 ]; then
|
|
137
|
+
say "Postgres did not become ready in 90s"
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
sleep 1
|
|
141
|
+
done
|
|
142
|
+
say "Postgres ready after ${_waited}s"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
DSN="postgresql+asyncpg://$USER_:$PASS@$HOST:$PORT/$DB"
|
|
146
|
+
emit() {
|
|
147
|
+
if [ "$EXPORT" -eq 1 ]; then
|
|
148
|
+
echo "export BIFFO_TEST_PG_DSN='$DSN'"
|
|
149
|
+
else
|
|
150
|
+
echo "$DSN"
|
|
151
|
+
fi
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# --- 2. is the existing schema current? --------------------------------------
|
|
155
|
+
#
|
|
156
|
+
# By CONTENT, not mtime: a branch switch changes content and leaves mtime
|
|
157
|
+
# anywhere. Stored inside the database, so it cannot outlive a drop or describe
|
|
158
|
+
# some other database.
|
|
159
|
+
fingerprint() {
|
|
160
|
+
{
|
|
161
|
+
[ -n "$ALEMBIC_DIR" ] && find "$ALEMBIC_DIR" -name '*.py' -path '*alembic*' -type f 2>/dev/null |
|
|
162
|
+
LC_ALL=C sort | xargs cat 2>/dev/null
|
|
163
|
+
[ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs cat 2>/dev/null
|
|
164
|
+
} | sha256sum | cut -d' ' -f1
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
WANT=$(fingerprint)
|
|
168
|
+
HAVE=""
|
|
169
|
+
if [ "$RECREATE" -eq 0 ] &&
|
|
170
|
+
psql_admin -tAc "SELECT 1 FROM pg_database WHERE datname='$DB'" 2>/dev/null | grep -q 1; then
|
|
171
|
+
HAVE=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
172
|
+
-c "SELECT value FROM biffo_pg_test_fingerprint LIMIT 1" 2>/dev/null || true)
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
if [ -n "$HAVE" ] && [ "$HAVE" = "$WANT" ]; then
|
|
176
|
+
say "schema is current, reusing $DB"
|
|
177
|
+
emit
|
|
178
|
+
exit 0
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
[ -n "$HAVE" ] && say "schema inputs changed - rebuilding rather than serving a stale schema"
|
|
182
|
+
|
|
183
|
+
# --- 3. rebuild the way the app and CI do ------------------------------------
|
|
184
|
+
say "rebuilding $DB"
|
|
185
|
+
psql_admin -c "DROP DATABASE IF EXISTS $DB WITH (FORCE)" >/dev/null
|
|
186
|
+
psql_admin -c "CREATE DATABASE $DB" >/dev/null
|
|
187
|
+
|
|
188
|
+
if [ -n "$ALEMBIC_DIR" ]; then
|
|
189
|
+
BIFFO_DATABASE_URL="$DSN" uv run --directory "$ALEMBIC_DIR" alembic upgrade head >/dev/null
|
|
190
|
+
say "alembic upgrade head"
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
if [ -n "$DDL_FILES" ]; then
|
|
194
|
+
# ONE psql session, sorted by filename, mirroring the API's own DDL import.
|
|
195
|
+
# Session state an early module sets -- typically `SET search_path` in the
|
|
196
|
+
# first file -- has to survive into later ones, so a per-file connection would
|
|
197
|
+
# silently change the meaning of every unqualified name after it. LC_ALL=C
|
|
198
|
+
# keeps the shell's sort byte-ordered to match Python's.
|
|
199
|
+
# shellcheck disable=SC2046
|
|
200
|
+
psql -q -v ON_ERROR_STOP=1 -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
201
|
+
--single-transaction $(echo "$DDL_FILES" | sed 's/^/-f /' | tr '\n' ' ') >/dev/null
|
|
202
|
+
say "$(echo "$DDL_FILES" | wc -l | tr -d ' ') DDL modules applied"
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
# --- 4. refuse to bless a half-built schema ----------------------------------
|
|
206
|
+
#
|
|
207
|
+
# The threshold is derived, not guessed: count the policies the DDL declares and
|
|
208
|
+
# require the database to hold at least half. Recording a fingerprint against a
|
|
209
|
+
# partial schema is worse than failing, because the NEXT run would trust it and
|
|
210
|
+
# every failure after that would look like the developer's own change.
|
|
211
|
+
if [ -n "$DDL_FILES" ]; then
|
|
212
|
+
_declared=$(echo "$DDL_FILES" | xargs grep -ciE '^[[:space:]]*CREATE[[:space:]]+POLICY' 2>/dev/null |
|
|
213
|
+
awk -F: '{s+=$NF} END {print s+0}')
|
|
214
|
+
if [ "${_declared:-0}" -gt 0 ]; then
|
|
215
|
+
_actual=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
216
|
+
-c "SELECT count(*) FROM pg_policies" 2>/dev/null || echo 0)
|
|
217
|
+
if [ "${_actual:-0}" -lt $((_declared / 2)) ]; then
|
|
218
|
+
say "only ${_actual:-0} policies present against $_declared declared - the schema did not build."
|
|
219
|
+
say "Not recording a fingerprint; fix the DDL and re-run."
|
|
220
|
+
exit 1
|
|
221
|
+
fi
|
|
222
|
+
say "$_actual RLS policies ($_declared declared)"
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
psql_db \
|
|
227
|
+
-c "CREATE TABLE IF NOT EXISTS biffo_pg_test_fingerprint (value text primary key)" \
|
|
228
|
+
-c "TRUNCATE biffo_pg_test_fingerprint" \
|
|
229
|
+
-c "INSERT INTO biffo_pg_test_fingerprint (value) VALUES ('$WANT')" >/dev/null
|
|
230
|
+
|
|
231
|
+
say "ready"
|
|
232
|
+
emit
|
|
@@ -478,6 +478,23 @@ pg_test_run() {
|
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
_pg_modules=$(pg_test_modules)
|
|
481
|
+
|
|
482
|
+
# Provision the database rather than requiring the operator to remember.
|
|
483
|
+
#
|
|
484
|
+
# A gate that only runs when you exported the right variable is a gate that runs
|
|
485
|
+
# on the days you did not need it. `scripts/pg-test-db.sh` is idempotent and
|
|
486
|
+
# cheap when the schema is unchanged (~0.3s; ~4s when it genuinely has to
|
|
487
|
+
# rebuild), so calling it is better than warning about it. Failure is silent
|
|
488
|
+
# BECAUSE the WARN below is the honest report of it -- no Docker, no server, no
|
|
489
|
+
# schema all end in the same place: the lane did not run, and the gate says so.
|
|
490
|
+
if [ -z "$PG_TEST_DSN" ] && [ -n "$_pg_modules" ] && [ -z "$LIST" ] && [ -f scripts/pg-test-db.sh ]; then
|
|
491
|
+
PG_TEST_DSN=$(sh scripts/pg-test-db.sh 2>/dev/null | tail -1) || PG_TEST_DSN=""
|
|
492
|
+
case "$PG_TEST_DSN" in
|
|
493
|
+
postgres*) ;;
|
|
494
|
+
*) PG_TEST_DSN="" ;;
|
|
495
|
+
esac
|
|
496
|
+
fi
|
|
497
|
+
|
|
481
498
|
# Order matters, and getting it wrong made these very tests machine-dependent:
|
|
482
499
|
# with `uv not installed` checked FIRST, a runner without uv skipped quietly and
|
|
483
500
|
# the gap warning never printed -- green on a workstation, red on CI, for a
|
|
@@ -550,6 +567,7 @@ fi
|
|
|
550
567
|
if [ -f scripts/biffo.sh ]; then
|
|
551
568
|
run_check plugin-tf sh scripts/biffo.sh check plugin-terraform
|
|
552
569
|
run_check plugin-names sh scripts/biffo.sh check plugin-collisions
|
|
570
|
+
run_check adr-numbering sh scripts/biffo.sh check adr-numbering
|
|
553
571
|
else
|
|
554
572
|
skip biffo-guards "no scripts/biffo.sh in this repo"
|
|
555
573
|
fi
|
|
@@ -0,0 +1,232 @@
|
|
|
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
|
+
set -eu
|
|
53
|
+
|
|
54
|
+
HOST="${BIFFO_PG_HOST:-localhost}"
|
|
55
|
+
PORT="${BIFFO_PG_PORT:-55432}"
|
|
56
|
+
USER_="${BIFFO_PG_USER:-postgres}"
|
|
57
|
+
PASS="${BIFFO_PG_PASSWORD:-postgres}"
|
|
58
|
+
DB="${BIFFO_PG_DB:-biffo_test}"
|
|
59
|
+
CONTAINER="${BIFFO_PG_CONTAINER:-biffo-pg-test}"
|
|
60
|
+
|
|
61
|
+
RECREATE=0
|
|
62
|
+
EXPORT=0
|
|
63
|
+
for arg in "$@"; do
|
|
64
|
+
case "$arg" in
|
|
65
|
+
--recreate) RECREATE=1 ;;
|
|
66
|
+
--export) EXPORT=1 ;;
|
|
67
|
+
-h | --help)
|
|
68
|
+
sed -n '2,48p' "$0" | sed 's/^#\{1,2\} \{0,1\}//'
|
|
69
|
+
exit 0
|
|
70
|
+
;;
|
|
71
|
+
*)
|
|
72
|
+
echo "unknown argument: $arg" >&2
|
|
73
|
+
exit 2
|
|
74
|
+
;;
|
|
75
|
+
esac
|
|
76
|
+
done
|
|
77
|
+
|
|
78
|
+
say() { echo "pg-test-db: $*" >&2; }
|
|
79
|
+
|
|
80
|
+
REPO_ROOT=$(cd "$(dirname "$0")/.." && pwd)
|
|
81
|
+
cd "$REPO_ROOT"
|
|
82
|
+
|
|
83
|
+
# --- what this repo's schema is made of --------------------------------------
|
|
84
|
+
#
|
|
85
|
+
# `db/imports/<name>/*.sql` is the Biffo DDL-import convention that the API's own
|
|
86
|
+
# `ddl_import.list_sql_files` reads at startup, so deriving from it means this
|
|
87
|
+
# script and the running app agree by construction rather than by someone
|
|
88
|
+
# remembering to update both.
|
|
89
|
+
DDL_FILES=$(find db/imports -mindepth 2 -maxdepth 2 -name '*.sql' 2>/dev/null | LC_ALL=C sort || true)
|
|
90
|
+
ALEMBIC_DIR=""
|
|
91
|
+
for _d in services/api .; do
|
|
92
|
+
[ -f "$_d/alembic.ini" ] && ALEMBIC_DIR="$_d" && break
|
|
93
|
+
done
|
|
94
|
+
|
|
95
|
+
if [ -z "$DDL_FILES" ] && [ -z "$ALEMBIC_DIR" ]; then
|
|
96
|
+
say "no db/imports/*/ DDL and no alembic.ini - this repo has no schema to build"
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# PostGIS or plain, decided by what the DDL asks for. A plain `postgres` image
|
|
101
|
+
# fails on the first `CREATE EXTENSION postgis`, and picking the heavier image
|
|
102
|
+
# unconditionally would slow every repo that does not need it.
|
|
103
|
+
if [ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs grep -liE 'EXTENSION[[:space:]]+(IF[[:space:]]+NOT[[:space:]]+EXISTS[[:space:]]+)?postgis' >/dev/null 2>&1; then
|
|
104
|
+
IMAGE="${BIFFO_PG_IMAGE:-postgis/postgis:16-3.4}"
|
|
105
|
+
else
|
|
106
|
+
IMAGE="${BIFFO_PG_IMAGE:-postgres:16}"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
export PGPASSWORD="$PASS"
|
|
110
|
+
psql_admin() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d postgres "$@"; }
|
|
111
|
+
psql_db() { psql -q -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" "$@"; }
|
|
112
|
+
|
|
113
|
+
# --- 1. a reachable server ---------------------------------------------------
|
|
114
|
+
#
|
|
115
|
+
# Started here rather than assumed, because "docker run one yourself" is exactly
|
|
116
|
+
# the tribal knowledge this script replaces. An already-running server is reused.
|
|
117
|
+
if ! psql_admin -c 'SELECT 1' >/dev/null 2>&1; then
|
|
118
|
+
if ! command -v docker >/dev/null 2>&1; then
|
|
119
|
+
say "no Postgres at $HOST:$PORT and docker is not installed."
|
|
120
|
+
say "Start one and re-run, or set BIFFO_PG_HOST / BIFFO_PG_PORT."
|
|
121
|
+
exit 1
|
|
122
|
+
fi
|
|
123
|
+
if docker ps -a --format '{{.Names}}' | grep -qx "$CONTAINER"; then
|
|
124
|
+
say "starting existing container $CONTAINER"
|
|
125
|
+
docker start "$CONTAINER" >/dev/null
|
|
126
|
+
else
|
|
127
|
+
say "creating container $CONTAINER ($IMAGE) on port $PORT"
|
|
128
|
+
docker run -d --name "$CONTAINER" \
|
|
129
|
+
-e POSTGRES_PASSWORD="$PASS" -p "$PORT:5432" "$IMAGE" >/dev/null
|
|
130
|
+
fi
|
|
131
|
+
# Polled, not slept: a cold image pull and a warm restart differ by an order of
|
|
132
|
+
# magnitude, and one fixed sleep is wrong for both.
|
|
133
|
+
_waited=0
|
|
134
|
+
until psql_admin -c 'SELECT 1' >/dev/null 2>&1; do
|
|
135
|
+
_waited=$((_waited + 1))
|
|
136
|
+
if [ "$_waited" -gt 90 ]; then
|
|
137
|
+
say "Postgres did not become ready in 90s"
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
sleep 1
|
|
141
|
+
done
|
|
142
|
+
say "Postgres ready after ${_waited}s"
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
DSN="postgresql+asyncpg://$USER_:$PASS@$HOST:$PORT/$DB"
|
|
146
|
+
emit() {
|
|
147
|
+
if [ "$EXPORT" -eq 1 ]; then
|
|
148
|
+
echo "export BIFFO_TEST_PG_DSN='$DSN'"
|
|
149
|
+
else
|
|
150
|
+
echo "$DSN"
|
|
151
|
+
fi
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# --- 2. is the existing schema current? --------------------------------------
|
|
155
|
+
#
|
|
156
|
+
# By CONTENT, not mtime: a branch switch changes content and leaves mtime
|
|
157
|
+
# anywhere. Stored inside the database, so it cannot outlive a drop or describe
|
|
158
|
+
# some other database.
|
|
159
|
+
fingerprint() {
|
|
160
|
+
{
|
|
161
|
+
[ -n "$ALEMBIC_DIR" ] && find "$ALEMBIC_DIR" -name '*.py' -path '*alembic*' -type f 2>/dev/null |
|
|
162
|
+
LC_ALL=C sort | xargs cat 2>/dev/null
|
|
163
|
+
[ -n "$DDL_FILES" ] && echo "$DDL_FILES" | xargs cat 2>/dev/null
|
|
164
|
+
} | sha256sum | cut -d' ' -f1
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
WANT=$(fingerprint)
|
|
168
|
+
HAVE=""
|
|
169
|
+
if [ "$RECREATE" -eq 0 ] &&
|
|
170
|
+
psql_admin -tAc "SELECT 1 FROM pg_database WHERE datname='$DB'" 2>/dev/null | grep -q 1; then
|
|
171
|
+
HAVE=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
172
|
+
-c "SELECT value FROM biffo_pg_test_fingerprint LIMIT 1" 2>/dev/null || true)
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
if [ -n "$HAVE" ] && [ "$HAVE" = "$WANT" ]; then
|
|
176
|
+
say "schema is current, reusing $DB"
|
|
177
|
+
emit
|
|
178
|
+
exit 0
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
[ -n "$HAVE" ] && say "schema inputs changed - rebuilding rather than serving a stale schema"
|
|
182
|
+
|
|
183
|
+
# --- 3. rebuild the way the app and CI do ------------------------------------
|
|
184
|
+
say "rebuilding $DB"
|
|
185
|
+
psql_admin -c "DROP DATABASE IF EXISTS $DB WITH (FORCE)" >/dev/null
|
|
186
|
+
psql_admin -c "CREATE DATABASE $DB" >/dev/null
|
|
187
|
+
|
|
188
|
+
if [ -n "$ALEMBIC_DIR" ]; then
|
|
189
|
+
BIFFO_DATABASE_URL="$DSN" uv run --directory "$ALEMBIC_DIR" alembic upgrade head >/dev/null
|
|
190
|
+
say "alembic upgrade head"
|
|
191
|
+
fi
|
|
192
|
+
|
|
193
|
+
if [ -n "$DDL_FILES" ]; then
|
|
194
|
+
# ONE psql session, sorted by filename, mirroring the API's own DDL import.
|
|
195
|
+
# Session state an early module sets -- typically `SET search_path` in the
|
|
196
|
+
# first file -- has to survive into later ones, so a per-file connection would
|
|
197
|
+
# silently change the meaning of every unqualified name after it. LC_ALL=C
|
|
198
|
+
# keeps the shell's sort byte-ordered to match Python's.
|
|
199
|
+
# shellcheck disable=SC2046
|
|
200
|
+
psql -q -v ON_ERROR_STOP=1 -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
201
|
+
--single-transaction $(echo "$DDL_FILES" | sed 's/^/-f /' | tr '\n' ' ') >/dev/null
|
|
202
|
+
say "$(echo "$DDL_FILES" | wc -l | tr -d ' ') DDL modules applied"
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
# --- 4. refuse to bless a half-built schema ----------------------------------
|
|
206
|
+
#
|
|
207
|
+
# The threshold is derived, not guessed: count the policies the DDL declares and
|
|
208
|
+
# require the database to hold at least half. Recording a fingerprint against a
|
|
209
|
+
# partial schema is worse than failing, because the NEXT run would trust it and
|
|
210
|
+
# every failure after that would look like the developer's own change.
|
|
211
|
+
if [ -n "$DDL_FILES" ]; then
|
|
212
|
+
_declared=$(echo "$DDL_FILES" | xargs grep -ciE '^[[:space:]]*CREATE[[:space:]]+POLICY' 2>/dev/null |
|
|
213
|
+
awk -F: '{s+=$NF} END {print s+0}')
|
|
214
|
+
if [ "${_declared:-0}" -gt 0 ]; then
|
|
215
|
+
_actual=$(psql -tAq -h "$HOST" -p "$PORT" -U "$USER_" -d "$DB" \
|
|
216
|
+
-c "SELECT count(*) FROM pg_policies" 2>/dev/null || echo 0)
|
|
217
|
+
if [ "${_actual:-0}" -lt $((_declared / 2)) ]; then
|
|
218
|
+
say "only ${_actual:-0} policies present against $_declared declared - the schema did not build."
|
|
219
|
+
say "Not recording a fingerprint; fix the DDL and re-run."
|
|
220
|
+
exit 1
|
|
221
|
+
fi
|
|
222
|
+
say "$_actual RLS policies ($_declared declared)"
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
psql_db \
|
|
227
|
+
-c "CREATE TABLE IF NOT EXISTS biffo_pg_test_fingerprint (value text primary key)" \
|
|
228
|
+
-c "TRUNCATE biffo_pg_test_fingerprint" \
|
|
229
|
+
-c "INSERT INTO biffo_pg_test_fingerprint (value) VALUES ('$WANT')" >/dev/null
|
|
230
|
+
|
|
231
|
+
say "ready"
|
|
232
|
+
emit
|
|
@@ -478,6 +478,23 @@ pg_test_run() {
|
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
_pg_modules=$(pg_test_modules)
|
|
481
|
+
|
|
482
|
+
# Provision the database rather than requiring the operator to remember.
|
|
483
|
+
#
|
|
484
|
+
# A gate that only runs when you exported the right variable is a gate that runs
|
|
485
|
+
# on the days you did not need it. `scripts/pg-test-db.sh` is idempotent and
|
|
486
|
+
# cheap when the schema is unchanged (~0.3s; ~4s when it genuinely has to
|
|
487
|
+
# rebuild), so calling it is better than warning about it. Failure is silent
|
|
488
|
+
# BECAUSE the WARN below is the honest report of it -- no Docker, no server, no
|
|
489
|
+
# schema all end in the same place: the lane did not run, and the gate says so.
|
|
490
|
+
if [ -z "$PG_TEST_DSN" ] && [ -n "$_pg_modules" ] && [ -z "$LIST" ] && [ -f scripts/pg-test-db.sh ]; then
|
|
491
|
+
PG_TEST_DSN=$(sh scripts/pg-test-db.sh 2>/dev/null | tail -1) || PG_TEST_DSN=""
|
|
492
|
+
case "$PG_TEST_DSN" in
|
|
493
|
+
postgres*) ;;
|
|
494
|
+
*) PG_TEST_DSN="" ;;
|
|
495
|
+
esac
|
|
496
|
+
fi
|
|
497
|
+
|
|
481
498
|
# Order matters, and getting it wrong made these very tests machine-dependent:
|
|
482
499
|
# with `uv not installed` checked FIRST, a runner without uv skipped quietly and
|
|
483
500
|
# the gap warning never printed -- green on a workstation, red on CI, for a
|
|
@@ -550,6 +567,7 @@ fi
|
|
|
550
567
|
if [ -f scripts/biffo.sh ]; then
|
|
551
568
|
run_check plugin-tf sh scripts/biffo.sh check plugin-terraform
|
|
552
569
|
run_check plugin-names sh scripts/biffo.sh check plugin-collisions
|
|
570
|
+
run_check adr-numbering sh scripts/biffo.sh check adr-numbering
|
|
553
571
|
else
|
|
554
572
|
skip biffo-guards "no scripts/biffo.sh in this repo"
|
|
555
573
|
fi
|
package/dist/index.js
CHANGED
|
@@ -9124,9 +9124,61 @@ siblingCommand.addCommand(siblingCheckIdentityCommand);
|
|
|
9124
9124
|
// src/commands/check.ts
|
|
9125
9125
|
import { Command as Command23 } from "commander";
|
|
9126
9126
|
|
|
9127
|
+
// src/scripts/check-adr-numbering.ts
|
|
9128
|
+
import { existsSync as existsSync32 } from "fs";
|
|
9129
|
+
import { join as join31 } from "path";
|
|
9130
|
+
import { execa as execa5 } from "execa";
|
|
9131
|
+
|
|
9132
|
+
// src/lib/adr-numbering-guard.ts
|
|
9133
|
+
import { existsSync as existsSync31, readdirSync as readdirSync13 } from "fs";
|
|
9134
|
+
var ADR_FILENAME = /^(\d{4})-.+\.md$/;
|
|
9135
|
+
function adrNumbersIn(adrDir) {
|
|
9136
|
+
const claims = /* @__PURE__ */ new Map();
|
|
9137
|
+
if (!existsSync31(adrDir)) return claims;
|
|
9138
|
+
for (const entry of readdirSync13(adrDir).sort()) {
|
|
9139
|
+
const match = ADR_FILENAME.exec(entry);
|
|
9140
|
+
if (!match) continue;
|
|
9141
|
+
const number = match[1];
|
|
9142
|
+
claims.set(number, [...claims.get(number) ?? [], entry]);
|
|
9143
|
+
}
|
|
9144
|
+
return claims;
|
|
9145
|
+
}
|
|
9146
|
+
function findAdrNumberCollisions(adrDir) {
|
|
9147
|
+
const collisions = [];
|
|
9148
|
+
for (const [number, files] of [...adrNumbersIn(adrDir).entries()].sort()) {
|
|
9149
|
+
if (files.length > 1) collisions.push({ number, files: [...files].sort() });
|
|
9150
|
+
}
|
|
9151
|
+
return collisions;
|
|
9152
|
+
}
|
|
9153
|
+
function formatAdrNumberCollisions(collisions) {
|
|
9154
|
+
return collisions.map(
|
|
9155
|
+
(c) => ` ADR-${c.number} is claimed by: ${c.files.join(", ")}
|
|
9156
|
+
Pick a different number for the newer one \u2014 citing "ADR-${c.number}" is
|
|
9157
|
+
ambiguous while both exist.`
|
|
9158
|
+
).join("\n");
|
|
9159
|
+
}
|
|
9160
|
+
|
|
9161
|
+
// src/scripts/check-adr-numbering.ts
|
|
9162
|
+
async function runAdrNumberingCheck() {
|
|
9163
|
+
const root = (await execa5("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
9164
|
+
const adrDir = join31(root, "docs", "ADR");
|
|
9165
|
+
if (!existsSync32(adrDir)) {
|
|
9166
|
+
console.log("\u2713 ADR numbering guard: no docs/ADR/ directory \u2014 nothing to compare");
|
|
9167
|
+
return;
|
|
9168
|
+
}
|
|
9169
|
+
const collisions = findAdrNumberCollisions(adrDir);
|
|
9170
|
+
if (collisions.length > 0) {
|
|
9171
|
+
console.error("\u2717 ADR numbering guard: two ADRs in docs/ADR/ share a number\n");
|
|
9172
|
+
console.error(formatAdrNumberCollisions(collisions));
|
|
9173
|
+
console.error("\nSee tabsii-platform#449 for how this class of collision happens.");
|
|
9174
|
+
process.exit(1);
|
|
9175
|
+
}
|
|
9176
|
+
console.log("\u2713 ADR numbering guard: OK");
|
|
9177
|
+
}
|
|
9178
|
+
|
|
9127
9179
|
// src/scripts/check-branch-protection.ts
|
|
9128
9180
|
import { Octokit as Octokit2 } from "@octokit/rest";
|
|
9129
|
-
import { execa as
|
|
9181
|
+
import { execa as execa6 } from "execa";
|
|
9130
9182
|
|
|
9131
9183
|
// src/lib/branch-protection-apply.ts
|
|
9132
9184
|
var CONTEXT_CONSISTENCY_THRESHOLD = 2 / 3;
|
|
@@ -9253,7 +9305,7 @@ async function resolveRepo(explicit) {
|
|
|
9253
9305
|
}
|
|
9254
9306
|
return { owner, repo };
|
|
9255
9307
|
}
|
|
9256
|
-
const { stdout } = await
|
|
9308
|
+
const { stdout } = await execa6("git", ["remote", "get-url", "origin"]);
|
|
9257
9309
|
const m = /github\.com[:/]([^/]+)\/(.+?)(?:\.git)?$/.exec(stdout.trim());
|
|
9258
9310
|
if (!m?.[1] || !m[2]) {
|
|
9259
9311
|
console.error(
|
|
@@ -9385,7 +9437,7 @@ async function runBranchProtectionCheck(explicitRepo, options = {}) {
|
|
|
9385
9437
|
}
|
|
9386
9438
|
|
|
9387
9439
|
// src/scripts/check-core-ownership.ts
|
|
9388
|
-
import { execa as
|
|
9440
|
+
import { execa as execa7 } from "execa";
|
|
9389
9441
|
var BOLD = "\x1B[1m";
|
|
9390
9442
|
var DIM = "\x1B[2m";
|
|
9391
9443
|
var RED = "\x1B[31m";
|
|
@@ -9396,7 +9448,7 @@ async function runOwnershipCheck(argv) {
|
|
|
9396
9448
|
const stagedFlag = args.indexOf("--staged");
|
|
9397
9449
|
const staged = stagedFlag !== -1;
|
|
9398
9450
|
const messageFile = staged ? args[stagedFlag + 1] : void 0;
|
|
9399
|
-
const root = (await
|
|
9451
|
+
const root = (await execa7("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
9400
9452
|
if (!isInstanceRepo(root)) {
|
|
9401
9453
|
console.log("\u2713 core ownership guard: skipped \u2014 this is the template, which owns these paths.");
|
|
9402
9454
|
return;
|
|
@@ -9405,11 +9457,11 @@ async function runOwnershipCheck(argv) {
|
|
|
9405
9457
|
let deletedFiles = [];
|
|
9406
9458
|
let commitMessage = "";
|
|
9407
9459
|
if (staged) {
|
|
9408
|
-
const { stdout } = await
|
|
9460
|
+
const { stdout } = await execa7("git", ["diff", "--cached", "--name-status"], { cwd: root });
|
|
9409
9461
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
9410
9462
|
if (messageFile) {
|
|
9411
|
-
const { readFileSync: readFileSync26, existsSync:
|
|
9412
|
-
if (
|
|
9463
|
+
const { readFileSync: readFileSync26, existsSync: existsSync37 } = await import("fs");
|
|
9464
|
+
if (existsSync37(messageFile)) commitMessage = readFileSync26(messageFile, "utf8");
|
|
9413
9465
|
}
|
|
9414
9466
|
} else {
|
|
9415
9467
|
const base = process.env["GITHUB_BASE_REF"] ?? args[0];
|
|
@@ -9417,18 +9469,18 @@ async function runOwnershipCheck(argv) {
|
|
|
9417
9469
|
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
9418
9470
|
process.exit(2);
|
|
9419
9471
|
}
|
|
9420
|
-
await
|
|
9421
|
-
const { stdout } = await
|
|
9472
|
+
await execa7("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
9473
|
+
const { stdout } = await execa7("git", ["diff", "--name-status", `origin/${base}...HEAD`], {
|
|
9422
9474
|
cwd: root
|
|
9423
9475
|
});
|
|
9424
9476
|
({ changed: changedFiles, deleted: deletedFiles } = parseNameStatus(stdout));
|
|
9425
|
-
const { stdout: log2 } = await
|
|
9477
|
+
const { stdout: log2 } = await execa7("git", ["log", "--format=%B", `origin/${base}..HEAD`], {
|
|
9426
9478
|
cwd: root,
|
|
9427
9479
|
reject: false
|
|
9428
9480
|
});
|
|
9429
9481
|
commitMessage = log2;
|
|
9430
9482
|
}
|
|
9431
|
-
const { stdout: gitBranch } = await
|
|
9483
|
+
const { stdout: gitBranch } = await execa7("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
9432
9484
|
cwd: root,
|
|
9433
9485
|
reject: false
|
|
9434
9486
|
});
|
|
@@ -9510,34 +9562,34 @@ ${BOLD}If the divergence is deliberate${OFF}
|
|
|
9510
9562
|
}
|
|
9511
9563
|
|
|
9512
9564
|
// src/scripts/check-plugin-collisions.ts
|
|
9513
|
-
import { existsSync as
|
|
9514
|
-
import { join as
|
|
9515
|
-
import { execa as
|
|
9565
|
+
import { existsSync as existsSync34 } from "fs";
|
|
9566
|
+
import { join as join33 } from "path";
|
|
9567
|
+
import { execa as execa8 } from "execa";
|
|
9516
9568
|
|
|
9517
9569
|
// src/lib/plugin-collision-guard.ts
|
|
9518
|
-
import { existsSync as
|
|
9519
|
-
import { join as
|
|
9570
|
+
import { existsSync as existsSync33, readdirSync as readdirSync14, statSync as statSync7 } from "fs";
|
|
9571
|
+
import { join as join32 } from "path";
|
|
9520
9572
|
var PYTEST_SPECIAL = /* @__PURE__ */ new Set(["conftest.py"]);
|
|
9521
9573
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([".venv", "node_modules", "__pycache__", ".git", "dist", "build"]);
|
|
9522
9574
|
function subdirectories(dir) {
|
|
9523
|
-
if (!
|
|
9524
|
-
return
|
|
9575
|
+
if (!existsSync33(dir)) return [];
|
|
9576
|
+
return readdirSync14(dir).filter((entry) => {
|
|
9525
9577
|
if (IGNORED_DIRS.has(entry) || entry.startsWith(".")) return false;
|
|
9526
9578
|
try {
|
|
9527
|
-
return statSync7(
|
|
9579
|
+
return statSync7(join32(dir, entry)).isDirectory();
|
|
9528
9580
|
} catch {
|
|
9529
9581
|
return false;
|
|
9530
9582
|
}
|
|
9531
9583
|
});
|
|
9532
9584
|
}
|
|
9533
9585
|
function regularPackagesOf(pluginDir2) {
|
|
9534
|
-
return subdirectories(pluginDir2).filter((name) =>
|
|
9586
|
+
return subdirectories(pluginDir2).filter((name) => existsSync33(join32(pluginDir2, name, "__init__.py"))).sort();
|
|
9535
9587
|
}
|
|
9536
9588
|
function bareTestModulesOf(pluginDir2) {
|
|
9537
|
-
const testsDir =
|
|
9538
|
-
if (!
|
|
9539
|
-
if (
|
|
9540
|
-
return
|
|
9589
|
+
const testsDir = join32(pluginDir2, "tests");
|
|
9590
|
+
if (!existsSync33(testsDir)) return [];
|
|
9591
|
+
if (existsSync33(join32(testsDir, "__init__.py"))) return [];
|
|
9592
|
+
return readdirSync14(testsDir).filter((f) => f.endsWith(".py") && !PYTEST_SPECIAL.has(f)).sort();
|
|
9541
9593
|
}
|
|
9542
9594
|
function findCollisions(servicesDir, pluginDirs) {
|
|
9543
9595
|
const plugins = (pluginDirs ?? subdirectories(servicesDir)).filter((name) => !name.startsWith("_")).filter((name) => name !== "api").sort();
|
|
@@ -9545,7 +9597,7 @@ function findCollisions(servicesDir, pluginDirs) {
|
|
|
9545
9597
|
const gather = (kind, namesOf) => {
|
|
9546
9598
|
const claims = /* @__PURE__ */ new Map();
|
|
9547
9599
|
for (const plugin of plugins) {
|
|
9548
|
-
for (const name of namesOf(
|
|
9600
|
+
for (const name of namesOf(join32(servicesDir, plugin))) {
|
|
9549
9601
|
claims.set(name, [...claims.get(name) ?? [], plugin]);
|
|
9550
9602
|
}
|
|
9551
9603
|
}
|
|
@@ -9582,9 +9634,9 @@ function formatCollisions(collisions) {
|
|
|
9582
9634
|
|
|
9583
9635
|
// src/scripts/check-plugin-collisions.ts
|
|
9584
9636
|
async function runPluginCollisionCheck() {
|
|
9585
|
-
const root = (await
|
|
9586
|
-
const servicesDir =
|
|
9587
|
-
if (!
|
|
9637
|
+
const root = (await execa8("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
9638
|
+
const servicesDir = join33(root, "services");
|
|
9639
|
+
if (!existsSync34(servicesDir)) {
|
|
9588
9640
|
console.log("\u2713 plugin collision guard: no services/ directory \u2014 nothing to compare");
|
|
9589
9641
|
return;
|
|
9590
9642
|
}
|
|
@@ -9601,11 +9653,11 @@ async function runPluginCollisionCheck() {
|
|
|
9601
9653
|
}
|
|
9602
9654
|
|
|
9603
9655
|
// src/scripts/check-plugin-terraform.ts
|
|
9604
|
-
import { execa as
|
|
9656
|
+
import { execa as execa9 } from "execa";
|
|
9605
9657
|
|
|
9606
9658
|
// src/lib/plugin-terraform-guard.ts
|
|
9607
|
-
import { existsSync as
|
|
9608
|
-
import { dirname as dirname9, join as
|
|
9659
|
+
import { existsSync as existsSync35, readFileSync as readFileSync24, readdirSync as readdirSync15 } from "fs";
|
|
9660
|
+
import { dirname as dirname9, join as join34, relative as relative6, sep as sep3 } from "path";
|
|
9609
9661
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".worktrees", "dist", ".venv", "__pycache__"]);
|
|
9610
9662
|
var PLUGIN_MANIFEST_FILE2 = "biffo.plugin.json";
|
|
9611
9663
|
function findPluginManifests(root) {
|
|
@@ -9613,16 +9665,16 @@ function findPluginManifests(root) {
|
|
|
9613
9665
|
const walk = (dir) => {
|
|
9614
9666
|
let entries;
|
|
9615
9667
|
try {
|
|
9616
|
-
entries =
|
|
9668
|
+
entries = readdirSync15(dir, { withFileTypes: true });
|
|
9617
9669
|
} catch {
|
|
9618
9670
|
return;
|
|
9619
9671
|
}
|
|
9620
9672
|
for (const entry of entries) {
|
|
9621
9673
|
if (entry.isDirectory()) {
|
|
9622
9674
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
9623
|
-
walk(
|
|
9675
|
+
walk(join34(dir, entry.name));
|
|
9624
9676
|
} else if (entry.isFile() && entry.name === PLUGIN_MANIFEST_FILE2) {
|
|
9625
|
-
found.push(relative6(root,
|
|
9677
|
+
found.push(relative6(root, join34(dir, entry.name)).split(sep3).join("/"));
|
|
9626
9678
|
}
|
|
9627
9679
|
}
|
|
9628
9680
|
};
|
|
@@ -9647,14 +9699,14 @@ function readSubscriptions(absManifestPath) {
|
|
|
9647
9699
|
}
|
|
9648
9700
|
function checkPluginTerraform(root) {
|
|
9649
9701
|
const violations = [];
|
|
9650
|
-
const coreManifest =
|
|
9702
|
+
const coreManifest = existsSync35(join34(root, CORE_MANIFEST_FILE)) ? readCoreManifest(root) : null;
|
|
9651
9703
|
for (const manifest of findPluginManifests(root)) {
|
|
9652
9704
|
if (coreManifest && !isTemplateOwned(manifest, coreManifest)) continue;
|
|
9653
|
-
const absManifest =
|
|
9705
|
+
const absManifest = join34(root, manifest);
|
|
9654
9706
|
const subscriptions = readSubscriptions(absManifest);
|
|
9655
9707
|
if (subscriptions === null) continue;
|
|
9656
9708
|
const pluginDir2 = dirname9(absManifest);
|
|
9657
|
-
if (
|
|
9709
|
+
if (existsSync35(join34(pluginDir2, "terraform"))) continue;
|
|
9658
9710
|
const relPluginDir = relative6(root, pluginDir2).split(sep3).join("/");
|
|
9659
9711
|
violations.push({
|
|
9660
9712
|
manifest,
|
|
@@ -9674,7 +9726,7 @@ function formatViolations(violations) {
|
|
|
9674
9726
|
|
|
9675
9727
|
// src/scripts/check-plugin-terraform.ts
|
|
9676
9728
|
async function runPluginTerraformCheck() {
|
|
9677
|
-
const root = (await
|
|
9729
|
+
const root = (await execa9("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
9678
9730
|
const violations = checkPluginTerraform(root);
|
|
9679
9731
|
if (violations.length > 0) {
|
|
9680
9732
|
console.error("\u2717 plugin Terraform guard: event subscriptions with no infrastructure\n");
|
|
@@ -9685,7 +9737,7 @@ async function runPluginTerraformCheck() {
|
|
|
9685
9737
|
}
|
|
9686
9738
|
|
|
9687
9739
|
// src/scripts/check-release-subject.ts
|
|
9688
|
-
import { execa as
|
|
9740
|
+
import { execa as execa10 } from "execa";
|
|
9689
9741
|
|
|
9690
9742
|
// src/lib/release-version.ts
|
|
9691
9743
|
var MINOR_TYPES = /* @__PURE__ */ new Set(["feat"]);
|
|
@@ -9723,13 +9775,13 @@ async function runReleaseSubjectCheck(argv) {
|
|
|
9723
9775
|
console.error("No base ref: set GITHUB_BASE_REF or pass a base branch as the first argument.");
|
|
9724
9776
|
process.exit(2);
|
|
9725
9777
|
}
|
|
9726
|
-
const root = (await
|
|
9727
|
-
await
|
|
9728
|
-
const { stdout } = await
|
|
9778
|
+
const root = (await execa10("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
9779
|
+
await execa10("git", ["fetch", "--quiet", "origin", base], { cwd: root, reject: false });
|
|
9780
|
+
const { stdout } = await execa10("git", ["diff", "--name-only", `origin/${base}...HEAD`], {
|
|
9729
9781
|
cwd: root
|
|
9730
9782
|
});
|
|
9731
9783
|
const changedFiles = stdout.split("\n").map((s) => s.trim()).filter(Boolean);
|
|
9732
|
-
const subject = process.env["PR_TITLE"]?.trim() || (await
|
|
9784
|
+
const subject = process.env["PR_TITLE"]?.trim() || (await execa10("git", ["log", "-1", "--format=%s"], { cwd: root })).stdout.trim();
|
|
9733
9785
|
const manifest = readCoreManifest(root);
|
|
9734
9786
|
const { unparseable, bump, templateOwnedChanges, skippedAsInstance } = checkReleaseSubject(
|
|
9735
9787
|
changedFiles,
|
|
@@ -9788,6 +9840,11 @@ checkCommand.command("plugin-collisions").description("Refuse two vendored plugi
|
|
|
9788
9840
|
checkCommand.command("plugin-terraform").description("Verify every template-owned plugin declaring infra ships a Terraform module").action(async () => {
|
|
9789
9841
|
await runPluginTerraformCheck();
|
|
9790
9842
|
});
|
|
9843
|
+
checkCommand.command("adr-numbering").description(
|
|
9844
|
+
"Refuse two ADRs in this repo's own docs/ADR/ claiming the same number (tabsii-platform#449)"
|
|
9845
|
+
).action(async () => {
|
|
9846
|
+
await runAdrNumberingCheck();
|
|
9847
|
+
});
|
|
9791
9848
|
checkCommand.command("branch-protection").description(
|
|
9792
9849
|
"Verify dev/staging/main are actually protected \u2014 scaffolding skips this on a 403 (#715)"
|
|
9793
9850
|
).option("--repo <owner/name>", "Repo to audit; defaults to this checkout's origin remote").option(
|
|
@@ -9802,8 +9859,8 @@ function rawArgsAfter(subcommand) {
|
|
|
9802
9859
|
}
|
|
9803
9860
|
|
|
9804
9861
|
// src/commands/doctor.ts
|
|
9805
|
-
import { existsSync as
|
|
9806
|
-
import { join as
|
|
9862
|
+
import { existsSync as existsSync36, readFileSync as readFileSync25 } from "fs";
|
|
9863
|
+
import { join as join35, resolve as resolve18 } from "path";
|
|
9807
9864
|
import chalk21 from "chalk";
|
|
9808
9865
|
import { Command as Command24 } from "commander";
|
|
9809
9866
|
|
|
@@ -9978,8 +10035,8 @@ async function runDoctor(options, deps = { git: new GitAdapter() }) {
|
|
|
9978
10035
|
return runDoctorChecks(facts);
|
|
9979
10036
|
}
|
|
9980
10037
|
function readLocalCoreVersion(cwd) {
|
|
9981
|
-
const path =
|
|
9982
|
-
if (!
|
|
10038
|
+
const path = join35(cwd, INSTANCE_CORE_FILE);
|
|
10039
|
+
if (!existsSync36(path)) return null;
|
|
9983
10040
|
try {
|
|
9984
10041
|
return parseCoreRecord(readFileSync25(path, "utf8"));
|
|
9985
10042
|
} catch {
|
|
@@ -9996,8 +10053,8 @@ function parseCoreRecord(contents) {
|
|
|
9996
10053
|
}
|
|
9997
10054
|
}
|
|
9998
10055
|
function readFossil(cwd) {
|
|
9999
|
-
const path =
|
|
10000
|
-
if (!
|
|
10056
|
+
const path = join35(cwd, CORE_VERSION_FILE);
|
|
10057
|
+
if (!existsSync36(path)) return null;
|
|
10001
10058
|
try {
|
|
10002
10059
|
const value = readFileSync25(path, "utf8").trim();
|
|
10003
10060
|
return value === "" ? null : value;
|