From 4c20c39973fd8fd923a4bfaa268fdae3bf34b1fa Mon Sep 17 00:00:00 2001 From: senthilr-nv Date: Thu, 2 Apr 2026 11:22:21 -0700 Subject: [PATCH] fix(scripts): fall back to curl when gh download fails install-openshell.sh used gh release download exclusively when the gh CLI was on PATH, with no recovery path if gh failed. When gh was installed but not authenticated (common on macOS with Homebrew), it blocked with an auth prompt and exited with code 4. The curl fallback only ran when gh was absent entirely. Try gh first (preserves faster path for authenticated users); if it fails for any reason, warn and fall back to anonymous curl. Partial files from a failed gh attempt are cleaned before the retry. Checksum verification is unchanged. Fixes #1318 Signed-off-by: senthilr-nv --- scripts/install-openshell.sh | 22 +++++++++---- test/runner.test.js | 62 ++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/scripts/install-openshell.sh b/scripts/install-openshell.sh index dde9f0911a8..b0a0baaac14 100755 --- a/scripts/install-openshell.sh +++ b/scripts/install-openshell.sh @@ -80,16 +80,26 @@ tmpdir="$(mktemp -d)" trap 'rm -rf "$tmpdir"' EXIT CHECKSUM_FILE="openshell-checksums-sha256.txt" -if command -v gh >/dev/null 2>&1; then - GH_TOKEN="${GITHUB_TOKEN:-}" gh release download --repo NVIDIA/OpenShell \ - --pattern "$ASSET" --dir "$tmpdir" - GH_TOKEN="${GITHUB_TOKEN:-}" gh release download --repo NVIDIA/OpenShell \ - --pattern "$CHECKSUM_FILE" --dir "$tmpdir" -else +download_with_curl() { curl -fsSL "https://github.com/NVIDIA/OpenShell/releases/latest/download/$ASSET" \ -o "$tmpdir/$ASSET" curl -fsSL "https://github.com/NVIDIA/OpenShell/releases/latest/download/$CHECKSUM_FILE" \ -o "$tmpdir/$CHECKSUM_FILE" +} + +if command -v gh >/dev/null 2>&1; then + if GH_PROMPT_DISABLED=1 GH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}" gh release download --repo NVIDIA/OpenShell \ + --pattern "$ASSET" --dir "$tmpdir" 2>/dev/null \ + && GH_PROMPT_DISABLED=1 GH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}" gh release download --repo NVIDIA/OpenShell \ + --pattern "$CHECKSUM_FILE" --dir "$tmpdir" 2>/dev/null; then + : # gh succeeded + else + warn "gh CLI download failed (auth may not be configured) — falling back to curl" + rm -f "$tmpdir/$ASSET" "$tmpdir/$CHECKSUM_FILE" + download_with_curl + fi +else + download_with_curl fi info "Verifying SHA-256 checksum..." diff --git a/test/runner.test.js b/test/runner.test.js index 120c532c201..3bc370ee700 100644 --- a/test/runner.test.js +++ b/test/runner.test.js @@ -530,6 +530,68 @@ describe("regression guards", () => { expect(src).toContain("openshell-checksums-sha256.txt"); expect(src).toContain("shasum -a 256 -c"); }); + + it("install-openshell.sh falls back to curl when gh fails (#1318)", () => { + const src = fs.readFileSync( + path.join(import.meta.dirname, "..", "scripts", "install-openshell.sh"), + "utf-8", + ); + expect(src).toContain("download_with_curl"); + const ghBlock = src.slice(src.indexOf("command -v gh")); + expect(ghBlock).toContain("2>/dev/null"); + expect(ghBlock).toContain("falling back to curl"); + expect(ghBlock).toContain("download_with_curl"); + }); + + it("install-openshell.sh gh-absent path uses curl directly", () => { + const src = fs.readFileSync( + path.join(import.meta.dirname, "..", "scripts", "install-openshell.sh"), + "utf-8", + ); + expect(src).toContain("download_with_curl"); + const ghCheck = src.indexOf("command -v gh"); + const elseBlock = src.indexOf("\nelse\n", ghCheck); + const finalFi = src.indexOf("\nfi\n", elseBlock); + expect(ghCheck).toBeGreaterThan(-1); + expect(elseBlock).toBeGreaterThan(ghCheck); + expect(finalFi).toBeGreaterThan(elseBlock); + const fallthrough = src.slice(elseBlock, finalFi); + expect(fallthrough).toContain("download_with_curl"); + expect(fallthrough).not.toContain("gh release"); + }); + + it("install-openshell.sh gh-present-but-fails path falls back to curl", () => { + const scriptPath = path.join(import.meta.dirname, "..", "scripts", "install-openshell.sh"); + const tmpBin = fs.mkdtempSync(path.join(os.tmpdir(), "gh-stub-")); + const ghStub = path.join(tmpBin, "gh"); + fs.writeFileSync(ghStub, "#!/bin/sh\nexit 4\n"); + fs.chmodSync(ghStub, 0o755); + + const stub = ` + #!/usr/bin/env bash + openshell() { echo "openshell 0.0.1"; } + export -f openshell + export PATH="${tmpBin}:/usr/bin:/bin" + curl() { echo "CURL_FALLBACK $*"; return 0; } + export -f curl + shasum() { echo "checksum OK"; return 0; } + export -f shasum + tar() { return 0; }; export -f tar + install() { return 0; }; export -f install + source "${scriptPath}" + `; + try { + const result = spawnSync("bash", ["-c", stub], { + encoding: "utf-8", + timeout: 5000, + }); + const out = (result.stdout || "") + (result.stderr || ""); + expect(out).toContain("falling back to curl"); + expect(out).toContain("CURL_FALLBACK"); + } finally { + fs.rmSync(tmpBin, { recursive: true, force: true }); + } + }); }); describe("curl-pipe-to-shell guards (#574, #583)", () => {