Skip to content

fix(cli): prevent silent zero-exit when the version-check fetch stalls#2767

Merged
ymc9 merged 1 commit into
devfrom
fix/cli-version-check-silent-exit
Jul 23, 2026
Merged

fix(cli): prevent silent zero-exit when the version-check fetch stalls#2767
ymc9 merged 1 commit into
devfrom
fix/cli-version-check-silent-exit

Conversation

@ymc9

@ymc9 ymc9 commented Jul 23, 2026

Copy link
Copy Markdown
Member

What

Fixes the intermittent CI failures where a random packages/cli test fails with "output file missing after a successful CLI run" (recently hit #2744's CI repeatedly, and reproduces on main).

Root cause

Every zen command awaits checkNewVersion() in a commander preAction hook, which fetches registry.npmjs.org with AbortSignal.timeout(2000). If the connection stalls in a way that strands the fetch promise with no active handle, nothing keeps the event loop alive — AbortSignal.timeout's internal timer is unref'd — so node exits with code 0 mid-command, before the action runs. The caller sees success; nothing was done.

On CI this struck ~1 in 100–300 invocations. With ~360 CLI invocations per Build-and-Test run, most runs failed — each time in whatever random test the stall struck (observed across generate, db push, db pull, migrate, on both sqlite and mysql matrix jobs, and even once in a sample-app build step).

Evidence

Diagnosed on throwaway draft PR #2766 by instrumenting the CLI. Failing invocations show exactly:

[zen-diag] main start pid=... argv=db push
[zen-diag] checkNewVersion start
[zen-diag] beforeExit fired! code=0 — event loop drained mid-execution

(beforeExit fires only on natural event-loop drain, never on process.exit — proving the stranded await.)

Fix

Race the fetch against a ref'd, always-settling timer (cleared in finally). The timer keeps the event loop alive and guarantees the await settles into the existing catch. A noop .catch on the losing fetch promise avoids unhandled rejections in long-lived modes (watch/proxy/studio).

Also hardens runCli in the CLI test harness to print the child's stdout/stderr on failure, so future failures of this kind are self-diagnosing instead of bare expected false to be true assertions.

Verification

Verified on #2766 (same fix, plus instrumentation): 3 consecutive green Build-and-Test runs with the version check still enabled (~1000+ exercised invocations, zero drain-exits), versus 5-of-6 runs failing before the fix.

Possible follow-up (not in this PR): pass --no-version-check from the CLI test harness so CI doesn't depend on npm-registry health at all — needs the option added to every command first, since it's currently per-command.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Version checks now time out promptly when the update service is slow or unavailable, allowing the CLI to continue without hanging.
  • Tests

    • Improved CLI test diagnostics by including the command, working directory, exit details, and captured output when execution fails.

Every zen command awaits checkNewVersion() in a preAction hook, which
fetches the npm registry with AbortSignal.timeout(2000). If the
connection stalls in a way that strands the fetch promise with no
active handle, nothing keeps the event loop alive (AbortSignal.timeout's
timer is unref'd), so node exits with code 0 before the command's
action runs — the CLI reports success having done nothing.

On CI runners this struck roughly 1 in 100-300 invocations, which made
most Build-and-Test runs fail in a random packages/cli test with
baffling "output file missing after successful CLI run" assertions
(observed on PRs and on main alike; root-caused and fix verified over
several instrumented runs on the throwaway diagnostic PR #2766).

Fix: race the fetch against a ref'd, always-settling timer that is
cleared afterward. The timer keeps the event loop alive and guarantees
the await settles into the existing catch. A noop catch on the losing
fetch promise avoids unhandled rejections in long-lived modes (watch,
proxy, studio).

Also: runCli in the CLI test harness now captures and prints the
child's stdout/stderr when a command fails, so future failures of this
kind are self-diagnosing instead of bare assertion errors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

CLI reliability and diagnostics

Layer / File(s) Summary
Bounded version checking
packages/cli/src/utils/version-utils.ts
checkNewVersion() now races version retrieval against a timeout, suppresses late promise rejections, and clears the timer after settlement.
CLI execution failure diagnostics
packages/cli/test/utils.ts
runCli now uses UTF-8 output and logs command, process, and captured stream details before rethrowing failures.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: preventing the CLI from exiting successfully when the version check stalls.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/cli-version-check-silent-exit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
packages/cli/test/utils.ts (1)

120-121: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Avoid shell interpolation when launching the CLI.

execSync parses command as shell syntax, so metacharacters can execute unintended commands and paths containing spaces can break invocation. Prefer execFileSync(process.execPath, [cli, ...args]) and change runCli to accept an argument array; at minimum, verify every caller supplies trusted, literal input.

Proposed direction
-export function runCli(command: string, cwd: string) {
+export function runCli(args: readonly string[], cwd: string) {
     const cli = path.join(__dirname, '../dist/index.mjs');
     try {
-        return execSync(`node ${cli} ${command}`, { cwd, encoding: 'utf8' });
+        return execFileSync(process.execPath, [cli, ...args], {
+            cwd,
+            encoding: 'utf8',
+        });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/test/utils.ts` around lines 120 - 121, Update runCli to accept
an argument array and replace execSync shell invocation with
execFileSync(process.execPath, [cli, ...args], preserving cwd and UTF-8 output
options. Update every runCli caller to pass separate trusted literal arguments
rather than a command string, including commands containing spaces or
metacharacters.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/cli/test/utils.ts`:
- Around line 120-121: Update runCli to accept an argument array and replace
execSync shell invocation with execFileSync(process.execPath, [cli, ...args],
preserving cwd and UTF-8 output options. Update every runCli caller to pass
separate trusted literal arguments rather than a command string, including
commands containing spaces or metacharacters.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ac500235-9aa2-4c4f-a2ba-734e29ccaa4d

📥 Commits

Reviewing files that changed from the base of the PR and between 83920a2 and a3fdc98.

📒 Files selected for processing (2)
  • packages/cli/src/utils/version-utils.ts
  • packages/cli/test/utils.ts

@ymc9
ymc9 merged commit 5421933 into dev Jul 23, 2026
9 checks passed
@ymc9
ymc9 deleted the fix/cli-version-check-silent-exit branch July 23, 2026 15:15
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.

1 participant