fix(cdxgen): report why cdxgen failed instead of exiting 1 in silence - #1470
Conversation
8747ded to
cbabc52
Compare
|
bugbot run |
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 5babdf9. Configure here.
When `socket cdxgen` failed on a CI runner it could exit 1 and print
nothing at all: no error, no hint, no way to tell whether cdxgen was
never downloaded, never started, or ran and died.
The command armed `process.exitCode = 1` before starting cdxgen, then
handled only the signal and numeric-exit-code cases. There was no
`else`, so a child that reported neither left the armed 1 standing and
printed nothing. Because cdxgen is spawned with `stdio: 'inherit'`, a
rejected spawn also reached the top level with an empty stderr, leaving
the shared formatter nothing to attach beyond a generic line.
Every way out of the run now either exits with cdxgen's own code or
prints where the CLI looked for cdxgen and what to try next. A new
`util/dlx/cdxgen-diagnostics.mts` builds those messages so the command
and the spawn helper share one wording. `spawnCdxgenDlx` also checks
`SOCKET_CLI_CDXGEN_LOCAL_PATH` on disk before spawning, so a wrong path
says so instead of surfacing as a bare ENOENT that never names the
variable.
An `InputError` message is passed through untouched rather than nested
inside a second Where/Saw/Fix block, which previously produced an outer
`Fix:` telling the user to set the very variable they had already set.
The underlying error is logged via `debugNs('error', ...)` so the
message's advice to re-run with `SOCKET_CLI_DEBUG=1` leads somewhere.
The successful path is unchanged: cdxgen's output still streams through
and its exit code is still forwarded exactly as before.
Refs SURF-1045.
5babdf9 to
000c379
Compare
|
[agent] Squashed the three commits into one and re-pushed as 000c379. The diff is byte-identical to what Bugbot last reviewed, so the two fixes discussed above are still in there — they are just no longer reachable under the shas the replies cite (14c786f and 5babdf9 were rewritten by the squash). On the two red checks: neither is caused by this branch. The exact same 4 checks and the same 1 test fail on main at 0bd8b9e, which is this PR's base — see the main-branch CI run https://github.com/SocketDev/socket-cli/actions/runs/30843921672. Details are in a comment on the base-branch breakage rather than here. |
|
[agent] Both red checks are pre-existing on main, not caused by this branch. Main at 0bd8b9e, which is the exact base of this PR, fails the identical set in run https://github.com/SocketDev/socket-cli/actions/runs/30843921672 — the same 4 checks, the same 129 warnings and 18 errors, and the same 1 test. Check, 4 failures, all repo-wide:
Test, 1 failure, and this one is a real bug worth its own fix: packages/cli/test/unit/commands/scan/perform-reachability-analysis-coana.test.mts reads the options bag at the wrong argument index. spawnCoanaDlx is declared as (args, options, spawnExtra) at src/util/dlx/spawn-coana.mts:53, and perform-reachability-analysis.mts:288 calls it with two arguments, so mock.calls[0][2] is undefined and reading opts.stdio throws a TypeError. Lines 315 and 327 should read mock.calls[0][1]. Run locally, both tests in that describe block fail for this reason; CI reports one because the two land in different shards. So the test has never actually asserted the stdio routing, it threw instead. Introduced by bd3f4a9, the port of #1371. None of that is fixable from inside this PR without unrelated churn, so I left it alone. |
When
socket cdxgenfails on a CI runner it can exit with code 1 and print nothing at all. There is no error, no hint, and no way to tell whether cdxgen was never downloaded, was never started, or ran and died. A build just goes red with an empty log.This change makes that impossible. Every way out of the cdxgen run now either exits deliberately with cdxgen's own exit code or prints a message saying what failed, where the CLI looked for cdxgen, and what to try next. It also checks the
SOCKET_CLI_CDXGEN_LOCAL_PATHoverride before using it, so pointing that variable at a path that is not there now says so instead of looking like the variable was ignored.The successful path is unchanged. cdxgen's own output still streams straight through, and its exit code is still passed along exactly as before.
Refs SURF-1045.
Why the failure was silent — the exit code was armed up front and one path out had no message
The command sets
process.exitCode = 1before it starts cdxgen. That is deliberate: it means an unexpected early exit cannot be mistaken for a successful scan. The problem was what came after it.There is no
else. When the child reports neither an exit code nor a signal, the function simply returns, the already-armed exit code of 1 stands, and nothing is ever printed. That is the silent exit.There is a second, quieter contributor. cdxgen is spawned with
stdio: 'inherit', which means the parent captures none of the child's output. So when the spawn rejects, the error that reaches the top-level handler has an emptystderr, and the shared spawn-error formatter has nothing to attach beyond a generic "command failed" line. The user is told something went wrong but not what or where.Two separate root causes — the silence and the failure itself are not the same bug
These are worth keeping apart, because only one of them is fixed here with confidence.
The silence is fully diagnosed and fixed. It is the missing
elsebranch described above, plus the loss of the underlying error on thestdio: 'inherit'path. Both are in this repository, both are reproduced by tests that fail without this change, and both are now covered.The underlying resolution failure is not conclusively identified. I could not reproduce a hosted CI runner with a Java and Gradle project, and I am not willing to guess at a fix and call it solved. What I can say is that the environment variable override is correctly wired: it has its own module, it is registered in the central environment table, and the resolver reads it directly. Since setting it changed nothing for the reporter, the failure is very likely happening either before cdxgen is resolved at all or after the child has already started, rather than in the choice of which cdxgen to run.
That is exactly why the silence had to be fixed first. With this change the next run prints where cdxgen was resolved from and what the underlying error was, which turns an unreproducible report into a single log line. Re-running with
SOCKET_CLI_DEBUG=1will now also surface the full error and stack.What changed — one new diagnostics module and two call sites
A new
util/dlx/cdxgen-diagnostics.mtsholds the message building, so both the command and the spawn helper can share it and so it can be tested on its own:describeCdxgenSourceformatCdxgenFailureMessageisMissingCdxgenLocalPathformatMissingCdxgenLocalPathMessageIn the command, the spawn is wrapped so a rejection is reported rather than escaping as a bare stack, and the exit handling gained the missing
elsebranch. In the spawn helper, a configured override is checked before the child is started.A message looks like this:
A note on the sibling tool that was asked about — it does not share this code
I was asked to check whether a related generator command has the same swallowed-error shape. It does not exist in this repository. There is no such command, no such module, and no shared resolution or error-handling path with cdxgen here. The only references are in build and release tooling, none of which is reachable from the CLI. So there is nothing to fix alongside this change, and nothing to report as at risk.
While tracing the cdxgen path I did find three other places where an error is caught and thrown away entirely: the conversion step that builds a temporary lockfile, the cleanup that removes it afterwards, and the background download that warms the tool cache. None of them is the cause of this report, and each would change behaviour in its own way, so I left them alone rather than widening this change. They are worth a separate look.
Verification — the new tests fail without the fix, which is how I know they test something
The load-bearing test is that a cdxgen failure produces a non-empty, actionable message rather than a bare exit 1. To prove that test is real, I reverted the source to the current default branch while keeping the new tests, and confirmed they went red:
The diagnostics tests were mutation-checked the same way. Breaking the message builder so it returns an empty string turned six named tests red, including
is never empty, even with no underlying error. Inverting the on-disk check turnedreports a path that is not on disk as missinganddoes not report an existing file as missingred. Both were restored afterwards.One test caught a real flaw in my own first attempt.
stays quiet on the success pathfailed becauseprocess.exitdoes not actually stop execution when it is stubbed in a test, so the code fell through into the failure branch. Rather than paper over it in the test, I restructured the exit handling into a single if / else if / else so there is exactly one outcome per run. That is better code, and the test found it.Ran:
pnpm --filter @socketsecurity/cli run test:unit test/unit/commands/manifest/ test/unit/util/dlx/— exit 0. 1054 passed, 1 skipped, 0 failed.pnpm run lint— exit 0, "Lint passed" across all six changed files.pnpm --filter @socketsecurity/cli run type— exit 0.pnpm run build:cli— exit 0.Did not run:
pnpm run check --allsuite as a gate. I recorded a baseline on a clean default branch first, where it exits 1 with 7 failing checks unrelated to this change, and compared against that rather than treating it as a signal.Note
Low Risk
CLI-only failure diagnostics and spawn pre-checks; the successful cdxgen path and exit forwarding are unchanged.
Overview
socket cdxgenno longer exits with code 1 and an empty log when the child never starts, the spawn promise rejects, or the process ends without an exit code or signal.The command wraps
runCdxgenin try/catch, logs underlying detail viadebugNs('error', …)(soSOCKET_CLI_DEBUG=1is useful), and prints a Where / Saw / Fix message from newcdxgen-diagnosticshelpers. A missingelsebranch now handles the no-code, no-signal case instead of returning silently withprocess.exitCodealready set to 1.spawnCdxgenDlxvalidatesSOCKET_CLI_CDXGEN_LOCAL_PATHon disk before spawn and raisesInputErrorwith a dedicated message;formatCdxgenFailureMessagepasses that through without nesting duplicate Fix blocks.Successful runs are unchanged: cdxgen still uses
stdio: 'inherit'and its exit code is still forwarded on success.Reviewed by Cursor Bugbot for commit 5babdf9. Configure here.