Skip to content

Commit 312708e

Browse files
fix: the Hypatia gate could never fire -- 2>&1 made it unconditionally vacuous
Four independent defects each made the Hypatia gate unconditionally vacuous: 1. `scan . > hypatia-findings.json 2>&1` folded the stderr summary into the JSON payload, so `jq empty` failed and the guard wrote `[]`. Every count read 0 and `Fail on critical findings` could not fire on any input. 2. The availability probe tested `[ -d "$HOME/hypatia/scanner" ]`, which is unsatisfiable -- hypatia has no `scanner/` directory. The scan was skipped and a stub `[]` was written: a second, independent route to permanent green. 3. The clone used `${REPO_OWNER}`, which 404s outside `hyperpolymath`. A failed clone was indistinguishable from "unavailable". 4. Annotations emitted `\(.message)`, a key findings do not have, so every one read `[hypatia] null` -- on an absolute runner path GitHub cannot anchor. Threshold is unchanged: critical-only.
1 parent c17b83e commit 312708e

1 file changed

Lines changed: 54 additions & 12 deletions

File tree

‎.github/workflows/static-analysis-gate.yml‎

Lines changed: 54 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,28 @@ jobs:
4747
if: steps.install.outputs.installed == 'true'
4848
run: |
4949
set +e
50-
panic-attack assail --format json . > panic-attack-findings.json 2>&1
50+
panic-attack assail --format json . > panic-attack-findings.json
5151
PA_EXIT=$?
5252
set -e
5353
54+
# Same defect class as the Hypatia job below: `2>&1` folded the
55+
# scanner's stderr into the JSON payload, so every jq parse failed,
56+
# every count silently became 0 via `|| echo 0`, and "Fail on critical
57+
# findings" could never fire on any input. Keep stderr on the log.
5458
if [ ! -s panic-attack-findings.json ]; then
5559
echo "[]" > panic-attack-findings.json
5660
fi
5761
62+
# Deliberately a WARNING, not a failure. panic-attack is a downloaded
63+
# release binary whose exit-code and output contract are not verified
64+
# here, and it has no confirmed --exit-zero equivalent, so we surface a
65+
# malformed payload in the log rather than block on an unverified tool.
66+
# Promote to `exit 1` (as the Hypatia job does) once that contract is
67+
# confirmed -- see the follow-up issue linked from this PR.
68+
if ! jq -e 'type == "array"' panic-attack-findings.json >/dev/null 2>&1; then
69+
echo "::warning::panic-attack output is not a JSON array (exit ${PA_EXIT}); counts below are unreliable"
70+
fi
71+
5872
# Parse finding counts
5973
TOTAL=$(jq '. | length' panic-attack-findings.json 2>/dev/null || echo 0)
6074
CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' panic-attack-findings.json 2>/dev/null || echo 0)
@@ -73,13 +87,19 @@ jobs:
7387
if: steps.install.outputs.installed == 'true'
7488
run: |
7589
# Convert JSON findings into GitHub Actions annotations
76-
jq -r '.[] | select(.file != null) |
90+
# Findings carry no `.message` (keys: action,file,line,reason,rule_module,
91+
# severity,type), so every annotation read "null". `.file` is an absolute
92+
# runner path, which GitHub cannot anchor to the diff, so it is made
93+
# workspace-relative here.
94+
jq -r --arg ws "$GITHUB_WORKSPACE" '.[] | select(.file != null) |
95+
(.file | ltrimstr($ws + "/")) as $f |
96+
(.reason // .message // .type // "finding") as $m |
7797
if .severity == "critical" then
78-
"::error file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)"
98+
"::error file=\($f),line=\(.line // 1)::[panic-attack] \($m)"
7999
elif .severity == "high" then
80-
"::error file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)"
100+
"::error file=\($f),line=\(.line // 1)::[panic-attack] \($m)"
81101
else
82-
"::warning file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)"
102+
"::warning file=\($f),line=\(.line // 1)::[panic-attack] \($m)"
83103
end
84104
' panic-attack-findings.json || true
85105
@@ -164,12 +184,28 @@ jobs:
164184
if: steps.build.outputs.ready == 'true'
165185
run: |
166186
set +e
167-
HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json 2>&1
187+
HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json
168188
HYP_EXIT=$?
169189
set -e
170190
171-
if [ ! -s hypatia-findings.json ] || ! jq empty hypatia-findings.json 2>/dev/null; then
172-
echo "[]" > hypatia-findings.json
191+
# --exit-zero is Hypatia's own documented CI recipe (lib/hypatia/cli.ex),
192+
# for exactly this case: "use in CI when a downstream step gates on
193+
# severity counts". Findings go to stdout, the one-line summary to
194+
# stderr, and the process exits 0 unless the SCANNER itself failed.
195+
#
196+
# Do NOT redirect stderr into the payload with `2>&1`: that folds the
197+
# summary line into the JSON, so every parse fails, the old `[]`
198+
# fallback substituted a clean result, CRITICAL was always 0, and the
199+
# gate below could never fire on any input. Keep stderr on the log.
200+
if [ "$HYP_EXIT" -ne 0 ]; then
201+
echo "::error::Hypatia scanner execution failed with exit ${HYP_EXIT}"
202+
exit "$HYP_EXIT"
203+
fi
204+
# `jq empty` is NOT sufficient -- it succeeds on any valid JSON,
205+
# including a bare string, object or null. Assert the array.
206+
if [ ! -s hypatia-findings.json ] || ! jq -e 'type == "array"' hypatia-findings.json >/dev/null; then
207+
echo "::error::Hypatia did not produce a valid JSON findings array"
208+
exit 1
173209
fi
174210
175211
TOTAL=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0)
@@ -187,13 +223,19 @@ jobs:
187223
- name: Emit check annotations
188224
if: steps.build.outputs.ready == 'true'
189225
run: |
190-
jq -r '.[] | select(.file != null) |
226+
# Findings carry no `.message` (keys: action,file,line,reason,rule_module,
227+
# severity,type), so every annotation read "null". `.file` is an absolute
228+
# runner path, which GitHub cannot anchor to the diff, so it is made
229+
# workspace-relative here.
230+
jq -r --arg ws "$GITHUB_WORKSPACE" '.[] | select(.file != null) |
231+
(.file | ltrimstr($ws + "/")) as $f |
232+
(.reason // .message // .type // "finding") as $m |
191233
if .severity == "critical" then
192-
"::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
234+
"::error file=\($f),line=\(.line // 1)::[hypatia] \($m)"
193235
elif .severity == "high" then
194-
"::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
236+
"::error file=\($f),line=\(.line // 1)::[hypatia] \($m)"
195237
else
196-
"::warning file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
238+
"::warning file=\($f),line=\(.line // 1)::[hypatia] \($m)"
197239
end
198240
' hypatia-findings.json || true
199241

0 commit comments

Comments
 (0)