Skip to content

Scan intermittently exits 0 but writes no valid JSON report (stdout truncation race, not diff-content-dependent) #1242

Description

@felipewilliam2

Summary

The GitHub Action (millionco/react-doctor@ced746f518f11e8283d488c4ff31c44e478bb0e5, v2.2.6, CLI 0.7.7) has been intermittently failing "Scan could not complete" on essentially every PR in our repo since 2026-07-12, regardless of diff content (docs-only, dependency-bump, and application-code diffs all affected). The action's own ensure-json-report.mjs fallback reports:

"error":{"name":"ReactDoctorActionError","message":"react-doctor exited with status 0 before producing a JSON report.","chain":[]}

Note the CLI's own recorded exit status is 0 (success) — the wrapper isn't seeing a crash, it's seeing a process that exited cleanly without ever writing a parseable JSON report to stdout.

What we ruled out (with direct evidence, not just hypothesis)

  • Shallow checkout / unreachable base commit: added fetch-depth: 0. This did eliminate the action's own "could not derive the PR's changed files from git ... base commit isn't reachable" warning on subsequent runs, but the scan continued to fail identically afterward. Not the cause of the report-file failure.
  • Corrupted/poisoned actions/cache toolchain install: deleted the react-doctor-toolchain-0.7.7-node24-Linux-X64 cache entry (repo-scoped GH Actions cache) to force a fresh npm install --prefix. Scan still failed identically on the next run.
  • Corrupted persistent scan-result cache (REACT_DOCTOR_CACHE_DIR, restored via broad cross-version restore-keys): deleted all react-doctor-scan-cache-* cache entries. Scan still failed identically.
  • Install method / npx vs npm install --prefix: reproduced the exact npm install --prefix $DIR --no-save --no-audit --no-fund react-doctor@0.7.7 step manually, matching flags (--json --json-compact --blocking none --project '*' --scope changed --changed-files-from <file>) and every env var the action forwards (REACT_DOCTOR_BASE_SHA, REACT_DOCTOR_CACHE_DIR, GITHUB_RUN_ID, REACT_DOCTOR_GITHUB_ACTION, REACT_DOCTOR_ACTION_*, NO_COLOR) — always succeeded (ok:true, valid JSON) both in a separate debug job and, most tellingly, in the same job/VM, immediately after the real scan step failed, re-running the exact same installed binary from ${RUNNER_TEMP}/react-doctor-toolchain/node_modules/.bin/react-doctor with the exact same flags against the exact same changed-files-from file — succeeded instantly.

What this points to

Since the identical binary, identical flags, identical files, identical environment, same VM, moments apart — fails once (empty/invalid report, exit 0) then succeeds immediately after — this looks like a timing-dependent stdout write race on process exit, not a deterministic bug tied to install state, cache state, or diff content. A likely mechanism: the CLI writes its JSON report to stdout and then calls (or reaches) process.exit() before an async stdout write has fully flushed to the redirected file — Node's documented behavior is that process.exit() does not wait for pending I/O, including writes to process.stdout/process.stderr, when the destination is non-blocking (this can apply even to regular-file redirects depending on how the write is issued, e.g. via a stream rather than a synchronous fs.writeSync).

Repro data

  • Action pin: millionco/react-doctor@ced746f518f11e8283d488c4ff31c44e478bb0e5 (v2.2.6)
  • CLI version: react-doctor@0.7.7
  • Runner: ubuntu-24.04, Node 24.18.0
  • Flags: --json --json-compact --blocking none --project '*' --scope changed --changed-files-from <file>
  • Example failing run: https://github.com/felipewilliam2/AV-SITE/actions/runs/29285300247/job/86936280449 (see the "DEBUG - inspect report file and re-run CLI in-situ" step's log, later removed from our workflow — happy to share the raw log text if useful)

Suggested fix direction

If the CLI writes its final report via an async stream write (e.g. process.stdout.write(json) followed by process.exit(0)), switching to a synchronous write (fs.writeSync(1, json)) or awaiting the stream's drain/finish event before calling process.exit() would eliminate the race. Happy to help narrow this down further if you can point to where the CLI writes its final report and calls exit.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions