@biffo/cli 0.182.0 → 0.183.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.
|
@@ -29,12 +29,60 @@
|
|
|
29
29
|
# customise does not belong in it — that is what the instance manifest's
|
|
30
30
|
# three-way merge is for, and this deliberately has no such subtlety.
|
|
31
31
|
#
|
|
32
|
+
# ## Rehearsal: why a distribution run is two phases
|
|
33
|
+
#
|
|
34
|
+
# Measured on 2026-07-29: **84 `chore(shared): sync template-shared files` PRs
|
|
35
|
+
# merged across 12 satellites in one day** — 7 rounds, where one would have done.
|
|
36
|
+
# That is 33.5% of the estate's entire merge volume for the day, and every one of
|
|
37
|
+
# those PRs is classified `toil` by the practices collector, which read 60.2%.
|
|
38
|
+
# Six of the seven rounds carried `scripts/verify.sh` **alone**: the gate was
|
|
39
|
+
# being iterated downstream, one estate-wide lap per defect found (root-only
|
|
40
|
+
# checks, `--no-cov` in a repo without pytest-cov, the pytest opt-in, `--list`
|
|
41
|
+
# disagreeing with the gate).
|
|
42
|
+
#
|
|
43
|
+
# The reason a lap cost 12 PRs rather than 1 is that this script used to be a
|
|
44
|
+
# single pass: for each repo in turn, stage -> push -> open a PR. A defect
|
|
45
|
+
# discovered while staging repo 7 left 6 PRs already open, so fixing it meant a
|
|
46
|
+
# whole new round rather than a correction.
|
|
47
|
+
#
|
|
48
|
+
# So: **rehearse every target before touching any of them.** Phase 1 stages the
|
|
49
|
+
# candidate files into each repo and runs that repo's own gate against them,
|
|
50
|
+
# locally, with no push and no PR. Phase 2 ships — and only if phase 1 was clean
|
|
51
|
+
# everywhere. All 14 satellite clones are already on disk under `--estate`, so
|
|
52
|
+
# proving the estate costs seconds; proving it through GitHub costs 12 PRs and 12
|
|
53
|
+
# CI runs per lap.
|
|
54
|
+
#
|
|
55
|
+
# **It is not enough to let the target repo's pre-push hook catch this**, which
|
|
56
|
+
# is what the push in phase 2 relies on. That hook is exactly the thing that was
|
|
57
|
+
# silently DEAD in every fresh worktree until #845, and `scripts/hook-audit.sh`
|
|
58
|
+
# exists because the failure was invisible. A repo whose hook is dead pushes
|
|
59
|
+
# happily and opens a PR carrying an unproven gate — the fail-open shape this
|
|
60
|
+
# whole file set exists to remove. Rehearsal runs the gate itself and reads its
|
|
61
|
+
# exit code, so it does not depend on the hook being armed.
|
|
62
|
+
#
|
|
32
63
|
# Usage:
|
|
33
|
-
# sh scripts/shared-sync.sh --check --estate ~/code
|
|
34
|
-
# sh scripts/shared-sync.sh --estate ~/code
|
|
64
|
+
# sh scripts/shared-sync.sh --check --estate ~/code # report drift, exit 1 if any
|
|
65
|
+
# sh scripts/shared-sync.sh --rehearse --estate ~/code # prove the candidates, ship nothing
|
|
66
|
+
# sh scripts/shared-sync.sh --estate ~/code # rehearse, then open a PR per repo
|
|
35
67
|
# sh scripts/shared-sync.sh --estate ~/code --repo tabsii-crm
|
|
68
|
+
# sh scripts/shared-sync.sh --estate ~/code --no-rehearse # ship unproven, loudly
|
|
36
69
|
|
|
37
|
-
set -
|
|
70
|
+
set -u
|
|
71
|
+
# `pipefail` is not POSIX, and this file is documented and invoked as
|
|
72
|
+
# `sh scripts/shared-sync.sh` -- including by scripts/practices-daily.sh.
|
|
73
|
+
#
|
|
74
|
+
# It carried a bare `set -uo pipefail` for as long as it only ever ran on the
|
|
75
|
+
# workstation, whose /bin/sh is dash 0.5.12, a version that happens to accept
|
|
76
|
+
# `-o pipefail`. The first time anything executed it elsewhere -- a test, on a
|
|
77
|
+
# CI runner with an older sh -- it died at this line with
|
|
78
|
+
# `set: Illegal option -o pipefail`, before parsing a single argument. Nothing
|
|
79
|
+
# had noticed, because nothing had ever run it anywhere else.
|
|
80
|
+
#
|
|
81
|
+
# That is the third instance of this class in the corpus: the same one-version
|
|
82
|
+
# difference in the same shell made js-dependency-audit.sh mangle every advisory
|
|
83
|
+
# payload while exiting 0 (#883). Enable it where it exists, carry on where it
|
|
84
|
+
# does not.
|
|
85
|
+
(set -o pipefail) 2>/dev/null && set -o pipefail || true
|
|
38
86
|
|
|
39
87
|
TEMPLATE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
40
88
|
MANIFEST="$TEMPLATE_ROOT/shared-files.json"
|
|
@@ -43,15 +91,24 @@ MANIFEST="$TEMPLATE_ROOT/shared-files.json"
|
|
|
43
91
|
CHECK=""
|
|
44
92
|
ESTATE=""
|
|
45
93
|
ONLY=""
|
|
94
|
+
REHEARSE_ONLY=""
|
|
95
|
+
NO_REHEARSE=""
|
|
46
96
|
while [ $# -gt 0 ]; do
|
|
47
97
|
case "$1" in
|
|
48
98
|
--check) CHECK=1; shift ;;
|
|
99
|
+
--rehearse) REHEARSE_ONLY=1; shift ;;
|
|
100
|
+
# In the open, per AGENTS.md section 7: a gate that can be skipped silently
|
|
101
|
+
# is not a gate. This prints the reason on every line it lets through, so a
|
|
102
|
+
# transcript shows what was shipped unproven.
|
|
103
|
+
--no-rehearse) NO_REHEARSE=1; shift ;;
|
|
49
104
|
--estate) ESTATE="$2"; shift 2 ;;
|
|
50
105
|
--repo) ONLY="$2"; shift 2 ;;
|
|
51
106
|
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
52
107
|
esac
|
|
53
108
|
done
|
|
54
109
|
[ -n "$ESTATE" ] || { echo "--estate <dir> is required" >&2; exit 2; }
|
|
110
|
+
[ -n "$REHEARSE_ONLY" ] && [ -n "$NO_REHEARSE" ] && {
|
|
111
|
+
echo "--rehearse and --no-rehearse are opposites" >&2; exit 2; }
|
|
55
112
|
|
|
56
113
|
FILES=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).files.join('\n'))")
|
|
57
114
|
MARKERS=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).appliesTo.join(' '))")
|
|
@@ -59,6 +116,12 @@ MARKERS=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST'
|
|
|
59
116
|
drifted=0
|
|
60
117
|
synced=0
|
|
61
118
|
current=0
|
|
119
|
+
failed=0
|
|
120
|
+
|
|
121
|
+
# Field separator for the two state files below. A literal tab in a `grep`
|
|
122
|
+
# pattern or a `${var%%...}` expansion is invisible in a diff and one editor
|
|
123
|
+
# away from becoming spaces.
|
|
124
|
+
TAB=$(printf '\t')
|
|
62
125
|
|
|
63
126
|
applies() {
|
|
64
127
|
# Instances are NOT in scope: they carry biffo.core.json and a
|
|
@@ -112,17 +175,35 @@ diff_files() {
|
|
|
112
175
|
echo "$out"
|
|
113
176
|
}
|
|
114
177
|
|
|
115
|
-
|
|
178
|
+
repo_slug() {
|
|
179
|
+
git -C "$1" remote get-url origin | sed -E 's#.*[:/]([^/]+/[^/]+)$#\1#; s#\.git$##'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# The absolute path of a working tree's shared git directory, which is the same
|
|
183
|
+
# for a primary checkout and every worktree linked to it -- i.e. an identity for
|
|
184
|
+
# the REPOSITORY rather than for one of its working trees.
|
|
185
|
+
repo_dir() {
|
|
186
|
+
(cd "$1" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
TEMPLATE_REPO=$(repo_dir "$TEMPLATE_ROOT")
|
|
190
|
+
[ -n "$TEMPLATE_REPO" ] || { echo "$TEMPLATE_ROOT is not a git repository" >&2; exit 2; }
|
|
191
|
+
|
|
192
|
+
# Phase 1: put the candidate files in place and make the repo ready to run its
|
|
193
|
+
# own gate against them. Deliberately stops short of committing anything -- a
|
|
194
|
+
# staged worktree is a question ("would this land?"), and until phase 2 it has no
|
|
195
|
+
# commit, no push and no PR.
|
|
196
|
+
stage_repo() {
|
|
116
197
|
d="$1"
|
|
117
198
|
label="$2"
|
|
118
|
-
|
|
119
|
-
base=$(gh repo view "$slug" --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null)
|
|
120
|
-
[ -n "$base" ] || { printf '%-26s \033[31mcannot resolve default branch\033[0m\n' "$label"; return 1; }
|
|
199
|
+
base="$3"
|
|
121
200
|
|
|
122
201
|
git -C "$d" fetch origin --quiet || return 1
|
|
123
202
|
wt="$d/.worktrees/shared-sync"
|
|
124
203
|
git -C "$d" worktree remove --force "$wt" 2>/dev/null
|
|
125
|
-
|
|
204
|
+
# `branch -D` reports on STDOUT, so a quiet run printed "Deleted branch
|
|
205
|
+
# chore/sync-shared" in the middle of the rehearsal table.
|
|
206
|
+
git -C "$d" branch -D chore/sync-shared >/dev/null 2>&1
|
|
126
207
|
git -C "$d" worktree add -q "$wt" -b chore/sync-shared "origin/$base" || return 1
|
|
127
208
|
|
|
128
209
|
for f in $FILES; do
|
|
@@ -161,9 +242,82 @@ sync_repo() {
|
|
|
161
242
|
git -C "$wt" add -A
|
|
162
243
|
if git -C "$wt" diff --cached --quiet; then
|
|
163
244
|
git -C "$d" worktree remove --force "$wt" 2>/dev/null
|
|
164
|
-
|
|
245
|
+
return 2
|
|
246
|
+
fi
|
|
247
|
+
return 0
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Phase 1's actual question: with the candidate files in place, does this repo's
|
|
251
|
+
# gate still work HERE?
|
|
252
|
+
#
|
|
253
|
+
# This is the check the estate did not have. `--check` compares bytes, and
|
|
254
|
+
# `gate-coverage.sh --estate` measures repos as they ARE -- neither can say
|
|
255
|
+
# anything about a file that has not been distributed yet. Six of the seven
|
|
256
|
+
# rounds on 2026-07-29 were fixing defects that only exist in a repo that is not
|
|
257
|
+
# this one: a check list tuned to the template's layout, a pytest-cov flag a
|
|
258
|
+
# plugin repo rejects, a package.json that is not at the root.
|
|
259
|
+
rehearse_repo() {
|
|
260
|
+
wt="$1"
|
|
261
|
+
|
|
262
|
+
# `sh`, not `bash`: this is exactly how `.githooks/pre-push` invokes it
|
|
263
|
+
# (`exec sh scripts/verify.sh`), and /bin/sh is dash on every machine in this
|
|
264
|
+
# estate. A gate proven under one shell and run under another is not the same
|
|
265
|
+
# gate -- `js-dependency-audit.sh` reported INCONCLUSIVE on every invocation
|
|
266
|
+
# while exiting 0 for exactly that reason, because dash's `echo` interprets
|
|
267
|
+
# backslash escapes and bash's does not (#883).
|
|
268
|
+
_out=$( (cd "$wt" && sh scripts/verify.sh 2>&1) )
|
|
269
|
+
_rc=$?
|
|
270
|
+
_checks=$(printf '%s' "$_out" | sed -n 's/.*verify passed[^-]*- *//p' | head -1)
|
|
271
|
+
|
|
272
|
+
# `gate-coverage.sh` reads `--list` rather than running anything, so it is
|
|
273
|
+
# cheap, and it answers the question `verify.sh` cannot: the gate ran without
|
|
274
|
+
# error, but how much of THIS repo's CI did it mirror? A gate can exit 0
|
|
275
|
+
# having covered 1 kind in 8 -- that IS #855, and it is invisible in an exit
|
|
276
|
+
# code. Reported, not enforced: what coverage number should block a
|
|
277
|
+
# distribution is H5's call to make with numbers, not this script's to assume.
|
|
278
|
+
#
|
|
279
|
+
# Match its three verdicts, not just the fraction. Reading only `N/M` reported
|
|
280
|
+
# a bare `?` for tabsii-map, which has no ci.yml at all -- so the one repo
|
|
281
|
+
# where the coverage question does not apply looked like the one repo where the
|
|
282
|
+
# measurement had failed. "Not applicable" and "could not tell" are different
|
|
283
|
+
# answers, and this file exists because conflating them is expensive.
|
|
284
|
+
_cov=$( (cd "$wt" && sh scripts/gate-coverage.sh 2>&1) |
|
|
285
|
+
sed 's/\x1b\[[0-9;]*m//g' |
|
|
286
|
+
grep -oE '([0-9]+/[0-9]+|no CI to mirror|NO GATE)' | head -1)
|
|
287
|
+
[ -n "$_cov" ] || _cov='coverage unknown'
|
|
288
|
+
case "$_cov" in
|
|
289
|
+
*/*) _cov="covers $_cov" ;;
|
|
290
|
+
esac
|
|
291
|
+
|
|
292
|
+
if [ "$_rc" -eq 0 ]; then
|
|
293
|
+
case "$_out" in
|
|
294
|
+
*"verify ran NOTHING"*)
|
|
295
|
+
# verify.sh has already established this is not the #855 bug: no
|
|
296
|
+
# `.github/workflows/ci.yml`, so this repo has no CI for the gate to
|
|
297
|
+
# mirror and no shift-left obligation. Reporting it as a pass would be
|
|
298
|
+
# the exact conflation the gate itself refuses to make, so it gets its
|
|
299
|
+
# own verdict.
|
|
300
|
+
printf '%s\t%s\n' NO-CI 'gate ran nothing; repo has no ci.yml' ;;
|
|
301
|
+
*)
|
|
302
|
+
printf '%s\t%s\n' PASS "$(printf '%s (%s)' "$_checks" "$_cov")" ;;
|
|
303
|
+
esac
|
|
165
304
|
return 0
|
|
166
305
|
fi
|
|
306
|
+
_why=$(printf '%s' "$_out" | grep -E 'verify failed|verify ran NOTHING' | head -1)
|
|
307
|
+
[ -n "$_why" ] || _why=$(printf '%s' "$_out" | tail -1)
|
|
308
|
+
printf '%s\t%s\n' FAIL "$(printf '%s' "$_why" | sed 's/\x1b\[[0-9;]*m//g')"
|
|
309
|
+
return 1
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# Phase 2: commit the already-staged worktree, push it, open the PR. Reached only
|
|
313
|
+
# when phase 1 came back clean for EVERY repo in this run.
|
|
314
|
+
ship_repo() {
|
|
315
|
+
d="$1"
|
|
316
|
+
label="$2"
|
|
317
|
+
slug="$3"
|
|
318
|
+
base="$4"
|
|
319
|
+
wt="$d/.worktrees/shared-sync"
|
|
320
|
+
|
|
167
321
|
git -C "$wt" -c commit.gpgsign=false commit -q --no-verify -m "chore(shared): sync template-shared files
|
|
168
322
|
|
|
169
323
|
Distributed by biffo-template's scripts/shared-sync.sh. These files are held
|
|
@@ -212,6 +366,14 @@ Run \`sh scripts/gate-coverage.sh\` after merging to see this repo's gate measur
|
|
|
212
366
|
return 0
|
|
213
367
|
}
|
|
214
368
|
|
|
369
|
+
# The list of repos this run will touch, one `label<TAB>dir<TAB>slug<TAB>base`
|
|
370
|
+
# per line. Written in the survey pass and read twice afterwards, so both phases
|
|
371
|
+
# work from the same set: a rehearsal that proved a different list of repos than
|
|
372
|
+
# the one that ships is worth nothing.
|
|
373
|
+
TARGETS=$(mktemp)
|
|
374
|
+
VERDICTS=$(mktemp)
|
|
375
|
+
trap 'rm -f "$TARGETS" "$VERDICTS"' EXIT
|
|
376
|
+
|
|
215
377
|
printf '\nshared-file sync - template -> repos core upgrade cannot reach\n\n'
|
|
216
378
|
for d in "$ESTATE"/*/; do
|
|
217
379
|
d="${d%/}"
|
|
@@ -220,7 +382,21 @@ for d in "$ESTATE"/*/; do
|
|
|
220
382
|
# The template is the source, not a target. It matched only because it carries
|
|
221
383
|
# scripts/verify.sh, and comparing it to itself through origin/<base> reported
|
|
222
384
|
# it as missing every file whenever its own dev was ahead of the checkout.
|
|
223
|
-
|
|
385
|
+
#
|
|
386
|
+
# Compare REPOSITORIES, not working-tree paths. `[ "$d" = "$TEMPLATE_ROOT" ]`
|
|
387
|
+
# held only when this script was run from the primary checkout -- and AGENTS.md
|
|
388
|
+
# section 1 mandates that all work happens in a worktree, where TEMPLATE_ROOT
|
|
389
|
+
# is `.worktrees/<name>` and the primary checkout beside it looks like just
|
|
390
|
+
# another satellite carrying scripts/verify.sh. It has no biffo.core.json and
|
|
391
|
+
# no sibling/plugin marker, so nothing else excluded it either.
|
|
392
|
+
#
|
|
393
|
+
# It never fired because a primary checkout is normally byte-identical to
|
|
394
|
+
# origin/dev, so `diff_files` reported `current` and skipped it. It fires the
|
|
395
|
+
# moment the candidate files differ from origin/dev -- which is the only
|
|
396
|
+
# situation this script is ever run in while iterating on a shared file. The
|
|
397
|
+
# first `--rehearse` from a worktree would have staged the template as a
|
|
398
|
+
# target of its own distribution and opened a sync PR against its own dev.
|
|
399
|
+
[ "$(repo_dir "$d")" = "$TEMPLATE_REPO" ] && continue
|
|
224
400
|
[ -n "$ONLY" ] && [ "$label" != "$ONLY" ] && continue
|
|
225
401
|
applies "$d" || continue
|
|
226
402
|
delta=$(diff_files "$d")
|
|
@@ -232,14 +408,134 @@ for d in "$ESTATE"/*/; do
|
|
|
232
408
|
drifted=$((drifted + 1))
|
|
233
409
|
if [ -n "$CHECK" ]; then
|
|
234
410
|
printf '%-26s \033[31mDRIFTED\033[0m%s\n' "$label" "$delta"
|
|
235
|
-
|
|
236
|
-
sync_repo "$d" "$label" && synced=$((synced + 1))
|
|
411
|
+
continue
|
|
237
412
|
fi
|
|
413
|
+
slug=$(repo_slug "$d")
|
|
414
|
+
base=$(gh repo view "$slug" --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null)
|
|
415
|
+
if [ -z "$base" ]; then
|
|
416
|
+
printf '%-26s \033[31mcannot resolve default branch\033[0m\n' "$label"
|
|
417
|
+
failed=$((failed + 1))
|
|
418
|
+
continue
|
|
419
|
+
fi
|
|
420
|
+
printf '%s\t%s\t%s\t%s\n' "$label" "$d" "$slug" "$base" >> "$TARGETS"
|
|
421
|
+
printf '%-26s \033[33mdrifted\033[0m%s\n' "$label" "$delta"
|
|
238
422
|
done
|
|
239
423
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
424
|
+
if [ -n "$CHECK" ]; then
|
|
425
|
+
printf '\n%s current, %s drifted\n' "$current" "$drifted"
|
|
426
|
+
if [ "$drifted" -gt 0 ]; then
|
|
427
|
+
printf '\033[31mShared files have drifted.\033[0m Run without --check to open sync PRs.\n\n'
|
|
428
|
+
exit 1
|
|
429
|
+
fi
|
|
430
|
+
printf '\n'
|
|
431
|
+
exit 0
|
|
432
|
+
fi
|
|
433
|
+
|
|
434
|
+
if [ ! -s "$TARGETS" ]; then
|
|
435
|
+
printf '\n%s current, %s drifted\n\n' "$current" "$drifted"
|
|
436
|
+
[ "${failed:-0}" -gt 0 ] && exit 1
|
|
437
|
+
exit 0
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
# ---- Phase 1: rehearse -------------------------------------------------------
|
|
441
|
+
#
|
|
442
|
+
# Every target, before any of them ships. The order matters and it is the whole
|
|
443
|
+
# point of the change: staging repo 7 and finding the gate broken there must not
|
|
444
|
+
# leave six PRs already open in repos 1-6.
|
|
445
|
+
if [ -n "$NO_REHEARSE" ]; then
|
|
446
|
+
printf '\n\033[31m--no-rehearse: shipping %s repos unproven.\033[0m ' "$(wc -l < "$TARGETS" | tr -d ' ')"
|
|
447
|
+
printf 'Nothing has run the gate against these candidates.\n'
|
|
448
|
+
else
|
|
449
|
+
printf '\nrehearsing %s repos - staging the candidates and running each gate\n\n' \
|
|
450
|
+
"$(wc -l < "$TARGETS" | tr -d ' ')"
|
|
451
|
+
rehearsal_failures=0
|
|
452
|
+
while IFS="$TAB" read -r label d slug base; do
|
|
453
|
+
# Read the status IMMEDIATELY. `if ! stage_repo ...` would have collapsed
|
|
454
|
+
# "could not stage" (1) and "nothing to sync" (2) into one branch, and
|
|
455
|
+
# reported a repo that was already current as a staging failure.
|
|
456
|
+
stage_repo "$d" "$label" "$base"
|
|
457
|
+
stage_rc=$?
|
|
458
|
+
if [ "$stage_rc" -eq 2 ]; then
|
|
459
|
+
printf '%-26s \033[32mnothing to sync\033[0m\n' "$label"
|
|
460
|
+
printf '%s%s%s%s%s\n' "$label" "$TAB" SKIP "$TAB" 'nothing to sync' >> "$VERDICTS"
|
|
461
|
+
continue
|
|
462
|
+
fi
|
|
463
|
+
if [ "$stage_rc" -ne 0 ]; then
|
|
464
|
+
printf '%-26s \033[31mCANNOT STAGE\033[0m - fetch or worktree failed\n' "$label"
|
|
465
|
+
printf '%s%s%s%s%s\n' "$label" "$TAB" FAIL "$TAB" 'could not stage' >> "$VERDICTS"
|
|
466
|
+
rehearsal_failures=$((rehearsal_failures + 1))
|
|
467
|
+
continue
|
|
468
|
+
fi
|
|
469
|
+
verdict_line=$(rehearse_repo "$d/.worktrees/shared-sync")
|
|
470
|
+
verdict=$(printf '%s' "$verdict_line" | cut -f1)
|
|
471
|
+
detail=$(printf '%s' "$verdict_line" | cut -f2-)
|
|
472
|
+
case "$verdict" in
|
|
473
|
+
PASS) printf '%-26s \033[32mPASS\033[0m %s\n' "$label" "$detail" ;;
|
|
474
|
+
NO-CI) printf '%-26s \033[90mNO-CI\033[0m %s\n' "$label" "$detail" ;;
|
|
475
|
+
*)
|
|
476
|
+
printf '%-26s \033[31mFAIL\033[0m %s\n' "$label" "$detail"
|
|
477
|
+
printf '%-26s staged tree left at %s\n' '' "$d/.worktrees/shared-sync"
|
|
478
|
+
rehearsal_failures=$((rehearsal_failures + 1)) ;;
|
|
479
|
+
esac
|
|
480
|
+
printf '%s%s%s%s%s\n' "$label" "$TAB" "$verdict" "$TAB" "$detail" >> "$VERDICTS"
|
|
481
|
+
done < "$TARGETS"
|
|
482
|
+
|
|
483
|
+
if [ "$rehearsal_failures" -gt 0 ]; then
|
|
484
|
+
printf '\n\033[31mrehearsal failed in %s repo(s) - NOTHING was pushed and no PR was opened.\033[0m\n' \
|
|
485
|
+
"$rehearsal_failures"
|
|
486
|
+
printf 'Fix the candidate files here in the template, then run this again. Each\n'
|
|
487
|
+
printf 'round that ships before it is proven costs one PR per satellite: there were\n'
|
|
488
|
+
printf '84 of them on 2026-07-29, in 7 rounds, and six of those rounds carried\n'
|
|
489
|
+
printf 'scripts/verify.sh alone.\n'
|
|
490
|
+
printf 'The failing repos keep their staged worktree so the gate can be run there;\n'
|
|
491
|
+
printf 'the clean ones were removed.\n\n'
|
|
492
|
+
while IFS="$TAB" read -r label verdict detail; do
|
|
493
|
+
[ "$verdict" = FAIL ] || continue
|
|
494
|
+
printf ' %-24s %s\n' "$label" "$detail"
|
|
495
|
+
done < "$VERDICTS"
|
|
496
|
+
printf '\n'
|
|
497
|
+
# Reap the worktrees of the repos that passed. They staged cleanly and are
|
|
498
|
+
# not evidence of anything; leaving 11 of them behind after a refusal is the
|
|
499
|
+
# orphan-worktree accumulation AGENTS.md section 1 exists to prevent.
|
|
500
|
+
while IFS="$TAB" read -r label d slug base; do
|
|
501
|
+
grep -q "^$label${TAB}FAIL${TAB}" "$VERDICTS" && continue
|
|
502
|
+
git -C "$d" worktree remove --force "$d/.worktrees/shared-sync" 2>/dev/null
|
|
503
|
+
git -C "$d" branch -D chore/sync-shared 2>/dev/null
|
|
504
|
+
done < "$TARGETS"
|
|
505
|
+
exit 1
|
|
506
|
+
fi
|
|
507
|
+
printf '\nrehearsal clean in every repo\n'
|
|
244
508
|
fi
|
|
245
|
-
|
|
509
|
+
|
|
510
|
+
if [ -n "$REHEARSE_ONLY" ]; then
|
|
511
|
+
# Staged worktrees are left in place deliberately: --rehearse answers "would
|
|
512
|
+
# this land?", and the tree that answered it is the thing to go and look at.
|
|
513
|
+
printf '\n--rehearse: nothing pushed, no PRs opened. Staged trees are at\n'
|
|
514
|
+
printf '<repo>/.worktrees/shared-sync; re-run without --rehearse to ship.\n\n'
|
|
515
|
+
exit 0
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
# ---- Phase 2: ship -----------------------------------------------------------
|
|
519
|
+
printf '\nopening PRs\n\n'
|
|
520
|
+
while IFS="$TAB" read -r label d slug base; do
|
|
521
|
+
grep -q "^$label${TAB}SKIP${TAB}" "$VERDICTS" 2>/dev/null && continue
|
|
522
|
+
# --no-rehearse skips phase 1 entirely, so nothing has staged these yet.
|
|
523
|
+
if [ -n "$NO_REHEARSE" ]; then
|
|
524
|
+
stage_repo "$d" "$label" "$base"
|
|
525
|
+
case $? in
|
|
526
|
+
2) printf '%-26s \033[32mnothing to sync\033[0m\n' "$label"; continue ;;
|
|
527
|
+
1) printf '%-26s \033[31mCANNOT STAGE\033[0m\n' "$label"; failed=$((failed + 1)); continue ;;
|
|
528
|
+
esac
|
|
529
|
+
fi
|
|
530
|
+
if ship_repo "$d" "$label" "$slug" "$base"; then
|
|
531
|
+
synced=$((synced + 1))
|
|
532
|
+
else
|
|
533
|
+
failed=$((failed + 1))
|
|
534
|
+
fi
|
|
535
|
+
done < "$TARGETS"
|
|
536
|
+
|
|
537
|
+
printf '\n%s current, %s drifted, %s PR(s) opened' "$current" "$drifted" "$synced"
|
|
538
|
+
[ "${failed:-0}" -gt 0 ] && printf ', \033[31m%s failed\033[0m' "$failed"
|
|
539
|
+
printf '\n\n'
|
|
540
|
+
[ "${failed:-0}" -gt 0 ] && exit 1
|
|
541
|
+
exit 0
|
|
@@ -29,12 +29,60 @@
|
|
|
29
29
|
# customise does not belong in it — that is what the instance manifest's
|
|
30
30
|
# three-way merge is for, and this deliberately has no such subtlety.
|
|
31
31
|
#
|
|
32
|
+
# ## Rehearsal: why a distribution run is two phases
|
|
33
|
+
#
|
|
34
|
+
# Measured on 2026-07-29: **84 `chore(shared): sync template-shared files` PRs
|
|
35
|
+
# merged across 12 satellites in one day** — 7 rounds, where one would have done.
|
|
36
|
+
# That is 33.5% of the estate's entire merge volume for the day, and every one of
|
|
37
|
+
# those PRs is classified `toil` by the practices collector, which read 60.2%.
|
|
38
|
+
# Six of the seven rounds carried `scripts/verify.sh` **alone**: the gate was
|
|
39
|
+
# being iterated downstream, one estate-wide lap per defect found (root-only
|
|
40
|
+
# checks, `--no-cov` in a repo without pytest-cov, the pytest opt-in, `--list`
|
|
41
|
+
# disagreeing with the gate).
|
|
42
|
+
#
|
|
43
|
+
# The reason a lap cost 12 PRs rather than 1 is that this script used to be a
|
|
44
|
+
# single pass: for each repo in turn, stage -> push -> open a PR. A defect
|
|
45
|
+
# discovered while staging repo 7 left 6 PRs already open, so fixing it meant a
|
|
46
|
+
# whole new round rather than a correction.
|
|
47
|
+
#
|
|
48
|
+
# So: **rehearse every target before touching any of them.** Phase 1 stages the
|
|
49
|
+
# candidate files into each repo and runs that repo's own gate against them,
|
|
50
|
+
# locally, with no push and no PR. Phase 2 ships — and only if phase 1 was clean
|
|
51
|
+
# everywhere. All 14 satellite clones are already on disk under `--estate`, so
|
|
52
|
+
# proving the estate costs seconds; proving it through GitHub costs 12 PRs and 12
|
|
53
|
+
# CI runs per lap.
|
|
54
|
+
#
|
|
55
|
+
# **It is not enough to let the target repo's pre-push hook catch this**, which
|
|
56
|
+
# is what the push in phase 2 relies on. That hook is exactly the thing that was
|
|
57
|
+
# silently DEAD in every fresh worktree until #845, and `scripts/hook-audit.sh`
|
|
58
|
+
# exists because the failure was invisible. A repo whose hook is dead pushes
|
|
59
|
+
# happily and opens a PR carrying an unproven gate — the fail-open shape this
|
|
60
|
+
# whole file set exists to remove. Rehearsal runs the gate itself and reads its
|
|
61
|
+
# exit code, so it does not depend on the hook being armed.
|
|
62
|
+
#
|
|
32
63
|
# Usage:
|
|
33
|
-
# sh scripts/shared-sync.sh --check --estate ~/code
|
|
34
|
-
# sh scripts/shared-sync.sh --estate ~/code
|
|
64
|
+
# sh scripts/shared-sync.sh --check --estate ~/code # report drift, exit 1 if any
|
|
65
|
+
# sh scripts/shared-sync.sh --rehearse --estate ~/code # prove the candidates, ship nothing
|
|
66
|
+
# sh scripts/shared-sync.sh --estate ~/code # rehearse, then open a PR per repo
|
|
35
67
|
# sh scripts/shared-sync.sh --estate ~/code --repo tabsii-crm
|
|
68
|
+
# sh scripts/shared-sync.sh --estate ~/code --no-rehearse # ship unproven, loudly
|
|
36
69
|
|
|
37
|
-
set -
|
|
70
|
+
set -u
|
|
71
|
+
# `pipefail` is not POSIX, and this file is documented and invoked as
|
|
72
|
+
# `sh scripts/shared-sync.sh` -- including by scripts/practices-daily.sh.
|
|
73
|
+
#
|
|
74
|
+
# It carried a bare `set -uo pipefail` for as long as it only ever ran on the
|
|
75
|
+
# workstation, whose /bin/sh is dash 0.5.12, a version that happens to accept
|
|
76
|
+
# `-o pipefail`. The first time anything executed it elsewhere -- a test, on a
|
|
77
|
+
# CI runner with an older sh -- it died at this line with
|
|
78
|
+
# `set: Illegal option -o pipefail`, before parsing a single argument. Nothing
|
|
79
|
+
# had noticed, because nothing had ever run it anywhere else.
|
|
80
|
+
#
|
|
81
|
+
# That is the third instance of this class in the corpus: the same one-version
|
|
82
|
+
# difference in the same shell made js-dependency-audit.sh mangle every advisory
|
|
83
|
+
# payload while exiting 0 (#883). Enable it where it exists, carry on where it
|
|
84
|
+
# does not.
|
|
85
|
+
(set -o pipefail) 2>/dev/null && set -o pipefail || true
|
|
38
86
|
|
|
39
87
|
TEMPLATE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
40
88
|
MANIFEST="$TEMPLATE_ROOT/shared-files.json"
|
|
@@ -43,15 +91,24 @@ MANIFEST="$TEMPLATE_ROOT/shared-files.json"
|
|
|
43
91
|
CHECK=""
|
|
44
92
|
ESTATE=""
|
|
45
93
|
ONLY=""
|
|
94
|
+
REHEARSE_ONLY=""
|
|
95
|
+
NO_REHEARSE=""
|
|
46
96
|
while [ $# -gt 0 ]; do
|
|
47
97
|
case "$1" in
|
|
48
98
|
--check) CHECK=1; shift ;;
|
|
99
|
+
--rehearse) REHEARSE_ONLY=1; shift ;;
|
|
100
|
+
# In the open, per AGENTS.md section 7: a gate that can be skipped silently
|
|
101
|
+
# is not a gate. This prints the reason on every line it lets through, so a
|
|
102
|
+
# transcript shows what was shipped unproven.
|
|
103
|
+
--no-rehearse) NO_REHEARSE=1; shift ;;
|
|
49
104
|
--estate) ESTATE="$2"; shift 2 ;;
|
|
50
105
|
--repo) ONLY="$2"; shift 2 ;;
|
|
51
106
|
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
52
107
|
esac
|
|
53
108
|
done
|
|
54
109
|
[ -n "$ESTATE" ] || { echo "--estate <dir> is required" >&2; exit 2; }
|
|
110
|
+
[ -n "$REHEARSE_ONLY" ] && [ -n "$NO_REHEARSE" ] && {
|
|
111
|
+
echo "--rehearse and --no-rehearse are opposites" >&2; exit 2; }
|
|
55
112
|
|
|
56
113
|
FILES=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).files.join('\n'))")
|
|
57
114
|
MARKERS=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).appliesTo.join(' '))")
|
|
@@ -59,6 +116,12 @@ MARKERS=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST'
|
|
|
59
116
|
drifted=0
|
|
60
117
|
synced=0
|
|
61
118
|
current=0
|
|
119
|
+
failed=0
|
|
120
|
+
|
|
121
|
+
# Field separator for the two state files below. A literal tab in a `grep`
|
|
122
|
+
# pattern or a `${var%%...}` expansion is invisible in a diff and one editor
|
|
123
|
+
# away from becoming spaces.
|
|
124
|
+
TAB=$(printf '\t')
|
|
62
125
|
|
|
63
126
|
applies() {
|
|
64
127
|
# Instances are NOT in scope: they carry biffo.core.json and a
|
|
@@ -112,17 +175,35 @@ diff_files() {
|
|
|
112
175
|
echo "$out"
|
|
113
176
|
}
|
|
114
177
|
|
|
115
|
-
|
|
178
|
+
repo_slug() {
|
|
179
|
+
git -C "$1" remote get-url origin | sed -E 's#.*[:/]([^/]+/[^/]+)$#\1#; s#\.git$##'
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
# The absolute path of a working tree's shared git directory, which is the same
|
|
183
|
+
# for a primary checkout and every worktree linked to it -- i.e. an identity for
|
|
184
|
+
# the REPOSITORY rather than for one of its working trees.
|
|
185
|
+
repo_dir() {
|
|
186
|
+
(cd "$1" 2>/dev/null && cd "$(git rev-parse --git-common-dir 2>/dev/null)" 2>/dev/null && pwd)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
TEMPLATE_REPO=$(repo_dir "$TEMPLATE_ROOT")
|
|
190
|
+
[ -n "$TEMPLATE_REPO" ] || { echo "$TEMPLATE_ROOT is not a git repository" >&2; exit 2; }
|
|
191
|
+
|
|
192
|
+
# Phase 1: put the candidate files in place and make the repo ready to run its
|
|
193
|
+
# own gate against them. Deliberately stops short of committing anything -- a
|
|
194
|
+
# staged worktree is a question ("would this land?"), and until phase 2 it has no
|
|
195
|
+
# commit, no push and no PR.
|
|
196
|
+
stage_repo() {
|
|
116
197
|
d="$1"
|
|
117
198
|
label="$2"
|
|
118
|
-
|
|
119
|
-
base=$(gh repo view "$slug" --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null)
|
|
120
|
-
[ -n "$base" ] || { printf '%-26s \033[31mcannot resolve default branch\033[0m\n' "$label"; return 1; }
|
|
199
|
+
base="$3"
|
|
121
200
|
|
|
122
201
|
git -C "$d" fetch origin --quiet || return 1
|
|
123
202
|
wt="$d/.worktrees/shared-sync"
|
|
124
203
|
git -C "$d" worktree remove --force "$wt" 2>/dev/null
|
|
125
|
-
|
|
204
|
+
# `branch -D` reports on STDOUT, so a quiet run printed "Deleted branch
|
|
205
|
+
# chore/sync-shared" in the middle of the rehearsal table.
|
|
206
|
+
git -C "$d" branch -D chore/sync-shared >/dev/null 2>&1
|
|
126
207
|
git -C "$d" worktree add -q "$wt" -b chore/sync-shared "origin/$base" || return 1
|
|
127
208
|
|
|
128
209
|
for f in $FILES; do
|
|
@@ -161,9 +242,82 @@ sync_repo() {
|
|
|
161
242
|
git -C "$wt" add -A
|
|
162
243
|
if git -C "$wt" diff --cached --quiet; then
|
|
163
244
|
git -C "$d" worktree remove --force "$wt" 2>/dev/null
|
|
164
|
-
|
|
245
|
+
return 2
|
|
246
|
+
fi
|
|
247
|
+
return 0
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Phase 1's actual question: with the candidate files in place, does this repo's
|
|
251
|
+
# gate still work HERE?
|
|
252
|
+
#
|
|
253
|
+
# This is the check the estate did not have. `--check` compares bytes, and
|
|
254
|
+
# `gate-coverage.sh --estate` measures repos as they ARE -- neither can say
|
|
255
|
+
# anything about a file that has not been distributed yet. Six of the seven
|
|
256
|
+
# rounds on 2026-07-29 were fixing defects that only exist in a repo that is not
|
|
257
|
+
# this one: a check list tuned to the template's layout, a pytest-cov flag a
|
|
258
|
+
# plugin repo rejects, a package.json that is not at the root.
|
|
259
|
+
rehearse_repo() {
|
|
260
|
+
wt="$1"
|
|
261
|
+
|
|
262
|
+
# `sh`, not `bash`: this is exactly how `.githooks/pre-push` invokes it
|
|
263
|
+
# (`exec sh scripts/verify.sh`), and /bin/sh is dash on every machine in this
|
|
264
|
+
# estate. A gate proven under one shell and run under another is not the same
|
|
265
|
+
# gate -- `js-dependency-audit.sh` reported INCONCLUSIVE on every invocation
|
|
266
|
+
# while exiting 0 for exactly that reason, because dash's `echo` interprets
|
|
267
|
+
# backslash escapes and bash's does not (#883).
|
|
268
|
+
_out=$( (cd "$wt" && sh scripts/verify.sh 2>&1) )
|
|
269
|
+
_rc=$?
|
|
270
|
+
_checks=$(printf '%s' "$_out" | sed -n 's/.*verify passed[^-]*- *//p' | head -1)
|
|
271
|
+
|
|
272
|
+
# `gate-coverage.sh` reads `--list` rather than running anything, so it is
|
|
273
|
+
# cheap, and it answers the question `verify.sh` cannot: the gate ran without
|
|
274
|
+
# error, but how much of THIS repo's CI did it mirror? A gate can exit 0
|
|
275
|
+
# having covered 1 kind in 8 -- that IS #855, and it is invisible in an exit
|
|
276
|
+
# code. Reported, not enforced: what coverage number should block a
|
|
277
|
+
# distribution is H5's call to make with numbers, not this script's to assume.
|
|
278
|
+
#
|
|
279
|
+
# Match its three verdicts, not just the fraction. Reading only `N/M` reported
|
|
280
|
+
# a bare `?` for tabsii-map, which has no ci.yml at all -- so the one repo
|
|
281
|
+
# where the coverage question does not apply looked like the one repo where the
|
|
282
|
+
# measurement had failed. "Not applicable" and "could not tell" are different
|
|
283
|
+
# answers, and this file exists because conflating them is expensive.
|
|
284
|
+
_cov=$( (cd "$wt" && sh scripts/gate-coverage.sh 2>&1) |
|
|
285
|
+
sed 's/\x1b\[[0-9;]*m//g' |
|
|
286
|
+
grep -oE '([0-9]+/[0-9]+|no CI to mirror|NO GATE)' | head -1)
|
|
287
|
+
[ -n "$_cov" ] || _cov='coverage unknown'
|
|
288
|
+
case "$_cov" in
|
|
289
|
+
*/*) _cov="covers $_cov" ;;
|
|
290
|
+
esac
|
|
291
|
+
|
|
292
|
+
if [ "$_rc" -eq 0 ]; then
|
|
293
|
+
case "$_out" in
|
|
294
|
+
*"verify ran NOTHING"*)
|
|
295
|
+
# verify.sh has already established this is not the #855 bug: no
|
|
296
|
+
# `.github/workflows/ci.yml`, so this repo has no CI for the gate to
|
|
297
|
+
# mirror and no shift-left obligation. Reporting it as a pass would be
|
|
298
|
+
# the exact conflation the gate itself refuses to make, so it gets its
|
|
299
|
+
# own verdict.
|
|
300
|
+
printf '%s\t%s\n' NO-CI 'gate ran nothing; repo has no ci.yml' ;;
|
|
301
|
+
*)
|
|
302
|
+
printf '%s\t%s\n' PASS "$(printf '%s (%s)' "$_checks" "$_cov")" ;;
|
|
303
|
+
esac
|
|
165
304
|
return 0
|
|
166
305
|
fi
|
|
306
|
+
_why=$(printf '%s' "$_out" | grep -E 'verify failed|verify ran NOTHING' | head -1)
|
|
307
|
+
[ -n "$_why" ] || _why=$(printf '%s' "$_out" | tail -1)
|
|
308
|
+
printf '%s\t%s\n' FAIL "$(printf '%s' "$_why" | sed 's/\x1b\[[0-9;]*m//g')"
|
|
309
|
+
return 1
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# Phase 2: commit the already-staged worktree, push it, open the PR. Reached only
|
|
313
|
+
# when phase 1 came back clean for EVERY repo in this run.
|
|
314
|
+
ship_repo() {
|
|
315
|
+
d="$1"
|
|
316
|
+
label="$2"
|
|
317
|
+
slug="$3"
|
|
318
|
+
base="$4"
|
|
319
|
+
wt="$d/.worktrees/shared-sync"
|
|
320
|
+
|
|
167
321
|
git -C "$wt" -c commit.gpgsign=false commit -q --no-verify -m "chore(shared): sync template-shared files
|
|
168
322
|
|
|
169
323
|
Distributed by biffo-template's scripts/shared-sync.sh. These files are held
|
|
@@ -212,6 +366,14 @@ Run \`sh scripts/gate-coverage.sh\` after merging to see this repo's gate measur
|
|
|
212
366
|
return 0
|
|
213
367
|
}
|
|
214
368
|
|
|
369
|
+
# The list of repos this run will touch, one `label<TAB>dir<TAB>slug<TAB>base`
|
|
370
|
+
# per line. Written in the survey pass and read twice afterwards, so both phases
|
|
371
|
+
# work from the same set: a rehearsal that proved a different list of repos than
|
|
372
|
+
# the one that ships is worth nothing.
|
|
373
|
+
TARGETS=$(mktemp)
|
|
374
|
+
VERDICTS=$(mktemp)
|
|
375
|
+
trap 'rm -f "$TARGETS" "$VERDICTS"' EXIT
|
|
376
|
+
|
|
215
377
|
printf '\nshared-file sync - template -> repos core upgrade cannot reach\n\n'
|
|
216
378
|
for d in "$ESTATE"/*/; do
|
|
217
379
|
d="${d%/}"
|
|
@@ -220,7 +382,21 @@ for d in "$ESTATE"/*/; do
|
|
|
220
382
|
# The template is the source, not a target. It matched only because it carries
|
|
221
383
|
# scripts/verify.sh, and comparing it to itself through origin/<base> reported
|
|
222
384
|
# it as missing every file whenever its own dev was ahead of the checkout.
|
|
223
|
-
|
|
385
|
+
#
|
|
386
|
+
# Compare REPOSITORIES, not working-tree paths. `[ "$d" = "$TEMPLATE_ROOT" ]`
|
|
387
|
+
# held only when this script was run from the primary checkout -- and AGENTS.md
|
|
388
|
+
# section 1 mandates that all work happens in a worktree, where TEMPLATE_ROOT
|
|
389
|
+
# is `.worktrees/<name>` and the primary checkout beside it looks like just
|
|
390
|
+
# another satellite carrying scripts/verify.sh. It has no biffo.core.json and
|
|
391
|
+
# no sibling/plugin marker, so nothing else excluded it either.
|
|
392
|
+
#
|
|
393
|
+
# It never fired because a primary checkout is normally byte-identical to
|
|
394
|
+
# origin/dev, so `diff_files` reported `current` and skipped it. It fires the
|
|
395
|
+
# moment the candidate files differ from origin/dev -- which is the only
|
|
396
|
+
# situation this script is ever run in while iterating on a shared file. The
|
|
397
|
+
# first `--rehearse` from a worktree would have staged the template as a
|
|
398
|
+
# target of its own distribution and opened a sync PR against its own dev.
|
|
399
|
+
[ "$(repo_dir "$d")" = "$TEMPLATE_REPO" ] && continue
|
|
224
400
|
[ -n "$ONLY" ] && [ "$label" != "$ONLY" ] && continue
|
|
225
401
|
applies "$d" || continue
|
|
226
402
|
delta=$(diff_files "$d")
|
|
@@ -232,14 +408,134 @@ for d in "$ESTATE"/*/; do
|
|
|
232
408
|
drifted=$((drifted + 1))
|
|
233
409
|
if [ -n "$CHECK" ]; then
|
|
234
410
|
printf '%-26s \033[31mDRIFTED\033[0m%s\n' "$label" "$delta"
|
|
235
|
-
|
|
236
|
-
sync_repo "$d" "$label" && synced=$((synced + 1))
|
|
411
|
+
continue
|
|
237
412
|
fi
|
|
413
|
+
slug=$(repo_slug "$d")
|
|
414
|
+
base=$(gh repo view "$slug" --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null)
|
|
415
|
+
if [ -z "$base" ]; then
|
|
416
|
+
printf '%-26s \033[31mcannot resolve default branch\033[0m\n' "$label"
|
|
417
|
+
failed=$((failed + 1))
|
|
418
|
+
continue
|
|
419
|
+
fi
|
|
420
|
+
printf '%s\t%s\t%s\t%s\n' "$label" "$d" "$slug" "$base" >> "$TARGETS"
|
|
421
|
+
printf '%-26s \033[33mdrifted\033[0m%s\n' "$label" "$delta"
|
|
238
422
|
done
|
|
239
423
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
424
|
+
if [ -n "$CHECK" ]; then
|
|
425
|
+
printf '\n%s current, %s drifted\n' "$current" "$drifted"
|
|
426
|
+
if [ "$drifted" -gt 0 ]; then
|
|
427
|
+
printf '\033[31mShared files have drifted.\033[0m Run without --check to open sync PRs.\n\n'
|
|
428
|
+
exit 1
|
|
429
|
+
fi
|
|
430
|
+
printf '\n'
|
|
431
|
+
exit 0
|
|
432
|
+
fi
|
|
433
|
+
|
|
434
|
+
if [ ! -s "$TARGETS" ]; then
|
|
435
|
+
printf '\n%s current, %s drifted\n\n' "$current" "$drifted"
|
|
436
|
+
[ "${failed:-0}" -gt 0 ] && exit 1
|
|
437
|
+
exit 0
|
|
438
|
+
fi
|
|
439
|
+
|
|
440
|
+
# ---- Phase 1: rehearse -------------------------------------------------------
|
|
441
|
+
#
|
|
442
|
+
# Every target, before any of them ships. The order matters and it is the whole
|
|
443
|
+
# point of the change: staging repo 7 and finding the gate broken there must not
|
|
444
|
+
# leave six PRs already open in repos 1-6.
|
|
445
|
+
if [ -n "$NO_REHEARSE" ]; then
|
|
446
|
+
printf '\n\033[31m--no-rehearse: shipping %s repos unproven.\033[0m ' "$(wc -l < "$TARGETS" | tr -d ' ')"
|
|
447
|
+
printf 'Nothing has run the gate against these candidates.\n'
|
|
448
|
+
else
|
|
449
|
+
printf '\nrehearsing %s repos - staging the candidates and running each gate\n\n' \
|
|
450
|
+
"$(wc -l < "$TARGETS" | tr -d ' ')"
|
|
451
|
+
rehearsal_failures=0
|
|
452
|
+
while IFS="$TAB" read -r label d slug base; do
|
|
453
|
+
# Read the status IMMEDIATELY. `if ! stage_repo ...` would have collapsed
|
|
454
|
+
# "could not stage" (1) and "nothing to sync" (2) into one branch, and
|
|
455
|
+
# reported a repo that was already current as a staging failure.
|
|
456
|
+
stage_repo "$d" "$label" "$base"
|
|
457
|
+
stage_rc=$?
|
|
458
|
+
if [ "$stage_rc" -eq 2 ]; then
|
|
459
|
+
printf '%-26s \033[32mnothing to sync\033[0m\n' "$label"
|
|
460
|
+
printf '%s%s%s%s%s\n' "$label" "$TAB" SKIP "$TAB" 'nothing to sync' >> "$VERDICTS"
|
|
461
|
+
continue
|
|
462
|
+
fi
|
|
463
|
+
if [ "$stage_rc" -ne 0 ]; then
|
|
464
|
+
printf '%-26s \033[31mCANNOT STAGE\033[0m - fetch or worktree failed\n' "$label"
|
|
465
|
+
printf '%s%s%s%s%s\n' "$label" "$TAB" FAIL "$TAB" 'could not stage' >> "$VERDICTS"
|
|
466
|
+
rehearsal_failures=$((rehearsal_failures + 1))
|
|
467
|
+
continue
|
|
468
|
+
fi
|
|
469
|
+
verdict_line=$(rehearse_repo "$d/.worktrees/shared-sync")
|
|
470
|
+
verdict=$(printf '%s' "$verdict_line" | cut -f1)
|
|
471
|
+
detail=$(printf '%s' "$verdict_line" | cut -f2-)
|
|
472
|
+
case "$verdict" in
|
|
473
|
+
PASS) printf '%-26s \033[32mPASS\033[0m %s\n' "$label" "$detail" ;;
|
|
474
|
+
NO-CI) printf '%-26s \033[90mNO-CI\033[0m %s\n' "$label" "$detail" ;;
|
|
475
|
+
*)
|
|
476
|
+
printf '%-26s \033[31mFAIL\033[0m %s\n' "$label" "$detail"
|
|
477
|
+
printf '%-26s staged tree left at %s\n' '' "$d/.worktrees/shared-sync"
|
|
478
|
+
rehearsal_failures=$((rehearsal_failures + 1)) ;;
|
|
479
|
+
esac
|
|
480
|
+
printf '%s%s%s%s%s\n' "$label" "$TAB" "$verdict" "$TAB" "$detail" >> "$VERDICTS"
|
|
481
|
+
done < "$TARGETS"
|
|
482
|
+
|
|
483
|
+
if [ "$rehearsal_failures" -gt 0 ]; then
|
|
484
|
+
printf '\n\033[31mrehearsal failed in %s repo(s) - NOTHING was pushed and no PR was opened.\033[0m\n' \
|
|
485
|
+
"$rehearsal_failures"
|
|
486
|
+
printf 'Fix the candidate files here in the template, then run this again. Each\n'
|
|
487
|
+
printf 'round that ships before it is proven costs one PR per satellite: there were\n'
|
|
488
|
+
printf '84 of them on 2026-07-29, in 7 rounds, and six of those rounds carried\n'
|
|
489
|
+
printf 'scripts/verify.sh alone.\n'
|
|
490
|
+
printf 'The failing repos keep their staged worktree so the gate can be run there;\n'
|
|
491
|
+
printf 'the clean ones were removed.\n\n'
|
|
492
|
+
while IFS="$TAB" read -r label verdict detail; do
|
|
493
|
+
[ "$verdict" = FAIL ] || continue
|
|
494
|
+
printf ' %-24s %s\n' "$label" "$detail"
|
|
495
|
+
done < "$VERDICTS"
|
|
496
|
+
printf '\n'
|
|
497
|
+
# Reap the worktrees of the repos that passed. They staged cleanly and are
|
|
498
|
+
# not evidence of anything; leaving 11 of them behind after a refusal is the
|
|
499
|
+
# orphan-worktree accumulation AGENTS.md section 1 exists to prevent.
|
|
500
|
+
while IFS="$TAB" read -r label d slug base; do
|
|
501
|
+
grep -q "^$label${TAB}FAIL${TAB}" "$VERDICTS" && continue
|
|
502
|
+
git -C "$d" worktree remove --force "$d/.worktrees/shared-sync" 2>/dev/null
|
|
503
|
+
git -C "$d" branch -D chore/sync-shared 2>/dev/null
|
|
504
|
+
done < "$TARGETS"
|
|
505
|
+
exit 1
|
|
506
|
+
fi
|
|
507
|
+
printf '\nrehearsal clean in every repo\n'
|
|
244
508
|
fi
|
|
245
|
-
|
|
509
|
+
|
|
510
|
+
if [ -n "$REHEARSE_ONLY" ]; then
|
|
511
|
+
# Staged worktrees are left in place deliberately: --rehearse answers "would
|
|
512
|
+
# this land?", and the tree that answered it is the thing to go and look at.
|
|
513
|
+
printf '\n--rehearse: nothing pushed, no PRs opened. Staged trees are at\n'
|
|
514
|
+
printf '<repo>/.worktrees/shared-sync; re-run without --rehearse to ship.\n\n'
|
|
515
|
+
exit 0
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
# ---- Phase 2: ship -----------------------------------------------------------
|
|
519
|
+
printf '\nopening PRs\n\n'
|
|
520
|
+
while IFS="$TAB" read -r label d slug base; do
|
|
521
|
+
grep -q "^$label${TAB}SKIP${TAB}" "$VERDICTS" 2>/dev/null && continue
|
|
522
|
+
# --no-rehearse skips phase 1 entirely, so nothing has staged these yet.
|
|
523
|
+
if [ -n "$NO_REHEARSE" ]; then
|
|
524
|
+
stage_repo "$d" "$label" "$base"
|
|
525
|
+
case $? in
|
|
526
|
+
2) printf '%-26s \033[32mnothing to sync\033[0m\n' "$label"; continue ;;
|
|
527
|
+
1) printf '%-26s \033[31mCANNOT STAGE\033[0m\n' "$label"; failed=$((failed + 1)); continue ;;
|
|
528
|
+
esac
|
|
529
|
+
fi
|
|
530
|
+
if ship_repo "$d" "$label" "$slug" "$base"; then
|
|
531
|
+
synced=$((synced + 1))
|
|
532
|
+
else
|
|
533
|
+
failed=$((failed + 1))
|
|
534
|
+
fi
|
|
535
|
+
done < "$TARGETS"
|
|
536
|
+
|
|
537
|
+
printf '\n%s current, %s drifted, %s PR(s) opened' "$current" "$drifted" "$synced"
|
|
538
|
+
[ "${failed:-0}" -gt 0 ] && printf ', \033[31m%s failed\033[0m' "$failed"
|
|
539
|
+
printf '\n\n'
|
|
540
|
+
[ "${failed:-0}" -gt 0 ] && exit 1
|
|
541
|
+
exit 0
|