Repository navigation
Conversation
The runner always leaves through process.exit() (main(), --bail, the SIGINT/SIGTERM/SIGHUP handlers), or through an uncaught exception. Neither emits beforeExit, which was the only place the ci-remap-server child was killed, so every CI-mode run left the `bun run --silent ci-remap-server` wrapper and its script alive under PID 1, holding the runner's inherited stderr open. The beforeExit handler also called server.off() without a listener, which throws before reaching kill(). Add killOnExit() to scripts/utils.mjs, hooked on the exit event (which process.exit() and uncaught exceptions both emit), and use it for the remap server and the docker coordinator, which already used that event inline.
|
Warning Review limit reached
Next review available in: 15 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (3)
Comment |
|
Status: ready for review. Reproduced on main with the real runner in CI mode ( Fix and test are described in the PR body; the regression test is |
There was a problem hiding this comment.
LGTM — straightforward fix moving the remap-server cleanup hook from beforeExit (never fires here) to exit.
What was reviewed:
- Verified removing the
exitingguard is safe: during processexitno further event-loop ticks run, so the child'sexithandler cannot fire; in thedid not startbrancherrorResolvewas already called even before this change (guard was still false there). - Checked that a second
kill()on the already-dead child (from the exit hook after the timeout branch killed it) is a no-op in Node. - Test design: control case proves stdout-EOF is a real signal,
BUN_FEATURE_FLAG_NO_ORPHANSis unset so bun's own reaping doesn't mask a missing hook, leaked helpers are SIGKILLed inafterAll, Windows is skipped (matches theisCI && !isWindowsgate at the call site).
Extended reasoning...
Overview
This PR fixes an orphaned-process leak in the CI test runner (scripts/runner.node.mjs). The crash-report remap server was hooked on process.once("beforeExit", ...), but the runner always leaves via process.exit() (main path, --bail, signal handlers) or an uncaught exception — none of which emit beforeExit. The fix adds a tiny killOnExit(child) helper to scripts/utils.mjs that hooks process.once("exit", ...) instead, applies it to both the remap server and the docker coordinator (which was already using an inline exit hook), and removes the now-dead exiting flag and broken server.off("error") calls (which would have thrown ERR_INVALID_ARG_TYPE anyway). A new test at test/internal/runner-kill-on-exit.test.ts covers all three exit paths under both node and bun, with a control case proving the assertion is not vacuous.
Security risks
None. This touches CI orchestration scripts only — no runtime code, no user-facing surface, no auth/crypto/permissions.
Level of scrutiny
Low-to-medium. This is internal CI tooling (scripts/), not shipped runtime code, and the change is small (~15 net lines of production diff). The diagnosis is unusually thorough — the PR description traces every exit path in the runner to a process.exit() call, explains why beforeExit never fires, documents why SIGTERM (not SIGKILL) is correct for the bun run wrapper tree on Linux vs macOS, and includes before/after probes on the real runner showing 2→0 leaked processes and pipe-EOF timing. No CODEOWNERS entry covers scripts/.
Other factors
- The removed
exitingguard was only ever set insideonBeforeExit, so removing it doesn't change behavior on the surviving code path (thedid not startbranch already callederrorResolvepost-kill before this change, harmlessly). - The test follows repo conventions well:
tempDir/bunEnv/bunExe/nodeExefrom harness,test.concurrentfor independent subprocess cases,describe.skipIf(isWindows || !exe)matching the!isWindowsgate at the call site,afterAllcleanup of any helpers a failing test would leak, and explicit unset ofBUN_FEATURE_FLAG_NO_ORPHANSso bun's own orphan-killing doesn't hide a missing hook. - The docker coordinator change is a pure refactor to the shared helper — identical semantics to its previous inline
process.once("exit", () => coordinator.kill()). - Placement in
test/internal/matches the existing pattern for tests of build/CI tooling (alongsideparallel-allowlist.test.ts,build-*.test.ts, etc.).
…ls never download from GitHub (#39446) ### Problem - The "Lint JavaScript" (lint.yml) and "Format" (format.yml) checks go red on PRs that did not touch anything they check. Their `bun install` step fails with: ``` error: failed to download bun-tracestrings@github:oven-sh/bun.report#912ca63: HTTP 5xx Failed to install 1 package ``` Seen on #29642 at 5309742 (a C++ comment change), both attempts of runs 32040959983 and 32040960113, while other PRs' Lint runs flapped red/green in the same minutes. - Cause: `package.json:13` pins `bun-tracestrings` as a `github:` dependency, so every root `bun install` downloads a tarball from GitHub. That install runs on a fresh runner (no cache) in lint.yml, format.yml, rust-lints.yml (4 jobs), bun-types.yml and packages-ci.yml, and in every build (`scripts/build/codegen.ts` `bun_install`). bun retries a tarball 5 times back to back, which does not cover an outage of a few minutes. - The only user of the package is `scripts/runner.node.mjs`, which runs its `ci-remap-server` bin on Buildkite test shards (`runner.node.mjs:803` on main). Nothing in lint, format, types, rust-lints or the build uses it. Removing it outright was tried in #25425 and closed for that reason. ### Fix - Move the dependency to a new `scripts/ci-remap-server/package.json` (+ `bun.lock`) and drop it from the root `package.json` / `bun.lock` (a pure removal: 94 lockfile entries, the package and its transitive closure; the lockfile stays `lockfileVersion: 1`). - `runner.node.mjs` installs that directory right before starting the server, through the same `spawnBunInstall` as root and test/ so it uses the agent's baked install cache, and runs the bin from there. The install is best-effort like the server start already is: on failure it warns and the tests run without crash remapping instead of failing the shard. - `bootstrap.sh` warms the new directory into the image's install cache next to root and test/. No image version bump needed: the new lockfile carries over the exact resolutions the root lockfile had (checked entry by entry), so the caches baked from the old root lockfile already contain everything except `@types/bun@1.3.14` and `bun-types@1.3.14`, which resolved to the workspace packages before and now come from npm. - New source lint `test/internal/source-lints/lockfile-registry-only.test.ts`: the root and test/ lockfiles (the two that every PR's checks and every shard install) may not contain `github:`, `git+` or tarball-URL resolutions. `source-lints.yml` now also triggers on `bun.lock` / `test/bun.lock`. - Why this is the right layer: the failing jobs had no use for the package, and a retry or cache in lint.yml/format.yml would still leave GitHub on the install path of rust-lints, bun-types, the build and every dev's `bun install`, and would still fail during a multi-minute blip. After this, root installs only talk to the npm registry; the one job that needs GitHub gets it from a baked cache and degrades gracefully when it is unavailable. - Verified: - `test/internal/source-lints/lockfile-registry-only.test.ts`: fails on main's `bun.lock` (`bun-tracestrings -> bun-tracestrings@github:oven-sh/bun.report#912ca63`), passes here, under `bun bd test`, the system bun and bun 1.3.14 (the version the workflows pin). Whole `test/internal/source-lints/` passes. - Root `bun install --frozen-lockfile` (lint.yml's command) with bun 1.3.14 and the debug build: no changes. Fresh checkout with GitHub unreachable (`GITHUB_API_URL=http://127.0.0.1:1`, empty cache): main fails with `failed to download bun-tracestrings@github:... ConnectionRefused`, this branch installs. - `bun install --frozen-lockfile` in `scripts/ci-remap-server` with bun 1.3.14, canary and the debug build: no changes; `bun run --silent ci-remap-server` from that directory prints a port and serves `/traces`. - This PR's own Buildkite build (#100049): every non-Windows shard logs `scripts/ci-remap-server/package.json` / `86 packages installed` in 0.4 to 1s (the baked cache, as predicted; the root install went from 102 packages to 21), followed by `crash reports parsed on port ...`. The server came up on 17 of 40 debian+ubuntu x64 shards against 14 of 40 on main's build #100080: the remaining shards hit the runner's pre-existing 5s startup timeout (`ci-remap server did not start: timeout`), which this PR does not change and is worth a follow-up of its own. - Locally, `CI=true node scripts/runner.node.mjs ...` with a cold cache got a 429 from codeload.github.com for the tarball; the runner warned `ci-remap server not installed (...), crash reports will not be remapped`, ran the test and exited 0. Same with the directory removed (`spawn error`). - `bun lint`, prettier `--check` on the touched files, `sh -n scripts/bootstrap.sh`, `bun bd` reconfigure after the root package.json change. - Overlaps with #38981 on neighbouring lines of the same block in `runner.node.mjs` (it changes how the server is killed, this changes where it is installed and started); either rebases trivially onto the other. ### Background - `bun-tracestrings` is the npm name of github.com/oven-sh/bun.report, the service that turns the trace strings in bun's crash reports back into stack traces. Its `ci-remap-server` bin is a local copy of that: `runner.node.mjs` starts it, points every spawned bun at it via `BUN_CRASH_REPORT_URL`, and prints the remapped traces of any test that crashed. It is purely diagnostic output; tests still fail on their exit code without it. - A `github:` dependency has no registry tarball: bun downloads it from GitHub on install (and caches it under the same name@resolution key as registry packages). - `bootstrap.sh` builds the CI agent images. It clones the repo and runs `bun install` in root and test/ with `BUN_INSTALL_CACHE_DIR` set, so test shards install from disk; a package only downloads when its resolution is not in the baked cache. The `# Version:` header is only bumped when the image itself must change, which this does not require. - `test/internal/source-lints/` holds repo-invariant tests that run on a bare checkout in source-lints.yml (no `bun install`), which is why the lint lives there and why that workflow's path filter had to learn about the lockfiles.
|
#39851 replaces the remap server block that this PR patches. It starts the server through a helper in utils.mjs that hooks the |
|
Closing in favor of #39851. #39851 rewrites the remap server block of the runner. Its The only part of this PR that #39851 does not carry is the coordinator call site, which already has an |
Problem
scripts/runner.node.mjs(Linux and macOS lanes) leaves its crash-report remap server behind: abun run --silent ci-remap-server ...process plus thebun node_modules/.bin/ci-remap-server ...script it wraps, both reparented to PID 1. On persistent agents these pile up one pair per job.stdio: ["ignore", "pipe", "inherit"]), so anything capturing the runner's output through a pipe keeps waiting after the runner has exited. Locally,node scripts/runner.node.mjs ... 2>&1 | catwas still blocked 5s after the runner exited; killing the orphan released it.server.kill()was only called from aprocess.once("beforeExit", ...)handler (scripts/runner.node.mjs:809-815), and the runner never exits in a way that emitsbeforeExit.main()ends inprocess.exit()(:3431), so do--bail(:753) and the SIGINT/SIGTERM/SIGHUP handler (:3076), and an uncaught exception does not emit it either.beforeExithad fired, the handler calledserver.off("error")/server.off("exit")without a listener, which throwsERR_INVALID_ARG_TYPEbefore reachingserver.kill().process.once("exit", () => coordinator.kill())and was cleaned up on the same runs, which is what pointed at the event.Fix
killOnExit(child)toscripts/utils.mjs:process.once("exit", () => child.kill()). Use it for the remap server and for the docker coordinator (same behavior as its inline hook).exitis the right event: Node emitsexitboth fromprocess.exit()and after an uncaught exception, which together are every way the runner ends;beforeExitis emitted only when the event loop drains, which never happens here.exitlisteners must be synchronous, andkill()is.kill()'s default SIGTERM, rather than SIGKILL, is enough for the two-process tree: on Linuxbun runstays alive as the parent of the script and forwards SIGTERM to it, then exits itself; SIGKILL is not forwarded and would orphan the script. Checked against the real tree: SIGTERM to the wrapper took both processes down and released the pipe. On macOSbun run --silentexecs the script in place, so there is only one process. The comment at the call site records this so nobody "hardens" it to SIGKILL.exitingflag and theoff()calls existed only to serve the removed handler; thedid not startbranch no longer unregisters anything since a secondkill()on a dead child is a no-op.test/internal/runner-kill-on-exit.test.ts. A fixture spawns a helper that inherits the fixture's stdout (as the remap server inherits the runner's stderr), hooks it withkillOnExit, and leaves viaprocess.exit(), an uncaught exception fromawait main(), or a SIGTERM handler that callsprocess.exit(); stdout reaching EOF is the proof the helper died. A control case without the hook checks the helper survives and keeps the pipe open, so the EOF signal is not vacuous. Runs under node (what the runner uses) and under bun.bun bd test test/internal/runner-kill-on-exit.test.ts: 8 pass.scripts/stashed: 8 fail (no export). WithkillOnExittemporarily hooked onbeforeExitinstead: the 6 positive cases time out waiting for the pipe, the 2 controls pass.ci-remap-serverprocesses left behind every time; after, 0, and a reader on the runner's output got EOF 4ms after the runner exited (log below).Background
ci-remap-server(from thebun-tracestringspackage) once per shard and points every test process at it withBUN_CRASH_REPORT_URL, so crash trace strings from tests get symbolicated and attached to the failure output. It is meant to live exactly as long as the runner.beforeExitvsexitin Node:beforeExitfires when the event loop has no more work and the process would exit on its own; it is not emitted forprocess.exit()or for a fatal error.exitis emitted in both of those cases as well, and its listeners run synchronously right before the process ends.bun run <bin>on Linux spawns the bin as a child and waits for it, forwarding catchable signals it receives (SIGTERM, SIGINT, SIGHUP, ...) to that child; with--silenton macOS it execs the bin in place instead.Runner probes (this container, GITHUB_ACTIONS=true, one test file)
Before, normal exit:
Before, SIGTERM to the runner:
After, normal exit:
After, uncaught exception (GITHUB_REPOSITORY unset makes the runner throw after starting the server) and SIGTERM: