@@ -45,14 +45,28 @@ jobs:
4545 if : steps.install.outputs.installed == 'true'
4646 run : |
4747 set +e
48- panic-attack assail --format json . > panic-attack-findings.json 2>&1
48+ panic-attack assail --format json . > panic-attack-findings.json
4949 PA_EXIT=$?
5050 set -e
5151
52+ # Same defect class as the Hypatia job below: `2>&1` folded the
53+ # scanner's stderr into the JSON payload, so every jq parse failed,
54+ # every count silently became 0 via `|| echo 0`, and "Fail on critical
55+ # findings" could never fire on any input. Keep stderr on the log.
5256 if [ ! -s panic-attack-findings.json ]; then
5357 echo "[]" > panic-attack-findings.json
5458 fi
5559
60+ # Deliberately a WARNING, not a failure. panic-attack is a downloaded
61+ # release binary whose exit-code and output contract are not verified
62+ # here, and it has no confirmed --exit-zero equivalent, so we surface a
63+ # malformed payload in the log rather than block on an unverified tool.
64+ # Promote to `exit 1` (as the Hypatia job does) once that contract is
65+ # confirmed -- see the follow-up issue linked from this PR.
66+ if ! jq -e 'type == "array"' panic-attack-findings.json >/dev/null 2>&1; then
67+ echo "::warning::panic-attack output is not a JSON array (exit ${PA_EXIT}); counts below are unreliable"
68+ fi
69+
5670 # Parse finding counts
5771 TOTAL=$(jq '. | length' panic-attack-findings.json 2>/dev/null || echo 0)
5872 CRITICAL=$(jq '[.[] | select(.severity == "critical")] | length' panic-attack-findings.json 2>/dev/null || echo 0)
@@ -71,13 +85,19 @@ jobs:
7185 if : steps.install.outputs.installed == 'true'
7286 run : |
7387 # Convert JSON findings into GitHub Actions annotations
74- jq -r '.[] | select(.file != null) |
88+ # Findings carry no `.message` (keys: action,file,line,reason,rule_module,
89+ # severity,type), so every annotation read "null". `.file` is an absolute
90+ # runner path, which GitHub cannot anchor to the diff, so it is made
91+ # workspace-relative here.
92+ jq -r --arg ws "$GITHUB_WORKSPACE" '.[] | select(.file != null) |
93+ (.file | ltrimstr($ws + "/")) as $f |
94+ (.reason // .message // .type // "finding") as $m |
7595 if .severity == "critical" then
76- "::error file=\(.file ),line=\(.line // 1)::[panic-attack] \(.message )"
96+ "::error file=\($f ),line=\(.line // 1)::[panic-attack] \($m )"
7797 elif .severity == "high" then
78- "::error file=\(.file ),line=\(.line // 1)::[panic-attack] \(.message )"
98+ "::error file=\($f ),line=\(.line // 1)::[panic-attack] \($m )"
7999 else
80- "::warning file=\(.file ),line=\(.line // 1)::[panic-attack] \(.message )"
100+ "::warning file=\($f ),line=\(.line // 1)::[panic-attack] \($m )"
81101 end
82102 ' panic-attack-findings.json || true
83103
@@ -160,12 +180,28 @@ jobs:
160180 if : steps.build.outputs.ready == 'true'
161181 run : |
162182 set +e
163- HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json 2>&1
183+ HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json
164184 HYP_EXIT=$?
165185 set -e
166186
167- if [ ! -s hypatia-findings.json ] || ! jq empty hypatia-findings.json 2>/dev/null; then
168- echo "[]" > hypatia-findings.json
187+ # --exit-zero is Hypatia's own documented CI recipe (lib/hypatia/cli.ex),
188+ # for exactly this case: "use in CI when a downstream step gates on
189+ # severity counts". Findings go to stdout, the one-line summary to
190+ # stderr, and the process exits 0 unless the SCANNER itself failed.
191+ #
192+ # Do NOT redirect stderr into the payload with `2>&1`: that folds the
193+ # summary line into the JSON, so every parse fails, the old `[]`
194+ # fallback substituted a clean result, CRITICAL was always 0, and the
195+ # gate below could never fire on any input. Keep stderr on the log.
196+ if [ "$HYP_EXIT" -ne 0 ]; then
197+ echo "::error::Hypatia scanner execution failed with exit ${HYP_EXIT}"
198+ exit "$HYP_EXIT"
199+ fi
200+ # `jq empty` is NOT sufficient -- it succeeds on any valid JSON,
201+ # including a bare string, object or null. Assert the array.
202+ if [ ! -s hypatia-findings.json ] || ! jq -e 'type == "array"' hypatia-findings.json >/dev/null; then
203+ echo "::error::Hypatia did not produce a valid JSON findings array"
204+ exit 1
169205 fi
170206
171207 TOTAL=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0)
@@ -183,13 +219,19 @@ jobs:
183219 - name : Emit check annotations
184220 if : steps.build.outputs.ready == 'true'
185221 run : |
186- jq -r '.[] | select(.file != null) |
222+ # Findings carry no `.message` (keys: action,file,line,reason,rule_module,
223+ # severity,type), so every annotation read "null". `.file` is an absolute
224+ # runner path, which GitHub cannot anchor to the diff, so it is made
225+ # workspace-relative here.
226+ jq -r --arg ws "$GITHUB_WORKSPACE" '.[] | select(.file != null) |
227+ (.file | ltrimstr($ws + "/")) as $f |
228+ (.reason // .message // .type // "finding") as $m |
187229 if .severity == "critical" then
188- "::error file=\(.file ),line=\(.line // 1)::[hypatia] \(.message )"
230+ "::error file=\($f ),line=\(.line // 1)::[hypatia] \($m )"
189231 elif .severity == "high" then
190- "::error file=\(.file ),line=\(.line // 1)::[hypatia] \(.message )"
232+ "::error file=\($f ),line=\(.line // 1)::[hypatia] \($m )"
191233 else
192- "::warning file=\(.file ),line=\(.line // 1)::[hypatia] \(.message )"
234+ "::warning file=\($f ),line=\(.line // 1)::[hypatia] \($m )"
193235 end
194236 ' hypatia-findings.json || true
195237
0 commit comments