Sitelet https://github.com/objectstack-ai/objectstack/pull/12142
Skip to content

fix(scripts): bump-objectui.sh runs on bash 3.2 — the pin-reachability warning is macOS-reachable again - #12142

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-12071-bump-objectui-bash3-portability
Aug 25, 2026
Merged

fix(scripts): bump-objectui.sh runs on bash 3.2 — the pin-reachability warning is macOS-reachable again#12142
yinlianghui merged 1 commit into
mainfrom
claude/issue-12071-bump-objectui-bash3-portability

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes #12071

scripts/bump-objectui.sh enumerated the branches containing an off-main pin with mapfile, a bash 4 builtin. /usr/bin/env bash is 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 in docs/releases-maintenance.md has 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.

mapfile sits 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:

$ bash bump-objectui.sh --no-commit --no-changeset $LOCAL_ONLY_SHA
EXIT=127
--stderr--
bump-objectui.sh: line 324: mapfile: command not found
--pin file now--
ef1430084eab0c57b736823a69654c171a227c0d      # unchanged — the pin was never written

set -e kills 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-sha by 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:

EXIT=0
⚠️  objectui pin 59e2f6049ac7 is NOT reachable from origin/main in …
    on local branches : wip/local-only
    on remote branches: none
    → It is on a LOCAL branch only — this commit has not been pushed to objectui.
--pin file now--
59e2f6049ac7fc3a7f98f53ec033cdc025484ffe      # written, with the verdict on stdout

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 …; fi rather than a trailing [[ … ]] && …. Under this script's set -euo pipefail, a false &&-list as the last command of the loop body makes the whole while return 1 and kills the run on an empty ref list — which is the common case for the "never pushed" branch.
  • local ref_line='' rather than local ref_line, since set -u would otherwise fire on the first read.

${#local_refs[@]} and the ${arr[@]+"${arr[@]}"} guards below were left alone: os-verify-lock.sh uses ${#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 mapfile exists, 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 readarray really does make them "command not found" with status 127 — the identical macOS symptom — and BASH_ENV is sourced by every non-interactive bash, so the script under test inherits it through spawnSync. Against the unrepaired script this reproduces the card's measurement byte for byte on Linux:

$ cat no-bash4.sh
enable -n mapfile readarray 2>/dev/null
$ BASH_ENV=no-bash4.sh node scripts/objectui-changeset-digest.mjs --self-test
⛔ objectui-changeset-digest --self-test: 7 failure(s)
   - #10495 R2c … — status=127
   - #10495 R3 …  — status=127
   … line 324: mapfile: command not found

2 — a static scan, because enable -n cannot simulate parse-level constructs (${x^^}, declare -A, &>>). It reads bump-objectui.sh, drops comment lines — the file has to name the constructs it refuses in order to explain why — and asserts no mapfile/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_ENV that 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 — mapfile restored to the first site, everything else untouched:

✗ #12071 R7 bump-objectui.sh names no bash 4+/5 construct … — 346: mapfile -t local_refs < <(
✓ #12071 R7a the simulated-3.2 harness really removes the builtin
✗ #12071 R7b on a shell without bash 4 builtins the #10495 warning still fires, in full — status=127
✗ #12071 R7c … exit 0, pin written, verdict on stdout — status=127
GATE_EXIT=1

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 (diff empty) and the gate re-run green; the mutation script carried a trap … EXIT INT TERM restore throughout. No rebuild step is involved on either leg: objectui-changeset-digest.mjs reads bump-objectui.sh from source on disk, so there is no dist/ 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 4 refusal 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_VERSINFO is 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 *.sh were swept for the full construct set, and bump-objectui.sh had only the two mapfile sites (the remaining mapfile mentions 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:585 uses readarray, and pnpm sdui:manifest is the next step of this very procedure — the step bump-objectui.sh prints as → NEXT STEP on 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 at dcd2eb09f with 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 — plus check:nul-bytes.

The derivation also flagged three families whose silent verdict "reads as a clearance and is not" because their roster sits under scripts/. 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-changeset passes 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-config ran over its own declared population: 5101 files, 0 errors, 0 warnings, exit 0. (eslint.config enables 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, so skip-changeset.


Generated by Claude Code

…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
@yinlianghui yinlianghui added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 25, 2026 — with Claude
@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 11:23
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 25, 2026
Merged via the queue into main with commit 4b47855 Aug 25, 2026
37 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12071-bump-objectui-bash3-portability branch August 25, 2026 11:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] check:objectui-changeset --self-test cannot run on macOS: bump-objectui.sh uses the bash-4 mapfile builtin

2 participants