Merge pull request #3194 from gsd-build/fix/3104-portable-bash-shebang

fix(hooks): use #!/usr/bin/env bash in community .sh hooks for portability (#3104)
This commit is contained in:
Tom Boucher
2026-05-06 20:35:34 -04:00
committed by GitHub
7 changed files with 37 additions and 11 deletions

View File

@@ -3,6 +3,13 @@
// "Prohibited: Raw Text Matching on Test Outputs". Per-file review may
// reclassify some entries as source-text-is-the-product during migration.
// allow-test-rule: structural-regression-guard
// The shebang line must be `#!/usr/bin/env bash` (PATH-resolved) rather than
// `#!/bin/bash` for cross-distro portability (NixOS, minimal Alpine do not
// ship /bin/bash). This is an architectural constraint that cannot be verified
// by executing the hooks — they run fine with either shebang on distros that
// have /bin/bash, so only a source assertion catches a future regression.
/**
* Regression tests for bug #2136 / #2206
*
@@ -100,11 +107,18 @@ describe('bug #2136 part 1: bash hook sources carry gsd-hook-version placeholder
}
test('version header is on line 2 (immediately after shebang)', () => {
// Placing the header immediately after #!/bin/bash ensures it is always
// found regardless of how much of the file is read.
// Placing the header immediately after the shebang ensures it is always
// found regardless of how much of the file is read. The shebang itself
// must use `#!/usr/bin/env bash` (PATH-resolved) rather than `#!/bin/bash`
// — POSIX guarantees /bin/sh but not /bin/bash, and distros like NixOS
// do not ship /bin/bash by default.
for (const sh of SH_HOOKS) {
const lines = fs.readFileSync(path.join(HOOKS_DIR, sh), 'utf8').split('\n');
assert.strictEqual(lines[0], '#!/bin/bash', `${sh} line 1 must be #!/bin/bash`);
assert.strictEqual(
lines[0],
'#!/usr/bin/env bash',
`${sh} line 1 must be "#!/usr/bin/env bash" for cross-distro portability`
);
assert.ok(
lines[1].startsWith('# gsd-hook-version:'),
`${sh} line 2 must be the gsd-hook-version header (got: "${lines[1]}")`

View File

@@ -20,7 +20,8 @@ process.env.GSD_TEST_MODE = '1';
* resolveNodeRunner helper, asserting on structured records:
* - the runner field is an absolute path (not bare 'node')
* - it ends with /node or \\node (or .exe on Windows simulation)
* - .sh hooks still use bare 'bash' (POSIX std PATH always has /bin)
* - .sh hooks still use bare 'bash' (PATH-resolved; portable across
* distros that don't ship /bin/bash, like NixOS)
*
* No source-grep on install.js content — assertions go against the
* value returned by the exported function and the parsed structure of