fix(gates): composite shell -e silently killed two gates - #32
Merged
Merged
Conversation
GitHub runs a composite `shell: bash` step as
`bash --noprofile --norc -e -o pipefail {0}`. The `-e` comes from the
harness, and a script's own `set -uo pipefail` does not clear it. Any
`x=$(grep ... )` is therefore a silent kill site whenever grep
legitimately matches nothing.
Two gates were dying on it, both in the case they exist to handle:
required-files-check
A comment-only CODEOWNERS made `grep -v ... | wc -l` exit 1, killing
the step ONE LINE ABOVE the message declaring that exact file valid
("valid for solo-maintained repos per Rule 1"). The gate killed
precisely the repos it was written to accept. Measured on
pons-asinorum run 35782895038: the log stops after the last
successful echo, with no `::error::` anywhere, and the check-run
annotation carries only "Process completed with exit code 1".
Because this is step 3 of the estate audit, its red SKIPS 25
downstream gates — one unguarded pipeline blanked the whole audit
for every solo-maintained repo in the estate.
spdx-license-check
`git grep -i "SPDX-License-Identifier" | wc -l` exits 1 on a repo
with zero SPDX lines, so the step hard-failed and the `::warning::`
branch beneath it was UNREACHABLE — inverting the intent stated in
the comment beside it ("disabled strictly to prevent total CI
blockage until adoption").
Fixes:
- required-files-check restores its own contract with `set +e`. The
script accumulates `fail=1` to report EVERY defect and exit once at
the end; `-e` means "die on the first", which is the opposite. Both
count sites are additionally guarded with `|| true` so the intent
survives someone re-adding `-e`.
- spdx-license-check guards its `git grep` so the zero-SPDX branch can
be reached at all.
- formatting-check and metrics-check already used `|| true`/`|| echo 0`
at every such site and are untouched; the sweep found no others.
tests/composite-shell-contract.sh runs both gates under the exact CI
shell against a fixture repo that is valid under every rule they state,
and KILLS A MUTANT: it reinstates the unguarded pipeline, asserts both
mutations actually applied, asserts the mutant parses, and requires it
to die SILENTLY with no `::error::` — the CI signature. Without that
last check the red could come from fixture drift and prove nothing
about `-e`. Wired into code-hygiene-self-test.yml. PASS=7 FAIL=0.
Refs: #787
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WRvDivYwLSeVCJUrfjic3f
Contributor
|
Warning Review limit reachedNext included review available in 27 minutes. View limit detailsLimit 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. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (4)
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 |
The `test` job invokes composite actions, which drag transitive `uses:` edges into actions.lock. One of those pins is currently stale — the lock records `hyperpolymath/cicd-suite@main` at 9adb390 while main is at 6ff6057, and `$/actions/manifest-check` resolves to the head SHA, which has no lock entry at all — so the job dies in `prepare` with "lockfile missing pin for hyperpolymath/deed-ecosystem@f9d999b6", before a single step runs. That is pre-existing (this change adds no `uses:`), and it is the same shape as the defect this suite exists to cover: a failure that happens before the thing being measured is ever reached. A suite that can be masked that way is not a measurement. So the contract suite gets its own job, touching nothing but checkout. Its square now always reflects the suite. The lock staleness is left for its own change rather than hand-edited here — the file is machine generated, and `gh actions-lock` rewrite mode has a recorded history of de-pinning SHAs and inventing invalid local refs. Refs: #787 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRvDivYwLSeVCJUrfjic3f
|
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



The defect
GitHub runs every composite
shell: bashstep as:That
-ecomes from the harness, not from the script, and a script's ownset -uo pipefaildoes not clear it — it never mentionse. So anyx=$(grep ... )is a silent kill site whenever grep legitimately matches nothing.Two gates were dying on it, each in the exact case it exists to handle.
1.
required-files-check— killed the repos it was written to acceptA comment-only CODEOWNERS makes
grep -vexit 1 →pipefail→-ekills thestep one line above the message declaring that file valid.
Measured on
pons-asinorumrun35782895038. The failure is invisible twiceover: no
::error::is ever reached, so the log simply stops after the lastsuccessful
echo, and the check-run annotation carries onlyProcess completed with exit code 1.Because this is step 3 of the estate audit, its red skips 25 downstream
gates. One unguarded pipeline has been blanking the entire audit for every
solo-maintained repo in the estate.
2.
spdx-license-check— its own warning branch was unreachablegit grepexits 1 on zero matches, so a repo with no SPDX headers — the onlycase this check exists to detect — hard-failed the step, doing the exact
opposite of what the comment beside it says.
The fix
required-files-check:set +e, restoring the script's own contract. Itaccumulates
fail=1so it can report every defect and exit once at the end;-emeans "die on the first", which is the opposite contract. Both count sitesare additionally guarded with
|| true, so the intent survives someonere-adding
-elater.spdx-license-check: guard thegit grepso the zero-SPDX branch isreachable at all.
formatting-checkandmetrics-checkalready used|| true/|| echo 0at every such site — theauthors knew about this hazard and missed these two files. No other instances.
Non-vacuity
tests/composite-shell-contract.shruns both gates under the exact CI shellagainst a fixture repo that is valid under every rule those gates state
(comment-only CODEOWNERS, zero SPDX headers), and kills a mutant:
set +e;$RFis yq-extracted, so itsindentation is not the
.yml's, and a no-opsedhere produces a fake green(this happened while writing the file);
bash -nproduces a fake red;::error::— the CI signature.Without that last check the red could come from fixture drift and would prove
nothing about
-e(this also happened while writing the file: a 4-lineMAINTAINERSstub made the mutant "die" for the wrong reason).Wired into
code-hygiene-self-test.ymlwith the matchingpaths:filters.Verification
Reproduced locally against
pons-asinorumat the same composite version CI ran(
@main=6ff6057):bash reqfiles.shbash -e -o pipefail reqfiles.sh-e -o pipefailRefs: #787
🤖 Generated with Claude Code
https://claude.ai/code/session_01WRvDivYwLSeVCJUrfjic3f