fix: stop build_escript() writing mix output into the caller's stdout - #747
Conversation
`build_escript()` corrupts the scan payload whenever it runs.
Callers use the documented CI recipe:
HYPATIA_FORMAT=json hypatia-cli.sh scan . --exit-zero > hypatia-findings.json
Two lines then write build chatter to stdout, i.e. into that JSON file:
1. `mix deps.get --quiet 2>/dev/null || true` silences stderr but leaves
stdout pointed at the payload.
2. `mix escript.build 2>&1 >&2` has its redirections BACKWARDS. They apply
left to right: `2>&1` points fd2 at wherever fd1 currently is -- the
caller's findings.json -- and `>&2` then points fd1 at that same file.
Both streams land in the payload. The intended idiom is `>&2 2>&1`.
The fix redirects the whole subshell once with `) >&2`, which is correct
regardless of ordering and cannot regress the same way.
Why it went unnoticed: interactively fd1 is the tty, so `2>&1 >&2` is a
no-op. It only fires when a caller redirects stdout -- exactly what a CI
JSON contract does.
Observed impact (2026-09-03): hyperpolymath/session-sentinel#67 reported
"Hypatia did not produce a valid JSON findings array" while the scan log
read `scan complete: 37 findings >= medium (critical=7, high=2, medium=28);
exit 0`. jq failed with "Invalid numeric literal at line 1, column 10" --
the first bytes of `Resolving Hex dependencies...`.
Positive control, caller redirection applied OUTSIDE the function:
old findings.json = Resolving Hex dependencies...|...|[{...}] jq: FAIL
new findings.json = [{"severity":"critical",...}] jq: PASS
This is latent in every consumer, not specific to one repo: ~85 estate
workflows clone this repo unpinned and invoke the wrapper the same way.
They are shielded only while a prebuilt escript is present, so any cache
miss or stale-rebuild reintroduces it.
`bash -n` clean; `shellcheck -S style` clean.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (1)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📜 Recent review details⏰ Context from checks skipped due to timeout. (17)
🔇 Additional comments (1)
📝 SummarySummary by CodeRabbit
WalkthroughChangesBuild output handling
Estimated code review effort: 1 (Trivial) | ~5 minutes Merge Risk: ⚪ Minimal · up to Build messages are now sent to stderr, leaving scan findings on stdout valid for JSON redirection. No current merge-readiness risk remains. Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Note Docstrings generation - SUCCESS |
Docstrings generation was requested by @hyperpolymath. * #747 (comment) The following files were modified: * `hypatia-cli.sh`
Docstrings generation was requested by @hyperpolymath. * #747 (comment) The following files were modified: * `hypatia-cli.sh` <details> <summary>ℹ️ Note</summary><blockquote> CodeRabbit cannot perform edits on its own pull requests yet. </blockquote></details> --------- Co-authored-by: Jonathan D.A. Jewell <6759885+hyperpolymath@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
PR #749 (`coderabbitai[bot]`, "Add docstrings") rewrote `hypatia-cli.sh` through a GitHub write API that carries **path and contents but no file mode**, so the blob landed at `100644`. The demotion is invisible in the diff — it lives in the tree object, not the patch text. ## Timeline | commit | time | mode | |---|---|---| | `8f0d1aec` (#747, the stdout fix) | 10:59:56Z | `100755` ✅ | | `12473951` (#749, docstrings) | 11:01:01Z | `100644` ❌ | **65 seconds.** ## Effect on consumers Every consumer that invokes the wrapper directly now dies before it can do anything: ``` /home/runner/hypatia/hypatia-cli.sh: Permission denied ##[error]Hypatia scanner execution failed with exit 126 ``` `Run Hypatia scan = failure` → `Emit check annotations`, `Upload hypatia findings` and the verdict step are all **skipped**, and the downstream `Deposit findings for gitbot-fleet` job then fails on `Artifact not found for name: hypatia-findings`. Observed live on `hyperpolymath/scaffoldia` run [`33749291439`](https://github.com/hyperpolymath/scaffoldia/actions/runs/33749291439) (started 11:22:03Z) and on four more repos. The split is **temporal, not invocation-shape**: every consumer run after 11:01:01Z fails this way. The two repos that still scanned cleanly — `hybrid-automation-router` run `33714565319` (04:18:31Z) and `session-sentinel` run `33714770073` (04:21:59Z) — are *pre-demotion witnesses*, and would fail with 126 if re-run today. ## Scope, measured not assumed Comparing the **full recursive trees** of `8f0d1aec` and `12473951` — 935 blobs each — yields exactly **one** mode change, this file. Every other `.sh` in the repo root is `100755`; this was the only `100644` among them, and the file carries `#!/usr/bin/env bash`. ## What this diff does Restores the mode. **Nothing else** — the blob SHA is `87e4470b` before and after, so the docstrings from #749 are kept byte-for-byte. A mode change cannot be expressed through GraphQL `createCommitOnBranch`, which is the API shape that caused this, so this is a real signed git commit (`%G?` = `G`). Consumers clone `main` unpinned, so merging cures the fleet on the next run with no per-repo change.
build_escript()corrupts the scan payload whenever it runs.Callers use the documented CI recipe:
Two lines then write build chatter to stdout, i.e. into that JSON file:
mix deps.get --quiet 2>/dev/null || truesilences stderr but leavesstdout pointed at the payload.
mix escript.build 2>&1 >&2has its redirections BACKWARDS. They applyleft to right:
2>&1points fd2 at wherever fd1 currently is -- thecaller's findings.json -- and
>&2then points fd1 at that same file.Both streams land in the payload. The intended idiom is
>&2 2>&1.The fix redirects the whole subshell once with
) >&2, which is correctregardless of ordering and cannot regress the same way.
Why it went unnoticed: interactively fd1 is the tty, so
2>&1 >&2is ano-op. It only fires when a caller redirects stdout -- exactly what a CI
JSON contract does.
Observed impact (2026-09-03): hyperpolymath/session-sentinel#67 reported
"Hypatia did not produce a valid JSON findings array" while the scan log
read
scan complete: 37 findings >= medium (critical=7, high=2, medium=28); exit 0. jq failed with "Invalid numeric literal at line 1, column 10" --the first bytes of
Resolving Hex dependencies....Positive control, caller redirection applied OUTSIDE the function:
This is latent in every consumer, not specific to one repo: ~85 estate
workflows clone this repo unpinned and invoke the wrapper the same way.
They are shielded only while a prebuilt escript is present, so any cache
miss or stale-rebuild reintroduces it.
bash -nclean;shellcheck -S styleclean.