diff --git a/.gitbot-fleet/report-backs/README.md b/.gitbot-fleet/report-backs/README.md index 32b48064..4647d911 100644 --- a/.gitbot-fleet/report-backs/README.md +++ b/.gitbot-fleet/report-backs/README.md @@ -7,10 +7,10 @@ should match the key in `.gitbot-fleet/enrolment.json` (`rhodibot`, `seambot`, . The Hypatia verdict grid reads this directory (`.github/workflows/hypatia-scan-reusable.yml` passes it to -`scripts/verdict-grid.sh --report-backs`), so a report-back written here is the -only way a member's row stops being orange. The envelope is the one in +`scripts/verdict-grid.sh --report-backs`), so a report-back here is the only +way a member's row stops being orange. The envelope is the one in `docs/VERDICT-GRID-SPEC.adoc` of -[hyperpolymath/standards](https://github.com/hyperpolymath/standards): +[metadatastician/standards](https://github.com/metadatastician/standards): ```json { "reporter": "rhodibot", @@ -29,12 +29,17 @@ required unless the member ran, and `not_applicable` renders grey. `severity` uses the fleet's own words (`error`, `warning`, `info`, `suggestion`) or the scanner words (`critical`, `high`, `medium`, `low`); both normalise. -The directory exists because this repository's deed declares it -(`MetaManifold-WebUI_chora.deed`, `:report-backs ".gitbot-fleet/report-backs/"`) -and a declared path that does not exist is a dangling reference. It is empty -on purpose: no member has reported back yet, and the grid says so - an -enrolled member with no report-back renders **orange** ("enrolled, but no -report-back was received"), never green. Do not add a placeholder envelope to -turn a row green; only a real run may. +**How the files get here.** The `fleet` job of `.github/workflows/hypatia-scan.yml` +(gitbot-fleet's `fleet-report-back-reusable.yml`) runs the enrolled adapters on +the PR head and uploads the envelopes as the `fleet-report-backs` artifact; +the fork-local Hypatia reusable downloads that artifact into this directory +just before it renders the grid (fork delta 4). Nothing is ever committed here: +an envelope is evidence about one commit, and a committed one would read as +current when it is stale. The directory exists because this repository's deed +declares it (`MetaManifold-WebUI_chora.deed`, +`:report-backs ".gitbot-fleet/report-backs/"`), and it is empty in git on +purpose. An enrolled member with no report-back renders **orange** ("enrolled, +but no report-back was received"), never green; do not add a placeholder +envelope to turn a row green. Only a real run may. Only files matching `*.json` are read, so this README is inert. diff --git a/.github/workflows/hypatia-scan-reusable.yml b/.github/workflows/hypatia-scan-reusable.yml index d89e0d5b..bef64c99 100644 --- a/.github/workflows/hypatia-scan-reusable.yml +++ b/.github/workflows/hypatia-scan-reusable.yml @@ -4,7 +4,7 @@ # # Fork-local copy of metadatastician/standards/.github/workflows/ # hypatia-scan-reusable.yml at 01c9b0c9f2646ebea0b30a9588462642ac94aee9. -# Keep that source pin when updating this file and reapply the three fork deltas: +# Keep that source pin when updating this file and reapply the four fork deltas: # 1. Do not upload SARIF for pull_request events. This lane is advisory on # PRs; uploading error-level results creates a separate, blocking # "Code scanning results / Hypatia" check even when the scan job passes. @@ -15,6 +15,12 @@ # 3. Filter only upstream-inherited TypeScript paths from the effective # findings, validate the raw/effective JSON with pinned ijson-jcs, and keep # raw scanner outputs plus fork-only paths/rules. +# 4. Download the gitbot-fleet report-backs (artifact `fleet-report-backs`, +# produced by the `fleet` job of hypatia-scan.yml) into +# .gitbot-fleet/report-backs/ before the verdict grid renders, so each +# enrolled member's row carries evidence instead of "enrolled, but no +# report-back was received". Upstream standards added the same step and +# input after 01c9b0c9; this copy carries them ahead of its source pin. # scripts/check-governance-contract.sh guards these deltas. # # Hypatia Neurosymbolic CI/CD Security Scan — Reusable Workflow @@ -28,6 +34,16 @@ on: type: boolean required: false default: false + report-backs-artifact: + description: >- + Name of an artifact, uploaded earlier in the same run, holding the + gitbot-fleet report-back envelopes (one .json each). Downloaded + into .gitbot-fleet/report-backs/ before the verdict grid renders; a + missing artifact is tolerated and leaves the enrolled rows orange. Set + to an empty string to skip the download. + type: string + required: false + default: "fleet-report-backs" hypatia-ref: description: >- Hypatia commit to build the scanner from. Defaults to a vetted SHA so a @@ -728,6 +744,26 @@ jobs: | last | .body // ""' \ > "$RUNNER_TEMP/grid-previous.md" || : > "$RUNNER_TEMP/grid-previous.md" + # Fork delta 4: fleet report-backs. Produced per run by the `fleet` job of + # hypatia-scan.yml and reaching this job only as an artifact; never + # committed. continue-on-error: no artifact leaves the enrolled rows orange. + # Nothing committed may pose as evidence: only files the artifact supplies + # are read. The committed README.md is not *.json and is left alone. + - name: Clear any checked-out fleet report-backs + if: ${{ always() }} + run: | + set -euo pipefail + mkdir -p .gitbot-fleet/report-backs + find .gitbot-fleet/report-backs -maxdepth 1 -name '*.json' -type f -print -delete + + - name: Download the fleet report-backs + if: ${{ always() && inputs.report-backs-artifact != '' }} + continue-on-error: true + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: ${{ inputs.report-backs-artifact }} + path: .gitbot-fleet/report-backs + - name: Render the verdict grid id: grid if: ${{ always() }} diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 83f21656..1d700e18 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -76,9 +76,24 @@ jobs: require-files: true fail-on-indeterminate: true + # Fleet report-backs, produced before the Hypatia lane renders so each enrolled + # gitbot-fleet member's row (.gitbot-fleet/enrolment.json) carries evidence + # instead of "enrolled, but no report-back was received". Same owner + # (hyperpolymath), so the reusable is allowed by this repository's Actions + # policy; scripts/check-governance-contract.sh guards the pin and the wiring. + fleet: + name: Fleet report-backs + needs: [workflow-contract] + uses: hyperpolymath/gitbot-fleet/.github/workflows/fleet-report-back-reusable.yml@e85b71bf139a61f956c7e0717a2912cd6e347cc5 + permissions: + contents: read + hypatia: name: Hypatia verdict grid - needs: [workflow-contract] + # After the fleet job whatever its result (a dead fleet job renders as orange + # fleet rows, never as a missing lane), but never after a failed contract. + needs: [workflow-contract, fleet] + if: ${{ !cancelled() && needs.workflow-contract.result == 'success' }} # Fork-local so advisory PR scans do not upload error-level SARIF and # create a second blocking check. The source pin and deltas are documented # in the called file and checked by scripts/check-governance-contract.sh. diff --git a/scripts/check-governance-contract.sh b/scripts/check-governance-contract.sh old mode 100755 new mode 100644 index f0064529..990cb3ee --- a/scripts/check-governance-contract.sh +++ b/scripts/check-governance-contract.sh @@ -126,7 +126,50 @@ not_contains "$caller" 'uses: metadatastician/standards/.github/workflows/hypati not_contains "$caller" 'uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@' \ 'the governance lane cannot bypass the fork guard through the pre-transfer name' contains "$caller" 'needs: [workflow-contract]' \ - 'this contract runs before the Hypatia scan' + 'this contract runs before the fleet job' +contains "$caller" 'needs: [workflow-contract, fleet]' \ + 'the Hypatia scan runs after both the contract and the fleet report-backs' +contains "$caller" "if: \${{ !cancelled() && needs.workflow-contract.result == 'success' }}" \ + 'a dead fleet job still lets the Hypatia lane render; a failed contract or a cancelled run does not' +# Fork delta 4: the fleet report-backs. The producer is pinned to a gitbot-fleet +# commit (same owner, so the Actions policy allows the reusable); the consumer +# step must sit before the grid renders, or the rows stay orange with the +# artifact sitting unread in the run. +if grep -Eq '^ uses: hyperpolymath/gitbot-fleet/\.github/workflows/fleet-report-back-reusable\.yml@[0-9a-f]{40}$' <<<"$caller"; then + pass 'the fleet report-backs come from gitbot-fleet at an immutable commit' +else + fail 'the fleet report-backs must come from hyperpolymath/gitbot-fleet pinned to a 40-hex commit' +fi +if fleet_clear="$(step_block "$REUSABLE" 'Clear any checked-out fleet report-backs')"; then + contains "$fleet_clear" "find .gitbot-fleet/report-backs -maxdepth 1 -name '*.json' -type f -print -delete" \ + 'a committed envelope can never pose as report-back evidence' + clear_line=$(grep -nF -- '- name: Clear any checked-out fleet report-backs' "$REUSABLE" | cut -d: -f1 | head -n1) + dl_line0=$(grep -nF -- '- name: Download the fleet report-backs' "$REUSABLE" | cut -d: -f1 | head -n1) + if [ -n "$clear_line" ] && [ -n "$dl_line0" ] && [ "$clear_line" -lt "$dl_line0" ]; then + pass 'the checked-out report-backs are cleared before the artifact is downloaded' + else + fail 'the report-backs clear step must precede the download' + fi +else + fail 'the fleet report-backs clear step exists' +fi +if fleet_dl="$(step_block "$REUSABLE" 'Download the fleet report-backs')"; then + contains "$fleet_dl" 'path: .gitbot-fleet/report-backs' \ + 'the report-backs land where the renderer reads them' + contains "$fleet_dl" 'continue-on-error: true' \ + 'a missing report-backs artifact leaves rows orange instead of killing the lane' + contains "$fleet_dl" 'name: ${{ inputs.report-backs-artifact }}' \ + 'the artifact name is the caller-visible input, not a literal' + dl_line=$(grep -nF -- '- name: Download the fleet report-backs' "$REUSABLE" | cut -d: -f1 | head -n1) + render_line=$(grep -nF -- '- name: Render the verdict grid' "$REUSABLE" | cut -d: -f1 | head -n1) + if [ -n "$dl_line" ] && [ -n "$render_line" ] && [ "$dl_line" -lt "$render_line" ]; then + pass 'the report-backs are downloaded before the verdict grid renders' + else + fail 'the report-backs download must precede the verdict grid render step' + fi +else + fail 'the fleet report-backs download step exists' +fi if grep -Eq '^ pull_request:$' <<<"$caller"; then pass 'governance still runs for pull requests' else diff --git a/scripts/tests/test-governance-contract.sh b/scripts/tests/test-governance-contract.sh index 5ce57161..052548aa 100644 --- a/scripts/tests/test-governance-contract.sh +++ b/scripts/tests/test-governance-contract.sh @@ -14,7 +14,7 @@ reset_fixture() { } reset_fixture bash "$fixture/scripts/check-governance-contract.sh" > "$fixture/log" 2>&1 -for mutation in allowlist-addition baseline-zero-gate baseline-operator-gate baseline-grouping-gate sarif upstream-exception effective-grid jcs-manifest jcs-canonicalizer raw-ijson k9-pin k9-require k9-indeterminate k9-verdict scanner-advisory scanner-secrets scanner-trigger scorecard-trigger stale-standards-owner scorecard-owner deed-remote scorecard-remote deed-source-sha scorecard-source-sha scorecard-no-upload scorecard-quiet-reconcile cross-owner-reusable; do +for mutation in allowlist-addition baseline-zero-gate baseline-operator-gate baseline-grouping-gate sarif upstream-exception effective-grid jcs-manifest jcs-canonicalizer raw-ijson k9-pin k9-require k9-indeterminate k9-verdict scanner-advisory scanner-secrets scanner-trigger scorecard-trigger stale-standards-owner scorecard-owner deed-remote scorecard-remote deed-source-sha scorecard-source-sha scorecard-no-upload scorecard-quiet-reconcile cross-owner-reusable fleet-unpinned fleet-before-contract fleet-dropped-download fleet-download-after-render fleet-download-fatal fleet-committed-evidence fleet-ignores-cancel; do reset_fixture case "$mutation" in allowlist-addition) echo 'frontend/src/fork-only.ts' >> "$fixture/config/hypatia/upstream-typescript-allowlist.txt" ;; @@ -49,6 +49,15 @@ for mutation in allowlist-addition baseline-zero-gate baseline-operator-gate bas deed-remote) sed -i 's|uses: ./.github/workflows/deed-conformance-reusable.yml|uses: metadatastician/standards/.github/workflows/deed-conformance.yml@75bd5994470661a099df562d6d5ad2fe744dbd66|' "$fixture/.github/workflows/hypatia-scan.yml" ;; scorecard-remote) sed -i 's|uses: ./.github/workflows/scorecard-reusable.yml|uses: metadatastician/standards/.github/workflows/scorecard-reusable.yml@c6d78606f3928c22c3b0bdc6f5f5b47b96e05a5d|' "$fixture/.github/workflows/scorecard.yml" ;; cross-owner-reusable) sed -i 's|uses: ./.github/workflows/k9-conformance-reusable.yml|uses: someone/else/.github/workflows/k9.yml@main|' "$fixture/.github/workflows/hypatia-scan.yml" ;; + # Fork delta 4: the fleet report-backs must come from an immutable gitbot-fleet + # commit, be downloaded before the grid renders, and never kill the lane. + fleet-unpinned) sed -i -E 's|(fleet-report-back-reusable\.yml)@[0-9a-f]{40}|\1@main|' "$fixture/.github/workflows/hypatia-scan.yml" ;; + fleet-before-contract) sed -i 's|needs: \[workflow-contract, fleet\]|needs: [fleet]|' "$fixture/.github/workflows/hypatia-scan.yml" ;; + fleet-dropped-download) sed -i '/^ - name: Download the fleet report-backs$/,/^ path: .gitbot-fleet\/report-backs$/d' "$fixture/.github/workflows/hypatia-scan-reusable.yml" ;; + fleet-download-after-render) awk '/^ - name: Download the fleet report-backs$/{hold=1} hold && /^ - name: Render the verdict grid$/{hold=0; print; printf "%s", buf; buf=""; next} hold{buf=buf $0 "\n"; next} {print}' "$fixture/.github/workflows/hypatia-scan-reusable.yml" > "$fixture/reordered.yml" && mv "$fixture/reordered.yml" "$fixture/.github/workflows/hypatia-scan-reusable.yml" ;; + fleet-download-fatal) sed -i '/^ - name: Download the fleet report-backs$/,/^ path: .gitbot-fleet\/report-backs$/{/continue-on-error: true/d}' "$fixture/.github/workflows/hypatia-scan-reusable.yml" ;; + fleet-committed-evidence) sed -i '/^ - name: Clear any checked-out fleet report-backs$/,/-print -delete$/d' "$fixture/.github/workflows/hypatia-scan-reusable.yml" ;; + fleet-ignores-cancel) sed -i "s|if: \${{ !cancelled() \&\& needs.workflow-contract.result == 'success' }}|if: \${{ always() \&\& needs.workflow-contract.result == 'success' }}|" "$fixture/.github/workflows/hypatia-scan.yml" ;; # A copy is only a copy while it still comes from the audited commit and # keeps the guards whose absence froze code scanning upstream. deed-source-sha) sed -i 's@75bd5994470661a099df562d6d5ad2fe744dbd66@main@' "$fixture/.github/workflows/deed-conformance-reusable.yml" ;;