Skip to content

finding(ci): hook-selftests.yml hand-enumerates the self-tests it claims to be the standing caller for — a new matrix ships uncalled #6906

Description

@claude

Found while mirroring the origin/main enumeration guard for objectstack-ai/objectstack#13305 (objectui PR for that card); out of scope for it, filed rather than redesigned there.

The gap

.github/workflows/hook-selftests.yml names itself "The standing caller for .claude/hooks/*.selftest.sh". It is not one. Its steps are a hand-kept enumeration, so a new self-test ships uncalled until someone remembers to edit this file:

      - name: guard-main-checkout-bash self-test
        run: .claude/hooks/guard-main-checkout-bash.selftest.sh

      - name: guard-shared-stash self-test
        run: .claude/hooks/guard-shared-stash.selftest.sh

objectstack's twin does not have this failure mode. Its collector in lint.yml discovers:

mapfile -t selftests < <(find .claude/hooks -type f -name '*.selftest.sh' | sort)
if [ "${#selftests[@]}" -eq 0 ]; then
  echo "... DISCOVERED NOTHING under .claude/hooks/."
  exit 1        # verifying nothing is a failure, not a pass
fi

That shape adds a new matrix with no workflow edit, and it fails loudly if the search ever stops matching — so it cannot silently degrade to a no-op.

Measured

On the card's branch, adding guard-tree-enum.selftest.sh to both repos:

repo runner shape new self-test picked up?
objectstack find .claude/hooks -name '*.selftest.sh' in lint.yml yes, automatically — 4 discovered, no workflow edit
objectui two hardcoded run: steps in hook-selftests.yml no — had to be added by hand

Why this is worth a card rather than a shrug

This workflow's own header argues at length that it is a RUNNER whose matrices "are meant to grow in .claude/** WITHOUT this file being touched", and it removed per-step case counts specifically to keep that independence safe. The enumeration in its steps: is the remaining half of exactly the drift it set out to avoid: the independence holds for cases inside an existing matrix, and breaks for a new matrix file.

It also interacts with #6451 (guard-main-checkout.sh has no self-test at all): whoever writes that missing matrix must remember this file too, or their new self-test is added and never run. Distinct mechanism though — #6451 is a missing self-test FILE, this is the runner that would not call it. Not a duplicate.

Suggested fix

Port objectstack's collector, including its two load-bearing properties: the empty-discovery branch is RED (a step that verified nothing is not a pass), and the loop tolerates-and-collects so one red matrix does not hide the others. scripts/dependabot-merge-gate.mjs's OPTIONAL_CONTEXTS description of this workflow should stay count-free (it was de-counted on the #13305 branch for exactly this reason).

Unassigned and unlabelled — grading and domain:* are triage's.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions