Skip to content

fix(workstreams): handle EPIPE when gh exits before reading stdin - #7

Merged
terraboops merged 1 commit into
mainfrom
fix/gh-stdin-epipe
Sep 28, 2026
Merged

terraboops merged 1 commit into
mainfrom
fix/gh-stdin-epipe

Conversation

@terraboops

Copy link
Copy Markdown
Contributor

main has been red since 2026-09-26 while every test passes. The quality_checks (workstreams) job reports 61 files and 990 tests green, then fails anyway:

Vitest caught 3 unhandled errors during the test run.
Error: write EPIPE
 ❯ host.ts:78:20   child.stdin?.end(stdin ?? "");

Root cause

ghRunner spawns gh and immediately ends its stdin. When gh exits before reading it — an unauthenticated or immediately-failing invocation does exactly that, which is why this reproduces on a runner but not on a developer machine where gh is logged in — the write lands on a closed pipe.

EPIPE is raised asynchronously on the stream. With no "error" listener attached it surfaces as a process-level uncaught exception rather than an error on this call, and vitest counts that as an unhandled error and exits non-zero even though no assertion failed.

The fix

A no-op error listener on child.stdin. The write error carries nothing worth acting on — the execFile callback above already resolves {ok:false} with the child's real stderr, so genuine failures are still reported unchanged.

Verified

  • Reproduced deterministically outside vitest: execFile a binary that never reads stdin, then end() a 1MB payload → uncaught EPIPE, non-zero exit
  • With the listener attached the same script exits 0, and the execFile callback still fires — so ghRunner still resolves correctly
  • host.ts:78 is the only stdin write in the repo, and there were no existing stdin error handlers anywhere
  • tsc --noEmit clean; vitest run → 61 files, 990 tests, all passing

Found during a maintenance sweep — a persistently red main masks every future real failure in this repo, which is why it was worth chasing despite no test actually failing.

🤖 Generated with Claude Code

https://claude.ai/code/session_018DDkZGNoSVspVq1Z9jcVtP

main has been red since 2026-09-26 with every test passing. The
`quality_checks (workstreams)` job reports 61 files and 990 tests all
green, then fails anyway:

  Vitest caught 3 unhandled errors during the test run.
  Error: write EPIPE
   ❯ host.ts:78:20  child.stdin?.end(stdin ?? "");

ghRunner spawns `gh` and immediately ends its stdin. When `gh` exits
before reading — an unauthenticated or immediately-failing invocation
does exactly that, which is why this reproduces on a runner and not on
a developer machine where `gh` is logged in — the write lands on a
closed pipe. EPIPE is then raised asynchronously on the stream, and
with no "error" listener attached it surfaces as a process-level
uncaught exception instead of an error on this call. Vitest counts
that as an unhandled error and exits non-zero even though no
assertion failed.

Attaching a no-op error listener is the fix. The write error carries
no information worth acting on: the execFile callback above already
resolves {ok:false} with the child's real stderr, so the genuine
failure is still reported.

Verified:
  - reproduced deterministically outside vitest: execFile of a binary
    that never reads stdin, then end() of a 1MB payload, raises an
    uncaught EPIPE and exits non-zero
  - with the listener attached the same script exits 0, and the
    execFile callback still fires, so ghRunner still resolves
  - host.ts:78 is the only stdin write in the repo, and there were no
    existing stdin error handlers anywhere
  - tsc --noEmit clean; vitest run: 61 files, 990 tests, all passing

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DDkZGNoSVspVq1Z9jcVtP
@terraboops
terraboops merged commit b2f7097 into main Sep 28, 2026
6 checks passed
@terraboops
terraboops deleted the fix/gh-stdin-epipe branch September 28, 2026 05:20
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.

2 participants