Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
7e5a2f6da7 fix(bunx): bypass cache for github:user/repo#HEAD refs
`bunx github:user/repo#HEAD` was incorrectly using cached binaries
because the cache-bust logic only activated for `.dist_tag` versions
(like `@latest`). Since `#HEAD` is a mutable reference that should
always resolve to the latest commit, treat it the same as `@latest`
by skipping the cache and always re-fetching.

Closes #27379

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-23 20:02:42 +00:00
3 changed files with 133 additions and 3 deletions

View File

@@ -544,8 +544,10 @@ pub const BunxCommand = struct {
const passthrough = opts.passthrough_list.items;
var do_cache_bust = update_request.version.tag == .dist_tag;
const look_for_existing_bin = update_request.version.literal.isEmpty() or update_request.version.tag != .dist_tag;
const is_github_head_ref = update_request.version.tag == .github and
strings.eqlComptime(update_request.version.value.github.committish.slice(update_request.version_buf), "HEAD");
var do_cache_bust = update_request.version.tag == .dist_tag or is_github_head_ref;
const look_for_existing_bin = update_request.version.literal.isEmpty() or (update_request.version.tag != .dist_tag and !is_github_head_ref);
debug("try run existing? {}", .{look_for_existing_bin});
if (look_for_existing_bin) try_run_existing: {

View File

@@ -279,7 +279,7 @@ it("should work for github repository with committish", async () => {
expect(out.trim()).toContain("hello bun!");
expect(exited).toBe(0);
// cached
// #HEAD should not use cache (like @latest), so --no-install should fail
const cached = spawn({
cmd: [bunExe(), "x", "--no-install", "github:piuccio/cowsay#HEAD", "hello bun!"],
cwd: x_dir,
@@ -295,6 +295,46 @@ it("should work for github repository with committish", async () => {
cached.exited,
]);
expect(exited).toBe(1);
});
it("should work for github repository with non-HEAD committish and cache it", async () => {
// A specific commit SHA should still be cached
const withoutCache = spawn({
cmd: [bunExe(), "x", "github:piuccio/cowsay#master", "hello bun!"],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env,
});
let [err, out, exited] = await Promise.all([
new Response(withoutCache.stderr).text(),
new Response(withoutCache.stdout).text(),
withoutCache.exited,
]);
expect(err).not.toContain("error:");
expect(out.trim()).toContain("hello bun!");
expect(exited).toBe(0);
// Non-HEAD committish should use cache, so --no-install should work
const cached = spawn({
cmd: [bunExe(), "x", "--no-install", "github:piuccio/cowsay#master", "hello bun!"],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env,
});
[err, out, exited] = await Promise.all([
new Response(cached.stderr).text(),
new Response(cached.stdout).text(),
cached.exited,
]);
expect(err).not.toContain("error:");
expect(out.trim()).toContain("hello bun!");
expect(exited).toBe(0);

View File

@@ -0,0 +1,88 @@
import { spawn } from "bun";
import { beforeEach, expect, it, setDefaultTimeout } from "bun:test";
import { rm } from "fs/promises";
import { bunEnv, bunExe, isWindows, tmpdirSync } from "harness";
import { readdirSync } from "node:fs";
import { tmpdir } from "os";
import { join } from "path";
// Regression test for https://github.com/oven-sh/bun/issues/27379
// `bunx github:user/repo#HEAD` should not use the cache (should always re-fetch).
let x_dir: string;
let current_tmpdir: string;
let install_cache_dir: string;
let env = { ...bunEnv };
setDefaultTimeout(1000 * 60 * 5);
beforeEach(async () => {
const waiting: Promise<void>[] = [];
if (current_tmpdir) {
waiting.push(rm(current_tmpdir, { recursive: true, force: true }));
}
if (install_cache_dir) {
waiting.push(rm(install_cache_dir, { recursive: true, force: true }));
}
const tmp = isWindows ? tmpdir() : "/tmp";
readdirSync(tmp).forEach(file => {
if (file.startsWith("bunx-") || file.startsWith("bun-x.test")) {
waiting.push(rm(join(tmp, file), { recursive: true, force: true }));
}
});
install_cache_dir = tmpdirSync();
current_tmpdir = tmpdirSync();
x_dir = tmpdirSync();
env.TEMP = current_tmpdir;
env.BUN_TMPDIR = env.TMPDIR = current_tmpdir;
env.TMPDIR = current_tmpdir;
env.BUN_INSTALL_CACHE_DIR = install_cache_dir;
await Promise.all(waiting);
});
it("bunx github:user/repo#HEAD should not use cached version", async () => {
// First run: install from GitHub
const firstRun = spawn({
cmd: [bunExe(), "x", "github:piuccio/cowsay#HEAD", "hello bun!"],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env,
});
const [err1, out1, exited1] = await Promise.all([
new Response(firstRun.stderr).text(),
new Response(firstRun.stdout).text(),
firstRun.exited,
]);
expect(err1).not.toContain("error:");
expect(out1.trim()).toContain("hello bun!");
expect(exited1).toBe(0);
// Second run with --no-install should fail because #HEAD bypasses cache
// (similar to how @latest works for npm packages)
const secondRun = spawn({
cmd: [bunExe(), "x", "--no-install", "github:piuccio/cowsay#HEAD", "hello bun!"],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env,
});
const [_err2, _out2, exited2] = await Promise.all([
new Response(secondRun.stderr).text(),
new Response(secondRun.stdout).text(),
secondRun.exited,
]);
// #HEAD should always trigger a fresh install, so --no-install should fail
expect(exited2).toBe(1);
});