Repository navigation
fix(ci): close actions.lock (Hypatia invalid_actions_lock) by moving stale reusable pins upstream - #446
Conversation
Repurposes the leftover one-off workflow from #441 to regenerate echidna's own lock in CI with the estate repair chain, since this session cannot reach the GitHub API. Removed again in a later commit on this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jzCHZx4Rt65NDaGLDrjLp
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jzCHZx4Rt65NDaGLDrjLp
actions.lock v0.0.3 named two job-level reusable refs under `workflows:` with no top-level record (slsa-github-generator@f7dd8c54 and panic-attack@b689a74). Hypatia at aa34a4cc reports that as invalid_actions_lock in workflow_audit and workflow_hardening. The estate repair chain (gh actions-lock, relock-sha-keys, complete-job-refs, close-lock, prune-stale; standards 3c00687a) could not close the lock with the old pins. standards@75bd5994 (mirror-reusable.yml) and panic-attack@b689a74 pin dtolnay/rust-toolchain at e2a55d2 and 02cb101e. dtolnay moved its v1 tag, and no tag names either commit any more, so close-lock refuses to write their leaf records. Upstream main of both repos pins 683b588, the current v1, so the callers move there: - security-scan.yml: panic-attack scan-and-report.yml -> 8955f615 (one line changes in the called file: the dtolnay pin) - governance, hypatia-scan, mirror, scorecard, secret-scanner and spark-theatre-gate: standards reusables -> 3c00687a (standards#1315, hypatia-ref aa34a4cc; same workflow_call inputs and secrets) The lock is the chain's output for this exact tree, generated in CI (sha256 58d32e2d...). It was not edited by hand. Measured in that run: close-lock reports 0 dangling refs, check-lock-sync.sh passes, check-actions-lock-gate.sh passes at 75bd5994 and at 3c00687a, and the Hypatia scan at aa34a4cc finds 0 invalid_actions_lock (2 before). Raw `gh actions-lock --verify` (v0.2.0) still flags the job-level reusable records as unused, on main's lock too. It cannot see job-level `uses:`. The estate verifier (update-actions-lock.sh --verify-local) accepts that coverage and passes. The one-off lock-regen workflow left behind by #441 is deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015jzCHZx4Rt65NDaGLDrjLp
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📜 Recent review details
📝 Summary
Merge Risk: ⚪ Minimal · up to
|
|
T1 Guix manifest check (Live Provers) is red on this PR, but the failure is not from this change.
Generated by Claude Code |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
…ent tag names the pin (#1323) ## Summary `scripts/close-lock.sh` won't write a leaf record for a SHA-pinned action when no current tag points at that SHA. It prints `SKIP … (cannot resolve a symbolic tag for leaf SHA)` and exits 3. `dtolnay/rust-toolchain` moves `v1`, so old pins can no longer be closed. Two examples: `@e2a55d2…` (standards@75bd5994 `mirror-reusable.yml`) and `@02cb101…` (panic-attack@b689a74 `scan-and-report.yml`). Every caller lock that reaches those pins is stuck with them (hyperpolymath/echidna#446, run 39423933176). This PR adds a fail-closed fallback. It does not bring back self-SHA leaf refs (cf8f912, standards#1271). ## Measurement: GitHub accepts a `ref:` naming a tag that has since moved [Run 39286638218](https://github.com/hyperpolymath/standards/actions/runs/39286638218), job `mirror / mirror-radicle`, standards@333b06a0, 2026-10-19 16:36 UTC. The runner log shows: ``` Using locked action versions from the workflow's lockfile Download action repository 'dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f' (SHA:e2a55d2ffb04f378e9626c28d38b36d230d1e12f) ``` The record it used was `ref: 'v1'`. "Setup Rust" passed. By then `v1` was already `683b588`: Dependabot moved standards' own pin to it at 14:45 UTC that day (e926751, #1300). So `ref:` is not re-resolved against the tag's current target. ## Change Fallback, tried **only when the tag listing succeeded and found no tag**, in this order: 1. **Prior record.** This is the same key's record in the lock as it stood before `gh actions-lock` rewrote it. close-lock reads it from a new `--prior FILE` option. `regen_lock` in `regen-dependabot-locks.sh` now snapshots the lock before the tool runs and passes it in. The record is reused only when its `commit` is the key SHA and its `owner_id`/`repo_id` equal what the API returns now. 2. **Call-site comment.** This is the `# <tag>` comment on a `uses:` of that SHA. close-lock looks in this repository's workflows and in the called workflows it already reads. A candidate is written only when both of these hold: - it is a tag of that repository now (`commits/tags/<t>`), and - that tag's commit descends from the pin (`compare/<sha>...<tagsha>` reports `ahead` or `identical`), which means the tag moved forward past the pin. Anything else is refused, and the key SKIPs with exit 3 as before: a branch name such as `# stable`, a deleted tag, a self-SHA ref, or a tag on another line. The SKIP line now lists why each candidate was refused. ## Testing Offline, locally: | Suite | Before | After | |---|---|---| | `close-lock-test.sh` | 34 | **53**, 0 failed | | `regen-dependabot-locks-test.sh` | 59 | **61**, 0 failed | | `lock-selfcheck-test.sh` | 9 | 9, 0 failed (needs mikefarah yq v4) | - **New cases.** Both sources are covered: a prior record, including a case-folded key; a comment in a called workflow; a comment in a KYAML local workflow; and fall-through from a refused prior record to the comment. Each refusal is a planted positive: wrong commit, wrong `owner_id`, self-SHA (refused by name before any lookup), a tag that doesn't exist, `# stable`, a tag that doesn't descend from the pin, a comment on another action's pin, and a failed tag listing (no fallback on an API error). - **Mutants.** Nine guard mutants were each killed: commit check, id check, ancestry check, tag existence, SHA-name check, fallback on a list error, key match, no prior source, and no comment source. - **awk.** The suites above, plus check-lock-sync, relock-sha-keys, complete-job-refs, update-actions-lock, relock-standards-pin and validate-actions-lock, pass under both mawk and gawk. - **shellcheck.** No new findings in the scripts. The test file only gains more SC2015 notes, from the `&& ok || bad` idiom it already uses. **Not yet done: a live run.** These lookups can't be made from this session, so the fallback has not run against the real API. Going by the API docs, `commits/tags/<t>` and the compare `status` behave as the stub assumes, but that is unverified until CI runs them. ## Known limit This alone does not close echidna@c020b63, the pre-#446 tree. The panic-attack call site reads `@02cb101… # v1`, which closes through source 2. But standards@75bd5994's call site reads `@e2a55d2… # stable`, which is a branch and so correctly refused. Echidna's own lock never held an `e2a55d2` record. The record *does* exist in standards@75bd5994's own `actions.lock` (`ref: 'v1'`), which is the lock GitHub validates the callee against. Whether to add that as a third source is an open question for a stacked follow-up. ## Related `gh actions-lock --verify` v0.2.0 reports job-level reusable records as "Unused lockfile entry". That conflicts with close-lock's closure requirement. `update-actions-lock.sh --verify-local` already accepts that coverage. It may be worth an upstream issue on github/gh-actions-lock. --- _Generated by [Claude Code](https://claude.ai/code/session_01JHfuPCv1pEiqkQGG5DQASb)_ Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
.github/workflows/actions.lock(v0.0.3) was not transitively closed. It listed two job-level reusable refs underworkflows:, and neither had a top-level record:slsa-framework/slsa-github-generator@f7dd8c54…(generator-generic-ossf-slsa3-publish.yml)hyperpolymath/panic-attack@b689a74…(security-scan.yml)Since hyperpolymath/hypatia#938 (scanner
aa34a4cc), Hypatia reports this asinvalid_actions_lock/{:workflow_dependencies_missing, …}in bothworkflow_auditandworkflow_hardening. That keepsgovernance / Validate Hypatia Baselinered once the hypatia-ref bump from hyperpolymath/standards#1315 lands.Why the lock couldn't simply be regenerated. I ran the estate repair chain (
gh actions-lock→relock-sha-keys→complete-job-refs→close-lock→prune-stale, standards3c00687a) on main's pins. close-lock stops with exit 3 on two leaf refs:dtolnay/rust-toolchain@e2a55d2…: pinned by standards@75bd5994mirror-reusable.ymldtolnay/rust-toolchain@02cb101e…: pinned by panic-attack@b689a74scan-and-report.ymldtolnay moved its
v1tag, so no tag names either commit any more. close-lock correctly refuses to write a self-SHA leaf (cf. standards cf8f9121). Upstream main of both repos now pins683b588, the currentv1. The fix is therefore to move the stale caller pins, then run the chain again.Changes
security-scan.yml: panic-attackscan-and-report.ymlb689a74→8955f615. Only one line changes in the called file: the dtolnay pin.governance,hypatia-scan,mirror,scorecard,secret-scanner,spark-theatre-gate: standards reusables75bd5994→3c00687a(standards#1315). This also brings inhypatia-refaa34a4cc. Theworkflow_callinputs and secrets are unchanged;hypatia-scan-reusablegains averdictoutput.actions.lock: the chain's output for this exact tree, regenerated in CI (sha25658d32e2d…). It was not edited by hand.zz-one-off-lock-regen.yml, a scratch workflow left behind by Claude/new session th4e9w lockregen #441. It was reused on this branch to run the chain (this session has no GitHub API access), then removed.RSR Quality Checklist
Required
actionlintclean on all workflowsunsafeblocks without// SAFETY:comments: n/a.envfiles includedAs Applicable
Testing
Measured in run 39423933176, on three variants of the caller pins:
invalid_actions_locke2a55d2)check-lock-sync.shpasses, and an independent count of edges with no matching key gives 0.gh actions-lock --verify(v0.2.0) fails on every variant, main's current lock included. It cannot see job-level reusableuses:, so it calls exactly the records close-lock requires "unused". The estate verifierupdate-actions-lock.sh --verify-local(what the governance gate runs) accepts that coverage and passes. With the maintainer's agreement, that wrapper counts as the verify criterion here.Generated by Claude Code