Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions scripts/bump-objectui.sh
Original file line numberDiff line numberDiff line change
Expand Up@@ -320,11 +320,37 @@ report_objectui_reachability() {

# Enumerated only on this branch: with ~941 branches a --contains walk is not
# free, and the healthy path must not pay for it.
#
# 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 has
# not shipped bash 4+ for licensing reasons). This script is run BY HAND, BY
# AN OPERATOR, on a laptop — the pin-bump procedure in
# `docs/releases-maintenance.md` has no CI path — so a bash-4 builtin here
# does not fail on a fringe host, it fails on the ordinary one. And it fails
# on exactly the branch that must not fail: `mapfile` is only ever reached
# once the verdict is "NOT on origin/main", i.e. the #10495 warning is the
# single thing this shell cannot deliver. Measured with `mapfile` disabled:
# `mapfile: command not found`, then `set -e` kills the run at status
# 127 BEFORE the pin is written, and the operator is handed a bare builtin
# error where the warning should have been — an error whose obvious remedy
# (edit `.objectui-sha` by hand) walks around every guard in this file.
# CI runs bash 5, so neither the defect nor this repair is observable in a
# normal CI run; the digest self-test pins it with the builtin disabled
# (`enable -n mapfile readarray` via `BASH_ENV`) plus a static scan.
# Keep this loop bash-3.2-clean: no `mapfile`, no `readarray`, no
# `declare -A`, no `${x^^}`/`${x,,}`. The `if` (rather than `[[ … ]] &&`) is
# load-bearing under `set -e`: a trailing false `&&`-list would make the
# whole `while` return 1 and kill the run on an empty ref list.
local -a local_refs=() remote_refs=()
mapfile -t local_refs < <(
local ref_line=''
while IFS= read -r ref_line; do
if [[ -n "$ref_line" ]]; then local_refs+=("$ref_line"); fi
done < <(
git -C "$OBJECTUI_ROOT" for-each-ref --contains "$sha" --format='%(refname:short)' refs/heads 2>/dev/null || true
)
mapfile -t remote_refs < <(
while IFS= read -r ref_line; do
if [[ -n "$ref_line" ]]; then remote_refs+=("$ref_line"); fi
done < <(
git -C "$OBJECTUI_ROOT" for-each-ref --contains "$sha" --format='%(refname:short)' refs/remotes 2>/dev/null || true
)

Expand Down
92 changes: 91 additions & 1 deletion scripts/objectui-changeset-digest.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -2505,14 +2505,17 @@ function selfTest() {
return dir;
};
// Offline by construction — a self-test must never reach the network.
const runBump = (fwDir, uiRoot, args) =>
// `extraEnv` exists for R7 below, which re-runs one of these cases under a
// shell with the bash 4 builtins taken away.
const runBump = (fwDir, uiRoot, args, extraEnv = {}) =>
spawnSync('bash', [join(fwDir, 'scripts', 'bump-objectui.sh'), ...args], {
encoding: 'utf8',
env: {
...process.env,
OBJECTUI_ROOT: uiRoot,
OBJECTUI_NO_DEEPEN: '1',
GIT_TERMINAL_PROMPT: '0',
...extraEnv,
},
});
const mkUi = (name) => {
Expand DownExpand Up@@ -2720,6 +2723,93 @@ function selfTest() {
r6.stderr,
);

// --- R7: the bash 3.2 floor --------------------------------------------
//
// `bump-objectui.sh` is run BY HAND by an operator (docs/releases-
// maintenance.md), and `/usr/bin/env bash` is bash 3.2.57 on macOS. Every
// case above ran on whatever bash the host has — bash 5 in CI — so the
// whole R1..R6 block is blind to a bash-4-only construct: it went green on
// a script that could not complete a single one of these runs on a Mac.
// Measured: with `mapfile` unavailable, R2/R2b/R2c/R2d/R3/R3b/R6 all fail,
// two of them with `status=127`, and the operator run dies at 127 with
// `mapfile: command not found` as its ONLY output — the #10495 warning,
// which is reached on no other branch, cannot print at all.
//
// Both halves are needed and they catch different things: the static scan
// sees parse-level constructs that `enable -n` cannot simulate, and the
// simulated run proves the real code path completes without the builtins.
const bash4Constructs = new RegExp(
[
'exec\\s+\\{[A-Za-z_]', // bash 4.1 fd auto-allocation
'\\$\\{[A-Za-z_][A-Za-z0-9_]*(\\[[^\\]]*\\])?(\\^|,)', // bash 4.0 ${x^^} / ${x,,}
'(declare|local|typeset)\\s+-[A-Za-z]*A\\s', // bash 4.0 associative arrays
'(mapfile|readarray)\\s', // bash 4.0 builtins — the one this card is about
'&>>', // bash 4.0 append-both redirection
'EPOCH(SECONDS|REALTIME)', // bash 5.0 variables
].join('|'),
);
const bumpSrc = readFileSync(join(__dirname, 'bump-objectui.sh'), 'utf8');
// Comments are exempt: this file has to NAME the constructs it refuses in
// order to explain why, and the simulated run below covers what actually
// executes.
const bash4Hits = bumpSrc
.split('\n')
.map((line, i) => [i + 1, line])
.filter(([, line]) => !/^\s*#/.test(line))
.filter(([, line]) => bash4Constructs.test(line))
.map(([n, line]) => `${n}: ${line.trim()}`);
check(
'#12071 R7 bump-objectui.sh names no bash 4+/5 construct (mapfile, ${x^^}, declare -A, &>>, EPOCH*)',
bash4Hits.length === 0,
bash4Hits.join('\n'),
);

// Bash 3.2's world imposed on this host: `enable -n` really does make
// `mapfile`/`readarray` "command not found" — the same status 127 macOS
// produces — and BASH_ENV is sourced by every non-interactive bash, so the
// script under test inherits it through `spawnSync`.
const noBash4 = join(tmp, 'no-bash4-builtins.sh');
writeFileSync(noBash4, 'enable -n mapfile readarray 2>/dev/null\n');
// The instrument must not be vacuous. If BASH_ENV were ever ignored (a
// posix-mode bash, a future harness change), R7b would pass by proving
// nothing, so the disabling is measured on a probe FIRST, both ways.
const mapfileProbe = join(tmp, 'mapfile-probe.sh');
writeFileSync(mapfileProbe, 'mapfile -t x < <(printf "a\\n") && echo MAPFILE-WORKS\n');
const probePlain = spawnSync('bash', [mapfileProbe], { encoding: 'utf8' });
const probeSim = spawnSync('bash', [mapfileProbe], {
encoding: 'utf8',
env: { ...process.env, BASH_ENV: noBash4 },
});
check(
'#12071 R7a the simulated-3.2 harness really removes the builtin (else R7b proves nothing)',
probePlain.stdout.includes('MAPFILE-WORKS') &&
!probeSim.stdout.includes('MAPFILE-WORKS') &&
probeSim.stderr.includes('mapfile'),
`plain=${probePlain.stdout.trim()} sim.out=${probeSim.stdout.trim()} sim.err=${probeSim.stderr.trim()}`,
);

// The R2 shape again — pushed, never merged — through a shell that has no
// bash 4 builtins. This is the operator's macOS run, and the assertion is
// the WARNING, not merely a zero exit: the defect's signature was a run
// that produced the builtin error INSTEAD of the warning.
const fwNoB4 = mkFramework('fw-reach-nobash4', rBase);
const r7 = runBump(fwNoB4, uiR.dir, ['--no-commit', rPushed], { BASH_ENV: noBash4 });
check(
'#12071 R7b on a shell without bash 4 builtins the #10495 warning still fires, in full',
r7.stderr.includes('is NOT reachable from origin/main') &&
r7.stderr.includes('origin/feature/pushed-never-merged') &&
r7.stderr.includes('It IS pushed, but only onto branch(es) that have not merged'),
`status=${r7.status}\nSTDERR:${r7.stderr}`,
);
check(
'#12071 R7c … and the run completes as a warning, not a 127: exit 0, pin written, verdict on stdout',
r7.status === 0 &&
!r7.stderr.includes('command not found') &&
readFileSync(join(fwNoB4, '.objectui-sha'), 'utf8').trim() === rPushed &&
r7.stdout.includes('(NOT on origin/main)'),
`status=${r7.status}\nSTDOUT:${r7.stdout}\nSTDERR:${r7.stderr}`,
);

} finally {
rmSync(tmp, { recursive: true, force: true });
}
Expand Down
Loading