From 774a3e26eb43a7449dcfdf9d4f04196912f9b42a Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 24 Oct 2026 12:21:43 -0700 Subject: [PATCH 1/3] Stop grep -q from failing pipelines under pipefail bin/ci decided whether gh-signoff is installed with `gh extension list | grep -q gh-signoff`. grep -q exits at its first match, gh then dies writing the rest of its list (SIGPIPE, 141), and pipefail reports the whole pipeline as failed. So a green full run usually printed "gh signoff not installed", skipped the signoff, and still exited 0: 4 of 5 checks failed here with the extension installed. Plain grep to /dev/null reads to the end; 10 of 10 pass. test/build's multiarch builder check has the same shape. Its input is small enough that it hasn't misfired, but it's the same latent bug, so it gets the same fix. --- bin/ci | 5 ++++- test/build | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bin/ci b/bin/ci index 9732c76..c5786ca 100755 --- a/bin/ci +++ b/bin/ci @@ -224,7 +224,10 @@ build_matrix() { # --- Signoff ---------------------------------------------------------------- signoff() { heading "📋 Signoff" "gh signoff" - if ! command -v gh >/dev/null || ! gh extension list 2>/dev/null | grep -q gh-signoff; then + # Not `grep -q`: it exits at the first match, gh dies writing the rest of its list + # (SIGPIPE, 141), and pipefail reports the pipeline as failed. That skipped the signoff + # on most green runs, while CI still exited 0. Plain grep reads to the end. + if ! command -v gh >/dev/null || ! gh extension list 2>/dev/null | grep gh-signoff >/dev/null; then printf '%s – skipped: gh signoff not installed (gh extension install basecamp/gh-signoff)%s\n' \ "$SUBTITLE" "$RESET" return 0 diff --git a/test/build b/test/build index 0b6fb11..2cf9c6b 100755 --- a/test/build +++ b/test/build @@ -34,9 +34,11 @@ JOBS=${JOBS:-$(( CORES / 2 ))} (( JOBS > 16 )) && JOBS=16 MAKE_JOBS=${MAKE_JOBS:-4} -# Use multi-platform builder if available (faster cross-arch builds) +# Use multi-platform builder if available (faster cross-arch builds). Plain grep to +# /dev/null, not `grep -q`: under pipefail, -q exiting early can SIGPIPE the writer and +# fail the whole test (bin/ci's signoff check lost runs to exactly that). BUILDER="" -if docker buildx ls 2>/dev/null | grep -A2 "^multiarch" | grep -q "running"; then +if docker buildx ls 2>/dev/null | grep -A2 "^multiarch" | grep "running" >/dev/null; then BUILDER="multiarch" fi From 8e54351eb562c3f066e63b820c11fd9b7bf425b6 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 24 Oct 2026 12:21:44 -0700 Subject: [PATCH 2/3] Install native builds' Homebrew deps once, before the parallel builds The definitions run `brew install` while ruby-build sources them, and `test/build native all` sources several at once. Each brew call auto-updates first, so they race for Homebrew's lock, and the losers die within seconds on "Another `brew update` process is already running": 1.9.3, 2.3.3 and 2.3.8 failed that way in one run, before compiling anything. Before launching native builds, install every formula the chosen definitions name, one at a time, then export HOMEBREW_NO_AUTO_UPDATE for the builds. Their own `brew install`s are then no-ops on installed formulae, which run concurrently without contending. --- test/build | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/build b/test/build index 2cf9c6b..0fd6266 100755 --- a/test/build +++ b/test/build @@ -304,6 +304,23 @@ case " $platforms " in exit 1 fi echo "Using ruby-build: $RUBY_BUILD" + + # The definitions `brew install` what they need, and parallel native builds then race + # for Homebrew's lock: each one auto-updates first, and the losers die within seconds + # on "Another `brew update` process is already running". Install everything the chosen + # definitions ask for once, here, one at a time, then turn auto-update off so their own + # `brew install`s are no-ops on installed formulae, which run side by side fine. + if command -v brew >/dev/null 2>&1; then + formulae=$(grep -ho 'brew install [^ ;&|)]*' $versions 2>/dev/null | awk '{ print $3 }' | sort -u || true) + for formula in $formulae; do + echo "Installing $formula for the native builds..." + if ! brew install "$formula" >/dev/null 2>&1; then + echo "error: 'brew install $formula' failed; run it yourself to see why." >&2 + exit 1 + fi + done + export HOMEBREW_NO_AUTO_UPDATE=1 + fi ;; esac From acc038d0bf05c72f7a1a24ee5222c79e09d4c62f Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Sat, 24 Oct 2026 12:21:44 -0700 Subject: [PATCH 3/3] Keep .claude and .git out of the test images' build context test/build builds its images from the repo root, which had no .dockerignore, so agent worktrees under .claude/ (whole extra checkouts) and .git were sent to the builder on every image build. The images copy only what they build from, so neither belongs in the context. --- .dockerignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3104300 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +# Agent worktrees under .claude/ are whole extra checkouts of this repo; the test images +# copy only what they build from, so neither they nor .git belong in the build context. +.claude +.git