Repository navigation
fix(scorecard): reconcile action-pin findings per workflow file - #1168
Conversation
reconcile-scorecard-actions-lock.rb raised on the first workflow that failed native action-lock verification. The CLI then exited 2 without writing results.reconciled.sarif, so scorecard-reusable.yml uploaded the RAW SARIF and every lock-pinned action in every workflow reappeared as a code-scanning alert. Measured on metadatastician/ZenodoDeposits.jl#15 (run 37472579223): 51 PinnedDependencies alerts, 48 of them in workflows the PR never touched, because quality.yml (deliberately desynced as a positive control) failed verification. Verification is now confined to the workflow that fails: - verify_workflow() wraps the job-level-ref lock check, the gh call and JSON parsing; any failure there (or unparseable workflow YAML) is recorded for that workflow only, and its findings are kept. - reconcile() returns [document, audit, failures]; existing two-value destructuring callers are unaffected. - CLI: on a partial failure it writes OUTPUT and AUDIT_JSON, logs each failed workflow and exits 3, so the workflow uploads the reconciled SARIF (only the failing workflow's alerts) and the run still fails by design. Usage/SARIF-structure errors still exit 2 with no OUTPUT. Nothing is weakened: a workflow that fails verification still surfaces all of its alerts and still fails the run. Two tests that asserted a raise now assert the stronger per-file contract (finding kept, no audit record, failure reported). New tests cover a failing workflow ahead of a verified one, an unparseable workflow, and the CLI exit/file contract. Mutants (re-raise; stop filtering after first failure; the pre-fix origin/main script) are each killed. Refs #1036 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing (2)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
K9 contract conformancerun https://github.com/hyperpolymath/standards/actions/runs/37497739263 K9 normative contract typecheckK9 contract self-testK9 conformance fixtures |
K9 contract conformancerun https://github.com/hyperpolymath/standards/actions/runs/37498356760 K9 normative contract typecheckK9 contract self-testK9 conformance fixtures |
K9 contract conformancerun https://github.com/hyperpolymath/standards/actions/runs/37498380657 K9 normative contract typecheckK9 contract self-testK9 conformance fixtures |
|
|
Add Carrot credits or activate Agent usage billing to use Autopilot |
|
❌ Failed to create Coding Agent finishing-touch task. Please try again. |
…1183) ## Summary `scorecard-reusable.yml` checks the reconciliation helper out at a pinned standards commit, `591cea1b`. That commit predates #1168 (per-file verification) and #1064 (stale-tolerance), so neither fix has ever run in CI. Every caller still gets the old, whole-repo reconciliation. This PR: - **Bumps the helper ref** in both jobs (`scheduled` and `pull-request`) from `591cea1b` to `5a0891e9`, the #1168 merge commit on `main`. - **Passes the reconcile exit code on** as a step output, `rc`. The helper exits 3 when it wrote and uploaded the reconciled SARIF but some workflows failed native lock verification and keep their findings. It exits 2 on a usage or SARIF-structure error, when nothing is written. - **Corrects the final error message.** On exit 3 it used to say "Raw SARIF was uploaded", which was false: the reconciled file was uploaded. It now says "reconciliation incomplete" and points at the step log that names each workflow. The old message stays for the real failure case. **Both cases still fail the run.** No gate is weakened. Refs #1036. The callers need no change: they pick this up through their existing pin to this reusable once they bump it, as tracked in #1036. ## Type of change - [x] 🐛 Bug fix (non-breaking change that fixes an issue): the reusable ran a reconciler without the fixes merged in #1168 and #1064, and misreported exit 3. - [ ] ✨ New feature: no new behaviour beyond running the already-merged helper. - [ ] 💥 Breaking change: the job fails in exactly the cases it failed before. - [ ] 🕳️ Soundness fix: no checker false negative is closed here. - [ ] 📖 Documentation: no docs change. - [ ] 🧹 Refactor / tech debt: behaviour changes (newer helper), so not behaviour-preserving. - [ ] ⚡ Performance: not affected. - [x] 🔧 Build / CI / tooling: reusable workflow change. ## 📌 New pins > **Head: `d13c091c23ffe3df071f5831bcdbd37111aaa69d`**, the commit CI runs on. > > - **`hyperpolymath/standards` checkout `ref:` `591cea1b074b56367f25ec0084a0c4ab66b42a7e` → `5a0891e9e58b785181a92e141505ca96ca987b42`** (the #1168 merge), in both jobs. This is an `actions/checkout` `with: ref:`, not a `uses:` ref, so it needs no `actions.lock` entry. > - No `uses:` refs, `actions.lock` entries, lockfiles or container digests are added or changed. ## How has this been verified? - `gh api repos/hyperpolymath/standards/git/commits/5a0891e9e58b785181a92e141505ca96ca987b42` returned the commit "fix(scorecard): reconcile action-pin findings per workflow file (#1168)". So the ref is a commit, not a blob or tag object (the #987 trap). - `git merge-base --is-ancestor 5a0891e… origin/main`: it is on `main`. - `git show 5a0891e:scripts/reconcile-scorecard-actions-lock.rb`: the helper's `exit 3` (line 222) and `exit 2` (line 226) are the two codes the new message branches on. - `actionlint .github/workflows/scorecard-reusable.yml`: rc=0, no findings. - `git merge-tree --write-tree origin/main HEAD`: merges cleanly. The branch is 8 commits behind `main`, but none of them touches this file. - **Not verified: a CI run of this reusable at this head.** The exit-3 path is exercised only when a caller's Scorecard run hits a workflow that fails native lock verification, and that has not happened yet. ## Checklist - [x] My commits are **signed** (`git commit -S`): `d13c091c` shows `G`. - [x] I ran the project's own checks/tests locally and they pass: actionlint is clean. No standards test covers this workflow's step logic. - [x] New files carry the correct `SPDX-License-Identifier`: there are no new files, and the existing file's header is unchanged. - [x] Docs are updated, and no public claim now overstates what the code does: the error text now states what was actually uploaded. - [x] I have not introduced a soundness hole: both failure paths still `exit 1`. ## Notes for reviewers - The `rc=0 … || rc=$?` form keeps `set -e` from killing the step before the code is recorded. The step then re-raises the same code with `exit "$rc"`, so `steps.reconcile.outcome` is unchanged from before. - Follow-up (#1036): bump the callers' pin to this reusable once this merges. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>



Summary
scripts/reconcile-scorecard-actions-lock.rbaborted the whole SARIF document on the first workflow that failed native action-lock verification. It raised inside the per-result filter, the CLI exited 2 without writingresults.reconciled.sarif, andscorecard-reusable.ymlthen fell back to uploading the raw SARIF. Every lock-pinned action in every workflow came back as a code-scanning alert.Observed:
metadatastician/ZenodoDeposits.jl#15(a deliberate positive control that desyncsquality.yml), run37472579223, job112299614531:That run produced 51
PinnedDependenciesIDalerts. 48 of them were in workflows the PR never touched (the PR changes onlyquality.yml).Fix. Verification is now confined to each workflow file:
New
verify_workflow(root, relative)wraps four things: the job-level reusable-ref lock check, thegh actions-lock --verifycall, JSON parsing and acceptance. Any failure raises for that file only.reconcilecatches that per file and recordsfailures[relative] = reason. It keeps all of that file's findings and goes on reconciling every other workflow. An unparseable workflow YAML is handled the same way; before this PR it also aborted everything. Failures are cached, soghruns at most once per workflow.reconcilenow returns[document, audit, failures]. Callers that destructure two values (result, audit = ...) are unaffected.CLI exit contract (documented in the script header):
0: everything verified.3: some workflows failed. OUTPUT and AUDIT_JSON are written first. Each failed workflow is logged to stderr, and only its findings remain.2: usage or SARIF-structure error. OUTPUT is not written, as before.With
scorecard-reusable.ymlas written, this keeps the existing semantics:select-sarifkeys on-s results.reconciled.sarif, so on exit 3 it uploads the reconciled SARIF, which contains only the failing workflow's alerts.Fail if reconciliation did not succeedkeys on any non-success outcome, so the run still fails by design.Not weakened: a workflow that fails verification still surfaces every one of its alerts and still fails the run. Only the collateral alerts from verified workflows are no longer resurrected.
Refs #1036. That issue is the adjacent defect: a false
staleverdict fromgh actions-lockon job-level reusable refs. It is the most common trigger of exactly this whole-document blast radius. By the same mechanism, #1036's index case (burble'srelease.ymlfailing on a falsestale) would have un-reconciled every other burble workflow's pin alerts too. That is inferred from the code path; burble's alert count was not measured here. This PR does not change the #1036 acceptance logic, which #1064 already landed inverification_accepted?. It confines the damage when any single file fails, for a #1036 reason or a real one. See the note on AC3 below for why #1036 isRefs, notCloses.Type of change
scorecard-reusable.ymlcalls.📌 New pins
e19b8023d4dc792b794f554c73e1892eca1aa865scripts/reconcile-scorecard-actions-lock.rband its test, with nouses:,actions.lock, lockfile or container digest changes. In particular, the helperref:inscorecard-reusable.ymlis deliberately not bumped here, because it cannot point at this PR's own merge commit. See Follow-up.How has this been verified?
All commands below were run locally in the worktree at the head SHA above. Ruby is 4.0.6 and
gh actions-lockis v0.1.6, the same pin the reusable installs.1. Test suite for this script. Baseline on
origin/mainwas 6 runs, 22 assertions, all green.There were no pre-existing failures in this suite. In CI this file runs in
Self Test(.github/workflows/self-test.yml→scripts/run-shell-test-suite.sh, which globsscripts/tests/*.shand installsruby-minitest), so the regression test is enforced on this PR. Separately,Self Testonmainwas already red at2f8c3eeb(2026-10-04); that predates this PR and was not investigated here.New or changed tests:
test_one_failing_workflow_does_not_unreconcile_the_othersis the regression test. Its fixture SARIF covers two workflows. The failing one (quality.yml, per-file stub responsevalid:false,ref-changed, exit 1) is placed first. The verified one (ci.yml) comes after it. The test asserts:quality.ymlfindings are kept;ci.ymlfindings are dropped and appear in the audit;failuresnamesquality.yml;ghwas invoked exactly twice.test_unparseable_workflow_is_confined_to_itself: malformed YAML in one file does not stop the other file being reconciled.test_cli_partial_failure_writes_output_and_exits_nonzeroruns the script as a subprocess, exactly as the reusable invokes it. It asserts exit 3, that the reconciled SARIF is written and holds only the failing workflow's finding, that the audit has 1 entry, and that stderr names the failing file.test_cli_success_and_structural_error_exit_codesasserts exit 0 with an empty result on success, and exit 2 with no output file on a bad SARIF.test_invalid_verification_never_becomes_a_clean_resultandtest_arm_d_…(renamed…_is_kept_and_reported). They usedassert_raises, which encoded the whole-document abort itself. They now assert the per-file contract instead: the document is unchanged, the audit is empty, and the failure is recorded for that file. Every input those tests fed in (malformed JSON,valid:falsewith no findings, non-zero exit, and the Arm D job-level ref absent from the lock) still produces no suppression. This is the same guarantee, stated at file granularity, not a weakening.ghstub gained per-workflow responses (responses/<basename>.jsonand.exit), so a single run can mix a verifying workflow with a failing one.2. Mutants. Each mutant was syntax-checked with
ruby -c(Syntax OK) and the fix was restored after each one.git show origin/main:scripts/reconcile-scorecard-actions-lock.rbdropped in placeraise10 runs, 23 assertions, 1 failures, 4 errors.test_one_failing_workflow…,test_unparseable…,test_cli_partial_failure…,test_invalid_verification…andtest_arm_d…fail.failures[relative] = error.message→raise error1 failures, 3 errors.test_one_failing_workflow…,test_cli_partial_failure…,test_invalid_verification…andtest_arm_d…fail.next false if failures.key?(relative)→next false unless failures.empty?3 failures.test_one_failing_workflow…,test_cli_partial_failure…andtest_unparseable…fail.After restoring:
10 runs, 65 assertions, 0 failures, 0 errors, 0 skips.3. Real data: ZenodoDeposits.jl#15 replayed locally. This used that run's own
scorecard-pr-resultsartifact (gh run download 37472579223 -n scorecard-pr-results) and agit archiveof PR headf82394a5(the run'sheadSha), verified with the realgh actions-lockv0.1.6.591cea1b(what the reusable pins today)origin/main(44a0de48).github/workflows/quality.ymlThe fixed run logged
Scorecard reconciliation kept findings for .github/workflows/quality.yml: Native action-lock verification failed for .github/workflows/quality.ymlandReconciled 48 verified native action-pin false positives. Total results went from 62 to 14, and the audit has 48 entries.quality.ymlfails for a real reason, not the #1036 falsestale:gh actions-lock .github/workflows/quality.yml --verifyreportsref-changed(error, "workflow uses ref v7.0.0 but lockfile pins v7.0.1") plus the consequentstale. Those 3 alerts are the correct output of that positive control, and they are kept.4. Hooks. The repo's pre-commit checks passed: actions-lock coverage, permissions, CodeQL config, bot directives and canonical names.
.githooks/docstring-scan.sh --worktree --checkreports both files asskippedbecause the scanner does not cover Ruby. That gives no verdict, so I checked by hand: every new function (verify_workflowand the test helpersassert_verification_failed,add_failing_workflow,run_cli, plus each new test) has a doc comment, and so do the touched-but-undocumentedverification_accepted?,setupand the renamed Arm D test.Checklist
git commit -S). The SSH signature is verified locally:git log -1 --format=%G?→G.scripts/tests/reconcile-scorecard-actions-lock-test.sh(10/10) and the pre-commit hooks.SPDX-License-Identifier. There are no new files; both edited files keep their existingMPL-2.0header, and nothing was relicensed.reconciledocstring no longer claims it "raises when verification fails". One caveat: the reusable's fail-step message says "Raw SARIF was uploaded". That becomes inaccurate for the exit-3 path only once the helperref:is bumped, so it is left for that follow-up PR (below).ghproblem that affects all workflows now yields per-file failures with every finding kept, plus exit 3. That has the same alert set as the old raw upload and still fails the run.Notes for reviewers
This fix is not live until three steps happen, in order.
scorecard-reusable.ymlchecks out the helper at a pinnedref:(591cea1b, in both thescorecardandpull-requestjobs), not atmain:ref:in both jobs of.github/workflows/scorecard-reusable.ymlto the merge SHA. At the same time, update theFail if reconciliation did not succeedmessage so it distinguishes exit 3 ("reconciled SARIF uploaded; N workflow(s) failed verification") from the raw-upload fallback.scorecard-reusable.ymlby SHA, so each caller'suses:andactions.lockentry need a bump to the SHA from step 2. That work is not done here.#1036 is
Refs, notCloses. The591cea1bpin also means the #1036 stale-tolerance from #1064 (verification_accepted?,lock_covers_reusable_deps?) has never run in CI. The ZenodoDeposits log above showsHEAD is now at 591cea1. So #1036 AC3 (burble and ephapax green without touching their YAML or lock) stays blocked on the same step-2 bump. AC4 (the upstreamgh-actions-lockreport) and AC2's arm-D direction are not addressed by this PR beyond keeping the existing Arm D test, now asserting kept-and-reported instead of raise.🤖 Generated with Claude Code
https://claude.ai/code/session_01X3hgXxWm6umMgZkjYyHnnm