Skip to content

Commit 4947df4

Browse files
fix: the Hypatia gate could never fire — the defects that made it unconditionally vacuous (#71)
1 parent d99e467 commit 4947df4

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
@@ -162,12 +182,28 @@ jobs:
162182
if: steps.build.outputs.ready == 'true'
163183
run: |
164184
set +e
165-
HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json 2>&1
185+
HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json
166186
HYP_EXIT=$?
167187
set -e
168188
169-
if [ ! -s hypatia-findings.json ] || ! jq empty hypatia-findings.json 2>/dev/null; then
170-
echo "[]" > hypatia-findings.json
189+
# --exit-zero is Hypatia's own documented CI recipe (lib/hypatia/cli.ex),
190+
# for exactly this case: "use in CI when a downstream step gates on
191+
# severity counts". Findings go to stdout, the one-line summary to
192+
# stderr, and the process exits 0 unless the SCANNER itself failed.
193+
#
194+
# Do NOT redirect stderr into the payload with `2>&1`: that folds the
195+
# summary line into the JSON, so every parse fails, the old `[]`
196+
# fallback substituted a clean result, CRITICAL was always 0, and the
197+
# gate below could never fire on any input. Keep stderr on the log.
198+
if [ "$HYP_EXIT" -ne 0 ]; then
199+
echo "::error::Hypatia scanner execution failed with exit ${HYP_EXIT}"
200+
exit "$HYP_EXIT"
201+
fi
202+
# `jq empty` is NOT sufficient -- it succeeds on any valid JSON,
203+
# including a bare string, object or null. Assert the array.
204+
if [ ! -s hypatia-findings.json ] || ! jq -e 'type == "array"' hypatia-findings.json >/dev/null; then
205+
echo "::error::Hypatia did not produce a valid JSON findings array"
206+
exit 1
171207
fi
172208
173209
TOTAL=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0)
@@ -185,13 +221,19 @@ jobs:
185221
- name: Emit check annotations
186222
if: steps.build.outputs.ready == 'true'
187223
run: |
188-
jq -r '.[] | select(.file != null) |
224+
# Findings carry no `.message` (keys: action,file,line,reason,rule_module,
225+
# severity,type), so every annotation read "null". `.file` is an absolute
226+
# runner path, which GitHub cannot anchor to the diff, so it is made
227+
# workspace-relative here.
228+
jq -r --arg ws "$GITHUB_WORKSPACE" '.[] | select(.file != null) |
229+
(.file | ltrimstr($ws + "/")) as $f |
230+
(.reason // .message // .type // "finding") as $m |
189231
if .severity == "critical" then
190-
"::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
232+
"::error file=\($f),line=\(.line // 1)::[hypatia] \($m)"
191233
elif .severity == "high" then
192-
"::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
234+
"::error file=\($f),line=\(.line // 1)::[hypatia] \($m)"
193235
else
194-
"::warning file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
236+
"::warning file=\($f),line=\(.line // 1)::[hypatia] \($m)"
195237
end
196238
' hypatia-findings.json || true
197239

0 commit comments

Comments
 (0)