fix: exit with the code a command set on process.exitCode instead of a hard 0 - #77
Merged
Conversation
…a hard 0 `integration connect` and `cloud connect` flag a browser-wait timeout with `process.exitCode = ExitCode.GENERAL` and return, but main.ts followed every completed command with `process.exit(0)`, discarding it — the CLI printed "Timed out waiting" and exited 0, and the telemetry event reported SUCCESS. The installer recorded a connect.github.accepted for a connect that never happened (nominal#1511 N9). main.ts now exits with the settled code and reports it in the telemetry event. Tests: unit on settledExitCode, plus a subprocess run of main.ts with process.exitCode preset (the shape the connect commands leave behind). Closes #75 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
LGTM: correctly propagates a command's process.exitCode through main.ts, reproduced the bug on pre-fix main.ts and confirmed the fix; no regressions found; full local suite and CI green.
Verification performed
- Checked out
pr-77(headd8951e1565d1c5f8c599d3eee5fab8bd02fd4027), confirmed viagit rev-parse HEAD— exact match to this PR'shead.sha. - True merge-base (
git merge-base pr-77 origin/main) =bd40cf75def6de3df2a70e1fdee79dfb13e83c53, which equals the PR's statedbase.sha(no staleness).git diff --statagainst that merge-base matches the API's reportedadditions/deletions/changed_filesexactly (64/2/4;src/exit-code.ts,src/main.ts,test/exit-code.test.ts,test/helpers/preset-exit-code.ts).
Code review
- Re-read
src/main.tsin full and grepped the wholesrc/tree for everyprocess.exit(andprocess.exitCode =call site. The only two places that ever setprocess.exitCodeareintegration/connect.ts:873andcloud/connect.ts:844(both pre-existing, untouched by this diff) — no double-set / race risk anywhere. - Important detail the checklist for this fix should call out explicitly:
process.exit()was not removed here, only its argument changed (hardcoded0→settledExitCode()).main.tsstill forces process exit unconditionally aftercommand.execute(), so the "event loop won't drain / process hangs" risk that a naive fix (just deleting theprocess.exitcall) would introduce does not apply to this implementation. - Telemetry
dispatch()is stillawaited beforeprocess.exit(exitCode)and has its own bounded abort timeout (telemetry/dispatch.ts), so no hang risk there either. - Manually reproduced the underlying bug end-to-end: with
src/main.tsreverted to the merge-base and a preload settingprocess.exitCode = 1beforeconfig showruns, the process exits0(the bug). With the PR'smain.ts, the same setup exits1. This matches the PR body's claimed repro exactly. - Test coverage genuinely exercises the process boundary, not just the isolated helper:
test/exit-code.test.ts's subprocess cases spawn a realnode --import tsx src/main.ts config showwithprocess.exitCodepreset viatest/helpers/preset-exit-code.ts, asserting the actual exit code — I ran this manually too and it behaves as claimed. - The two pre-existing
process.exit(0)calls incommands/helpers.ts:307andcommands/integration/slack-channels.ts:170are SIGINT handlers for "stop waiting for the browser" — they short-circuit the whole process beforecommand.execute()would return, so they don't interact with the newsettledExitCode()logic. - Non-blocking, out of scope for this diff:
cloud/connect.ts's AWS CloudFormation background-wait path (awsWait.settle()around lines 843-850) can return'pending'without ever settingprocess.exitCode, e.g. if the user picks "Done" while the stack is still deploying — same class of "soft failure exits 0" issue this PR fixes for the timeout case, just not covered by it. Might be worth a follow-up issue, but it's pre-existing code untouched by this PR.
Local checks (Node 22, matching one of the 3 CI matrix legs)
npm cicleannpm run codegen→npm run typecheck: passnpm run lint: pass (no output)npm run test: 368/368 pass, 0 fail, 0 skipnpm run build: pass,dist/polylane.mjs(522.1 KB) written — validates the PR body's build claim too
CI (fresh query against head SHA d8951e1565d1c5f8c599d3eee5fab8bd02fd4027)
check-runs: 3 total —checks / Test & typecheckon 20.x, 22.x, 24.x — allcompleted/success- Combined status API returned
total_count: 0(this repo doesn't use the legacy statuses API; check-runs is authoritative and fully green)
Author note: PR authored by justinhelmer (human), not a bot — no self-approval pattern applies here.
Generated by Claude Code
There was a problem hiding this comment.
Auto-approved: Claude reviewed this PR and posted an LGTM verdict (see its review). A repo admin enabled this via the auto-approve workflow.
Generated by Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When
integration connectorcloud connecttimed out waiting for the browser, the CLI said so but still exited 0 and reported success in telemetry. This makes the exit code and the telemetry event carry the code the command actually set.What & why
Fixes cli#75, found on the first fresh-user install check for nominal#1511:
polylane integration connect --type githubprintedTimed out waiting — the connection has not shown up yet.and exited 0, so the installer recordedconnect.github.acceptedfor a connect that never happened.Both connect commands signal the timeout with
process.exitCode = ExitCode.GENERALandreturn(src/commands/integration/connect.ts,src/commands/cloud/connect.ts).src/main.tsthen ranprocess.exit(0)unconditionally aftercommand.execute(...), overriding it, and built the telemetry event with a hard-codedExitCode.SUCCESS. Every soft failure that relies onprocess.exitCodebecame a success at the process boundary; only thrownCLIErrors ever exited non-zero.Changes
src/exit-code.ts:settledExitCode()—process.exitCodewhen a command set a number, else 0.src/main.ts: aftercommand.execute, read the settled code once, report it in the telemetrybuildEvent(instead ofExitCode.SUCCESS), andprocess.exit(exitCode).test/exit-code.test.ts: unit tests forsettledExitCode, and a subprocess test that runssrc/main.ts config showwithprocess.exitCodepreset by a preload (test/helpers/preset-exit-code.ts) — the exact state the connect commands leave — asserting the process exits with that code. Fails onmain(expected 1, actual 0), passes here.Decisions
process.exitCoderather than throwing on timeout. Both connect commands already express "finished, but not successfully" throughprocess.exitCode; throwing aCLIErrorwould route a non-error throughhandleErrorand print it as an error. The bug was the boundary, not the signal.error: null. A timeout is not an exception; the event keeps the success shape but with the truthfulexitCode, so funnel queries can separate "connect finished" from "connect timed out" without a new category.$?.run()for importability:main.tsexecutes on import, and a realnode --import tsx src/main.tsrun is the only thing that proves the process boundary.Validation
settledExitCode()is 0 with nothing flagged, and the flagged number otherwisetest/exit-code.test.tsunit cases ✅process.exitCode = 1exits the process with 1mainfails withexpected 1, actual 0(run withsrc/main.tsstashed)buildEvent({ exitCode })wired from the same value (src/main.ts); covered by the existingbuildEventtests for the field shapenpm run test368/368 ✅ ·npm run typecheck✅ ·npm run lint✅ ·npm run build✅polylane integration connect --type github, let the browser wait time out →echo $?prints 1🤖 Generated with Claude Code