Skip to content

fix(ci): restore the composite-reached lock pins #28 silently dropped - #34

Merged
hyperpolymath merged 1 commit into
mainfrom
fix/lock-transitive-closure-self-test
Sep 22, 2026
Merged

hyperpolymath merged 1 commit into
mainfrom
fix/lock-transitive-closure-self-test

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Closes #33.

What was broken

Gate controls has been dead since 3b4afaf (#28), dying in Set up job with 1 step and no gate ever executed. Measured on run 35786094563 (push, main, fa71ac2):

##[error]lockfile missing pin for hyperpolymath/deed-ecosystem@f9d999b60cb5f383679ea19912bcdc49c944973a
Gate controls: failure (1 steps)

Mechanism

uses: $/actions/manifest-check is a same-repo ref — inherently pinned at the running commit, no lockfile entry of its own required. But the runner also validates that composite's internals against the lock, and it does so against the calling workflow's entry in the workflows: map. manifest-check reaches deed-ecosystem and k9-ecosystem at full SHA, so both must appear under code-hygiene-self-test.yml. Being present in dependencies: is not sufficient — the map is keyed per workflow.

Why it stayed invisible

commit effect
ecd0240 (#16) added both transitive SHA refs
#17, #18, #25 survived; Gate controls green throughout
3b4afaf (#28) regenerated the lock and silently dropped both

gh actions-lock's extractor reads step-level uses: only, so it cannot derive a ref reached through a composite — a regenerate deletes any added by hand. --verify then returns rc=0 on the result. #28's commit message states it "pins the SHA-form transitive deps reached via called reusables" while its diff removes exactly those, and that rc=0 was its cited proof. gh actions-lock --no-fix still prints Scanning 4 workflows, rc=0, against the broken file today.

The tool cannot see the defect it creates. So the lockfile needs a gate that is not the tool.

Three fixes, because the defect had three parts

  1. Restore the two pins — byte-for-byte the shape that was green at d6bc01a, keeping the two [] keys fix(ci): reconcile actions.lock so the lockfile validates #28 correctly added.

  2. tests/lock-transitive-closure.sh — walks each onboarded workflow's $/ composites to a fixpoint and asserts every remote ref reached is declared for that workflow, and that every declared ref resolves in dependencies:. Reads YAML with yq, never grep (Rule Y-1).

  3. actions.lock added to both paths: filters. fix(ci): reconcile actions.lock so the lockfile validates #28 was a lock-only commit; no filter matched, so this workflow never ran on the commit that broke it.

The guard runs in Composite shell contract, not Gate controls: a missing composite-reached pin kills Gate controls in Set up job, so a check living there could never report the very defect it tests for.

Non-vacuity

Asserted, not assumed. The checked population must be non-empty — and that assertion earned its place on this file's first run, catching a jq-vs-mikefarah dialect bug where --arg failed as a lexer error and a stray 2>/dev/null turned it into a clean empty pass.

mutant expected actual
the real broken lock at fa71ac2 red, naming both refs ✅ PASS=3 FAIL=2
green-era lock at d6bc01a green ✅ rc=0
declared ref with no dependencies: entry red ✅ caught

Local: lock-transitive-closure.sh PASS=7 FAIL=0; composite-shell-contract.sh PASS=7 FAIL=0 with its mutant killed.

The witness

This PR's own run is the evidence. Gate controls must spawn more than 1 step and actually execute the gate controls — 1 step means it died in prepare again.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WRvDivYwLSeVCJUrfjic3f

`Gate controls` has been dead since 3b4afaf (#28), failing in `Set up job`
with one step and no gate ever executed:

    ##[error]lockfile missing pin for
             hyperpolymath/deed-ecosystem@f9d999b

Mechanism. `uses: $/actions/manifest-check` is a same-repo ref, inherently
pinned at the running commit, and needs no lockfile entry of its own. But the
runner also validates that composite's INTERNALS against the lock, and it does
so against the calling workflow's entry in the `workflows:` map. manifest-check
reaches deed-ecosystem and k9-ecosystem at full SHA, so both must be listed
under `code-hygiene-self-test.yml` — being present in `dependencies:` is not
enough. This restores the exact shape that was green at d6bc01a, keeping the
two `[]` keys #28 correctly added.

Why it was invisible for a day, and why a comment would not have helped:

  ecd0240 (#16)  added both transitive SHA refs
  ...            survived #17, #18, #25 — `Gate controls` green throughout
  3b4afaf (#28)  regenerated the lock and dropped both

`gh actions-lock`'s extractor reads step-level `uses:` only, so it cannot
derive a ref reached through a composite and a regenerate deletes any added by
hand. `--verify` then returns rc=0 on the result: #28's message says it "pins
the SHA-form transitive deps reached via called reusables" while its diff
removes exactly those, and the rc=0 was its cited proof. `--no-fix` still
prints "Scanning 4 workflows", rc=0, against the broken file today. The tool
cannot see the defect it creates.

Three fixes, because the defect had three parts:

1. Restore the two pins.
2. `tests/lock-transitive-closure.sh` — walks each onboarded workflow's `$/`
   composites to a fixpoint and asserts every remote ref they reach is declared
   for that workflow, plus that every declared ref resolves in `dependencies:`.
   Read with yq, never grep (Y-1). Non-vacuity is asserted, not assumed: the
   checked population must be non-empty (that assertion caught a jq-vs-mikefarah
   dialect bug on this file's first run, where `--arg` failed as a lexer error
   and a stray 2>/dev/null turned it into a clean empty pass). Mutants: the real
   broken lock at fa71ac2 goes red naming both refs, the green-era lock at
   d6bc01a passes, and a declared-but-undefined ref is caught.
3. `actions.lock` added to both `paths:` filters. #28 was a lock-only commit, so
   no filter matched and this workflow never ran on the commit that broke it.

The guard runs in `Composite shell contract`, not `Gate controls`: a missing
composite-reached pin kills `Gate controls` in `Set up job`, so a check living
there could never report the defect it tests for.

Closes #33.

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

coderabbitai Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 1 minute.

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: 5d79a0e5-88c1-4b12-ad36-b770523e7ddb

📥 Commits

Reviewing files that changed from the base of the PR and between fa71ac2 and f4ac372.

⛔ Files ignored due to path filters (1)
  • .github/workflows/actions.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • .github/workflows/code-hygiene-self-test.yml
  • tests/lock-transitive-closure.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.

@sonarqubecloud

Copy link
Copy Markdown

@hyperpolymath
hyperpolymath merged commit 0c1bc9f into main Sep 22, 2026
8 checks passed
@hyperpolymath
hyperpolymath deleted the fix/lock-transitive-closure-self-test branch September 22, 2026 21:36
hyperpolymath added a commit that referenced this pull request Sep 22, 2026
…nsumer (#35)

## The cure was green here and inert everywhere else

`fa71ac2` (#32) cured the composite `-e` kill. `0c1bc9f` (#34) restored
the
lockfile pins #28 dropped. Both merged green. **Neither reached a single
consumer**, and nothing was red to say so.

pons-asinorum was repointed at `0c1bc9f` (pons#26) and its estate audit
*still*
died at `Required Files Gate` — 82 ms, silent, exit 1, 25 downstream
gates
skipped. Identical to before the cure.

### Why

The reusable invokes its gates by **branch** ref:

```yaml
uses: hyperpolymath/cicd-suite/actions/required-files-check@main
```

A branch ref is *trusted from the lockfile*, and the runner executes
**the commit
the lockfile names** — not the branch tip. This lock pinned
`cicd-suite@main` at
`9adb3908`, four commits back:

| commit | PR | carried |
|---|---|---|
| `0c1bc9f` | #34 | lock pins restored |
| `fa71ac2` | #32 | the `-e` cure (3 + 1 guards) |
| `6ff6057` | #31 | linguist-check |
| `3b4afaf` | #28 | (the regression) |

`9adb3908` has **0** of those guards. So no SHA a consumer picks for the
*reusable* can reach a cured *composite*. The lock's `@main` entry is
the real
gate, and re-pinning it is the actual delivery step — the one #31, #32
and #34
all skipped.

## What this does

1. Bumps `dependencies['hyperpolymath/cicd-suite@main'].commit` to
`0c1bc9f`.
Ancestor-clean fast-forward (`9adb3908` is an ancestor of `0c1bc9f`);
the
transitive `uses:` list is unchanged, so `dependencies:` needs no other
churn.
2. **Makes it impossible to forget again.**
`tests/lock-transitive-closure.sh`
gains a third assertion: for any self-referencing branch pin, the
`actions/`
tree at the locked commit must equal `HEAD`'s. On drift it names the
files.
3. `shell-contract` gains `fetch-depth: 0` so the locked commit is
present to
   compare against.

The house pattern for this is a follow-up *"pin cicd-suite lock at
`<sha>`"*
commit — `f8c8f4a`, `4f9a7a4`, `373714a` all do exactly this. It has
been
carried by memory, and memory dropped it three times running. Now it is
a test.

## Non-vacuity — twice

**The assertion was written before the bump and caught the live
defect**, naming
all three drifted composites:

```
FAIL hyperpolymath/cicd-suite@main pins 9adb390..., whose actions/ tree differs from HEAD
       actions/linguist-check/action.yml
       actions/required-files-check/action.yml
       actions/spdx-license-check/action.yml
```

And its first draft printed its header while checking *nothing* — an ERE
`sed`
has no lazy quantifiers, so `.git` stayed on the repo name and no
dependency key
ever matched. A header is not a check, so the block now carries its own
non-vacuity counter, separate from block 1's.

## Local

```
tests/lock-transitive-closure.sh   PASS=9 FAIL=0
tests/composite-shell-contract.sh  PASS=7 FAIL=0   (mutant dies silently)
```

## Acceptance

`Composite shell contract` green with the new assertion passing — and
then
pons#26's audit re-measured. Per the standing owner ruling, the gates
that have
been *skipped rather than passing* may now surface new findings; those
become
issues with acceptance criteria, not blockers on this PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01WRvDivYwLSeVCJUrfjic3f

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.

Self-test job dies in prepare on every PR branch: actions.lock is not transitively closed for $/actions refs

1 participant