Skip to content

fix(scripts): drop the bash 4 mapfile/readarray builtins from the three remaining hand-run scripts - #12219

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-12141-bash4-builtins-sweep
Aug 25, 2026
Merged

fix(scripts): drop the bash 4 mapfile/readarray builtins from the three remaining hand-run scripts#12219
yinlianghui merged 1 commit into
mainfrom
claude/issue-12141-bash4-builtins-sweep

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #12141

Three hand-run shell scripts still called the bash 4 builtins mapfile/readarray. /usr/bin/env bash is bash 3.2.57 on macOS, all three run under set -e, so on a Mac each aborted at status 127 rather than degrading. Same defect class and same repair idiom as the scripts/bump-objectui.sh fix that landed in 4b47855c05.

file was now
scripts/gen-sdui-manifest.sh:585 readarray -t DUMP_DEV_ARGV bash-3.2 read loop
scripts/pm/os-regen-merge.sh:68 mapfile -t regen_paths bash-3.2 read loop
scripts/publish-smoke.sh:674 mapfile -t DEV_ARGV bash-3.2 read loop

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 (verified on the merged tree — scripts/bump-objectui.sh:560-564), so a macOS operator now gets past the pin bump and stopped one command later.

No BASH_VERSINFO version refusal was added, for the reason the sibling repair recorded: after the rewrite these scripts have no bash-4 dependency, so a version guard would refuse on precisely the host they now support.

Verification — a fix that cannot be demonstrated on Linux is not a fix

None of these three has a PR-CI path, so a green CI run says nothing in either direction. Everything below was run on Linux (bash 5.2.21) with the bash 4 builtins taken away by enable -n mapfile readarray through BASH_ENV.

The instrument is not vacuous. Measured on a probe first, both ways, so a BASH_ENV that stopped being honoured cannot leave the real case passing while proving nothing:

plain      : MAPFILE-WORKS                              rc=0
simulated  : probe.sh: line 1: mapfile: command not found   rc=127

scripts/pm/os-regen-merge.sh — a real end-to-end run. Driven inside a synthetic repo (upstream + clone + feature branch + merge=os-regen entries in .gitattributes), so the whole merge sequence executes:

  • pre-fix bytes, simulated 3.2: os-regen-merge.sh: line 68: mapfile: command not found, HARNESS-EXIT=127 — after git fetch origin main, before step 1, i.e. with the ordering the script exists to enforce not begun.
  • post-fix bytes, simulated 3.2: steps 1-3 complete, step 4 prints, HARNESS-EXIT=0.
  • post-fix bytes, simulated 3.2, no merge=os-regen entries — the set -u empty-array branch: ✗ no merge=os-regen entries found in .gitattributes — refusing to guess, exit 1. Same verdict and same exit code as the pre-fix bytes on bash 5. This branch is why the loop needs an explicit regen_paths=(); without it the empty read dies at regen_paths: unbound variable instead of reaching the refusal (measured both ways).

scripts/gen-sdui-manifest.sh and scripts/publish-smoke.sh — extracted-bytes runs. Both are too heavy to run end to end here (an objectui build tree plus a Playwright browser; a full pack-and-scaffold smoke). So the real bytes of the producer function and of the array-fill block are extracted from the file on disk, composed under set -euo pipefail, and executed:

unit pre-fix, simulated 3.2 post-fix, simulated 3.2
sdui_dev_server_cmdDUMP_DEV_ARGV readarray: command not found, exit 127 9 argv tokens, exit 0
smoke_dev_server_argvDEV_ARGV mapfile: command not found, exit 127 8 argv tokens, exit 0

and the resulting argv arrays are byte-identical across pre-fix/post-fix and across plain/simulated shells (diff clean, 9 and 8 tokens) — the repair changes no behaviour on bash 5 either.

What the sweep found that the card did not

  • The tracked *.sh census is 18, not 17. .claude/hooks/guard-main-checkout.selftest.sh was added today, after the card's census. It is clean, but the drift is the point: the population moves daily.
  • scripts/publish-smoke.sh is CI-wired, contrary to the card. .github/workflows/publish-smoke.yml:225 and :260 both run it. Its mapfile line is therefore green on Linux today; what it broke was the hand-run reproduction of a publish-smoke failure on a Mac. Lower stakes than the other two, still a real break.
  • scripts/gen-sdui-manifest.sh is not entirely CI-less either. .github/workflows/cut-rc.yml:443 runs pnpm sdui:manifest before publish, a handful of times a month. The sdui.manifest.json 的来源未定:声明一致性 ratchet 目前只在手工 pnpm sdui:manifest 时跑,CI 里从来不跑(#4690 的遗留决定) #5960 ruling keeps it off PR CI, which is what matters here.
  • scripts/pm/os-regen-merge.sh really is CI-less — nothing under .github/workflows/ invokes it.
  • No second bash-4 dependency in any of the three. Swept for coproc, globstar, ;;&, wait -n, [[ -v, declare -n/-g, printf %(…)T, ${x@Q}, lastpipe, ${arr[-1]} and exec {fd} on top of the card's construct set: none present. bash -n parses all three clean.
  • One detail of the sibling repair's note does not reproduce. It attributes the trailing-&& trap to the empty-list case; measured here, an all-empty read leaves the loop body unexecuted and the while at status 0. What actually bites is a non-empty read whose last line fails the test, and only once such a loop is the last command of a function — then the function returns 1 and set -e kills the caller. The if form is still the right one; the comments added here state the measured mechanism.

The generalisation question, measured

check:-ing this class repo-wide was called out as in scope but a judgment call. The numbers at this commit:

  • 18 tracked *.sh; 3 carried a bash-4-only construct before this PR, 0 after; 4 sites total across this card and the sibling one.
  • The only remaining file naming those tokens is scripts/pm/os-verify-lock.sh, and every occurrence there is either a guarded ${EPOCH*:-} read or lives inside its own self-test that hunts them — clean by construction.
  • A repo-wide scan is cheap to run: a regex over 18 files, well under a second.

The gate is well-earned — a 4-for-4 hit rate on a class that stayed invisible until it bit twice, over a population that gained a member today. But it needs a new scripts/check-*.mjs, its own --self-test, a package.json alias and two lint.yml steps, and it would then join check-self-test-wired.mjs's population. That is the "new gate file plus wiring" shape this card was told to report before building, so it is reported, not built here.

Gates

Derived at the final commit b9438a425d with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (re-derived after the commit; same 7 families). Exit codes captured before any pipe.

check:nul-bytes                          EXIT=0
check:agent-test-spelling                EXIT=0
check:cross-package-test-inputs          EXIT=0
check:entry-guard                        EXIT=0
check:parse-guard                        EXIT=0
check:pnpm-filter-targets                EXIT=0
scripts/check-ci-filter-parity.mjs       EXIT=0
scripts/check-cross-package-test-inputs.mjs  EXIT=0  (run via check:cross-package-test-inputs)

Repo-wide pnpm lint was narrowed, and the narrowing is measured, not assumed: this diff is three .sh files, and eslint's own configuration resolution reports File ignored because no matching configuration was supplied for each of them (--format json: 3 files, 0 errors, 3 ignored-warnings). No eslint program includes a .sh file, so this diff cannot move the verdict on any untouched file.

Changeset: none. Root scripts/ only, nothing published changes ⇒ skip-changeset.


Generated by Claude Code

…hand-run scripts with the bash 3.2 read loop

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s 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.

Three more hand-run shell scripts carry bash-4 builtins — pnpm sdui:manifest is the next step of the same macOS pin-bump procedure

2 participants