Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 199 additions & 0 deletions .github/workflows/hypatia-scan-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ on:
callers pass ONLY this secret instead of `secrets: inherit`,
which forwarded every caller secret (standards#1108 §6b).
required: false
outputs:
verdict:
description: >-
Overall state of the lane's verdict grid (docs/VERDICT-GRID-SPEC.adoc):
purple, red, orange, yellow, blue, green or grey. Callers that comment
on or gate from the grid read this and the verdict-grid artifact.
value: ${{ jobs.scan.outputs.verdict }}

permissions:
actions: read
Expand All @@ -44,12 +51,18 @@ jobs:
# at 5, so a slow build or a hung scan records "not_run" or "error" instead
# of timing out, and so failing, the whole job.
timeout-minutes: 45
outputs:
verdict: ${{ steps.grid.outputs.state }}
env:
# panic-attack v2.5.5, built from source at this commit (cargo --rev
# checks out exactly this object; --locked holds every dependency to the
# Cargo.lock checksums). There is no release binary to pin instead.
# The binary reports "2.1.0" for --version; that string is stale upstream.
PANIC_ATTACK_SHA: 2dc1393c0ac982dbad98ba07f78a580d319456a5
# pons asinorum, built from source at this commit the same way. Its
# `scan --format json` envelope carries each finding's tier and
# evidence class, so a heuristic is never read as a proof.
PONS_SHA: 32e55677d8fd1f3010848dee22e822c2f385c2a0
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down Expand Up @@ -372,6 +385,103 @@ jobs:
} >> "$GITHUB_STEP_SUMMARY"
fi

# pons asinorum is ADVISORY on the same terms as panic-attack: a status
# envelope (ran | not_run | error) that is never a bare pass, built from
# source at PONS_SHA and cached by that commit.
- name: Restore pons build cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.pons
key: pons-v1-${{ runner.os }}-${{ runner.arch }}-${{ env.PONS_SHA }}

- name: Install pons asinorum (pinned source build)
id: pons-install
run: |
set -euo pipefail
root="$HOME/.pons"
bin="$root/bin/pons"
reason=""
if [ -x "$bin" ] && grep -qF "#$PONS_SHA" "$root/.crates.toml" 2>/dev/null; then
echo "pons restored from cache at $PONS_SHA"
elif ! command -v cargo >/dev/null 2>&1; then
reason="cargo is not available on this runner"
elif ! timeout 600 cargo install --git https://github.com/hyperpolymath/pons-asinorum \
--rev "$PONS_SHA" --locked --force --root "$root" pons-cli; then
reason="source build at $PONS_SHA failed or exceeded 10 minutes"
Comment on lines +408 to +410

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Budget the added pons runtime

On a cold or slow runner, the new pons install and scan can consume 15 minutes after panic-attack already consumes up to 15 minutes, while the job still has its pre-existing 45-minute limit intended for 30 + 10 + 5 minutes. If the earlier Hypatia work takes near its allotted 30 minutes, the job is terminated during pons before the always() grid and artifact steps can record an indeterminate result; increase the job timeout or reduce the component timeouts.

Useful? React with 👍 / 👎.

elif [ ! -x "$bin" ]; then
reason="no binary after the source build at $PONS_SHA"
fi
if [ -n "$reason" ]; then
rm -rf "$root"
echo "::warning title=pons not installed::$reason"
echo "reason=$reason" >> "$GITHUB_OUTPUT"
else
echo "bin=$bin" >> "$GITHUB_OUTPUT"
fi

- name: Run pons asinorum scan
id: pons
env:
PONS_BIN: ${{ steps.pons-install.outputs.bin }}
PONS_INSTALL_REASON: ${{ steps.pons-install.outputs.reason }}
run: |
set -euo pipefail
# pons-findings.json is the same status envelope as panic-attack's;
# `report` is pons's own JSON envelope when status is "ran".
out=pons-findings.json
marker() {
jq -n --arg status "$1" --arg reason "$2" --argjson exit_code "$3" \
--arg ref "$PONS_SHA" '{status: $status, tool: "pons-asinorum",
ref: $ref, reason: $reason, exit_code: $exit_code, counts: null,
report: null}' > "$out"
}
if [ -z "${PONS_BIN:-}" ] || [ ! -x "$PONS_BIN" ]; then
marker not_run "${PONS_INSTALL_REASON:-pons is not installed}" null
else
report=$(mktemp)
log=$(mktemp)
rc=0
timeout 300 "$PONS_BIN" scan --format json . > "$report" 2> "$log" || rc=$?
if [ "$rc" -eq 124 ]; then
marker error "pons scan did not finish within 5 minutes" "$rc"
elif [ "$rc" -ne 0 ]; then
marker error "pons scan exited $rc" "$rc"
elif ! jq -e -s 'length == 1 and (.[0] | type == "object" and
(.findings | type) == "array" and all(.findings[]; type == "object" and
((.severity | ascii_upcase) as $s | ["INFO", "WARN", "ERROR"] | index($s) != null)))' \
"$report" > /dev/null 2>&1; then
marker error "pons scan exited 0 without a valid report" 0
else
jq --arg ref "$PONS_SHA" '
[.findings[].severity | ascii_upcase] as $s
| {status: "ran", tool: "pons-asinorum", ref: $ref, reason: null, exit_code: 0,
counts: {error: ($s | map(select(. == "ERROR")) | length),
warn: ($s | map(select(. == "WARN")) | length),
info: ($s | map(select(. == "INFO")) | length)},
report: .}' "$report" > "$out"
fi
if [ "$(jq -r .status "$out")" = error ]; then
echo "::group::pons output"
cat "$log"
echo "::endgroup::"
fi
fi
status=$(jq -r .status "$out")
echo "status=$status" >> "$GITHUB_OUTPUT"
if [ "$status" = ran ]; then
{
echo "## pons asinorum scan (advisory, does not gate)"
echo ""
echo "| Severity | Count |"
echo "|----------|-------|"
jq -r '"| ERROR | \(.counts.error) |", "| WARN | \(.counts.warn) |", "| INFO | \(.counts.info) |"' "$out"
echo ""
echo "Built from hyperpolymath/pons-asinorum@$PONS_SHA. Each finding carries its tier and evidence class; a heuristic is labelled as one."
} >> "$GITHUB_STEP_SUMMARY"
else
echo "::warning title=pons $status::$(jq -r .reason "$out"). pons-findings.json records status $status, which is not a pass."
fi

- name: Check out standards for the SARIF baseline filter
id: fetch_filter
# ⚠ continue-on-error is on the CHECKOUT, never on the check. If the
Expand Down Expand Up @@ -448,6 +558,95 @@ jobs:
path: |
hypatia-findings.json
panic-attack-findings.json
pons-findings.json
retention-days: 90

# ── VERDICT GRID (docs/VERDICT-GRID-SPEC.adoc) ──────────────────────
# One table for the lane: Hypatia, panic-attack, pons and every
# gitbot-fleet report-back under .gitbot-fleet/, graded by the worst
# finding present, with missing or unreadable evidence orange and
# explicit exclusions grey. It lands in the job summary, in the
# verdict-grid artifact (report.json, report.md, badge.json) and in the
# `verdict` output. The PR comment is the CALLER's job: this reusable
# declares only the permissions every existing caller already grants,
# and a called workflow may not ask for more than its caller.
- name: Check out the verdict grid renderer at this workflow's commit
if: ${{ always() }}
# continue-on-error on the CHECKOUT only: the render step below
# reports a missing renderer as orange, never as a pass.
continue-on-error: true
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: hyperpolymath/standards
# `job.workflow_sha`: the commit of THIS reusable file, i.e. what the
# caller pinned. See ci-pipeline.yml for why neither
# github.workflow_sha nor github.job_workflow_sha is that.
ref: ${{ job.workflow_sha }}
path: .standards-grid
persist-credentials: false
sparse-checkout: |
scripts/verdict-grid.sh
scripts/lib/verdict-grid.jq
sparse-checkout-cone-mode: false

- name: Read the previous verdict grid
if: ${{ always() && github.event_name == 'pull_request' }}
# Best effort: the token may lack pull-request read on the caller. A
# missing previous grid only means "no comparable baseline".
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh api --paginate --slurp "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \
| jq -r '[.[][] | select(.user.login == "github-actions[bot]" and (.body | contains("<!-- verdict-grid:")))]
| last | .body // ""' \
> "$RUNNER_TEMP/grid-previous.md" || : > "$RUNNER_TEMP/grid-previous.md"
Comment thread
hyperpolymath marked this conversation as resolved.
Comment on lines +601 to +604

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Grant read access before fetching private PR comments

For a private caller repository, this authenticated issue-comments request requires either issues: read or pull-requests: read (GitHub REST documentation), but neither the reusable workflow's permission block nor the documented caller grants either permission. Because permissions passed through reusable workflows can only be maintained or reduced (GitHub reusable-workflow documentation), the request always fails for private callers and the fallback empties grid-previous.md, making every revision show no comparable baseline. Add the read permission to the reusable contract and callers, or perform the lookup in the caller's posting job.

Useful? React with 👍 / 👎.


- name: Render the verdict grid
id: grid
if: ${{ always() }}
env:
HYPATIA_REF: ${{ steps.hypatia-rev.outputs.sha }}
PR_NUMBER: ${{ github.event.pull_request.number || 'local' }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report the commit that was actually scanned

On a pull_request run, the initial Checkout repository step does not specify ref, so the scanners operate on GitHub's pull-request merge commit; actions/checkout documents that ${{ github.event.pull_request.head.sha }} must be supplied explicitly to scan the head instead (checkout documentation). This assignment nevertheless labels the grid with the head SHA. Findings introduced by the merge are therefore attributed to the wrong commit, and when the base changes without a new head commit the newly scanned merge tree retains the same reported SHA, causing delta to reject the comparison as a same-commit rerun. Either scan the head explicitly or pass the checked-out commit SHA to the renderer.

Useful? React with 👍 / 👎.

run: |
set -uo pipefail
renderer=.standards-grid/scripts/verdict-grid.sh
mkdir -p verdict-grid
if [ ! -f "$renderer" ]; then
# No renderer is missing evidence, rendered as such, never a pass.
jq -n '{schemaVersion: 1, label: "Hypatia grid", message: "indeterminate", color: "orange"}' > verdict-grid/badge.json
printf '%s\n%s\n' '<!-- verdict-grid:hypatia regenerated on every run -->' \
'🟠 indeterminate · the verdict grid renderer could not be checked out from hyperpolymath/standards; see the job log.' \
> verdict-grid/report.md
Comment on lines +619 to +622

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit report.json in the missing-renderer fallback

If the renderer checkout fails, this fallback writes only badge.json and report.md, yet the artifact upload still succeeds because the directory is nonempty. Callers following the documented contract to gate from verdict-grid/report.json therefore receive a successful but incomplete artifact precisely on this error path; write a structured orange report.json alongside the other fallback files.

Useful? React with 👍 / 👎.

cat verdict-grid/report.md >> "$GITHUB_STEP_SUMMARY"
echo "state=orange" >> "$GITHUB_OUTPUT"
exit 0
fi
findings=hypatia-findings.json
[ -f hypatia-findings.relativized.json ] && findings=hypatia-findings.relativized.json
enrolment=""
[ -f .gitbot-fleet/enrolment.json ] && enrolment=.gitbot-fleet/enrolment.json
# The renderer appends the grid to the step summary and writes
# state= to GITHUB_OUTPUT itself; its exit code is 0 for any
# rendered verdict and 2 only for a usage error.
bash "$renderer" render --out verdict-grid \
--hypatia "$findings" --panic panic-attack-findings.json --pons pons-findings.json \
--report-backs .gitbot-fleet/report-backs --enrolment "$enrolment" \
--previous "$RUNNER_TEMP/grid-previous.md" \
--sha "$HEAD_SHA" --run "$GITHUB_RUN_ID" \
--url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
--pr "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" \
--hypatia-ref "$HYPATIA_REF" --panic-ref "$PANIC_ATTACK_SHA" --pons-ref "$PONS_SHA" > /dev/null

- name: Upload the verdict grid
if: ${{ always() }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: verdict-grid
path: verdict-grid
if-no-files-found: error
retention-days: 90

- name: Gate on baseline (blocking when a baseline is committed)
Expand Down
6 changes: 6 additions & 0 deletions 1-formats/k9/tools/CONFORMANCE-REPORTING.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
produces a structured result plus one compact Markdown report. This is local
reporting policy, not a new normative contract. It adds no K9 conformance rules.

The report's shape (badge, identity line, grid, accordion, scope footer,
embedded baseline) is the *verdict grid* described in
link:../../../docs/VERDICT-GRID-SPEC.adoc[docs/VERDICT-GRID-SPEC.adoc]; this
lane is its conformance-family reference. The Hypatia lane renders the same
shape with the findings family.

== What each category establishes

[cols="1,3,3",options="header"]
Expand Down
Loading
Loading