mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-14 12:27:18 +01:00
fix(capy-install): read project pins with an isolated Bun (GST-1747) (#4115)
* fix(capy-install): read project pins with an isolated Bun (GST-1747)
bin/gstack-capy-install runs bin/gstack-runtime-pins.ts from the caller's
project directory. Its three calls (read_pins, pins_accept, node_accept)
now pass --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}", so the
project's bunfig.toml and .env do not load into the installer.
test/bun-entry-isolation.test.ts also checks Bun runners held in a
variable: any variable at a command position launching a TypeScript file,
and any Bun-named variable running -e or a script.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(capy-install): note that the pins parser runs with an isolated Bun (GST-1747)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
---------
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
94392a61ec
commit
40071a4237
5 files changed
+57
-6
No files matched your search
@@ -52,6 +52,7 @@
|
||||
# Exit: 0 ok, 1 a component failed, 2 usage, 3 preflight stopped before ./setup.
|
||||
set -u
|
||||
BASH_COMPAT=50
|
||||
GSTACK_BUN_CONFIG=/dev/null; case "${OSTYPE:-}" in msys*|cygwin*) if command -v cygpath >/dev/null 2>&1; then GSTACK_BUN_CONFIG=NUL; fi ;; esac
|
||||
|
||||
SELF="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
ROOT="${GSTACK_CAPY_ROOT:-$SELF}"
|
||||
@@ -190,7 +191,7 @@ PINS_OUT="" PINS_MODE=none RUNNER="" PIN="" PIN_SRC="" PIN_EXACT="" PIN_ALTS=""
|
||||
read_pins() {
|
||||
RUNNER="$1" PINS_OUT="" PINS_MODE=none PIN="" PIN_SRC="" PIN_EXACT="" PIN_ALTS=""
|
||||
[ -n "$PROJECT" ] && [ "$PROFILE" -eq 0 ] || return 0
|
||||
[ -n "$RUNNER" ] && [ -f "$ROOT/bin/gstack-runtime-pins.ts" ] && PINS_OUT=$("$RUNNER" "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT" 2>/dev/null)
|
||||
[ -n "$RUNNER" ] && [ -f "$ROOT/bin/gstack-runtime-pins.ts" ] && PINS_OUT=$("$RUNNER" --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT" 2>/dev/null)
|
||||
if printf '%s\n' "$PINS_OUT" | grep -q '^PINS_VERDICT: '; then
|
||||
PINS_MODE=parser
|
||||
_exact=$(printf '%s\n' "$PINS_OUT" | sed -n 's/^PIN_EXACT_REQUIRED: bun \([^ ]*\) \([^ ]*\)$/\1 \2/p')
|
||||
@@ -229,7 +230,7 @@ read_pins() {
|
||||
# pins_accept VERSION — the resolver's eligibility hook: VERSION satisfies every project pin.
|
||||
pins_accept() {
|
||||
case "$PINS_MODE" in
|
||||
parser) "$RUNNER" "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT" --bun "$1" 2>/dev/null | grep -Eq '^PINS_VERDICT: (pass|none)$' ;;
|
||||
parser) "$RUNNER" --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT" --bun "$1" 2>/dev/null | grep -Eq '^PINS_VERDICT: (pass|none)$' ;;
|
||||
jq) case "$PIN" in
|
||||
'>='*) _gstack_bun_below "$1" "${PIN#>=}"; [ $? -eq 1 ] ;;
|
||||
[0-9]*.[0-9]*.[0-9]*) [ "$1" = "$PIN" ] ;;
|
||||
@@ -352,7 +353,7 @@ fi
|
||||
# satisfies the pins, and never installs.
|
||||
NODE_STORE=$(gstack_node_store "$GSTACK_STATE_ROOT")
|
||||
NODE_BIN="" NODE_VER="" NODE_RULE="" NODE_WANT=""
|
||||
node_accept() { ! "$RUNNER" "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT" --node "$1" 2>/dev/null | grep -q '^PIN_FAIL: node '; }
|
||||
node_accept() { ! "$RUNNER" --no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}" "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT" --node "$1" 2>/dev/null | grep -q '^PIN_FAIL: node '; }
|
||||
node_pins_text() { printf '%s\n' "$PINS_OUT" | sed -n 's/^PIN_SUMMARY: //p' | grep '^node' | tr '\n' ';' | sed 's/;$//; s/;/; /g'; }
|
||||
if [ "$PINS_MODE" = parser ] && printf '%s\n' "$PINS_OUT" | grep -q '^PIN: node '; then
|
||||
_nreq=$(printf '%s\n' "$PINS_OUT" | sed -n 's/^PIN_EXACT_REQUIRED: node \([^ ]*\) \([^ ]*\)$/\1 \2/p')
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
important: security
|
||||
---
|
||||
- **The Capy installer reads project pins with an isolated Bun.** `bin/gstack-capy-install` runs `bin/gstack-runtime-pins.ts` from the caller's project directory to read its Bun and Node pins. Those three calls now pass `--no-env-file --config="${GSTACK_BUN_CONFIG:-/dev/null}"` like every other Bun gstack starts, so a project's `bunfig.toml` and `.env` no longer load into the installer. `test/bun-entry-isolation.test.ts` now also checks Bun runners held in a variable (`"$RUNNER" x.ts`, and any variable whose name says Bun running `-e` or a script) in shipped shell files. (GST-1747)
|
||||
@@ -295,7 +295,9 @@ uses it to refuse a base merge under an open run
|
||||
runs; the pins, the doctor and the install verdict all use the resolved
|
||||
directory) with the doctor's own parser (`bin/gstack-runtime-pins.ts`: engines,
|
||||
packageManager, `.tool-versions`, `.bun-version` and CI `bun-version:` lanes
|
||||
for this platform). CI exact pins for this platform are alternatives: a
|
||||
for this platform), run with an isolated Bun (`--no-env-file
|
||||
--config="${GSTACK_BUN_CONFIG:-/dev/null}"`) so the project's `bunfig.toml`
|
||||
and `.env` never load into the installer. CI exact pins for this platform are alternatives: a
|
||||
repository may test several Bun versions (gstack's own CI pins 1.4.2 and
|
||||
runs one job on 1.4.4), so a Bun equal to any one of them satisfies CI;
|
||||
CI ranges and every non-CI pin stay required. One resolver picks the Bun (`gstack_bun_resolve` in
|
||||
|
||||
@@ -43,11 +43,16 @@ function firstLine(file: string): string {
|
||||
} finally { fs.closeSync(fd); }
|
||||
}
|
||||
|
||||
const BUN_WORD = /(^|[\s;&|({`"']|\$\()(bun|"\$\{?BUN(?:_[A-Z]+)?\}?"|\$\{?BUN(?:_[A-Z]+)?\}?)(?=[ \t])/g;
|
||||
const BUN_WORD = /(^|[\s;&|({`"']|\$\()(bun|"\$\{?\w*(?:BUN|[Bb]un)\w*\}?"|\$\{?\w*(?:BUN|[Bb]un)\w*\}?)(?=[ \t])/g;
|
||||
/** Any variable at a command position (`"$RUNNER" x.ts`, `OUT=$("$RUNNER" ...)`, `case) "$RUNNER" ...`); it is a Bun when it launches a TypeScript file. */
|
||||
const VAR_COMMAND = /(^\s*|[;&|({)`!]\s*|\$\(\s*|\b(?:then|do|else|exec)\s+)("\$\{?[A-Za-z_]\w*\}?"|\$\{?[A-Za-z_]\w*\}?)(?=[ \t])/g;
|
||||
|
||||
const afterFlags = (rest: string) => rest.replace(/^\s+/, '').replace(/^(?:--(?:no-env-file|cwd[= ]\S+|config(?:=\S*|\s+\S+))\s+)*/, '');
|
||||
const runsTypeScript = (rest: string) => /^(?:run\s+)?["']?[^\s"'|;&)]*\.(?:ts|tsx|mts)(?:["'\s)|;&\\`]|$)/.test(afterFlags(rest));
|
||||
|
||||
/** Whether the text after a `bun` word starts a program: -e/-p, a script path, `run <var>` or a bin path. A package script (`bun run build`) is not one. */
|
||||
function runsCode(rest: string): boolean {
|
||||
const r = rest.replace(/^\s+/, '').replace(/^(?:--(?:no-env-file|cwd[= ]\S+|config(?:=\S*|\s+\S+))\s+)*/, '');
|
||||
const r = afterFlags(rest);
|
||||
return /^(?:run\s+)?(?:-e|-p|--eval|--print)(?:\s|=|$)/.test(r)
|
||||
|| /^(?:run\s+)?["']?[^\s"'|;&)]*\.(?:ts|js|mjs|cjs|tsx)(?:["'\s)|;&\\`]|$)/.test(r)
|
||||
|| /^run\s+["']?\$\{?[A-Za-z_]/.test(r)
|
||||
@@ -64,6 +69,12 @@ function unisolatedBunCalls(line: string, flags = FLAGS): string[] {
|
||||
const rest = line.slice(at + m[2]!.length);
|
||||
if (!rest.startsWith(` ${flags}`) && runsCode(rest)) out.push(line.slice(at, at + 80));
|
||||
}
|
||||
for (const m of line.matchAll(VAR_COMMAND)) {
|
||||
const at = m.index! + m[1]!.length;
|
||||
if (new RegExp(BUN_WORD.source).test(m[2]!)) continue;
|
||||
const rest = line.slice(at + m[2]!.length);
|
||||
if (!rest.startsWith(` ${flags}`) && runsTypeScript(rest)) out.push(line.slice(at, at + 80));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -126,6 +137,12 @@ describe('GSTA-56: gstack never takes configuration from the audited repository'
|
||||
'"$BUN" -e \'1\'',
|
||||
'bun --no-env-file run "$X/bin/gstack-design-detect.ts"',
|
||||
'running=$(bun -p process.execPath)',
|
||||
'PINS_OUT=$("$RUNNER" "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT" 2>/dev/null)',
|
||||
' parser) "$RUNNER" "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT" --bun "$1" | grep -q x ;;',
|
||||
'node_accept() { ! "$RUNNER" "$ROOT/bin/gstack-runtime-pins.ts" --node "$1"; }',
|
||||
'"$RUNNER" run "$ROOT/bin/gstack-runtime-pins.ts"',
|
||||
'v=$("$REC_BUN" -e "console.log(1)")',
|
||||
'"$_bun" run "$X/bin/gstack-design-detect"',
|
||||
]) expect(unisolatedBunCalls(bad)).not.toEqual([]);
|
||||
for (const ok of [
|
||||
`bun ${FLAGS} run ~/.claude/skills/gstack/bin/gstack-render.ts x.html`,
|
||||
@@ -136,6 +153,13 @@ describe('GSTA-56: gstack never takes configuration from the audited repository'
|
||||
'# bun run scripts/x.ts in a comment',
|
||||
'say "installing bun $WANT from its release archive"',
|
||||
'ln -sf bun "$HOME/.bun/bin/bunx"',
|
||||
`PINS_OUT=$("$RUNNER" ${FLAGS} "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT")`,
|
||||
'[ -f "$ROOT/bin/gstack-runtime-pins.ts" ] && echo yes',
|
||||
'cp "$SRC" "$DEST/pins.ts"',
|
||||
'echo "$X" a.ts',
|
||||
'"$NODE" -e "console.log(1)"',
|
||||
'_got=$("$RUNNER" --version 2>/dev/null | head -1)',
|
||||
'out=$("$GSTACK_BUN_CONFIG")',
|
||||
]) expect(unisolatedBunCalls(ok)).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -150,6 +174,7 @@ describe('GSTA-56: gstack never takes configuration from the audited repository'
|
||||
`printf x | bun ${SHELL_FLAGS} -e "console.log(1)"`,
|
||||
`exec bun ${SHELL_FLAGS} "$HERE/question-log-hook.ts"`,
|
||||
`probe_cmd="bun ${SHELL_FLAGS} --eval 'import x from \\"y\\"'"`,
|
||||
`PINS_OUT=$("$RUNNER" ${SHELL_FLAGS} "$ROOT/bin/gstack-runtime-pins.ts" --project "$PROJECT")`,
|
||||
]) expect(unisolatedBunCalls(ok, SHELL_FLAGS)).toEqual([]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1372,6 +1372,25 @@ describe.skipIf(process.platform !== 'linux')('gstack-capy-install: the project
|
||||
expect(JSON.parse(fs.readFileSync(onPath.stdout.match(/GSTACK_INSTALL_VERDICT=(\S+)/)![1], 'utf8'))).toMatchObject({ node_path: path.join(f.stub, 'node'), node_rule: 'PATH', launch_path: '' });
|
||||
});
|
||||
|
||||
test('the pins parser ignores the project\'s bunfig.toml: install and --check read the Bun and Node pins from the project directory without running its preload', () => {
|
||||
const f = fixture();
|
||||
withPins(f);
|
||||
realBun(path.join(f.stub, 'bun'), '1.4.2');
|
||||
fakeNode(path.join(f.stub, 'node'), '24.21.0');
|
||||
const nvmNode = path.join(f.home, '.nvm/versions/node/v24.18.0/bin/node');
|
||||
fakeNode(nvmNode, '24.18.0');
|
||||
const project = nodeProject(f, '24.18.0');
|
||||
const marker = path.join(f.base, 'preload-ran.log');
|
||||
write(path.join(project, 'bunfig.toml'), 'preload = ["./preload.ts"]\n');
|
||||
write(path.join(project, 'preload.ts'), `require('fs').appendFileSync(${JSON.stringify(marker)}, process.argv.slice(2).join(' ') + '\\n');\n`);
|
||||
const r = run(f, ['--for', 'review'], {}, project);
|
||||
expect(r.status, r.stdout + r.stderr).toBe(0);
|
||||
expect(r.stdout).toContain(`node 24.18.0 at ${nvmNode} (nvm; satisfies the project's Node pins`);
|
||||
const c = run(f, ['--check', '--for', 'review'], {}, project);
|
||||
expect(c.status, c.stdout + c.stderr).toBe(0);
|
||||
expect(fs.existsSync(marker) ? fs.readFileSync(marker, 'utf8') : '').toBe('');
|
||||
});
|
||||
|
||||
test('no installed Node satisfies the pin: --check installs nothing; install downloads it into the runtime store checked against SHASUMS256.txt; a mismatch installs nothing', () => {
|
||||
const f = fixture();
|
||||
withPins(f);
|
||||
|
||||
Reference in new issue
Block a user