@@ -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