fix(cli-command): exit non-zero on any signal-aborted run (PER-8678)#2262
fix(cli-command): exit non-zero on any signal-aborted run (PER-8678)#2262pranavz28 wants to merge 2 commits into
Conversation
A run aborted during startup by a non-SIGINT/SIGTERM signal (SIGHUP/SIGUSR1/SIGUSR2) exited 0, producing a false-positive CI success when the wrapped command never launched. beginShutdown only records SIGINT/SIGTERM into shutdownState, so the exit-code gate keyed on shutdownState.signal never fired for the other registered signals; their AbortError carries exitCode:0, so the err.exitCode!==0 gate was also skipped and the process exited 0. Key the gate off err.signal (the signal that actually aborted the run) and map SIGHUP/SIGUSR1/SIGUSR2 to their POSIX 128+signum codes, with a non-zero fallback for any other signal. SIGINT(130)/SIGTERM(143), PERCY_EXIT_WITH_ZERO_ON_ERROR, and exitOnError:false clean-unwind behavior are all preserved. Adds a SIGHUP regression spec. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
e3b6a20 to
19c0284
Compare
The `?? 1` fallback in the signal exit-code gate is unreachable in practice — every signal in the registered handler set is mapped in SIGNAL_EXIT_CODES — but kept as a non-zero safety net. Mark it ignored to satisfy the package's 100% branch-coverage threshold. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — PER-8678Reviewed the diff ( SummaryFixes a false-positive CI success: Review Table
Notes (no blockers)A — Linux-specific signal numbers (cosmetic). B — Broadened gate now intercepts any VerdictApprove. Correct root-cause analysis, minimal well-targeted fix, comment-documented rationale, and a real regression test. The two notes are cosmetic/informational. 🤖 Generated with Claude Code |
Fixes PER-8678 / closes #2255
Problem
percy exec -- <cmd>could exit 0 during startup before launching the wrapped command, producing a false-positive CI success. Reported symptom: GitHub Actions reports a passing E2E run while Cypress never starts — last log isDownloading Chromium 1300309..., then minutes of[percy:monitoring]metrics, then exit 0.Root cause
packages/cli-command/src/command.jsThe per-signal handler is registered for
SIGUSR1, SIGUSR2, SIGTERM, SIGINT, SIGHUPand aborts withnew AbortError(signal, { signal, exitCode: 0 }). ButbeginShutdownonly records SIGINT/SIGTERM intoshutdownState.signal. The exit-code gate in the runner's catch was keyed onshutdownState.signal:So a SIGHUP/SIGUSR1/SIGUSR2 abort during startup:
shutdownState.signalis null), andif (err.exitCode !== 0)gate (the AbortError carriesexitCode: 0),…so the runner returns normally and the process exits 0 — even though
percy.yield.start()was interrupted and the wrapped command was never spawned. In CI a stalled startup is typically torn down by SIGHUP; the sibling[webpack-dev-server] Gracefully shutting downline in the report corroborates an OS signal hitting the process group. PR #2199 fixed SIGINT/SIGTERM but left every other signal on the exit-0 path.Fix
SIGNAL_EXIT_CODESwithSIGHUP→129,SIGUSR1→138,SIGUSR2→140(POSIX 128+signum).err.signal(the signal that actually aborted the run) instead ofshutdownState.signal, with a non-zero?? 1fallback for any unmapped signal.Any signal-aborted run now surfaces a non-zero exit code in production, so a startup interrupted before the wrapped command launches can never report CI success. Preserved: SIGINT→130 / SIGTERM→143,
PERCY_EXIT_WITH_ZERO_ON_ERROR=trueoverride, andexitOnError:falseclean-unwind for programmatic/test usage.Testing
packages/cli-command/test/shutdown.test.js: emitsSIGHUPmid-run, assertsprocess.exitCode === 129(wasundefined/0).yarn workspace @percy/cli-command test→ 65/65 specs pass.🤖 Generated with Claude Code