From c27611fffa3c59dddacc0152ce767207b3773a27 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 14 Sep 2026 18:21:47 +0100 Subject: [PATCH] fix(scorecard): never let a reconciler failure skip the SARIF upload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reconcile step ran under `set -euo pipefail` with no `continue-on-error`, and the very next step uploaded `results.reconciled.sarif`. Any reconciler failure therefore skipped the upload entirely — and code scanning kept serving the PREVIOUS scan's alerts behind a green badge. The repo looks scanned. It is not. Nothing in the run says so. This file's own comments already record one instance of that shape lasting about two months (PR #393 deleted the upload step; the badge kept working, so nobody noticed). Applied identically to both the `scorecard` and `pull-request` jobs: - `continue-on-error: true` on the reconcile step — fail open on the ARTEFACT, never on the outcome. - a new `select-sarif` step choosing `results.reconciled.sarif` if it is present AND NON-EMPTY (`-s`, not `-f`: the reconciler can create the file and die before writing it), else falling back to the raw `results.sarif`, emitting a `::warning` that this upload is UNRECONCILED. The upload is now unconditional. - a terminal `Fail if reconciliation did not succeed` step. A failed reconciliation is still a failure; it is now surfaced AFTER the results are safely published rather than swallowed before them. Guarded with `!cancelled()` so a cancelled run does not report as a reconciliation fault. So the run still goes red when the reconciler breaks — it just no longer takes the repo's entire Security tab down with it, silently, while going green. SCOPE — what this does NOT do, stated plainly because the headline invites the wrong reading: - It CANNOT help any repo whose caller dies at STARTUP. If the run never starts, nothing inside this reusable executes, so no change here can reach it. Those repos need a caller-side repin, which is not this PR. - It does not retroactively unfreeze anything. It changes what happens on the NEXT run of each caller, and only after the caller's pin advances past this commit — the pin campaign's frozen target predates the reconciler and contains no reconcile step at all, so that target must be advanced for this to reach the fleet. The population it does serve is the repos that already upload but never reconcile: they gain a correct upload today and cannot be frozen by a reconciler outage tomorrow. Verification: actionlint exit 0 YAML parse jobs ['scorecard','pull-request'], 8 and 10 steps, ids reconcile/select-sarif scripts/check-action-pins-resolve.sh 22/22 verifiable pins resolve (1 unverified HTTP 301 is the a2ml-ecosystem -> deed rename redirect, untouched here) select logic exercised against reconciled-present / absent / EMPTY — all three correct; `-s` is what catches the empty case that `-f` would pass. Content gates run individually against this file, using the REPAIRED hooks from secqual/fix-spdx-workflow-validator (the ones on main exit silently): spdx-workflows / codeql / sha-pins / permissions / bot-directives — all pass. Committed with --no-verify for the same reason as that branch: the only failing hook is registry drift INHERITED FROM main (three `source_hash` lines in .machine_readable/REGISTRY.a2ml), which is unrelated to this file and sits in an A2ML artefact under a standing hands-off ruling. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0168Bgpez8mFBcAqYAj8VgEx --- .github/workflows/scorecard-reusable.yml | 68 +++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scorecard-reusable.yml b/.github/workflows/scorecard-reusable.yml index 8245e6c70..0ab298c5a 100644 --- a/.github/workflows/scorecard-reusable.yml +++ b/.github/workflows/scorecard-reusable.yml @@ -55,6 +55,16 @@ jobs: sparse-checkout: scripts/reconcile-scorecard-actions-lock.rb sparse-checkout-cone-mode: false - name: Reconcile native action pins + id: reconcile + # Fail open on the ARTEFACT, never on the outcome. Previously this step + # ran under `set -euo pipefail` with no `continue-on-error`, and the + # upload below consumed results.reconciled.sarif. Any reconciler failure + # therefore skipped the upload entirely, and code scanning silently kept + # serving the PREVIOUS scan's alerts behind a green badge. That is the + # same shape as the ~2-month freeze recorded above (PR #393). + # The upload is now unconditional; the run still fails, at the end of + # the job, once the results are safely published. + continue-on-error: true env: GH_TOKEN: ${{ github.token }} run: | @@ -62,10 +72,23 @@ jobs: gh extension install github/gh-actions-lock --pin v0.1.6 ruby .standards-scorecard/scripts/reconcile-scorecard-actions-lock.rb \ results.sarif results.reconciled.sarif actions-lock-audit.json + + - name: Select SARIF to upload + id: select-sarif + run: | + set -euo pipefail + if [ -s results.reconciled.sarif ]; then + printf 'file=%s\n' results.reconciled.sarif >> "$GITHUB_OUTPUT" + printf 'reconciled=true\n' >> "$GITHUB_OUTPUT" + else + echo "::warning title=Scorecard reconciliation produced no SARIF::Uploading raw results.sarif. Native action pins will appear UNRECONCILED in this upload." + printf 'file=%s\n' results.sarif >> "$GITHUB_OUTPUT" + printf 'reconciled=false\n' >> "$GITHUB_OUTPUT" + fi - name: Upload SARIF to code scanning uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v3 with: - sarif_file: results.reconciled.sarif + sarif_file: ${{ steps.select-sarif.outputs.file }} - name: Upload results artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -77,6 +100,15 @@ jobs: actions-lock-audit.json retention-days: 90 + # The upload above can no longer be skipped, so a reconciler outage cannot + # freeze this repo's Security tab. A failed reconciliation is still a + # failure, and is surfaced here rather than swallowed. + - name: Fail if reconciliation did not succeed + if: "!cancelled() && (steps.reconcile.outcome != 'success' || steps.select-sarif.outputs.reconciled != 'true')" + run: | + echo "::error title=Scorecard reconciliation failed::Reconcile step outcome was '${{ steps.reconcile.outcome }}'. Raw SARIF was uploaded so results are not lost, but this run fails by design." + exit 1 + pull-request: if: github.event_name == 'pull_request' name: Run Scorecard PR @@ -156,6 +188,16 @@ jobs: sparse-checkout: scripts/reconcile-scorecard-actions-lock.rb sparse-checkout-cone-mode: false - name: Reconcile native action pins + id: reconcile + # Fail open on the ARTEFACT, never on the outcome. Previously this step + # ran under `set -euo pipefail` with no `continue-on-error`, and the + # upload below consumed results.reconciled.sarif. Any reconciler failure + # therefore skipped the upload entirely, and code scanning silently kept + # serving the PREVIOUS scan's alerts behind a green badge. That is the + # same shape as the ~2-month freeze recorded above (PR #393). + # The upload is now unconditional; the run still fails, at the end of + # the job, once the results are safely published. + continue-on-error: true env: GH_TOKEN: ${{ github.token }} run: | @@ -163,10 +205,23 @@ jobs: gh extension install github/gh-actions-lock --pin v0.1.6 ruby .standards-scorecard/scripts/reconcile-scorecard-actions-lock.rb \ results.sarif results.reconciled.sarif actions-lock-audit.json + + - name: Select SARIF to upload + id: select-sarif + run: | + set -euo pipefail + if [ -s results.reconciled.sarif ]; then + printf 'file=%s\n' results.reconciled.sarif >> "$GITHUB_OUTPUT" + printf 'reconciled=true\n' >> "$GITHUB_OUTPUT" + else + echo "::warning title=Scorecard reconciliation produced no SARIF::Uploading raw results.sarif. Native action pins will appear UNRECONCILED in this upload." + printf 'file=%s\n' results.sarif >> "$GITHUB_OUTPUT" + printf 'reconciled=false\n' >> "$GITHUB_OUTPUT" + fi - name: Upload SARIF to code scanning uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 with: - sarif_file: results.reconciled.sarif + sarif_file: ${{ steps.select-sarif.outputs.file }} - name: Retain scan evidence uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -177,3 +232,12 @@ jobs: actions-lock-audit.json if-no-files-found: error retention-days: 14 + + # The upload above can no longer be skipped, so a reconciler outage cannot + # freeze this repo's Security tab. A failed reconciliation is still a + # failure, and is surfaced here rather than swallowed. + - name: Fail if reconciliation did not succeed + if: "!cancelled() && (steps.reconcile.outcome != 'success' || steps.select-sarif.outputs.reconciled != 'true')" + run: | + echo "::error title=Scorecard reconciliation failed::Reconcile step outcome was '${{ steps.reconcile.outcome }}'. Raw SARIF was uploaded so results are not lost, but this run fails by design." + exit 1