From f492a5a9d0fac34cac0740b5b3babb33bc763cfe Mon Sep 17 00:00:00 2001 From: garrytan Date: Tue, 13 Oct 2026 04:00:58 +0000 Subject: [PATCH] fix(capy-install): follow a project's exact Bun pin above the minimum when gstack has its verified archive (adds Bun 1.4.3) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- bin/gstack-bun-version.sh | 32 ++++++++++++++++++++++---------- bin/gstack-capy-install | 28 +++++++++++++++++++--------- test/gstack-capy-install.test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 19 deletions(-) diff --git a/bin/gstack-bun-version.sh b/bin/gstack-bun-version.sh index e085d6fcf..900644f55 100644 --- a/bin/gstack-bun-version.sh +++ b/bin/gstack-bun-version.sh @@ -16,17 +16,29 @@ GSTACK_BUN_FLOOR="1.3.3" GSTACK_BUN_TESTED="1.4.2" -# gstack_bun_archive_sha256 TARGET — the SHA-256 of -# https://github.com/oven-sh/bun/releases/download/bun-v$GSTACK_BUN_TESTED/bun-TARGET.zip -# from that release's SHASUMS256.txt, for the targets bin/gstack-capy-install -# installs on Capy cloud machines (the same exact-archive recipe as -# .github/docker/Dockerfile.ci; test/bun-version-drift.test.ts keeps them -# equal). Prints nothing and returns 1 for a target without a verified archive. +# gstack_bun_archive_sha256 TARGET [VERSION] — the SHA-256 of +# https://github.com/oven-sh/bun/releases/download/bun-v$VERSION/bun-TARGET.zip +# (VERSION defaults to $GSTACK_BUN_TESTED) from that release's SHASUMS256.txt, +# for the targets bin/gstack-capy-install installs on Capy cloud machines (the +# same exact-archive recipe as .github/docker/Dockerfile.ci; +# test/bun-version-drift.test.ts keeps the $GSTACK_BUN_TESTED rows equal). A +# newer version is listed so the installer can follow a project's exact pin +# above the minimum. Prints nothing and returns 1 without a verified archive. +GSTACK_BUN_VERIFIED="1.4.2 1.4.3" gstack_bun_archive_sha256() { - case "$1" in - linux-x64) echo 36368faef7527875d5ffa52e53cd48021741f2a83eb6208a8dd64068d422a913 ;; - linux-x64-baseline) echo c678040f14fe0440eb839d37cbd0ce4c051a32da72806ac97de6a6aab6bf728f ;; - linux-aarch64) echo 54328bbc2d9c8e0c9f892c544d66c57a83b84139e34909e5ee81758f1ac8fda7 ;; + case "${2:-$GSTACK_BUN_TESTED}" in + 1.4.2) case "$1" in + linux-x64) echo 36368faef7527875d5ffa52e53cd48021741f2a83eb6208a8dd64068d422a913 ;; + linux-x64-baseline) echo c678040f14fe0440eb839d37cbd0ce4c051a32da72806ac97de6a6aab6bf728f ;; + linux-aarch64) echo 54328bbc2d9c8e0c9f892c544d66c57a83b84139e34909e5ee81758f1ac8fda7 ;; + *) return 1 ;; + esac ;; + 1.4.3) case "$1" in + linux-x64) echo 3e730528d89775f03f87f05c06787c4df4099d0a37f3f252e7b829cb23816e18 ;; + linux-x64-baseline) echo 1fc2edac843102909e3a1be1d8d9802cc6071cf074e67e88231f4ffe0f8b397b ;; + linux-aarch64) echo efa9813da5ed72423bf847f916e8d2c47c0d776add972354026a75e10da9aa21 ;; + *) return 1 ;; + esac ;; *) return 1 ;; esac } diff --git a/bin/gstack-capy-install b/bin/gstack-capy-install index b81de60fc..ca8399b55 100755 --- a/bin/gstack-capy-install +++ b/bin/gstack-capy-install @@ -103,7 +103,7 @@ install_bun() { # REASON — download, verify and install Bun $WANT into $BUN_DI 'Linux aarch64'|'Linux arm64') TARGET=linux-aarch64 ;; *) stop INSTALL_PREFLIGHT_FAILED "no verified Bun $WANT archive for $(uname -ms) (this installer is for Capy cloud Ubuntu)" "install Bun $WANT by hand (https://bun.sh), then re-run" ;; esac - SHA=$(gstack_bun_archive_sha256 "$TARGET") || stop INSTALL_PREFLIGHT_FAILED "no verified Bun $WANT archive for $TARGET" "install Bun $WANT by hand, then re-run" + SHA=$(gstack_bun_archive_sha256 "$TARGET" "$WANT") || stop INSTALL_PREFLIGHT_FAILED "no verified Bun $WANT archive for $TARGET" "install Bun $WANT by hand, then re-run" TMP=$(mktemp -d "${TMPDIR:-/tmp}/gstack-bun.XXXXXX") || stop INSTALL_PREFLIGHT_FAILED "cannot create a temp dir" "point TMPDIR at a writable directory" curl --retry 3 -fsSL "https://github.com/oven-sh/bun/releases/download/bun-v$WANT/bun-$TARGET.zip" -o "$TMP/bun.zip" || stop INSTALL_PREFLIGHT_FAILED "could not download bun-v$WANT/bun-$TARGET.zip" "check network access to github.com, then re-run" echo "$SHA $TMP/bun.zip" | sha256sum -c - >/dev/null 2>&1 || { rm -rf "$TMP"; stop INSTALL_PREFLIGHT_FAILED "bun-$TARGET.zip SHA-256 mismatch (expected $SHA); nothing installed" "update gstack_bun_archive_sha256 in bin/gstack-bun-version.sh only after checking the release's SHASUMS256.txt"; } @@ -130,6 +130,14 @@ for _d in "$BUN_DIR" "$HOME/.bun/bin"; do _v=$("$_d/bun" --version 2>/dev/null | head -1) [ "$(gstack_bun_status "${_v:-0.0.0}")" = ok ] && { LD_BIN="$_d/bun" LD_VER="$_v"; break; } done +# follow_pin VERSION SOURCE — an exact project pin becomes the Bun to install +# when it is at or above gstack's minimum and has a verified archive; anything +# else is a conflict naming both constraints and the verified versions. +follow_pin() { + [ "$1" = "$WANT" ] && return 0 + if [ "$WANT" = "$GSTACK_BUN_TESTED" ] && [ "$(gstack_bun_status "$1")" = ok ] && gstack_bun_archive_sha256 linux-x64 "$1" >/dev/null; then WANT="$1"; return 0; fi + stop RUNTIME_PIN_CONFLICT "Bun conflict: gstack needs >=$GSTACK_BUN_TESTED (verified archives: $GSTACK_BUN_VERIFIED), $2 pins exactly $1" "raise the project's pin to a verified version, or install gstack on a machine the project already provisions" +} reuse_ld() { launch_with "$(dirname "$LD_BIN")"; say "reusing bun $LD_VER at $LD_BIN (not first on PATH; the GSTACK_LAUNCH line puts it first)"; } # No Bun at or above the security floor anywhere: install the verified one # first, so the project's pins can be read with it. @@ -144,16 +152,18 @@ if printf '%s\n' "$PINS_OUT" | grep -q '^PINS_VERDICT: '; then if [ "$HAVE_OK" -eq 1 ] && pins_ok "$HAVE"; then say "bun $HAVE satisfies gstack's supported minimum $WANT and the project pins" elif [ -n "$LD_BIN" ] && pins_ok "$LD_VER"; then reuse_ld else - printf '%s\n' "$PINS_OUT" | sed -n 's/^PIN: bun \([^ ]*\) exact \([^ ]*\) lane=[a-z]*$/\1 \2/p' | while read -r _c _src; do - [ "$_c" = "$WANT" ] || stop RUNTIME_PIN_CONFLICT "Bun conflict: gstack needs >=$WANT (its verified archive is $WANT), $PROJECT/${_src%%:*} pins exactly $_c (${_src})" "raise the project's pin to $WANT, or install gstack on a machine the project accepts" - done || exit 3 + while read -r _c _src; do + [ -z "$_c" ] || follow_pin "$_c" "$PROJECT/${_src%%:*}" + done <=$WANT, $PROJECT pins $(printf '%s\n' "$PINS_OUT" | sed -n 's/^PIN_SUMMARY: //p' | grep '^bun' | tr '\n' ';' | sed 's/;$//')" "raise the project's pin to $WANT or newer, or install gstack on a machine the project accepts" if [ "$LD_VER" = "$WANT" ]; then reuse_ld; else install_bun "bun ${HAVE:-absent} does not satisfy the project pins (or gstack's minimum $WANT)"; fi fi else # No pin report (no project, or a Bun that cannot run the parser): the # package.json / .bun-version subset, read with jq. - PIN="" PIN_SRC="" + PIN="" PIN_SRC="" EXACT=0 if [ -n "$PROJECT" ] && [ -f "$PROJECT/package.json" ]; then PIN=$(jq -r '.engines.bun // (.packageManager // "" | select(startswith("bun@")) | ltrimstr("bun@")) // empty' "$PROJECT/package.json" 2>/dev/null | head -1) [ -n "$PIN" ] && PIN_SRC="$PROJECT/package.json" @@ -162,12 +172,12 @@ else case "$PIN" in '') ;; '>='*) _gstack_bun_below "$WANT" "${PIN#>=}" && stop RUNTIME_PIN_CONFLICT "Bun conflict: gstack needs >=$WANT, $PIN_SRC pins $PIN" "raise the project's pin to $WANT or newer, or install gstack on a machine the project accepts" ;; - [0-9]*.[0-9]*.[0-9]*) [ "$PIN" = "$WANT" ] || stop RUNTIME_PIN_CONFLICT "Bun conflict: gstack needs >=$WANT, $PIN_SRC pins exactly $PIN" "raise the project's pin to $WANT, or install gstack on a machine the project accepts" ;; + [0-9]*.[0-9]*.[0-9]*) follow_pin "$PIN" "$PIN_SRC"; EXACT=1 ;; *) say "note: project pin '$PIN' ($PIN_SRC) is not evaluated here; gstack-doctor's pins row checks it after install" ;; esac - if [ "$HAVE_OK" -eq 1 ]; then say "bun $HAVE satisfies gstack's supported minimum $WANT${PIN:+ and the project pin $PIN}" - elif [ -n "$LD_BIN" ]; then reuse_ld - elif [ -z "$LAUNCH_DIR" ]; then install_bun "bun ${HAVE:-absent} is below gstack's supported minimum $WANT${PIN:+ (project pin $PIN agrees)}" + if [ "$HAVE_OK" -eq 1 ] && { [ "$EXACT" -eq 0 ] || [ "$HAVE" = "$WANT" ]; }; then say "bun $HAVE satisfies gstack's supported minimum $GSTACK_BUN_TESTED${PIN:+ and the project pin $PIN}" + elif [ -n "$LD_BIN" ] && { [ "$EXACT" -eq 0 ] || [ "$LD_VER" = "$WANT" ]; }; then reuse_ld + elif [ -z "$LAUNCH_DIR" ] || [ "$LD_VER" != "$WANT" ]; then install_bun "bun ${HAVE:-absent} does not satisfy gstack's supported minimum $GSTACK_BUN_TESTED${PIN:+ and the project pin $PIN}" fi fi fi diff --git a/test/gstack-capy-install.test.ts b/test/gstack-capy-install.test.ts index 432eedd24..02a703805 100644 --- a/test/gstack-capy-install.test.ts +++ b/test/gstack-capy-install.test.ts @@ -160,6 +160,30 @@ describe('gstack-capy-install', () => { expect(conflictRange.stderr).toContain('pins >=2.0.0'); }); + test('an exact project pin above the minimum with a verified archive is the Bun installed; an unverified one stops naming the verified versions', () => { + const f = fixture({ bun: '1.3.14' }); + const project = path.join(f.base, 'project'); + write(path.join(project, '.bun-version'), '1.4.3\n'); + sh('git init -q', project); + const zip = path.join(f.base, 'bad.zip'); + fs.mkdirSync(path.join(f.base, 'z/bun-linux-x64'), { recursive: true }); + script(path.join(f.base, 'z/bun-linux-x64/bun'), ['echo 1.4.3']); + expect(sh(`cd "${f.base}/z" && zip -qr "${zip}" bun-linux-x64`).status).toBe(0); + const adopted = run(f, ['--project', project], { STUB_CURL_ZIP: zip }); + expect(adopted.stdout).toContain('installing bun 1.4.3 from its release archive'); + expect(f.calls().filter(l => l.startsWith('curl '))).toEqual([expect.stringContaining('https://github.com/oven-sh/bun/releases/download/bun-v1.4.3/bun-linux')]); + expect(adopted.status).toBe(3); + expect(adopted.stderr).toContain('SHA-256 mismatch'); + const g = fixture({ bun: '1.3.14' }); + write(path.join(project, '.bun-version'), '1.4.9\n'); + const unverified = run(g, ['--project', project]); + expect(unverified.status).toBe(3); + expect(unverified.stderr).toContain(`${project}/.bun-version pins exactly 1.4.9`); + expect(unverified.stderr).toContain('verified archives: 1.4.2 1.4.3'); + expect(unverified.stderr).toContain('(RUNTIME_PIN_CONFLICT; '); + expect(g.calls().some(l => l.startsWith('curl ') || l.startsWith('setup '))).toBe(false); + }); + test('Bun below the minimum: the exact release archive is downloaded and SHA-256 verified; a mismatch installs nothing', () => { const f = fixture({ bun: '1.3.14' }); const zip = path.join(f.base, 'bad.zip');