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