@biffo/cli 0.223.1 → 0.223.2
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/package.json
CHANGED
|
@@ -1,541 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# Distribute the template's shared files to the repos `biffo core upgrade`
|
|
4
|
-
# cannot reach, and make drift visible when it happens.
|
|
5
|
-
#
|
|
6
|
-
# ## Why this exists
|
|
7
|
-
#
|
|
8
|
-
# Instances carry `biffo.core.json` and a `core-manifest.json`, so the CLI
|
|
9
|
-
# three-way-merges template-owned paths into them. **Sibling apps and plugin
|
|
10
|
-
# repos are separate repositories with neither.** The documented channel to them
|
|
11
|
-
# was "vendor it into the skeleton, plus a one-time manual copy-in for existing
|
|
12
|
-
# ones", which is not a mechanism: the skeleton only helps repos created
|
|
13
|
-
# afterwards, and nothing ever prompts the copy-in.
|
|
14
|
-
#
|
|
15
|
-
# The cost, twice over:
|
|
16
|
-
#
|
|
17
|
-
# - AGENTS.md drifted 68 lines behind in tabsii, missing the very workflow
|
|
18
|
-
# guardrails the template had already written (#559).
|
|
19
|
-
# - Eight repos ran a local gate two versions old. `tabsii-crm` checked ONE
|
|
20
|
-
# thing in eight on a 700-line change and printed `verify passed` (#855).
|
|
21
|
-
#
|
|
22
|
-
# Both were found by a human noticing, months and hours late respectively. This
|
|
23
|
-
# turns that into a command that reports, and a `--check` that fails.
|
|
24
|
-
#
|
|
25
|
-
# ## What it is not
|
|
26
|
-
#
|
|
27
|
-
# A **one-way overwrite**, not a merge. `shared-files.json` may only list files
|
|
28
|
-
# every sibling and plugin should hold verbatim. Anything a repo is expected to
|
|
29
|
-
# customise does not belong in it — that is what the instance manifest's
|
|
30
|
-
# three-way merge is for, and this deliberately has no such subtlety.
|
|
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
|
-
#
|
|
63
|
-
# Usage:
|
|
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
|
|
67
|
-
# sh scripts/shared-sync.sh --estate ~/code --repo tabsii-crm
|
|
68
|
-
# sh scripts/shared-sync.sh --estate ~/code --no-rehearse # ship unproven, loudly
|
|
69
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
TEMPLATE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
88
|
-
MANIFEST="$TEMPLATE_ROOT/shared-files.json"
|
|
89
|
-
[ -f "$MANIFEST" ] || { echo "no shared-files.json beside $0" >&2; exit 2; }
|
|
90
|
-
|
|
91
|
-
CHECK=""
|
|
92
|
-
ESTATE=""
|
|
93
|
-
ONLY=""
|
|
94
|
-
REHEARSE_ONLY=""
|
|
95
|
-
NO_REHEARSE=""
|
|
96
|
-
while [ $# -gt 0 ]; do
|
|
97
|
-
case "$1" in
|
|
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 ;;
|
|
104
|
-
--estate) ESTATE="$2"; shift 2 ;;
|
|
105
|
-
--repo) ONLY="$2"; shift 2 ;;
|
|
106
|
-
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
107
|
-
esac
|
|
108
|
-
done
|
|
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; }
|
|
112
|
-
|
|
113
|
-
FILES=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).files.join('\n'))")
|
|
114
|
-
MARKERS=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).appliesTo.join(' '))")
|
|
115
|
-
|
|
116
|
-
drifted=0
|
|
117
|
-
synced=0
|
|
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')
|
|
125
|
-
|
|
126
|
-
applies() {
|
|
127
|
-
# Instances are NOT in scope: they carry biffo.core.json and a
|
|
128
|
-
# core-manifest.json, so `biffo core upgrade` three-way-merges these paths
|
|
129
|
-
# into them. Two mechanisms writing the same files would fight, and the
|
|
130
|
-
# core-ownership guard would refuse this script's commit anyway -- correctly,
|
|
131
|
-
# since these are template-owned paths in an instance.
|
|
132
|
-
[ -f "$1/biffo.core.json" ] && return 1
|
|
133
|
-
for m in $MARKERS; do [ -f "$1/$m" ] && return 0; done
|
|
134
|
-
# Also: any repo already carrying the gate. The runner repos have neither
|
|
135
|
-
# marker but did receive verify.sh, and a mechanism that distributes a file
|
|
136
|
-
# once and then stops tracking it is the drift this script exists to end --
|
|
137
|
-
# it would have recreated the exact hole in the exact repos nobody watches.
|
|
138
|
-
[ -f "$1/scripts/verify.sh" ] && return 0
|
|
139
|
-
return 1
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
# Which of the shared files differ in this repo. Missing counts as drifted: a
|
|
143
|
-
# repo that never received a file is exactly as unprotected as one holding a
|
|
144
|
-
# stale copy, and reporting them differently invites triaging only the second.
|
|
145
|
-
diff_files() {
|
|
146
|
-
d="$1"
|
|
147
|
-
out=""
|
|
148
|
-
# Compare against the REPO's integration branch, not the local working copy.
|
|
149
|
-
#
|
|
150
|
-
# The first version read "$d/$f" straight off disk, so a clone that had not
|
|
151
|
-
# been pulled reported DRIFTED for twelve repos that were entirely current --
|
|
152
|
-
# right after their sync PRs merged. The question is "is this repository
|
|
153
|
-
# current", not "is my laptop current", and a drift detector that fires on a
|
|
154
|
-
# stale checkout is one you learn to ignore.
|
|
155
|
-
git -C "$d" fetch origin --quiet 2>/dev/null
|
|
156
|
-
# `dev` first, per AGENTS.md section 2: it is the integration branch in every
|
|
157
|
-
# Biffo repo. origin/HEAD is NOT a substitute -- it points at `main` in
|
|
158
|
-
# several clones, and `main` is a stale release branch that legitimately does
|
|
159
|
-
# not carry these files, so resolving through it reported three repos as
|
|
160
|
-
# missing everything.
|
|
161
|
-
if git -C "$d" rev-parse --verify --quiet origin/dev >/dev/null 2>&1; then
|
|
162
|
-
base=dev
|
|
163
|
-
else
|
|
164
|
-
base=$(git -C "$d" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
|
|
165
|
-
[ -n "$base" ] || base=$(git -C "$d" rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
166
|
-
fi
|
|
167
|
-
for f in $FILES; do
|
|
168
|
-
remote=$(git -C "$d" show "origin/$base:$f" 2>/dev/null)
|
|
169
|
-
if [ -z "$remote" ]; then
|
|
170
|
-
out="$out $f(missing)"
|
|
171
|
-
elif [ "$remote" != "$(cat "$TEMPLATE_ROOT/$f")" ]; then
|
|
172
|
-
out="$out $f"
|
|
173
|
-
fi
|
|
174
|
-
done
|
|
175
|
-
echo "$out"
|
|
176
|
-
}
|
|
177
|
-
|
|
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() {
|
|
197
|
-
d="$1"
|
|
198
|
-
label="$2"
|
|
199
|
-
base="$3"
|
|
200
|
-
|
|
201
|
-
git -C "$d" fetch origin --quiet || return 1
|
|
202
|
-
wt="$d/.worktrees/shared-sync"
|
|
203
|
-
git -C "$d" worktree remove --force "$wt" 2>/dev/null
|
|
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
|
|
207
|
-
git -C "$d" worktree add -q "$wt" -b chore/sync-shared "origin/$base" || return 1
|
|
208
|
-
|
|
209
|
-
for f in $FILES; do
|
|
210
|
-
mkdir -p "$wt/$(dirname "$f")"
|
|
211
|
-
cp "$TEMPLATE_ROOT/$f" "$wt/$f"
|
|
212
|
-
chmod +x "$wt/$f" 2>/dev/null
|
|
213
|
-
done
|
|
214
|
-
|
|
215
|
-
# Stamp the version these files CAME FROM, so a repo can say which template
|
|
216
|
-
# its gate is, without a template clone or a network call (#869, H5 gap 1).
|
|
217
|
-
#
|
|
218
|
-
# Read from $TEMPLATE_ROOT, never from $wt. H5 pre-registered this as the most
|
|
219
|
-
# likely way to make the whole thing meaningless: a stamp generated from the
|
|
220
|
-
# RECEIVING repo always matches itself and reports perfect health forever --
|
|
221
|
-
# the shape of every instrument defect found on 2026-07-29. The test asserts
|
|
222
|
-
# the two differ.
|
|
223
|
-
_tv=$(git -C "$TEMPLATE_ROOT" describe --tags --match 'core-v*' --abbrev=0 2>/dev/null || echo unknown)
|
|
224
|
-
printf '%s\n' "$_tv" > "$wt/.biffo-shared-version"
|
|
225
|
-
|
|
226
|
-
# Install the JS/Python deps the gate will now check, or its own pre-push
|
|
227
|
-
# correctly refuses this push -- and reaching for BIFFO_SKIP_VERIFY to ship a
|
|
228
|
-
# gate would be the counter-metric H4 pre-registered as refuting itself.
|
|
229
|
-
# The ROOT package too, not only the nested ones. The first version matched
|
|
230
|
-
# `--dir` paths only, so a repo whose package.json is at the root -- tabsii-map
|
|
231
|
-
# -- got no install and the gate refused the push with `tsc: not found`. It
|
|
232
|
-
# then reported GATE REFUSED, which was true and useless: the gate was right,
|
|
233
|
-
# the installer had skipped the one layout it could not see.
|
|
234
|
-
[ -f "$wt/package.json" ] && (cd "$wt" && pnpm install --frozen-lockfile >/dev/null 2>&1 || true)
|
|
235
|
-
[ -f "$wt/pyproject.toml" ] && (cd "$wt" && uv sync --all-groups >/dev/null 2>&1 || true)
|
|
236
|
-
for p in $( (cd "$wt" && sh scripts/verify.sh --list 2>/dev/null) | grep -oE '\-\-dir(ectory)? \./[A-Za-z0-9_./-]+' | awk '{print $2}' | sort -u); do
|
|
237
|
-
(cd "$wt/$p" 2>/dev/null && { pnpm install --frozen-lockfile >/dev/null 2>&1 ||
|
|
238
|
-
pnpm install --frozen-lockfile --ignore-workspace >/dev/null 2>&1 ||
|
|
239
|
-
uv sync --all-groups >/dev/null 2>&1; }) || true
|
|
240
|
-
done
|
|
241
|
-
|
|
242
|
-
git -C "$wt" add -A
|
|
243
|
-
if git -C "$wt" diff --cached --quiet; then
|
|
244
|
-
git -C "$d" worktree remove --force "$wt" 2>/dev/null
|
|
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
|
|
304
|
-
return 0
|
|
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
|
-
|
|
321
|
-
git -C "$wt" -c commit.gpgsign=false commit -q --no-verify -m "chore(shared): sync template-shared files
|
|
322
|
-
|
|
323
|
-
Distributed by biffo-template's scripts/shared-sync.sh. These files are held
|
|
324
|
-
verbatim from the template; see shared-files.json there for the list and why
|
|
325
|
-
this mechanism exists.
|
|
326
|
-
|
|
327
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>" >/dev/null 2>&1
|
|
328
|
-
|
|
329
|
-
# --force-with-lease: this branch is regenerated from origin/<base> on every
|
|
330
|
-
# run, so a previous sync's branch is legitimately replaced -- but the lease
|
|
331
|
-
# still refuses if someone else has pushed to it.
|
|
332
|
-
#
|
|
333
|
-
# The output is CAPTURED and classified rather than discarded. The first
|
|
334
|
-
# version printed "PUSH REFUSED (run the gate there and look)" for every
|
|
335
|
-
# failure, and the first real cause was a plain non-fast-forward against a
|
|
336
|
-
# previous run's branch. The gate was green in that repo. A diagnostic that
|
|
337
|
-
# names the wrong cause sends you to read a passing log, which is the same
|
|
338
|
-
# class of defect as everything else this gate exists to catch.
|
|
339
|
-
push_out=$(git -C "$wt" push --force-with-lease -u origin HEAD 2>&1)
|
|
340
|
-
push_rc=$?
|
|
341
|
-
if [ "$push_rc" -ne 0 ]; then
|
|
342
|
-
case "$push_out" in
|
|
343
|
-
*"verify failed"*|*"verify ran NOTHING"*)
|
|
344
|
-
printf '%-26s \033[31mGATE REFUSED THE PUSH\033[0m - run scripts/verify.sh there\n' "$label" ;;
|
|
345
|
-
*"stale info"*|*"non-fast-forward"*)
|
|
346
|
-
printf '%-26s \033[31mbranch diverged\033[0m - someone else pushed to chore/sync-shared\n' "$label" ;;
|
|
347
|
-
*)
|
|
348
|
-
printf '%-26s \033[31mpush failed\033[0m: %s\n' "$label" "$(echo "$push_out" | tail -1)" ;;
|
|
349
|
-
esac
|
|
350
|
-
return 1
|
|
351
|
-
fi
|
|
352
|
-
url=$(gh pr create --repo "$slug" --base "$base" --head chore/sync-shared \
|
|
353
|
-
--title "chore(shared): sync template-shared files" \
|
|
354
|
-
--body "Distributed by \`biffo-template\`'s \`scripts/shared-sync.sh\`.
|
|
355
|
-
|
|
356
|
-
Sibling and plugin repos are separate repositories with no \`core-manifest.json\`, so \`biffo core upgrade\` cannot reach them. The documented channel was \"vendor into the skeleton plus a one-time manual copy-in\", which only ever helped repos created afterwards — and is why this repo was running a local gate two versions old.
|
|
357
|
-
|
|
358
|
-
Files synced verbatim from the template (see \`shared-files.json\` there):
|
|
359
|
-
|
|
360
|
-
$(for f in $FILES; do echo "- \`$f\`"; done)
|
|
361
|
-
|
|
362
|
-
Run \`sh scripts/gate-coverage.sh\` after merging to see this repo's gate measured against its own CI.
|
|
363
|
-
|
|
364
|
-
🤖 Generated with [Claude Code](https://claude.com/claude-code)" 2>&1 | tail -1)
|
|
365
|
-
printf '%-26s %s\n' "$label" "$url"
|
|
366
|
-
return 0
|
|
367
|
-
}
|
|
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
|
-
|
|
377
|
-
printf '\nshared-file sync - template -> repos core upgrade cannot reach\n\n'
|
|
378
|
-
for d in "$ESTATE"/*/; do
|
|
379
|
-
d="${d%/}"
|
|
380
|
-
label=$(basename "$d")
|
|
381
|
-
[ -e "$d/.git" ] || continue
|
|
382
|
-
# The template is the source, not a target. It matched only because it carries
|
|
383
|
-
# scripts/verify.sh, and comparing it to itself through origin/<base> reported
|
|
384
|
-
# it as missing every file whenever its own dev was ahead of the checkout.
|
|
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
|
|
400
|
-
[ -n "$ONLY" ] && [ "$label" != "$ONLY" ] && continue
|
|
401
|
-
applies "$d" || continue
|
|
402
|
-
delta=$(diff_files "$d")
|
|
403
|
-
if [ -z "$delta" ]; then
|
|
404
|
-
current=$((current + 1))
|
|
405
|
-
printf '%-26s \033[32mcurrent\033[0m\n' "$label"
|
|
406
|
-
continue
|
|
407
|
-
fi
|
|
408
|
-
drifted=$((drifted + 1))
|
|
409
|
-
if [ -n "$CHECK" ]; then
|
|
410
|
-
printf '%-26s \033[31mDRIFTED\033[0m%s\n' "$label" "$delta"
|
|
411
|
-
continue
|
|
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"
|
|
422
|
-
done
|
|
423
|
-
|
|
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'
|
|
508
|
-
fi
|
|
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
|
|
@@ -1,541 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# Distribute the template's shared files to the repos `biffo core upgrade`
|
|
4
|
-
# cannot reach, and make drift visible when it happens.
|
|
5
|
-
#
|
|
6
|
-
# ## Why this exists
|
|
7
|
-
#
|
|
8
|
-
# Instances carry `biffo.core.json` and a `core-manifest.json`, so the CLI
|
|
9
|
-
# three-way-merges template-owned paths into them. **Sibling apps and plugin
|
|
10
|
-
# repos are separate repositories with neither.** The documented channel to them
|
|
11
|
-
# was "vendor it into the skeleton, plus a one-time manual copy-in for existing
|
|
12
|
-
# ones", which is not a mechanism: the skeleton only helps repos created
|
|
13
|
-
# afterwards, and nothing ever prompts the copy-in.
|
|
14
|
-
#
|
|
15
|
-
# The cost, twice over:
|
|
16
|
-
#
|
|
17
|
-
# - AGENTS.md drifted 68 lines behind in tabsii, missing the very workflow
|
|
18
|
-
# guardrails the template had already written (#559).
|
|
19
|
-
# - Eight repos ran a local gate two versions old. `tabsii-crm` checked ONE
|
|
20
|
-
# thing in eight on a 700-line change and printed `verify passed` (#855).
|
|
21
|
-
#
|
|
22
|
-
# Both were found by a human noticing, months and hours late respectively. This
|
|
23
|
-
# turns that into a command that reports, and a `--check` that fails.
|
|
24
|
-
#
|
|
25
|
-
# ## What it is not
|
|
26
|
-
#
|
|
27
|
-
# A **one-way overwrite**, not a merge. `shared-files.json` may only list files
|
|
28
|
-
# every sibling and plugin should hold verbatim. Anything a repo is expected to
|
|
29
|
-
# customise does not belong in it — that is what the instance manifest's
|
|
30
|
-
# three-way merge is for, and this deliberately has no such subtlety.
|
|
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
|
-
#
|
|
63
|
-
# Usage:
|
|
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
|
|
67
|
-
# sh scripts/shared-sync.sh --estate ~/code --repo tabsii-crm
|
|
68
|
-
# sh scripts/shared-sync.sh --estate ~/code --no-rehearse # ship unproven, loudly
|
|
69
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
TEMPLATE_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
88
|
-
MANIFEST="$TEMPLATE_ROOT/shared-files.json"
|
|
89
|
-
[ -f "$MANIFEST" ] || { echo "no shared-files.json beside $0" >&2; exit 2; }
|
|
90
|
-
|
|
91
|
-
CHECK=""
|
|
92
|
-
ESTATE=""
|
|
93
|
-
ONLY=""
|
|
94
|
-
REHEARSE_ONLY=""
|
|
95
|
-
NO_REHEARSE=""
|
|
96
|
-
while [ $# -gt 0 ]; do
|
|
97
|
-
case "$1" in
|
|
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 ;;
|
|
104
|
-
--estate) ESTATE="$2"; shift 2 ;;
|
|
105
|
-
--repo) ONLY="$2"; shift 2 ;;
|
|
106
|
-
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
|
107
|
-
esac
|
|
108
|
-
done
|
|
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; }
|
|
112
|
-
|
|
113
|
-
FILES=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).files.join('\n'))")
|
|
114
|
-
MARKERS=$(node -e "console.log(JSON.parse(require('fs').readFileSync('$MANIFEST','utf8')).appliesTo.join(' '))")
|
|
115
|
-
|
|
116
|
-
drifted=0
|
|
117
|
-
synced=0
|
|
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')
|
|
125
|
-
|
|
126
|
-
applies() {
|
|
127
|
-
# Instances are NOT in scope: they carry biffo.core.json and a
|
|
128
|
-
# core-manifest.json, so `biffo core upgrade` three-way-merges these paths
|
|
129
|
-
# into them. Two mechanisms writing the same files would fight, and the
|
|
130
|
-
# core-ownership guard would refuse this script's commit anyway -- correctly,
|
|
131
|
-
# since these are template-owned paths in an instance.
|
|
132
|
-
[ -f "$1/biffo.core.json" ] && return 1
|
|
133
|
-
for m in $MARKERS; do [ -f "$1/$m" ] && return 0; done
|
|
134
|
-
# Also: any repo already carrying the gate. The runner repos have neither
|
|
135
|
-
# marker but did receive verify.sh, and a mechanism that distributes a file
|
|
136
|
-
# once and then stops tracking it is the drift this script exists to end --
|
|
137
|
-
# it would have recreated the exact hole in the exact repos nobody watches.
|
|
138
|
-
[ -f "$1/scripts/verify.sh" ] && return 0
|
|
139
|
-
return 1
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
# Which of the shared files differ in this repo. Missing counts as drifted: a
|
|
143
|
-
# repo that never received a file is exactly as unprotected as one holding a
|
|
144
|
-
# stale copy, and reporting them differently invites triaging only the second.
|
|
145
|
-
diff_files() {
|
|
146
|
-
d="$1"
|
|
147
|
-
out=""
|
|
148
|
-
# Compare against the REPO's integration branch, not the local working copy.
|
|
149
|
-
#
|
|
150
|
-
# The first version read "$d/$f" straight off disk, so a clone that had not
|
|
151
|
-
# been pulled reported DRIFTED for twelve repos that were entirely current --
|
|
152
|
-
# right after their sync PRs merged. The question is "is this repository
|
|
153
|
-
# current", not "is my laptop current", and a drift detector that fires on a
|
|
154
|
-
# stale checkout is one you learn to ignore.
|
|
155
|
-
git -C "$d" fetch origin --quiet 2>/dev/null
|
|
156
|
-
# `dev` first, per AGENTS.md section 2: it is the integration branch in every
|
|
157
|
-
# Biffo repo. origin/HEAD is NOT a substitute -- it points at `main` in
|
|
158
|
-
# several clones, and `main` is a stale release branch that legitimately does
|
|
159
|
-
# not carry these files, so resolving through it reported three repos as
|
|
160
|
-
# missing everything.
|
|
161
|
-
if git -C "$d" rev-parse --verify --quiet origin/dev >/dev/null 2>&1; then
|
|
162
|
-
base=dev
|
|
163
|
-
else
|
|
164
|
-
base=$(git -C "$d" symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')
|
|
165
|
-
[ -n "$base" ] || base=$(git -C "$d" rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
166
|
-
fi
|
|
167
|
-
for f in $FILES; do
|
|
168
|
-
remote=$(git -C "$d" show "origin/$base:$f" 2>/dev/null)
|
|
169
|
-
if [ -z "$remote" ]; then
|
|
170
|
-
out="$out $f(missing)"
|
|
171
|
-
elif [ "$remote" != "$(cat "$TEMPLATE_ROOT/$f")" ]; then
|
|
172
|
-
out="$out $f"
|
|
173
|
-
fi
|
|
174
|
-
done
|
|
175
|
-
echo "$out"
|
|
176
|
-
}
|
|
177
|
-
|
|
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() {
|
|
197
|
-
d="$1"
|
|
198
|
-
label="$2"
|
|
199
|
-
base="$3"
|
|
200
|
-
|
|
201
|
-
git -C "$d" fetch origin --quiet || return 1
|
|
202
|
-
wt="$d/.worktrees/shared-sync"
|
|
203
|
-
git -C "$d" worktree remove --force "$wt" 2>/dev/null
|
|
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
|
|
207
|
-
git -C "$d" worktree add -q "$wt" -b chore/sync-shared "origin/$base" || return 1
|
|
208
|
-
|
|
209
|
-
for f in $FILES; do
|
|
210
|
-
mkdir -p "$wt/$(dirname "$f")"
|
|
211
|
-
cp "$TEMPLATE_ROOT/$f" "$wt/$f"
|
|
212
|
-
chmod +x "$wt/$f" 2>/dev/null
|
|
213
|
-
done
|
|
214
|
-
|
|
215
|
-
# Stamp the version these files CAME FROM, so a repo can say which template
|
|
216
|
-
# its gate is, without a template clone or a network call (#869, H5 gap 1).
|
|
217
|
-
#
|
|
218
|
-
# Read from $TEMPLATE_ROOT, never from $wt. H5 pre-registered this as the most
|
|
219
|
-
# likely way to make the whole thing meaningless: a stamp generated from the
|
|
220
|
-
# RECEIVING repo always matches itself and reports perfect health forever --
|
|
221
|
-
# the shape of every instrument defect found on 2026-07-29. The test asserts
|
|
222
|
-
# the two differ.
|
|
223
|
-
_tv=$(git -C "$TEMPLATE_ROOT" describe --tags --match 'core-v*' --abbrev=0 2>/dev/null || echo unknown)
|
|
224
|
-
printf '%s\n' "$_tv" > "$wt/.biffo-shared-version"
|
|
225
|
-
|
|
226
|
-
# Install the JS/Python deps the gate will now check, or its own pre-push
|
|
227
|
-
# correctly refuses this push -- and reaching for BIFFO_SKIP_VERIFY to ship a
|
|
228
|
-
# gate would be the counter-metric H4 pre-registered as refuting itself.
|
|
229
|
-
# The ROOT package too, not only the nested ones. The first version matched
|
|
230
|
-
# `--dir` paths only, so a repo whose package.json is at the root -- tabsii-map
|
|
231
|
-
# -- got no install and the gate refused the push with `tsc: not found`. It
|
|
232
|
-
# then reported GATE REFUSED, which was true and useless: the gate was right,
|
|
233
|
-
# the installer had skipped the one layout it could not see.
|
|
234
|
-
[ -f "$wt/package.json" ] && (cd "$wt" && pnpm install --frozen-lockfile >/dev/null 2>&1 || true)
|
|
235
|
-
[ -f "$wt/pyproject.toml" ] && (cd "$wt" && uv sync --all-groups >/dev/null 2>&1 || true)
|
|
236
|
-
for p in $( (cd "$wt" && sh scripts/verify.sh --list 2>/dev/null) | grep -oE '\-\-dir(ectory)? \./[A-Za-z0-9_./-]+' | awk '{print $2}' | sort -u); do
|
|
237
|
-
(cd "$wt/$p" 2>/dev/null && { pnpm install --frozen-lockfile >/dev/null 2>&1 ||
|
|
238
|
-
pnpm install --frozen-lockfile --ignore-workspace >/dev/null 2>&1 ||
|
|
239
|
-
uv sync --all-groups >/dev/null 2>&1; }) || true
|
|
240
|
-
done
|
|
241
|
-
|
|
242
|
-
git -C "$wt" add -A
|
|
243
|
-
if git -C "$wt" diff --cached --quiet; then
|
|
244
|
-
git -C "$d" worktree remove --force "$wt" 2>/dev/null
|
|
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
|
|
304
|
-
return 0
|
|
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
|
-
|
|
321
|
-
git -C "$wt" -c commit.gpgsign=false commit -q --no-verify -m "chore(shared): sync template-shared files
|
|
322
|
-
|
|
323
|
-
Distributed by biffo-template's scripts/shared-sync.sh. These files are held
|
|
324
|
-
verbatim from the template; see shared-files.json there for the list and why
|
|
325
|
-
this mechanism exists.
|
|
326
|
-
|
|
327
|
-
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>" >/dev/null 2>&1
|
|
328
|
-
|
|
329
|
-
# --force-with-lease: this branch is regenerated from origin/<base> on every
|
|
330
|
-
# run, so a previous sync's branch is legitimately replaced -- but the lease
|
|
331
|
-
# still refuses if someone else has pushed to it.
|
|
332
|
-
#
|
|
333
|
-
# The output is CAPTURED and classified rather than discarded. The first
|
|
334
|
-
# version printed "PUSH REFUSED (run the gate there and look)" for every
|
|
335
|
-
# failure, and the first real cause was a plain non-fast-forward against a
|
|
336
|
-
# previous run's branch. The gate was green in that repo. A diagnostic that
|
|
337
|
-
# names the wrong cause sends you to read a passing log, which is the same
|
|
338
|
-
# class of defect as everything else this gate exists to catch.
|
|
339
|
-
push_out=$(git -C "$wt" push --force-with-lease -u origin HEAD 2>&1)
|
|
340
|
-
push_rc=$?
|
|
341
|
-
if [ "$push_rc" -ne 0 ]; then
|
|
342
|
-
case "$push_out" in
|
|
343
|
-
*"verify failed"*|*"verify ran NOTHING"*)
|
|
344
|
-
printf '%-26s \033[31mGATE REFUSED THE PUSH\033[0m - run scripts/verify.sh there\n' "$label" ;;
|
|
345
|
-
*"stale info"*|*"non-fast-forward"*)
|
|
346
|
-
printf '%-26s \033[31mbranch diverged\033[0m - someone else pushed to chore/sync-shared\n' "$label" ;;
|
|
347
|
-
*)
|
|
348
|
-
printf '%-26s \033[31mpush failed\033[0m: %s\n' "$label" "$(echo "$push_out" | tail -1)" ;;
|
|
349
|
-
esac
|
|
350
|
-
return 1
|
|
351
|
-
fi
|
|
352
|
-
url=$(gh pr create --repo "$slug" --base "$base" --head chore/sync-shared \
|
|
353
|
-
--title "chore(shared): sync template-shared files" \
|
|
354
|
-
--body "Distributed by \`biffo-template\`'s \`scripts/shared-sync.sh\`.
|
|
355
|
-
|
|
356
|
-
Sibling and plugin repos are separate repositories with no \`core-manifest.json\`, so \`biffo core upgrade\` cannot reach them. The documented channel was \"vendor into the skeleton plus a one-time manual copy-in\", which only ever helped repos created afterwards — and is why this repo was running a local gate two versions old.
|
|
357
|
-
|
|
358
|
-
Files synced verbatim from the template (see \`shared-files.json\` there):
|
|
359
|
-
|
|
360
|
-
$(for f in $FILES; do echo "- \`$f\`"; done)
|
|
361
|
-
|
|
362
|
-
Run \`sh scripts/gate-coverage.sh\` after merging to see this repo's gate measured against its own CI.
|
|
363
|
-
|
|
364
|
-
🤖 Generated with [Claude Code](https://claude.com/claude-code)" 2>&1 | tail -1)
|
|
365
|
-
printf '%-26s %s\n' "$label" "$url"
|
|
366
|
-
return 0
|
|
367
|
-
}
|
|
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
|
-
|
|
377
|
-
printf '\nshared-file sync - template -> repos core upgrade cannot reach\n\n'
|
|
378
|
-
for d in "$ESTATE"/*/; do
|
|
379
|
-
d="${d%/}"
|
|
380
|
-
label=$(basename "$d")
|
|
381
|
-
[ -e "$d/.git" ] || continue
|
|
382
|
-
# The template is the source, not a target. It matched only because it carries
|
|
383
|
-
# scripts/verify.sh, and comparing it to itself through origin/<base> reported
|
|
384
|
-
# it as missing every file whenever its own dev was ahead of the checkout.
|
|
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
|
|
400
|
-
[ -n "$ONLY" ] && [ "$label" != "$ONLY" ] && continue
|
|
401
|
-
applies "$d" || continue
|
|
402
|
-
delta=$(diff_files "$d")
|
|
403
|
-
if [ -z "$delta" ]; then
|
|
404
|
-
current=$((current + 1))
|
|
405
|
-
printf '%-26s \033[32mcurrent\033[0m\n' "$label"
|
|
406
|
-
continue
|
|
407
|
-
fi
|
|
408
|
-
drifted=$((drifted + 1))
|
|
409
|
-
if [ -n "$CHECK" ]; then
|
|
410
|
-
printf '%-26s \033[31mDRIFTED\033[0m%s\n' "$label" "$delta"
|
|
411
|
-
continue
|
|
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"
|
|
422
|
-
done
|
|
423
|
-
|
|
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'
|
|
508
|
-
fi
|
|
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
|