From d8f1742783cddd6bbc86033d0db06dc6ec746304 Mon Sep 17 00:00:00 2001 From: Bilawal Sidhu <106619546+bilawalsidhu@users.noreply.github.com> Date: Mon, 31 Aug 2026 19:00:33 -0500 Subject: [PATCH] fix(dev-fresh): keyless launch no longer crashes on stock macOS bash 3.2 (#144) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Expanding the empty KEY_SETUP_EXTERNAL_KEYS array under set -u is a fatal 'unbound variable' on bash 3.2, which macOS ships as /bin/bash — so a keyless ./scripts/dev-fresh.sh died at the provenance-marker line before doing anything. The ${arr[*]:-} guard keeps 3.2 alive and leaves populated launches byte-identical (same idiom the script already uses for DEV_UNSET). Verified with /bin/bash 3.2.57: the extracted block runs clean under set -u with no keys (empty CSV) and with keys (ordered comma-joined names); the full launcher boots keyless to a live server, and exported keys still classify as externally managed in Provider Settings. A behavioral guard now runs the real block from the script both ways in the dev-fresh test surface, plus a textual assertion pinning the 3.2-safe idiom. --- CHANGELOG.md | 4 ++++ scripts/dev-fresh.sh | 2 +- src/devFreshDotenv.test.mjs | 28 ++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7624b0..eb2a171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md the two largest sources and dropped them entirely — leaving roughly a third of global detections while reporting each dropped source twice, once as successful with its real count and once as failed. +- `./scripts/dev-fresh.sh` no longer crashes on stock macOS bash 3.2 when no + provider keys are exported: expanding the empty external-keys provenance + array under `set -u` was fatal there. Launches with exported keys are + unchanged. ### Security diff --git a/scripts/dev-fresh.sh b/scripts/dev-fresh.sh index 330774c..9f21f42 100755 --- a/scripts/dev-fresh.sh +++ b/scripts/dev-fresh.sh @@ -38,7 +38,7 @@ KEY_SETUP_EXTERNAL_KEYS=() [[ -n "${OPENSKY_CLIENT_ID:-}" ]] && KEY_SETUP_EXTERNAL_KEYS+=(OPENSKY_CLIENT_ID) [[ -n "${OPENSKY_CLIENT_SECRET:-}" ]] && KEY_SETUP_EXTERNAL_KEYS+=(OPENSKY_CLIENT_SECRET) [[ -n "${LL2_API_TOKEN:-}" ]] && KEY_SETUP_EXTERNAL_KEYS+=(LL2_API_TOKEN) -KEY_SETUP_EXTERNAL_KEYS_CSV="$(IFS=,; printf '%s' "${KEY_SETUP_EXTERNAL_KEYS[*]}")" +KEY_SETUP_EXTERNAL_KEYS_CSV="$(IFS=,; printf '%s' "${KEY_SETUP_EXTERNAL_KEYS[*]:-}")" if command -v npm >/dev/null 2>&1; then DEV_COMMAND=(npm run dev --) diff --git a/src/devFreshDotenv.test.mjs b/src/devFreshDotenv.test.mjs index c4016eb..c488325 100644 --- a/src/devFreshDotenv.test.mjs +++ b/src/devFreshDotenv.test.mjs @@ -79,3 +79,31 @@ test('dev-fresh passes names-only boot provenance before resolving file fallback assert.match(source, /put_env GEV_LAUNCHER "dev-fresh"/); assert.match(source, /put_env GEV_KEY_SETUP_EXTERNAL_KEYS "\$\{KEY_SETUP_EXTERNAL_KEYS_CSV\}"/); }); + + +// Stock macOS ships bash 3.2, where expanding an EMPTY array under `set -u` +// is a fatal "unbound variable" — the `:-` guard on the provenance CSV is what +// keeps the keyless `dev-fresh.sh` launch alive there. Newer bash never fails +// this way, so the idiom itself is asserted textually and the block's behavior +// is exercised for both the keyless and the populated case. +const bashTest = process.platform === 'win32' ? test.skip : test; +bashTest('the external-keys provenance block survives set -u keyless and joins names when keys are exported', async () => { + const script = await fs.readFile(new URL('../scripts/dev-fresh.sh', import.meta.url), 'utf8'); + const start = script.indexOf('KEY_SETUP_EXTERNAL_KEYS=()'); + const csvAt = script.indexOf('KEY_SETUP_EXTERNAL_KEYS_CSV='); + const end = script.indexOf('\n', csvAt); + assert.ok(start > 0 && csvAt > start && end > csvAt, 'provenance block not found in dev-fresh.sh'); + const block = script.slice(start, end); + assert.match(block, /\$\{KEY_SETUP_EXTERNAL_KEYS\[\*\]:-\}/); + + const { execFile } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const run = promisify(execFile); + const probe = `set -u\n${block}\nprintf '%s' "$KEY_SETUP_EXTERNAL_KEYS_CSV"`; + const keyless = await run('bash', ['-c', probe], { env: { PATH: process.env.PATH } }); + assert.equal(keyless.stdout, ''); + const keyed = await run('bash', ['-c', probe], { + env: { PATH: process.env.PATH, FIRMS_MAP_KEY: 'x', TOMTOM_API_KEY: 'y' }, + }); + assert.equal(keyed.stdout, 'FIRMS_MAP_KEY,TOMTOM_API_KEY'); +});