diff --git a/scripts/__tests__/check-control-bytes.test.ts b/scripts/__tests__/check-control-bytes.test.ts index 9624b91d65..d04cc47758 100644 --- a/scripts/__tests__/check-control-bytes.test.ts +++ b/scripts/__tests__/check-control-bytes.test.ts @@ -306,8 +306,13 @@ describe('repo state — the gate is green on this tree', () => { * GNU grep prefixes `grep: : `, older builds print `Binary file * matches` on stdout. Reading BOTH streams means this does not depend on which. */ -function contentSearch(needle: string, file: string, cwd: string = repoRoot) { - const run = spawnSync('grep', ['-n', needle, file], { cwd, encoding: 'utf8' }); +function contentSearch( + needle: string, + file: string, + cwd: string = repoRoot, + env: NodeJS.ProcessEnv = process.env, +) { + const run = spawnSync('grep', ['-n', needle, file], { cwd, encoding: 'utf8', env }); const both = `${run.stdout ?? ''}${run.stderr ?? ''}`; return { status: run.status, @@ -356,7 +361,15 @@ describe('objectstack#5425 — the file that started this is readable again', () fs.writeFileSync(probe, `const includeKey = 1;${String.fromCharCode(0)}\n`); const declined = contentSearch('includeKey', probe, dir); expect(declined.refusedAsBinary, 'grep must decline a NUL-bearing file').toBe(true); - expect(declined.stdout, 'and print no line at all — that is the search outage').toBe(''); + expect( + declined.stdout, + 'and print no MATCHING LINE — that is the search outage. ⚠️ objectui#8404: this read ' + + "`toBe('')` until a stock macOS host reddened it on bytes CI called green. An empty " + + 'stdout is not the outage, it is one userland\'s way of reporting it: GNU grep >= 3.5 ' + + 'writes its refusal to stderr and leaves stdout empty, BSD grep writes ' + + '`Binary file matches` to STDOUT. Both refuse, both print no line of the file, ' + + 'and the refusal itself is already pinned by `refusedAsBinary` above.', + ).not.toMatch(/^\d+:/m); expect(declined.status, 'while exiting 0, which is what makes the outage silent').toBe(0); } finally { fs.rmSync(dir, { recursive: true, force: true }); @@ -375,6 +388,58 @@ describe('objectstack#5425 — the file that started this is readable again', () * there would be a fabricated pin, so it is asserted only on the byte's absence, * which is the harm it actually had: an unreviewable literal. */ +/** + * ⭐ objectui#8404 — the two assertions above hold on EITHER grep userland. + * + * Established without a macOS host, because the discriminator is not the + * platform: it is which stream grep writes its refusal to. GNU grep 3.11 (this + * container, and what CI runs) writes `grep: : binary file matches` to + * stderr and leaves stdout empty; BSD grep writes `Binary file matches` to + * stdout. Both exit 0 and both print no line of the file. A shim first on PATH + * stands in for the second, so the host-independence is pinned here, on Linux, + * on every run — ⛔ not left as a claim nobody can re-measure. + */ +describe('objectui#8404 — the refusal is read the same on either grep userland', () => { + it('recognises the BSD spelling, which arrives on stdout', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'control-bytes-bsd-grep-')); + try { + const shim = path.join(dir, 'grep'); + fs.writeFileSync( + shim, + [ + '#!/bin/sh', + '# Stands in for BSD grep declining a binary file: the refusal on STDOUT,', + '# exit 0, and no line of the file. $3 is the path, after `-n `.', + 'printf "Binary file %s matches\\n" "$3"', + 'exit 0', + '', + ].join('\n'), + ); + fs.chmodSync(shim, 0o755); + fs.writeFileSync(path.join(dir, 'probe.ts'), 'const includeKey = 1;\n'); + + const bsd = contentSearch('includeKey', 'probe.ts', dir, { + ...process.env, + PATH: `${dir}${path.delimiter}${process.env.PATH ?? ''}`, + }); + + // The control: the shim really is the grep that ran, or this measures GNU + // grep again and proves nothing about the other userland. + expect(bsd.stdout, 'the shim on PATH was not the grep that ran').toMatch(/^Binary file /m); + expect(bsd.refusedAsBinary, 'the BSD spelling must be read as the refusal it is').toBe(true); + expect( + bsd.stdout, + 'and it is not a line of the file — which is why the search-outage assertion is written ' + + "against matching lines rather than against an empty stdout (the `toBe('')` spelling " + + 'was false here, and only here, which is how objectui#8404 was found).', + ).not.toMatch(/^\d+:/m); + expect(bsd.status, 'exit 0 either way — that is what makes the outage silent').toBe(0); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); +}); + describe('objectstack#5450 — the four baselined files are clean', () => { const cleaned = [ { file: 'packages/core/src/evaluator/listConditional.ts', grepFor: 'warnedError' }, diff --git a/scripts/__tests__/ensure-chromium-ready.test.ts b/scripts/__tests__/ensure-chromium-ready.test.ts index a52bec87d2..2c586c9a39 100644 --- a/scripts/__tests__/ensure-chromium-ready.test.ts +++ b/scripts/__tests__/ensure-chromium-ready.test.ts @@ -63,6 +63,31 @@ function marker(name: string): string { return path.join(fs.mkdtempSync(path.join(os.tmpdir(), `os-5304-${name}-`)), 'deps-ran'); } +/** + * A PATH carrying the externals this script needs and NOTHING else — in + * particular neither `timeout` nor `gtimeout`. + * + * objectui#8404. `timeout` is GNU coreutils and a stock macOS host has neither + * it nor Homebrew's `gtimeout`. Two of the cases in this file were red there on + * bytes CI called green, because the bare `timeout` call failed at 127 before + * the dependency install ever ran. + * + * ⚠️ The stand-in is a CAPABILITY, not a platform name: what makes those hosts + * different is the absent binary, so hiding the binary reproduces it exactly — + * on Linux, in this container, with no macOS anywhere. A + * `process.platform === 'darwin'` skip would have measured nothing and would + * red again on the next non-GNU host. + */ +function pathWithoutTimeout(): { dir: string; env: Record } { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'os-8404-no-timeout-')); + for (const tool of ['bash', 'rm', 'sleep', 'touch']) { + const found = spawnSync('sh', ['-c', `command -v ${tool}`], { encoding: 'utf8' }).stdout.trim(); + if (!found) throw new Error(`cannot build the restricted PATH: this host has no ${tool}`); + fs.symlinkSync(found, path.join(dir, tool)); + } + return { dir, env: { PATH: dir } }; +} + describe('ensure-chromium-ready.sh — apt is off the happy path', () => { it('never invokes the dependency install when Chromium already launches', () => { const ran = marker('happy'); @@ -137,6 +162,81 @@ describe('ensure-chromium-ready.sh — the gate still reports', () => { }); }); +/** + * ⭐ objectui#8404 — the bound survives a host with no GNU coreutils. + * + * The two cases below are the two this file had that a stock macOS host reddens, + * run under a PATH where `timeout` and `gtimeout` are genuinely absent. They are + * not a re-run of the cases above with a decoration: on `origin/main`'s script, + * under this same PATH, the recovery case exits 1 with the dependency install + * never executed, and the deadline case never prints the #5304 signature. + * + * ⛔ Nothing here is skipped, and nothing above is weakened — the GNU path is + * still measured by the cases above, on the same run. + */ +describe('ensure-chromium-ready.sh — the bound holds where GNU `timeout` is absent (objectui#8404)', () => { + it('the restricted PATH really hides both spellings — the control for the two cases below', () => { + // Without this, both cases below could be measuring the GNU path again and + // would pass for a reason that has nothing to do with what they assert. + const { dir, env } = pathWithoutTimeout(); + try { + const found = spawnSync('bash', ['-c', 'command -v timeout || command -v gtimeout'], { + env, + encoding: 'utf8', + }); + expect(found.status, `PATH=${dir} still resolves a timeout: ${found.stdout}`).not.toBe(0); + // And the positive half of the same control: the PATH is not simply empty. + expect( + spawnSync('bash', ['-c', 'command -v sleep'], { env, encoding: 'utf8' }).status, + 'the restricted PATH resolves nothing at all, so the two cases below would fail for that reason', + ).toBe(0); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it('installs the system libraries and succeeds once Chromium launches', () => { + const ran = marker('recover-no-timeout'); + const { dir, env } = pathWithoutTimeout(); + try { + const result = run({ ...env, OS_CHROMIUM_PROBE_CMD: `test -f ${ran}`, OS_CHROMIUM_DEPS_CMD: `touch ${ran}` }); + expect(result.status, result.stderr).toBe(0); + expect( + fs.existsSync(ran), + 'the dependency install never ran. That is the objectui#8404 signature: the bound was ' + + 'spelled as a bare `timeout`, so on a host without it the call failed at 127 before ' + + 'install-deps was reached, and the recovery path could not recover.', + ).toBe(true); + expect(result.stdout).toMatch(/Chromium launches after installing its system libraries/); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it('terminates well inside the job ceiling when the dependency install never returns', () => { + const { dir, env } = pathWithoutTimeout(); + try { + const result = run({ + ...env, + OS_CHROMIUM_PROBE_CMD: 'false', + OS_CHROMIUM_DEPS_CMD: 'sleep 600', + OS_CHROMIUM_DEPS_TIMEOUT: '2', + }); + + expect( + result.elapsedMs, + `The script ran for ${result.elapsedMs}ms with no GNU timeout on PATH. An unbounded ` + + 'dependency install is objectui#5304, and a host without coreutils must not be the ' + + 'way back to it.', + ).toBeLessThan(60_000); + expect(result.status, 'a browser that never became usable must still fail the job').not.toBe(0); + expect(result.stderr).toMatch(/objectui#5304 signature/); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }, 120_000); +}); + describe('the workflows actually use it', () => { const workflows = ['.github/workflows/ci.yml', '.github/workflows/live-e2e.yml']; diff --git a/scripts/check-bash32-floor.mjs b/scripts/check-bash32-floor.mjs index 89b9e2a7d9..539666d3c0 100644 --- a/scripts/check-bash32-floor.mjs +++ b/scripts/check-bash32-floor.mjs @@ -857,11 +857,14 @@ const SELF_TEST_BATTERIES = Object.freeze({ '⭐ end to end, through the real discovery path': 6, '⭐ the instrument is real: the flagged construct really does break': 4, 'the real tree': 3, + // objectui#8404. Declared LAST because its third case reads `narrowed`, which + // only has its final value once every host-dependent leg above has run. + '⭐ objectui#8404: the self-test declares its own host premise': 5, }); // DELETING an entry silences that battery's floor exactly as effectively as // zeroing it, so the roster's own size is pinned too. -const SELF_TEST_BATTERY_FLOOR = 17; +const SELF_TEST_BATTERY_FLOOR = 18; // The key an assertion is filed under when no battery is open. It is not a // declared battery, so it reds by the same set difference rather than silently @@ -897,6 +900,62 @@ function selfTest() { }; const ids = (text) => scanText('f.sh', text).map((f) => f.id); + // ── objectui#8404: the self-test's own host premise, made explicit ─────── + // + // ⭐ A floor gate whose battery cannot run ON the floor it declares is a gate + // asserting something it has never demonstrated. That was this file: every + // probe, harness and `bash -n` leg below drives a bash-4+ construct through + // the HOST's bash, and the host this gate exists for is macOS, which ships + // 3.2.57 and no bash 4+. There, `coproc`, `|&` and `&>>` are syntax errors + // `bash -n` refuses, `mapfile` is not a builtin there is anything to disable, + // and `[[ -v ]]` is a conditional operator `bash -n` also refuses (this + // file's `has-v` row already records that `bash -n` judges `[[` operators). + // So `--self-test` exited non-zero on the one host whose behaviour the whole + // gate is about, and `bash32-floor-wiring.test.ts` reported it as a red test. + // + // ⛔ The production scan is NOT affected and is not narrowed: `scanText` is + // pure JS, so `node scripts/check-bash32-floor.mjs` judges a 3.2 host exactly + // as it judges CI. Only `--self-test` needs the newer parser. + // + // The premise is detected by CAPABILITY and never by platform name — a + // `process.platform === 'darwin'` branch would red again on the next non-GNU + // host, which is the very species this gate is about. Two probes, because the + // legs below need two different things from the host: a parser that accepts + // bash-4 GRAMMAR, and a runtime that has a bash-4 BUILTIN to disable. + const HOST_PROBES = [ + // The canonical bash-4.0 operator. A shell at the floor answers with a + // syntax error near the `&`. + { need: 'grammar', probe: 'echo a |& cat', run: () => spawnSync('bash', ['-n'], { input: 'echo a |& cat\n', encoding: 'utf8' }) }, + // The canonical bash-4.0 builtin. A shell at the floor answers 127. + { need: 'builtin', probe: 'mapfile -t x < /dev/null', run: () => spawnSync('bash', ['-c', 'mapfile -t x < /dev/null'], { encoding: 'utf8' }) }, + ]; + const hostCan = Object.fromEntries(HOST_PROBES.map((h) => [h.need, h.run().status === 0])); + const HOST_JUDGES_BASH4 = HOST_PROBES.every((h) => hostCan[h.need]); + const NARROWING = HOST_PROBES.filter((h) => !hostCan[h.need]).map((h) => `\`${h.probe}\``).join(' and '); + let narrowed = 0; + + /** + * An assertion whose SUBJECT is a bash-4 construct run through the host bash. + * + * Where the host can judge bash-4, this is `t` unchanged — CI and every + * bash-4+ host lose nothing. Where it cannot, the case still REGISTERS (so + * its battery floor still holds and a battery that stops running still names + * itself) but is reported as NARROWED rather than passed, and the verdict + * line carries the count. ⛔ A narrowed case is a real coverage loss and is + * printed as one: at the floor a typo and a bash-4 construct are the same + * syntax error, so "the probe is not a typo" is genuinely unmeasurable there. + */ + const tHostBash4 = (label, ok, detail = '') => { + if (HOST_JUDGES_BASH4) { + t(label, ok, detail); + return; + } + registerCase(); + cases += 1; + narrowed += 1; + console.log(` \u2298 ${label} — NARROWED: this host's bash refuses ${NARROWING}, so it is at or below the 3.2 floor this gate declares and cannot judge the case.`); + }; + console.log('check-bash32-floor --self-test\n'); // --- the table itself ---------------------------------------------------- @@ -937,7 +996,7 @@ function selfTest() { battery('⭐ and the probes are real shell, not plausible-looking text'); for (const c of CONSTRUCTS) { const parse = spawnSync('bash', ['-n'], { input: `${c.probe}\n`, encoding: 'utf8' }); - t(`${c.id}: the probe is shell this host can parse`, parse.status === 0, (parse.stderr || '').trim()); + tHostBash4(`${c.id}: the probe is shell a bash-4 parser accepts`, parse.status === 0, (parse.stderr || '').trim()); } // --- E1: full-line comments are prose, trailing comments are not --------- @@ -1070,7 +1129,7 @@ function selfTest() { `got ${JSON.stringify(ids(line))}`, ); const vParse = spawnSync('bash', ['-n'], { input: `${line}\n`, encoding: 'utf8' }); - t(`\`${label}\` is shell this host can parse`, vParse.status === 0, (vParse.stderr || '').trim()); + tHostBash4(`\`${label}\` is shell a bash-4 parser accepts`, vParse.status === 0, (vParse.stderr || '').trim()); } t('has-v after `&&` → RED', ids('cd "$d" && [ -v name ]').includes('has-v')); t('has-v inside `$( )` → RED', ids('n=$( [ -v name ] && echo 1 )').includes('has-v')); @@ -1260,12 +1319,17 @@ function selfTest() { writeFileSync(probe, 'mapfile -t x < /dev/null && echo MAPFILE-WORKS\n'); const plain = spawnSync('bash', [probe], { encoding: 'utf8' }); const sim = spawnSync('bash', [probe], { encoding: 'utf8', env: { ...process.env, BASH_ENV: noBash4 } }); - t( + // ⚠️ objectui#8404: both legs need a host that HAS `mapfile` to remove. At + // the 3.2 floor `plain` never prints MAPFILE-WORKS either, so the first leg + // is false for the wrong reason and the second passes for the wrong one — + // 127 there is the absent builtin, not the harness. Narrowed together, + // because the second leg's meaning is carried by the first. + tHostBash4( 'the simulated-3.2 harness really removes the builtin (else the next leg proves nothing)', plain.stdout.includes('MAPFILE-WORKS') && !sim.stdout.includes('MAPFILE-WORKS') && /mapfile/.test(sim.stderr), `plain=${plain.stdout.trim()} sim.out=${sim.stdout.trim()} sim.err=${sim.stderr.trim()}`, ); - t( + tHostBash4( 'and a script this gate flags really does die at 127 under it', sim.status === 127, `status=${sim.status} err=${sim.stderr.trim()}`, @@ -1336,6 +1400,50 @@ function selfTest() { + `${live.byShebang} by shebang alone — ${live.findings.length} finding(s)`, ); + // --- ⭐ objectui#8404: the self-test declares its own host premise ------- + // + // The narrowing mechanism above is itself a place a gate can go quiet, so it + // is pinned in both directions: a host that CAN judge bash-4 must narrow + // nothing, and a host that cannot must narrow something. A mechanism that + // narrowed on every host would read as green while measuring nothing. + battery('⭐ objectui#8404: the self-test declares its own host premise'); + // ⚠️ E1 again, and for E1's reason: this file has to NAME the branch it + // refuses in order to explain why, and a check hunting that name has to name + // it too. So the needle is assembled from parts rather than written as a + // literal, and the prose lines are dropped before the search — exactly the + // mention-versus-use rule the CONSTRUCTS table is built on. A real branch + // lives on a code line, which is what survives the filter. + const PLATFORM_BRANCH = new RegExp(['process', 'platform'].join('\\.')); + const codeLines = readFileSync(SELF, 'utf8') + .split('\n') + .filter((l) => !/^\s*(\/\/|\*|\/\*)/.test(l)) + .join('\n'); + t( + '⛔ the premise is a capability, never a platform name — a `darwin` branch reds again on the next non-GNU host', + !PLATFORM_BRANCH.test(codeLines), + 'a code line here branches on the platform name; the host premise must be measured instead', + ); + t( + 'and the filter that allows the prose above is not blanket permission — a code line IS searched', + PLATFORM_BRANCH.test(codeLines + '\nconst x = process' + '.platform;'), + 'the mention/use filter matched nothing at all, so the leg above proves nothing', + ); + t( + 'and the two capability probes are constructs THIS gate declares above the floor', + ids(HOST_PROBES[0].probe).includes('pipe-both') && ids(HOST_PROBES[1].probe).includes('mapfile'), + `got ${JSON.stringify(HOST_PROBES.map((h) => ids(h.probe)))}`, + ); + t( + 'judging and narrowing are exclusive: a host that judges bash-4 narrowed nothing', + !HOST_JUDGES_BASH4 || narrowed === 0, + `HOST_JUDGES_BASH4=${HOST_JUDGES_BASH4} narrowed=${narrowed}`, + ); + t( + 'and a host that does NOT judge bash-4 really did narrow — a narrowing nobody takes is a claim nobody made', + HOST_JUDGES_BASH4 || narrowed > 0, + `HOST_JUDGES_BASH4=${HOST_JUDGES_BASH4} narrowed=${narrowed}`, + ); + // ── The floor: every declared battery RAN, and ran its cases (#13489) ─── // // Evaluated after every battery has had its chance and BEFORE the verdict, so @@ -1386,7 +1494,18 @@ function selfTest() { console.error(`\n✗ check-bash32-floor self-test failed (${failed} of ${cases} case(s)).`); process.exit(1); } - console.log(`\n✓ check-bash32-floor self-test: ${cases} cases pass.`); + if (narrowed > 0) { + console.log( + `\n✓ check-bash32-floor self-test: ${cases - narrowed} cases pass, ${narrowed} NARROWED.\n` + + ` This host's bash refuses ${NARROWING}, so it is at or below the bash 3.2 floor this\n` + + ' gate declares and cannot judge a bash-4 construct. Those cases did not run here:\n' + + ' a real coverage loss, stated rather than hidden. CI runs bash 5 and runs them.\n' + + ' ⛔ The SCAN is not narrowed — `node scripts/check-bash32-floor.mjs` is pure JS and\n' + + ' judges this host exactly as it judges CI (objectui#8404).', + ); + } else { + console.log(`\n✓ check-bash32-floor self-test: ${cases} cases pass.`); + } selfTestReachedVerdict = true; } diff --git a/scripts/ensure-chromium-ready.sh b/scripts/ensure-chromium-ready.sh index 358e88d8dd..f235f53778 100755 --- a/scripts/ensure-chromium-ready.sh +++ b/scripts/ensure-chromium-ready.sh @@ -56,6 +56,73 @@ DEPS_TIMEOUT=${OS_CHROMIUM_DEPS_TIMEOUT:-240} DEPS_CMD=${OS_CHROMIUM_DEPS_CMD:-"pnpm exec playwright install-deps chromium"} PROBE_CMD=${OS_CHROMIUM_PROBE_CMD:-} +# ── Bounding the recovery call on a host that has no GNU coreutils ──────────── +# `timeout` is GNU coreutils, and a stock macOS host has neither it nor +# Homebrew's `gtimeout` (objectui#8404). Written as a bare `timeout`, the bound +# did not merely go missing there: the call itself failed at 127 before +# `install-deps` ever ran, so the recovery path could not recover and reported +# only "install-deps failed with exit 127". A contributor running this script on +# their own machine is exactly the reader this repo floors its shell for. +# +# Three resolutions, in order, and ⛔ there is no fourth branch that runs the +# command unbounded — an unbounded dependency install IS objectui#5304. +# +# ⚠️ The floor is bash 3.2 (scripts/check-bash32-floor.mjs), so the fallback +# uses no `wait -n`, no `$EPOCHSECONDS` and no bash-4 operator. It polls with +# `kill -0` and reports the deadline through a marker file, because `wait` +# cannot tell a watchdog's TERM from any other TERM. +run_bounded() { + bounded_secs=$1 + bounded_cmd=$2 + + if command -v timeout > /dev/null 2>&1; then + timeout -k 15s "${bounded_secs}s" bash -c "$bounded_cmd" + return $? + fi + if command -v gtimeout > /dev/null 2>&1; then + gtimeout -k 15s "${bounded_secs}s" bash -c "$bounded_cmd" + return $? + fi + + echo "Neither 'timeout' nor 'gtimeout' is on this host; bounding the call from bash itself." + bounded_fired="${TMPDIR:-/tmp}/os-chromium-deps-deadline.$$" + rm -f "$bounded_fired" + + bash -c "$bounded_cmd" & + bounded_pid=$! + + # Redirected to /dev/null on purpose: a background child holding the inherited + # stdout keeps the pipe open, and a caller that reads this script's output to + # completion (the test does) would wait for the watchdog rather than the work. + ( + bounded_left=$bounded_secs + while [ "$bounded_left" -gt 0 ]; do + kill -0 "$bounded_pid" 2> /dev/null || exit 0 + sleep 1 + bounded_left=$((bounded_left - 1)) + done + : > "$bounded_fired" + kill -TERM "$bounded_pid" 2> /dev/null + sleep 15 + kill -KILL "$bounded_pid" 2> /dev/null + ) > /dev/null 2>&1 & + bounded_watchdog=$! + + wait "$bounded_pid" + bounded_status=$? + kill -TERM "$bounded_watchdog" 2> /dev/null + wait "$bounded_watchdog" 2> /dev/null + + if [ -f "$bounded_fired" ]; then + rm -f "$bounded_fired" + # 124 is `timeout`'s own "deadline expired", so the caller below reads one + # verdict whichever of the three branches produced it. + return 124 + fi + rm -f "$bounded_fired" + return $bounded_status +} + # Launches the bundled Chromium exactly as `playwright.config.ts` does — its # `chromium` project is `devices['Desktop Chrome']` with no `channel`, so the # default launch is the representative one. @@ -83,8 +150,9 @@ echo "Falling back to 'playwright install-deps', bounded at ${DEPS_TIMEOUT}s so echo "stalled Ubuntu mirror cannot hang this job the way it did in objectui#5304." # `-k` escalates to KILL if apt ignores the TERM: a process blocked in a socket -# read is exactly the shape that does. 124 is timeout's own "deadline expired". -timeout -k 15s "${DEPS_TIMEOUT}s" bash -c "$DEPS_CMD" +# read is exactly the shape that does. 124 is timeout's own "deadline expired", +# and `run_bounded` reports the same 124 from whichever branch bounded the call. +run_bounded "$DEPS_TIMEOUT" "$DEPS_CMD" deps_status=$? if [ "$deps_status" -eq 124 ] || [ "$deps_status" -eq 137 ]; then