From d3786c75c78c797c533ff78b79591b45fed21e96 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 3 Sep 2026 04:42:45 +0100 Subject: [PATCH] fix: the Hypatia gate could never fire -- 2>&1 made it unconditionally vacuous Four independent defects each made the Hypatia gate unconditionally vacuous: 1. `scan . > hypatia-findings.json 2>&1` folded the stderr summary into the JSON payload, so `jq empty` failed and the guard wrote `[]`. Every count read 0 and `Fail on critical findings` could not fire on any input. 2. The availability probe tested `[ -d "$HOME/hypatia/scanner" ]`, which is unsatisfiable -- hypatia has no `scanner/` directory. The scan was skipped and a stub `[]` was written: a second, independent route to permanent green. 3. The clone used `${REPO_OWNER}`, which 404s outside `hyperpolymath`. A failed clone was indistinguishable from "unavailable". 4. Annotations emitted `\(.message)`, a key findings do not have, so every one read `[hypatia] null` -- on an absolute runner path GitHub cannot anchor. Threshold is unchanged: critical-only. --- .github/workflows/static-analysis-gate.yml | 40 +++++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/.github/workflows/static-analysis-gate.yml b/.github/workflows/static-analysis-gate.yml index 38adc6e..7e0bb3a 100644 --- a/.github/workflows/static-analysis-gate.yml +++ b/.github/workflows/static-analysis-gate.yml @@ -34,7 +34,7 @@ jobs: id: install run: | # Try to fetch the latest release binary from the org - PA_URL="https://github.com/${REPO_OWNER}/panic-attack/releases/latest/download/panic-attack-linux-x86_64" + PA_URL="https://github.com/hyperpolymath/panic-attack/releases/latest/download/panic-attack-linux-x86_64" mkdir -p "$HOME/.local/bin" if curl -fsSL --head "$PA_URL" >/dev/null 2>&1; then curl -fsSL -o "$HOME/.local/bin/panic-attack" "$PA_URL" @@ -151,7 +151,7 @@ jobs: id: build continue-on-error: true run: | - git clone "https://github.com/${REPO_OWNER}/hypatia.git" "$HOME/hypatia" 2>/dev/null || true + git clone "https://github.com/hyperpolymath/hypatia.git" "$HOME/hypatia" 2>/dev/null || true if [ -f "$HOME/hypatia/mix.exs" ]; then cd "$HOME/hypatia" if [ ! -f hypatia ] && [ ! -f hypatia-v2 ]; then @@ -169,12 +169,28 @@ jobs: if: steps.build.outputs.ready == 'true' run: | set +e - HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . > hypatia-findings.json 2>&1 + HYPATIA_FORMAT=json "$HOME/hypatia/hypatia-cli.sh" scan . --exit-zero > hypatia-findings.json HYP_EXIT=$? set -e - if [ ! -s hypatia-findings.json ] || ! jq empty hypatia-findings.json 2>/dev/null; then - echo "[]" > hypatia-findings.json + # --exit-zero is Hypatia's own documented CI recipe (lib/hypatia/cli.ex), + # for exactly this case: "use in CI when a downstream step gates on + # severity counts". Findings go to stdout, the one-line summary to + # stderr, and the process exits 0 unless the SCANNER itself failed. + # + # Do NOT redirect stderr into the payload with `2>&1`: that folds the + # summary line into the JSON, so every parse fails, the old `[]` + # fallback substituted a clean result, CRITICAL was always 0, and the + # gate below could never fire on any input. Keep stderr on the log. + if [ "$HYP_EXIT" -ne 0 ]; then + echo "::error::Hypatia scanner execution failed with exit ${HYP_EXIT}" + exit "$HYP_EXIT" + fi + # `jq empty` is NOT sufficient -- it succeeds on any valid JSON, + # including a bare string, object or null. Assert the array. + if [ ! -s hypatia-findings.json ] || ! jq -e 'type == "array"' hypatia-findings.json >/dev/null; then + echo "::error::Hypatia did not produce a valid JSON findings array" + exit 1 fi TOTAL=$(jq '. | length' hypatia-findings.json 2>/dev/null || echo 0) @@ -192,13 +208,19 @@ jobs: - name: Emit check annotations if: steps.build.outputs.ready == 'true' run: | - jq -r '.[] | select(.file != null) | + # Findings carry no `.message` (keys: action,file,line,reason,rule_module, + # severity,type), so every annotation read "null". `.file` is an absolute + # runner path, which GitHub cannot anchor to the diff, so it is made + # workspace-relative here. + jq -r --arg ws "$GITHUB_WORKSPACE" '.[] | select(.file != null) | + (.file | ltrimstr($ws + "/")) as $f | + (.reason // .message // .type // "finding") as $m | if .severity == "critical" then - "::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)" + "::error file=\($f),line=\(.line // 1)::[hypatia] \($m)" elif .severity == "high" then - "::error file=\(.file),line=\(.line // 1)::[hypatia] \(.message)" + "::error file=\($f),line=\(.line // 1)::[hypatia] \($m)" else - "::warning file=\(.file),line=\(.line // 1)::[hypatia] \(.message)" + "::warning file=\($f),line=\(.line // 1)::[hypatia] \($m)" end ' hypatia-findings.json || true