Skip to content

Commit 81b77a7

Browse files
fix: the Hypatia gate could never fire — the defects that made it unconditionally vacuous (#75)
1 parent 0aa0d31 commit 81b77a7

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
@@ -48,14 +48,28 @@ jobs:
4848
if: steps.install.outputs.installed == 'true'
4949
run: |
5050
set +e
51-
panic-attack assail --format json . > panic-attack-findings.json 2>&1
51+
panic-attack assail --format json . > panic-attack-findings.json
5252
PA_EXIT=$?
5353
set -e
5454
55+
# Same defect class as the Hypatia job below: `2>&1` folded the
56+
# scanner's stderr into the JSON payload, so every jq parse failed,
57+
# every count silently became 0 via `|| echo 0`, and "Fail on critical
58+
# findings" could never fire on any input. Keep stderr on the log.
5559
if [ ! -s panic-attack-findings.json ]; then
5660
echo "[]" > panic-attack-findings.json
5761
fi
5862
63+
# Deliberately a WARNING, not a failure. panic-attack is a downloaded
64+
# release binary whose exit-code and output contract are not verified
65+
# here, and it has no confirmed --exit-zero equivalent, so we surface a
66+
# malformed payload in the log rather than block on an unverified tool.
67+
# Promote to `exit 1` (as the Hypatia job does) once that contract is
68+
# confirmed -- see the follow-up issue linked from this PR.
69+
if ! jq -e 'type == "array"' panic-attack-findings.json >/dev/null 2>&1; then
70+
echo "::warning::panic-attack output is not a JSON array (exit ${PA_EXIT}); counts below are unreliable"
71+
fi
72+
5973
# Parse finding counts
6074
TOTAL=$(jq '. | length' panic-attack-findings.json 2>/dev/null || echo 0)
6175
CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' panic-attack-findings.json 2>/dev/null || echo 0)
@@ -74,13 +88,19 @@ jobs:
7488
if: steps.install.outputs.installed == 'true'
7589
run: |
7690
# Convert JSON findings into GitHub Actions annotations
77-
jq -r '.[] | select(.file != null) |
91+
# Findings carry no `.message` (keys: action,file,line,reason,rule_module,
92+
# severity,type), so every annotation read "null". `.file` is an absolute
93+
# runner path, which GitHub cannot anchor to the diff, so it is made
94+
# workspace-relative here.
95+
jq -r --arg ws "$GITHUB_WORKSPACE" '.[] | select(.file != null) |
96+
(.file | ltrimstr($ws + "/")) as $f |
97+
(.reason // .message // .type // "finding") as $m |
7898
if .severity == "critical" then
79-
"::error file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)"
99+
"::error file=\($f),line=\(.line // 1)::[panic-attack] \($m)"
80100
elif .severity == "high" then
81-
"::error file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)"
101+
"::error file=\($f),line=\(.line // 1)::[panic-attack] \($m)"
82102
else
83-
"::warning file=\(.file),line=\(.line // 1)::[panic-attack] \(.message)"
103+
"::warning file=\($f),line=\(.line // 1)::[panic-attack] \($m)"
84104
end
85105
' panic-attack-findings.json || true
86106
@@ -163,12 +183,28 @@ jobs:
163183
if: steps.build.outputs.ready == 'true'
164184
run: |
165185
set +e
166-
HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json 2>&1
186+
HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json
167187
HYP_EXIT=$?
168188
set -e
169189
170-
if [ ! -s hypatia-findings.json ] || ! jq empty hypatia-findings.json 2>/dev/null; then
171-
echo "[]" > hypatia-findings.json
190+
# --exit-zero is Hypatia's own documented CI recipe (lib/hypatia/cli.ex),
191+
# for exactly this case: "use in CI when a downstream step gates on
192+
# severity counts". Findings go to stdout, the one-line summary to
193+
# stderr, and the process exits 0 unless the SCANNER itself failed.
194+
#
195+
# Do NOT redirect stderr into the payload with `2>&1`: that folds the
196+
# summary line into the JSON, so every parse fails, the old `[]`
197+
# fallback substituted a clean result, CRITICAL was always 0, and the
198+
# gate below could never fire on any input. Keep stderr on the log.
199+
if [ "$HYP_EXIT" -ne 0 ]; then
200+
echo "::error::Hypatia scanner execution failed with exit ${HYP_EXIT}"
201+
exit "$HYP_EXIT"
202+
fi
203+
# `jq empty` is NOT sufficient -- it succeeds on any valid JSON,
204+
# including a bare string, object or null. Assert the array.
205+
if [ ! -s hypatia-findings.json ] || ! jq -e 'type == "array"' hypatia-findings.json >/dev/null; then
206+
echo "::error::Hypatia did not produce a valid JSON findings array"
207+
exit 1
172208
fi
173209
174210
TOTAL=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0)
@@ -186,13 +222,19 @@ jobs:
186222
- name: Emit check annotations
187223
if: steps.build.outputs.ready == 'true'
188224
run: |
189-
jq -r '.[] | select(.file != null) |
225+
# Findings carry no `.message` (keys: action,file,line,reason,rule_module,
226+
# severity,type), so every annotation read "null". `.file` is an absolute
227+
# runner path, which GitHub cannot anchor to the diff, so it is made
228+
# workspace-relative here.
229+
jq -r --arg ws "$GITHUB_WORKSPACE" '.[] | select(.file != null) |
230+
(.file | ltrimstr($ws + "/")) as $f |
231+
(.reason // .message // .type // "finding") as $m |
190232
if .severity == "critical" then
191-
"::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
233+
"::error file=\($f),line=\(.line // 1)::[hypatia] \($m)"
192234
elif .severity == "high" then
193-
"::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
235+
"::error file=\($f),line=\(.line // 1)::[hypatia] \($m)"
194236
else
195-
"::warning file=\(.file),line=\(.line // 1)::[hypatia] \(.message)"
237+
"::warning file=\($f),line=\(.line // 1)::[hypatia] \($m)"
196238
end
197239
' hypatia-findings.json || true
198240

0 commit comments

Comments
 (0)