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