From b9438a425dbd7d27093e1c25254c6cffd9ce8db0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 14:08:08 +0000 Subject: [PATCH] fix(scripts): replace the bash 4 mapfile/readarray builtins in three hand-run scripts with the bash 3.2 read loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS ships bash 3.2.57 and `/usr/bin/env bash` resolves to it, so `mapfile`/`readarray` are `command not found` there. All three scripts run under `set -e`, so each aborted at status 127 rather than degrading. scripts/gen-sdui-manifest.sh is the sharp one: `pnpm sdui:manifest` is the literal NEXT STEP that scripts/bump-objectui.sh prints on its way out, and the ratchet is an on-demand gate by decision (#5960) whose only routine CI invocation is cut-rc.yml. So the host it is normally run on is the operator's laptop. Same read-loop idiom as the bump-objectui.sh repair, including the two load-bearing details it documents: `if [[ -n "$x" ]]; then …; fi` rather than a trailing `&&`-list, and an explicit `arr=()` before the loop so `set -u` cannot turn an empty result into `unbound variable`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6 --- scripts/gen-sdui-manifest.sh | 28 +++++++++++++++++++++++++- scripts/pm/os-regen-merge.sh | 39 +++++++++++++++++++++++++++++++++++- scripts/publish-smoke.sh | 25 ++++++++++++++++++++++- 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/scripts/gen-sdui-manifest.sh b/scripts/gen-sdui-manifest.sh index 53136ce35e..5b4e9e7e17 100755 --- a/scripts/gen-sdui-manifest.sh +++ b/scripts/gen-sdui-manifest.sh @@ -582,7 +582,33 @@ trap 'sdui_on_signal INT' INT trap 'sdui_on_signal TERM' TERM trap 'sdui_on_signal HUP' HUP -readarray -t DUMP_DEV_ARGV < <(sdui_dev_server_cmd "$DUMP_PORT") +# READ LOOP, NOT `readarray` — THE BASH 3.2 FLOOR. `readarray`/`mapfile` are +# bash 4 builtins and `/usr/bin/env bash` is bash 3.2.57 on macOS (Apple ships +# no bash 4+, for licensing reasons). This is the sharpest site in the repo for +# that defect: `pnpm sdui:manifest` is the literal `→ NEXT STEP` that +# `scripts/bump-objectui.sh` prints on its way out, and per the comment at the +# head of this file the ratchet is an on-demand gate by decision (#5960) — the +# only routine CI invocation is `cut-rc.yml`'s pre-publish run, a handful of +# times a month on Linux. So the ONE host this line is normally executed on is +# the operator's laptop, and on a Mac it aborted there. Measured with the +# builtin disabled (`enable -n mapfile readarray` via `BASH_ENV`, which +# reproduces the macOS symptom byte for byte): `readarray: command not found`, +# status 127 under `set -e` — and it aborts HERE, after the EXIT/INT/TERM traps +# are armed but before any server is spawned, so the operator is handed a bare +# builtin error at the exact step the pin bump just told them to run. +# +# Keep this loop bash-3.2-clean: no `mapfile`, no `readarray`, no `declare -A`, +# no `${x^^}`/`${x,,}`. Two details are load-bearing: `DUMP_DEV_ARGV=()` before +# the loop (under `set -u` a loop that appends nothing never creates the array, +# and the expansion below would abort with `unbound variable`), and the `if` +# rather than a trailing `[[ … ]] &&` (with the `&&` form the `while` takes +# status 1 whenever the last line read fails the test, which under `set -e` +# kills the caller as soon as such a loop sits last in a function). +DUMP_DEV_ARGV=() +dump_argv_line='' +while IFS= read -r dump_argv_line; do + if [[ -n "$dump_argv_line" ]]; then DUMP_DEV_ARGV+=("$dump_argv_line"); fi +done < <(sdui_dev_server_cmd "$DUMP_PORT") sdui_spawn_detached "$DUMP_PID_FILE" "$DUMP_DEV_LOG" "${DUMP_DEV_ARGV[@]}" # Failing here is a REFUSAL TO GUESS, not an inconvenience: the alternative is diff --git a/scripts/pm/os-regen-merge.sh b/scripts/pm/os-regen-merge.sh index 109cddc4ff..85e85f9887 100644 --- a/scripts/pm/os-regen-merge.sh +++ b/scripts/pm/os-regen-merge.sh @@ -65,7 +65,44 @@ echo "→ fetching origin/main" git fetch origin main # The single authoritative list, read at run time. -mapfile -t regen_paths < <(grep 'merge=os-regen' .gitattributes | awk '{print $1}') +# +# READ LOOP, NOT `mapfile` — THE BASH 3.2 FLOOR. `mapfile`/`readarray` are bash +# 4 builtins and `/usr/bin/env bash` is bash 3.2.57 on macOS (Apple ships no +# bash 4+, for licensing reasons). This script is run BY HAND, in an agent's or +# a maintainer's worktree, and it has no CI path at all — nothing in +# `.github/workflows/` invokes it — so a bash-4 builtin here does not fail on a +# fringe host, it fails on the ordinary one, and no CI run can ever say so. +# Measured with the builtin disabled (`enable -n mapfile readarray` via +# `BASH_ENV`, which reproduces the macOS symptom byte for byte): +# `mapfile: command not found`, then `set -e` kills the run at status 127 — +# AFTER `git fetch origin main` and BEFORE step 1, i.e. with the merge sequence +# whose ORDER is this script's entire reason for existing not begun. The +# operator is left to perform steps 1–3 by hand, which is the trap the script +# was written to remove. +# +# Keep this loop bash-3.2-clean: no `mapfile`, no `readarray`, no `declare -A`, +# no `${x^^}`/`${x,,}`. Two details are load-bearing and neither is obvious: +# +# `regen_paths=()` BEFORE the loop. Under `set -u` a loop that appends +# nothing never creates the array at all, so `${#regen_paths[@]}` below +# aborts with `unbound variable` — and "grep matched nothing" is precisely +# the case the refusal below exists to REPORT. Measured: without the +# declaration the empty input dies at `regen_paths: unbound variable`; with +# it, `count=0` and the refusal prints. +# +# `if [[ -n … ]]; then …; fi`, not a trailing `[[ -n … ]] && …`. Measured: +# with the `&&` form the `while` takes status 1 whenever the LAST line read +# fails the test, and under `set -e` that kills the caller the moment such a +# loop is the last command of a function — silently correct today, a landmine +# for the next refactor. The `if` form returns 0 on the same input. (This is +# the form #12142 standardised; its own note attributes the trap to the empty +# list, which measures clean — an all-empty read leaves the body unexecuted +# and the `while` at status 0.) +regen_paths=() +regen_line='' +while IFS= read -r regen_line; do + if [[ -n "$regen_line" ]]; then regen_paths+=("$regen_line"); fi +done < <(grep 'merge=os-regen' .gitattributes | awk '{print $1}') if [ "${#regen_paths[@]}" -eq 0 ]; then echo "✗ no merge=os-regen entries found in .gitattributes — refusing to guess" >&2 exit 1 diff --git a/scripts/publish-smoke.sh b/scripts/publish-smoke.sh index 8c5541d027..f8e0cef579 100644 --- a/scripts/publish-smoke.sh +++ b/scripts/publish-smoke.sh @@ -671,7 +671,30 @@ mkdir -p "$DEV_TMPDIR" # `\x1b[31m…ERROR…` line the negative test produced). # TMPDIR: see the collision-safety block — it is what makes the runtime state # file this run reads back provably its own. -mapfile -t DEV_ARGV < <(smoke_dev_server_argv) +# READ LOOP, NOT `mapfile` — THE BASH 3.2 FLOOR. `mapfile`/`readarray` are +# bash 4 builtins and `/usr/bin/env bash` is bash 3.2.57 on macOS (Apple ships +# no bash 4+, for licensing reasons). Unlike its two siblings in this sweep +# this line IS reached by CI — `publish-smoke.yml` runs this script on Linux +# with bash 5, where the builtin exists — so CI is green over it in both +# directions and says nothing at all about the defect or this repair. What CI +# does not cover is the OTHER way this script is run: by hand, to reproduce a +# publish-smoke failure locally, which on a Mac died here. Measured with the +# builtin disabled (`enable -n mapfile readarray` via `BASH_ENV`, which +# reproduces the macOS symptom byte for byte): `mapfile: command not found`, +# status 127 under `set -e`. +# +# Keep this loop bash-3.2-clean: no `mapfile`, no `readarray`, no `declare -A`, +# no `${x^^}`/`${x,,}`. Two details are load-bearing: `DEV_ARGV=()` before the +# loop (under `set -u` a loop that appends nothing never creates the array, and +# the expansion below would abort with `unbound variable`), and the `if` rather +# than a trailing `[[ … ]] &&` (with the `&&` form the `while` takes status 1 +# whenever the last line read fails the test, which under `set -e` kills the +# caller as soon as such a loop sits last in a function). +DEV_ARGV=() +dev_argv_line='' +while IFS= read -r dev_argv_line; do + if [[ -n "$dev_argv_line" ]]; then DEV_ARGV+=("$dev_argv_line"); fi +done < <(smoke_dev_server_argv) (cd "$APP_DIR" && exec "${DEV_ARGV[@]}") \ > "$SERVER_LOG" 2>&1 & SERVER_PID=$!