mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-05 08:23:59 +01:00
fix(capy-install): the launch file appends the checkout's bin/ to PATH (GSTA-167)
A /ship child that sourced only the launch file got 'gstack-ship-budget: command not found' (exit 127). The file now appends <checkout>/bin once, after everything already on PATH, so it shadows nothing. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
9fa5e97c96
commit
4b623a490d
3 files changed
+18
-9
No files matched your search
@@ -656,13 +656,16 @@ WARN behind — installed $INSTALLED is ${GSTACK_CAPY_BEHIND:-?} commits behind
|
||||
[ -n "$NOTES" ] && TRAILER="$TRAILER ($NOTES)" ;;
|
||||
esac
|
||||
# The launch file: the same assignments as a sourceable script, mode 0600,
|
||||
# beside the verdict. `. <file>` applies them without ever printing them; the
|
||||
# beside the verdict. It also appends the checkout's bin directory to PATH
|
||||
# (after everything already there, so it shadows nothing): a skill's bare
|
||||
# `gstack-ship-budget` resolves in a shell that sourced only this file. `. <file>` applies them without ever printing them; the
|
||||
# line carries no secret, but a caller that re-typed it as `export $(...)`
|
||||
# ran a bare `export` when the substitution came back empty and dumped the
|
||||
# whole environment, API keys included, into its tool output (GSTA-96). The
|
||||
# file is part of the ok verdict: a passing run that cannot write it fails,
|
||||
# so no caller is left with only the line and the re-typing path.
|
||||
LAUNCH_FILE="${VERDICT%.json}.env"
|
||||
BIN_DIR=$(cd "$ROOT/bin" 2>/dev/null && pwd -P)
|
||||
write_launch_file() {
|
||||
local tmp
|
||||
mkdir -p "$(dirname "$LAUNCH_FILE")" || return 1
|
||||
@@ -672,6 +675,7 @@ write_launch_file() {
|
||||
echo "# Apply it with: . <this file>"
|
||||
echo "# Never print it or the environment: no \`export \$(...)\`, \`env\`, \`printenv\` or bare \`set\` in a tool fence."
|
||||
[ -n "$LAUNCH_DIR" ] && echo "case \":\$PATH:\" in *:$(shq "$LAUNCH_DIR"):*) ;; *) PATH=$(shq "$LAUNCH_DIR"):\"\$PATH\" ;; esac; export PATH"
|
||||
[ -n "$BIN_DIR" ] && echo "case \":\$PATH:\" in *:$(shq "$BIN_DIR"):*) ;; *) PATH=\"\${PATH:+\$PATH:}\"$(shq "$BIN_DIR") ;; esac; export PATH"
|
||||
echo "export GSTACK_SESSION_KIND=unattended"
|
||||
echo "export GSTACK_STATE_ROOT=$(shq "$GSTACK_STATE_ROOT")"
|
||||
echo "export GSTACK_EPHEMERAL=$(shq "${GSTACK_EPHEMERAL:-1}")"
|
||||
|
||||
@@ -423,6 +423,9 @@ GSTACK_LAUNCH_FILE: <verdict file without .json>.env
|
||||
|
||||
`GSTACK_LAUNCH_FILE` names the same assignments as a sourceable script (mode
|
||||
0600, beside the verdict under the state root; the PATH prepend is idempotent).
|
||||
The file also appends the checkout's `bin/` directory to PATH, after everything
|
||||
already there and only once, so a shell that sourced only this file runs a bare
|
||||
`gstack-ship-budget` (GSTA-167: a /ship child got `command not found`, exit 127).
|
||||
The file is part of the ok verdict: a passing run that cannot write it exits 1
|
||||
with `gstack: fail launch-file — could not write the launch file <path>; fix:
|
||||
make <dir> writable, or set GSTACK_STATE_ROOT to a writable directory
|
||||
|
||||
@@ -524,15 +524,17 @@ describe('gstack-capy-install: launch PATH, Bun reuse, CI pins, setup skip, verd
|
||||
expect(r.stdout).toContain(`GSTACK_LAUNCH_FILE: ${files[0].replace(/\.json$/, '.env')}`);
|
||||
expect(fs.readFileSync(files[0].replace(/\.json$/, '.env'), 'utf8')).toContain(`export GSTACK_INSTALL_VERDICT='${files[0]}'`);
|
||||
// /ship Step 7 value card. protects: with the certified Bun first on PATH (no GSTACK_LAUNCH_PATH)
|
||||
// the launch file carries no PATH line, sources under POSIX sh, and leaves PATH exactly as it was;
|
||||
// --check rewrites and prints it too. fails_when: the PATH guard is dropped (an empty prepend
|
||||
// leaves ":"-led PATH), the file uses a bashism, or --check stops printing GSTACK_LAUNCH_FILE.
|
||||
// why_existing_misses: the PATH-bearing case above is sourced by bash only, and the --check run
|
||||
// below asserted only the GSTACK_LAUNCH line. seam=none.
|
||||
const sourced = spawnSync('sh', ['-c', '. "$1" && printf "%s\\n" "$PATH" "$GSTACK_SESSION_KIND" "$GSTACK_CAPY" "$GSTACK_INSTALL_VERDICT"', '_', files[0].replace(/\.json$/, '.env')],
|
||||
// the launch file prepends nothing, sources under POSIX sh, and only appends the checkout's bin
|
||||
// directory once (GSTA-167: a /ship child that sourced only the launch file got
|
||||
// `gstack-ship-budget: command not found`, exit 127); --check rewrites and prints it too.
|
||||
// fails_when: the PATH guard is dropped (an empty prepend leaves ":"-led PATH, a second source
|
||||
// appends the bin directory twice), the file uses a bashism, the bin directory is not on PATH,
|
||||
// or --check stops printing GSTACK_LAUNCH_FILE. why_existing_misses: the PATH-bearing case above
|
||||
// is sourced by bash only, and the --check run below asserted only the GSTACK_LAUNCH line. seam=none.
|
||||
const sourced = spawnSync('sh', ['-c', '. "$1" && . "$1" && printf "%s\\n" "$PATH" "$GSTACK_SESSION_KIND" "$GSTACK_CAPY" "$GSTACK_INSTALL_VERDICT" "$(command -v gstack-paths)"', '_', files[0].replace(/\.json$/, '.env')],
|
||||
{ encoding: 'utf8', timeout: 30_000, env: { PATH: '/usr/bin:/bin' } });
|
||||
expect(sourced.status, sourced.stderr).toBe(0);
|
||||
expect(sourced.stdout.trimEnd().split('\n')).toEqual(['/usr/bin:/bin', 'unattended', '1', files[0]]);
|
||||
expect(sourced.stdout.trimEnd().split('\n')).toEqual([`/usr/bin:/bin:${f.root}/bin`, 'unattended', '1', files[0], path.join(f.root, 'bin', 'gstack-paths')]);
|
||||
const v = JSON.parse(fs.readFileSync(files[0], 'utf8'));
|
||||
expect(v).toMatchObject({ schema_version: 2, checkout: f.root, head: sh('git rev-parse HEAD', f.root).stdout.trim(), for: 'autoplan', ok: true, trailer: 'gstack: ok 1.2.3 for=autoplan',
|
||||
project: '', bun_path: path.join(f.stub, 'bun'), bun_version: '1.4.2', bun_rule: 'PATH' });
|
||||
@@ -975,7 +977,7 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: the per-dire
|
||||
const sourced = spawnSync('sh', ['-c', '. "$1" && . "$1" && printf "%s\\n" "$PATH" "$GSTACK_STATE_ROOT" "$GSTACK_EPHEMERAL" "$GSTACK_INSTALL_VERDICT"', '_', launchFile],
|
||||
{ encoding: 'utf8', timeout: 30_000, env: { PATH: '/usr/bin:/bin' } });
|
||||
expect(sourced.status, sourced.stderr).toBe(0);
|
||||
expect(sourced.stdout.trimEnd().split('\n')).toEqual([`${state}/runtimes/bun/${BUN_DEFAULT}:/usr/bin:/bin`, state, '0', launchFile.replace(/\.env$/, '.json')]);
|
||||
expect(sourced.stdout.trimEnd().split('\n')).toEqual([`${state}/runtimes/bun/${BUN_DEFAULT}:/usr/bin:/bin:${f.root}/bin`, state, '0', launchFile.replace(/\.env$/, '.json')]);
|
||||
const r = fresh(f, f.base, 'bun --version && bunx --version', { GSTACK_BUN_SHIM_EXPLAIN: '1' });
|
||||
expect(r.status, r.stderr).toBe(0);
|
||||
expect(r.stdout.trim().split('\n')[0]).toBe(BUN_DEFAULT);
|
||||
|
||||
Reference in new issue
Block a user