Skip to content

ci: fail loudly when artifact download times out - #29039

Merged
Jarred-Sumner merged 1 commit into
mainfrom
claude/runner-artifact-download-timeout
Apr 8, 2026
Merged

Jarred-Sumner merged 1 commit into
mainfrom
claude/runner-artifact-download-timeout

Conversation

@dylan-conway

Copy link
Copy Markdown
Member

getExecPathFromBuildKite runs buildkite-agent artifact download via spawnSafe with a 60 s timeout. On timeout spawnSafe kills the process and returns { error: "timeout" } — it doesn't throw — and the caller doesn't check it. The loop then picks whichever bun*.zip made it to disk and continues.

On a slow win-aarch64 VM (build #44517, 38 MiB in 52 s) the 149 MiB *-profile.zip never finished, so the runner silently fell back to the release bun.exe instead of bun-profile.exe. That in turn made which.test.ts fail because basename(process.execPath) became "bun.exe", which bootstrap.ps1 bakes into C:\Windows\System32\ on the v14 image.

Bump the timeout to 120 s and throw if it's hit so the job fails with a clear message instead of running the wrong binary.

spawnSafe's timeout kills buildkite-agent mid-download but returns
{error: 'timeout'} without throwing. The caller then picked whichever
zip(s) had finished, so on a slow VM the 149 MiB profile zip would be
dropped and tests silently ran against the release bun.exe instead of
bun-profile.exe. Bump the timeout to 120s and throw on timeout instead
of proceeding with a partial download.
@robobun

robobun commented Apr 8, 2026

Copy link
Copy Markdown
Collaborator
Updated 1:30 PM PT - Apr 8th, 2026

@dylan-conway, your commit f3ad744 is building: #44555

@coderabbitai

coderabbitai Bot commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1465d012-d14b-421c-8e3b-dc3857d6dfed

📥 Commits

Reviewing files that changed from the base of the PR and between 1afabdd and f3ad744.

📒 Files selected for processing (1)
  • scripts/runner.node.mjs

Walkthrough

Modified getExecPathFromBuildKite to capture error values from spawnSafe. Increased artifact download timeout from 60,000 ms to 120,000 ms. Added explicit error handling that throws when timeout is detected instead of proceeding with potentially incomplete downloads.

Changes

Cohort / File(s) Summary
Build artifact download error handling
scripts/runner.node.mjs
Enhanced getExecPathFromBuildKite to capture error values from artifact downloads, increased timeout threshold from 60,000 ms to 120,000 ms, and added explicit timeout error handling that prevents proceeding with incomplete downloads.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly describes the main change: failing loudly (throwing an error) when artifact download times out, which is the core fix in this PR.
Description check ✅ Passed The PR description covers both required sections: it explains what the PR does (timeout handling and error throwing) and verifies the code works by describing the real-world issue it fixes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread scripts/runner.node.mjs
Comment on lines +1975 to 1988
const { error } = await spawnSafe({
command: "buildkite-agent",
args,
timeout: 60000,
timeout: 120000,
});
if (error === "timeout") {
throw new Error(
`buildkite-agent artifact download timed out after 120s for step '${target}'. ` +
`Refusing to continue with a partial download (would silently fall back to the wrong binary).`,
);
}

zipPath = readdirSync(releasePath, { recursive: true, encoding: "utf-8" })
.filter(filename => /^bun.*\.zip$/i.test(filename))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 The PR only checks error === "timeout" but does not check the ok field, leaving all other spawnSafe failure modes (non-zero exit codes like "code 1", "spawn error", signals like "SIGKILL") silently ignored. When buildkite-agent artifact download fails for any non-timeout reason, the code falls through to readdirSync(releasePath) and — if a stale or partial zip exists from a prior run or an earlier loop iteration — breaks out of the download loop and proceeds to use that wrong binary. This is the exact silent fallback the PR description says it wants to prevent; the fix only closes it for the timeout case. To fix, also capture ok and throw when !ok: const { ok, error } = await spawnSafe({...}); if (!ok) { throw new Error(...); }

Extended reasoning...

What the bug is and how it manifests

The PR introduces a check for error === "timeout" after calling spawnSafe for buildkite-agent artifact download, but it never checks the ok field that spawnSafe returns. The ok field is exitCode === 0 && !signalCode && !spawnError — so it is false for any non-zero exit code, spawn errors, and signals, not just timeouts. When buildkite-agent exits non-zero for reasons other than the 120 s timeout (authentication failure, a transient network reset that causes exit code 1, an internal buildkite-agent timeout returning a signal, etc.), the code silently swallows the error and drops straight into readdirSync(releasePath).

The specific code path that triggers it

At lines 1975-1988, the destructuring const { error } = await spawnSafe(...) discards the ok field. Only the error === "timeout" case is handled. All other failure values for error ("code 1", "spawn error", "SIGKILL", etc.) cause the code to fall through directly to readdirSync(releasePath). If any zip is found there, zipPath is set, break downloadLoop fires, and the stale artifact is used.

Why existing code does not prevent it

The final safety check (throw new Error("Could not find ${target}.zip")) only fires when zipPath is undefined after all 10 iterations. If any zip file is present in releasePath — from a previous CI build (Buildkite agents commonly reuse workspaces), from a partial write by a prior loop iteration, or from a prior run of the same job — the readdirSync call finds it and the loop breaks immediately with that stale artifact. The sort logic preferring *-profile.zip mirrors the exact scenario from the PR description (profile zip vs release zip).

Step-by-step proof of the scenario

  1. Buildkite agent with a warm workspace runs iteration i=0 of the download loop. The release/ directory already contains bun-windows-x64.zip from the previous build.
  2. buildkite-agent artifact download starts downloading but exits with code 1 (e.g., network reset mid-transfer, or an auth error).
  3. spawnSafe returns { ok: false, error: "code 1", ... }.
  4. The if (error === "timeout") check is false — no throw.
  5. readdirSync(releasePath) finds bun-windows-x64.zip (the stale one from the previous build).
  6. zipPath is set, break downloadLoop fires.
  7. The stale zip is unzipped and bun.exe from the previous build is used for the entire test run — exactly the "silently fell back to the wrong binary" scenario the PR description explicitly called out.

What the impact would be

CI jobs would silently use the wrong binary (wrong commit, wrong profile/release variant) and potentially report misleading test results — the same class of failure the PR was written to fix. The error is completely invisible in the logs since spawnSafe does not throw.

How to fix it

Capture and check ok alongside error:

const { ok, error } = await spawnSafe({
  command: "buildkite-agent",
  args,
  timeout: 120000,
});
if (!ok) {
  throw new Error(
    `buildkite-agent artifact download failed for step '${target}': ${error}. ` +
      `Refusing to continue (would silently fall back to the wrong binary).`
  );
}

This closes the gap for all failure modes, not just timeouts. If retry behavior is desired for transient non-timeout failures, the loop should at minimum continue rather than fall through to readdirSync so that a stale zip is never used.

@Jarred-Sumner
Jarred-Sumner merged commit 329ea9d into main Apr 8, 2026
59 of 64 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the claude/runner-artifact-download-timeout branch April 8, 2026 21:54
structwafel pushed a commit to structwafel/bun that referenced this pull request Apr 25, 2026
`getExecPathFromBuildKite` runs `buildkite-agent artifact download` via
`spawnSafe` with a 60 s timeout. On timeout `spawnSafe` kills the
process and returns `{ error: "timeout" }` — it doesn't throw — and the
caller doesn't check it. The loop then picks whichever `bun*.zip` made
it to disk and continues.

On a slow win-aarch64 VM (build #44517, 38 MiB in 52 s) the 149 MiB
`*-profile.zip` never finished, so the runner silently fell back to the
release `bun.exe` instead of `bun-profile.exe`. That in turn made
`which.test.ts` fail because `basename(process.execPath)` became
`"bun.exe"`, which `bootstrap.ps1` bakes into `C:\Windows\System32\` on
the v14 image.

Bump the timeout to 120 s and throw if it's hit so the job fails with a
clear message instead of running the wrong binary.
xhjkl pushed a commit to xhjkl/bun that referenced this pull request May 14, 2026
`getExecPathFromBuildKite` runs `buildkite-agent artifact download` via
`spawnSafe` with a 60 s timeout. On timeout `spawnSafe` kills the
process and returns `{ error: "timeout" }` — it doesn't throw — and the
caller doesn't check it. The loop then picks whichever `bun*.zip` made
it to disk and continues.

On a slow win-aarch64 VM (build #44517, 38 MiB in 52 s) the 149 MiB
`*-profile.zip` never finished, so the runner silently fell back to the
release `bun.exe` instead of `bun-profile.exe`. That in turn made
`which.test.ts` fail because `basename(process.execPath)` became
`"bun.exe"`, which `bootstrap.ps1` bakes into `C:\Windows\System32\` on
the v14 image.

Bump the timeout to 120 s and throw if it's hit so the job fails with a
clear message instead of running the wrong binary.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants