fix(scripts): bump-objectui.sh runs on bash 3.2 — the pin-reachability warning is macOS-reachable again - #12142
Merged
yinlianghui merged 1 commit intoAug 25, 2026
Conversation
…ing is macOS-reachable again `report_objectui_reachability` used the bash 4 builtin `mapfile` to enumerate the branches that contain an off-main pin. `/usr/bin/env bash` is bash 3.2.57 on macOS, and this script is run BY HAND by an operator — the pin-bump procedure in docs/releases-maintenance.md has no CI path — so the builtin fails on the ordinary host, not a fringe one. It also fails on the one branch that must not fail. `mapfile` is reached only after the verdict is "NOT on origin/main", so the #10495 branch-reachability warning is the single thing that shell cannot deliver. Measured on a shell with the builtin disabled: the run dies at status 127 with `mapfile: command not found` as its ONLY output, before the pin is written — the operator is handed a bare builtin error where the warning belongs, and its obvious workaround (hand-editing .objectui-sha) walks around every guard in the file. Replaced both sites with the bash-3.2 read loop already idiomatic in scripts/pm/os-verify-lock.sh, which carries this repo's bash-3.2 floor doctrine. The `if` rather than a trailing `[[ … ]] &&` is load-bearing under `set -e`. CI runs bash 5, so neither the defect nor the repair is observable in a normal CI run. The digest self-test therefore gains R7: a static scan for bash 4+/5 constructs, a vacuity check that the simulation really removes the builtin, and a real re-run of the R2 reachability case under `enable -n mapfile readarray`. Ablation: restoring `mapfile` turns R7, R7b and R7c red (127); restore verified byte-identical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
This was referenced Aug 25, 2026
yinlianghui
marked this pull request as ready for review
August 25, 2026 11:23
yinlianghui
deleted the
claude/issue-12071-bump-objectui-bash3-portability
branch
August 25, 2026 11:40
This was referenced Aug 25, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #12071
scripts/bump-objectui.shenumerated the branches containing an off-main pin withmapfile, a bash 4 builtin./usr/bin/env bashis bash 3.2.57 on macOS (Apple has not shipped bash 4+ for licensing reasons), and this script is run by hand, by an operator — the pin-bump procedure indocs/releases-maintenance.mdhas no CI path — so the builtin fails on the ordinary host, not a fringe one.What the defect actually does to the operator
The card described it as a silent degradation. Measured, it is sharper than that, and worse in a different way.
mapfilesits on exactly one branch: it is reached only after the reachability verdict is already "NOT on origin/main". So the #10495 branch-reachability warning is the single thing that shell cannot deliver — the healthy path never touches the builtin and works fine on a Mac. A real hand-run bump of an unpushed commit, on a shell without the builtin, before this PR:set -ekills the run at 127 before the first mutation of the working tree. So the operator is not handed a half-applied pin; they are handed a bare builtin error standing exactly where a nine-line warning about an unpushed pin should have been, and a bump that did nothing. The obvious next move — edit.objectui-shaby hand, since "the script is broken" — walks around every guard in the file, including the one that was trying to speak.Same run, same shell, after this PR:
The fix
Both sites become the bash-3.2 read loop already idiomatic in
scripts/pm/os-verify-lock.sh, which carries this repo's bash-3.2-floor doctrine and its own simulated-3.2 self-test. Two details are load-bearing and are commented in place:if [[ -n "$ref_line" ]]; then …; firather than a trailing[[ … ]] && …. Under this script'sset -euo pipefail, a false&&-list as the last command of the loop body makes the wholewhilereturn 1 and kills the run on an empty ref list — which is the common case for the "never pushed" branch.local ref_line=''rather thanlocal ref_line, sinceset -uwould otherwise fire on the first read.${#local_refs[@]}and the${arr[@]+"${arr[@]}"}guards below were left alone:os-verify-lock.shuses${#q[@]}inside its own bash-3.2 floor region and its simulated-3.2 run passes, so that idiom is already pinned as safe here.Demonstrating it, given that CI cannot
This is the crux. CI runs bash 5, where
mapfileexists, so a green CI run says nothing about this defect or this repair in either direction. Both halves below were added, because they catch different things.1 — a real run under a shell that genuinely lacks the builtin.
enable -n mapfile readarrayreally does make them "command not found" with status 127 — the identical macOS symptom — andBASH_ENVis sourced by every non-interactive bash, so the script under test inherits it throughspawnSync. Against the unrepaired script this reproduces the card's measurement byte for byte on Linux:2 — a static scan, because
enable -ncannot simulate parse-level constructs (${x^^},declare -A,&>>). It readsbump-objectui.sh, drops comment lines — the file has to name the constructs it refuses in order to explain why — and asserts nomapfile/readarray/declare -A/${x^^}/&>>/EPOCH*survives.Both land as R7/R7a/R7b/R7c in
objectui-changeset-digest.mjs's self-test, the gate the card named. No existing case was deleted, narrowed, or skipped; R1–R6 are untouched and still drive the shell-out.R7a exists because the instrument must not be vacuous: it proves on a probe script, both ways, that the harness really removes the builtin. Without it, a
BASH_ENVthat stopped being honoured (a posix-mode bash, a future harness change) would leave R7b passing while proving nothing.What it would have caught: the defect as shipped. Ablation —
mapfilerestored to the first site, everything else untouched:The static half names the offending line and number; the runtime half reproduces the operator's 127. R7a correctly stays green — it measures the harness, not the script. Restore was verified byte-identical against a pre-ablation snapshot (
diffempty) and the gate re-run green; the mutation script carried atrap … EXIT INT TERMrestore throughout. No rebuild step is involved on either leg:objectui-changeset-digest.mjsreadsbump-objectui.shfrom source on disk, so there is nodist/for a stale artifact to hide in.Why the second remedy from the card is not here
The card offered an up-front
BASH_VERSINFO[0] -lt 4refusal as an alternative or a companion. After this rewrite it would be actively wrong, not merely redundant: the script no longer has a bash-4 dependency, so a version refusal would refuse on precisely the host it now supports, and a macOS operator would be told to go install another bash to run a script that works.The real concern behind that suggestion — a future bash-4-ism gets reintroduced and nobody notices — is what R7's static scan is for, and the scan strictly dominates a runtime guard on it: the scan fires at authoring time, on Linux, on every PR that touches the file, whereas a version check fires only on a macOS seat and only once someone runs the script by hand. It also covers the constructs a version check never would, since a bash-4-ism in a script gated on
BASH_VERSINFOis still a bash-4-ism.The sweep, and what it found
The dispatch asked whether the population is really two lines before treating it as two lines. It is, in this file — all 17 tracked
*.shwere swept for the full construct set, andbump-objectui.shhad only the twomapfilesites (the remainingmapfilementions in it now are the comment explaining the floor).It is not two lines in the repo. Three other scripts carry the same builtins, filed as #12141 rather than fixed here — they are outside this card's declared file surface and their verification surfaces are not the one this gate covers. One of them matters more than its line count suggests:
scripts/gen-sdui-manifest.sh:585usesreadarray, andpnpm sdui:manifestis the next step of this very procedure — the stepbump-objectui.shprints as→ NEXT STEPon the way out, and one with no CI path by decision. A macOS operator now gets past the bump and stops one command later. That is a real remaining gap in the same operator journey and it is the reason the finding is worth reading rather than filing away; sizing it belongs to triage, not to this PR.Verification
Gate union derived in-worktree at the final commit with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(8 families), all run atdcd2eb09fwith exit codes captured before any pipe — all exit 0:check:agent-test-spelling·check:cross-package-test-inputs·check:entry-guard·check:objectui-changeset·check:parse-guard·check:pnpm-filter-targets·check-ci-filter-parity.mjs·check-cross-package-test-inputs.mjs— pluscheck:nul-bytes.The derivation also flagged three families whose
silentverdict "reads as a clearance and is not" because their roster sits underscripts/. Ran them rather than reasoning about them:check:i18n-stale-fill,check:pm-dispatch-gates,check:where-matcher— all exit 0.check:objectui-bump(bump-objectui.selftest.sh, 13 assertions across 4 cases) passes both on plain bash 5 and under the simulated-3.2 shell.check:objectui-changesetpasses on both as well; before this PR it was 7 failures under the simulated shell and green on plain bash 5, which is the whole point of the card.Lint was not narrowed — the full
eslint . --no-inline-configran over its own declared population: 5101 files, 0 errors, 0 warnings, exit 0. (eslint.configenables no type-aware linting, so no untouched file's verdict can move.)No changeset: the diff is two root
scripts/files, no published-package source, soskip-changeset.Generated by Claude Code