Skip to content

fix: stop build_escript() writing mix output into the caller's stdout - #747

Merged
hyperpolymath merged 1 commit into
mainfrom
fix/build-escript-stdout-pollution
Sep 3, 2026
Merged

fix: stop build_escript() writing mix output into the caller's stdout#747
hyperpolymath merged 1 commit into
mainfrom
fix/build-escript-stdout-pollution

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

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.

`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.
@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Team

Run ID: 3764010e-0a17-43ca-9400-c54da28d3040

📥 Commits

Reviewing files that changed from the base of the PR and between 5ec5e9f and b74e052.

📒 Files selected for processing (1)
  • hypatia-cli.sh

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)
  • GitHub Check: Cargo test
  • GitHub Check: Rust Tests
  • GitHub Check: Build AsciiDoc
  • GitHub Check: CodeQL Analysis (actions)
  • GitHub Check: Generate SBOM
  • GitHub Check: Build Test Images
  • GitHub Check: Rust Dependency Audit
  • GitHub Check: stress-test
  • GitHub Check: Rust Dependency Audit
  • GitHub Check: Generate Rust SBOM
  • GitHub Check: E2E — Rust CLI Scan
  • GitHub Check: Container Security (Trivy) (deploy/Containerfile)
  • GitHub Check: Build Rust - x86_64-unknown-linux-gnu
  • GitHub Check: Build Rust - aarch64-apple-darwin
  • GitHub Check: Build Rust - x86_64-apple-darwin
  • GitHub Check: Build Rust - x86_64-pc-windows-msvc
  • GitHub Check: Build AsciiDoc
🔇 Additional comments (1)
hypatia-cli.sh (1)

40-58: LGTM!


📝 Summary

Summary by CodeRabbit

  • Bug Fixes
    • Prevented build messages from being mixed into scan results written to standard output.
    • Improved reliability of redirected scan output, avoiding malformed JSON and related parsing failures in CI environments.

Walkthrough

Changes

Build output handling

Layer / File(s) Summary
Redirect build output away from scan results
hypatia-cli.sh
The build subshell redirects all output to stderr. Comments document why the previous 2>&1 >&2 ordering could corrupt redirected scan output.

Estimated code review effort: 1 (Trivial) | ~5 minutes

Merge Risk: ⚪ Minimal · up to b74e0

Build messages are now sent to stderr, leaving scan findings on stdout valid for JSON redirection. No current merge-readiness risk remains.

Poem

A rabbit checks the streams at night
Build chatter hops to stderr’s side
JSON stays clean in its file
The scan completes with a quiet smile
Redirections now run right

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: preventing build_escript() from writing Mix output to the caller's stdout.
Description check ✅ Passed The description directly explains the stdout contamination, its impact on JSON scan output, and the proposed subshell redirection fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • ✅ Generated successfully - (🔄 Check to regenerate)
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #749

coderabbitai Bot added a commit that referenced this pull request Sep 3, 2026
Docstrings generation was requested by @hyperpolymath.

* #747 (comment)

The following files were modified:

* `hypatia-cli.sh`
@hyperpolymath
hyperpolymath merged commit 8f0d1ae into main Sep 3, 2026
83 of 111 checks passed
@hyperpolymath
hyperpolymath deleted the fix/build-escript-stdout-pollution branch September 3, 2026 11:00
hyperpolymath added a commit that referenced this pull request Sep 3, 2026
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>
hyperpolymath added a commit that referenced this pull request Sep 3, 2026
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.
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.

1 participant