fix(dev-fresh): keyless launch no longer crashes on stock macOS bash 3.2 (#144)

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.
This commit is contained in:
Bilawal Sidhu 2026-08-31 19:00:33 -05:00 committed by GitHub
parent 9db0813daf
commit d8f1742783
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 33 additions and 1 deletions

View File

@ -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

View File

@ -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 --)

View File

@ -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');
});