Skip to content

ci(lock): gate every commit on its own actions.lock self-consistency - #821

Merged
hyperpolymath merged 5 commits into
mainfrom
ci/lock-selfcheck-gate
Sep 17, 2026
Merged

hyperpolymath merged 5 commits into
mainfrom
ci/lock-selfcheck-gate

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

A caller writes uses: hyperpolymath/standards/.github/workflows/x.yml@<sha>. GitHub resolves that at workflow STARTUP and validates the callee against the CALLEE repo's own .github/workflows/actions.lock AS IT EXISTS AT THAT SHA. If a reusable here names an action ref the lock at that SHA does not key, every caller pinned there dies before a single job exists.

That death is nearly invisible. The run's conclusion is failure, NOT startup_failure; it has ZERO jobs; its name equals its path; and neither REST nor GraphQL carries a reason -- only the run page states one. A required context whose workflow dies this way never reports at all, so the gate reads as ABSENT rather than failing and the branch looks clean. That is how this class stays unnoticed while it spreads.

Capability is therefore NOT monotonic in time, and it OSCILLATES. Measured across the 84 commits da2c748..317101e: 43 POISON / 41 SELF-CONSISTENT, alternating, because Dependabot bumps a uses: inside a reusable without regenerating the lock in the same commit. "Pin to HEAD" is the wrong reflex -- the current tip is itself poison. This gate makes main safe to pin to by construction rather than by luck.

Contents:

scripts/lock-selfcheck.sh
Answers one question: is a given commit safe to pin a caller to? Reads
git objects only (git show), never checks out, fetches or writes, so it
is safe against a checkout shared with a live writer. Exit 0 all examined
SHAs self-validate, 1 any POISON, 2 misuse.

  It distinguishes three states that are easy to conflate: POISON (the
  workflow is keyed but this ref is not listed -- startup-fatal), ABSENT
  (the workflow has no lock entry at all -- untidy, not fatal), and NO-LOCK
  (the commit pre-dates the lock regime -- not a defect). Only POISON
  fails. It compares case-insensitively, because the lockfile lowercases
  the owner/repo it keys while workflows carry upstream casing, and it
  normalises subpaths, because `github/codeql-action/upload-sarif@SHA` is
  keyed as `github/codeql-action@SHA`. Both of those produced false
  verdicts before they were fixed.

.github/workflows/lock-selfcheck.yml
Runs it against the commit under test. Deliberately carries NO paths:
filter: this check is meant to be REQUIRED, and a required check that
filters itself off does not report as passing -- it does not report at
all, leaving the PR blocked on a context that can never arrive.

Also repins three stale lock entries this check flags. codeql-reusable.yml, hypatia-scan-reusable.yml and scorecard-reusable.yml all use github/codeql-action@b96794f0 (v4.38.0, 2026-09-09) while the lock still keyed @cdf488f5 (v4.37.9, 2026-08-26). The workflows are newer and the lock is stale, so the lock is what moves: @cdf488f5 appears 6 times in the lock and is used by ZERO workflow files. The three other stale entries are left untouched on purpose -- they belong to workflows that call no codeql, so widening the diff would buy nothing.

KNOWN RED, AND CORRECTLY SO: this gate still fails on a fourth ref. governance-reusable.yml uses denoland/setup-deno@22d081ff, which is both unkeyed and a banned runtime. It is not keyed here deliberately -- keying it would make a banned runtime a required lock key for every caller in the estate. Nor is the job deleted: its name is a required context estate-wide. The step is load-bearing (line 383 runs deno run against
scripts/check-ts-allowlist.deno.js, which is 360 lines of compiled output from an AffineScript source), so removing it is a compiler-target change, not a text edit. That is task #15. Until #15 lands, this gate reports the deno ref red, and that red is accurate: a caller pinned at such a commit really does die.

Landing this advisory-first and marking it required after #15 is a reasonable sequence; so is fixing #15 first. That ordering is an owner call.

Claude-Session: https://claude.ai/code/session_01WgqXnnNWBkiKMyUeLqzcuN

Summary

Closes #

Type of change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (would change existing behaviour)
  • 🕳️ Soundness fix (fixes a checker/proof false-negative)
  • 📖 Documentation
  • 🧹 Refactor / tech debt (behaviour-preserving)
  • ⚡ Performance
  • 🔧 Build / CI / tooling

How has this been verified?

Checklist

  • My commits are signed (git commit -S).
  • I ran the project's own checks/tests locally and they pass.
  • New files carry the correct SPDX-License-Identifier (code/config MPL-2.0,
    prose CC-BY-SA-4.0); I did not relicense existing files.
  • Docs are updated, and no public claim now overstates what the code does.
  • I have not introduced a soundness hole (or I have flagged where I might have).

Notes for reviewers

A caller writes `uses: hyperpolymath/standards/.github/workflows/x.yml@<sha>`.
GitHub resolves that at workflow STARTUP and validates the callee against the
CALLEE repo's own `.github/workflows/actions.lock` AS IT EXISTS AT THAT SHA.
If a reusable here names an action ref the lock at that SHA does not key, every
caller pinned there dies before a single job exists.

That death is nearly invisible. The run's conclusion is `failure`, NOT
`startup_failure`; it has ZERO jobs; its `name` equals its `path`; and neither
REST nor GraphQL carries a reason -- only the run page states one. A required
context whose workflow dies this way never reports at all, so the gate reads as
ABSENT rather than failing and the branch looks clean. That is how this class
stays unnoticed while it spreads.

Capability is therefore NOT monotonic in time, and it OSCILLATES. Measured
across the 84 commits da2c748..317101e: 43 POISON / 41 SELF-CONSISTENT,
alternating, because Dependabot bumps a `uses:` inside a reusable without
regenerating the lock in the same commit. "Pin to HEAD" is the wrong reflex --
the current tip is itself poison. This gate makes `main` safe to pin to by
construction rather than by luck.

Contents:

  scripts/lock-selfcheck.sh
      Answers one question: is a given commit safe to pin a caller to? Reads
      git objects only (`git show`), never checks out, fetches or writes, so it
      is safe against a checkout shared with a live writer. Exit 0 all examined
      SHAs self-validate, 1 any POISON, 2 misuse.

      It distinguishes three states that are easy to conflate: POISON (the
      workflow is keyed but this ref is not listed -- startup-fatal), ABSENT
      (the workflow has no lock entry at all -- untidy, not fatal), and NO-LOCK
      (the commit pre-dates the lock regime -- not a defect). Only POISON
      fails. It compares case-insensitively, because the lockfile lowercases
      the owner/repo it keys while workflows carry upstream casing, and it
      normalises subpaths, because `github/codeql-action/upload-sarif@SHA` is
      keyed as `github/codeql-action@SHA`. Both of those produced false
      verdicts before they were fixed.

  .github/workflows/lock-selfcheck.yml
      Runs it against the commit under test. Deliberately carries NO `paths:`
      filter: this check is meant to be REQUIRED, and a required check that
      filters itself off does not report as passing -- it does not report at
      all, leaving the PR blocked on a context that can never arrive.

Also repins three stale lock entries this check flags. codeql-reusable.yml,
hypatia-scan-reusable.yml and scorecard-reusable.yml all use
github/codeql-action@b96794f0 (v4.38.0, 2026-09-09) while the lock still keyed
@cdf488f5 (v4.37.9, 2026-08-26). The workflows are newer and the lock is stale,
so the lock is what moves: @cdf488f5 appears 6 times in the lock and is used by
ZERO workflow files. The three other stale entries are left untouched on
purpose -- they belong to workflows that call no codeql, so widening the diff
would buy nothing.

KNOWN RED, AND CORRECTLY SO: this gate still fails on a fourth ref.
governance-reusable.yml uses denoland/setup-deno@22d081ff, which is both
unkeyed and a banned runtime. It is not keyed here deliberately -- keying it
would make a banned runtime a required lock key for every caller in the estate.
Nor is the job deleted: its name is a required context estate-wide. The step is
load-bearing (line 383 runs `deno run` against
scripts/check-ts-allowlist.deno.js, which is 360 lines of compiled output from
an AffineScript source), so removing it is a compiler-target change, not a text
edit. That is task #15. Until #15 lands, this gate reports the deno ref red,
and that red is accurate: a caller pinned at such a commit really does die.

Landing this advisory-first and marking it required after #15 is a reasonable
sequence; so is fixing #15 first. That ordering is an owner call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WgqXnnNWBkiKMyUeLqzcuN
@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 54 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: c513c823-0d98-415e-9120-056923efbcb2

📥 Commits

Reviewing files that changed from the base of the PR and between 37395b3 and 5f4a3cc.

⛔ Files ignored due to path filters (1)
  • .github/workflows/actions.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • .github/workflows/lock-selfcheck.yml
  • scripts/lock-selfcheck.sh

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hyperpolymath
hyperpolymath enabled auto-merge (squash) September 17, 2026 19:55
@hyperpolymath
hyperpolymath merged commit 176d1f7 into main Sep 17, 2026
13 of 18 checks passed
@hyperpolymath
hyperpolymath deleted the ci/lock-selfcheck-gate branch September 17, 2026 19:57
@sonarqubecloud

Copy link
Copy Markdown

hyperpolymath added a commit that referenced this pull request Sep 17, 2026
## What

`main` is **POISON** by this repo's own `scripts/lock-selfcheck.sh`,
added in #821. The gate went red on its own merge commit, and the cause
predates it:

```
.github/workflows/governance-reusable.yml
  uses  denoland/setup-deno@22d081f
  lock keys for this workflow: actions/cache … actions/checkout … editorconfig-checker/… erlef/setup-beam …
```

The ref is used but not keyed. It arrived with `cdec573` (#818, the
2.0.2 release) and no lock regeneration rode along.

## Why it matters

GitHub resolves a reusable's action refs against the **callee** repo's
`actions.lock` **at the pinned SHA**. An unkeyed ref kills every caller
at workflow startup with **zero jobs**, conclusion `failure` (not
`startup_failure`), and no reason carried in either REST or GraphQL —
the run page is the only place that states one. A required context that
dies this way never reports at all, so the gate reads as **absent**
rather than failing and the branch looks clean. `main` is not currently
a safe thing to pin to.

## Why this fix is deterministic

The lockfile validator requires **key-SHA == the digest written inline
in the workflow**, so the correct entry has exactly one possible value
and needs no resolution, network, or judgement. This reproduces what `gh
actions-lock` + `scripts/relock-sha-keys.sh` would key, with no other
entry moved.

## Verification

```
scripts/lock-selfcheck.sh 176d1f7   -> VERDICT: POISON — 1 ref used but NOT keyed   (rc=1)
scripts/lock-selfcheck.sh 46ef434   -> VERDICT: SELF-CONSISTENT                     (rc=0)
```

Diff is one added line in `.github/workflows/actions.lock` — the key,
inserted in the file's alphabetical convention.

## Not fixed here

`lock-selfcheck.sh` also reports **6 workflows with no lock entry at
all** (`apply-workflow-pins.yml`, `pages-archive.yml`,
`propagate-hooks.yml`, `security-gate-pr-target.yml`,
`settings-drift-detect.yml`, `tag-ruleset-canon.yml` — 18 refs). The
gate states this is a separate question from the verdict and does not
count it. Left alone deliberately: an empty `[]` entry is not obviously
the intended answer for a workflow that may not be a reusable.

## Related

`uses ⊆ actions.lock` is red on `main` with the same root cause (it
shells out to `update-actions-lock.sh --verify-local`). Expect it to
clear with this change; if it does not, it is a second defect, not the
same one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant