Skip to content

ci: main red since #16 — the test job never checks out skills-vendor/, so test_skills_hook dereferences a dangling symlink #27

Description

@gregoryfoster

main has been red since 2026-08-06 05:21. Three consecutive runs, one cause, unrelated to
what any of them changed:

Run Commit Result
31073873872 7bf5988 #16 fix: symlink the skills SessionStart hook into the submodule failure
31077798153 Merge #23 (#22) failure
31117568515 Merge #26 (#24) failure

Last green was 31072707067,
Merge #19 — the run immediately before 7bf5988.

The failure

FAILED tests/test_skills_hook.py::test_the_hook_the_symlink_points_at_actually_exists
  AssertionError: skills-vendor/ is not checked out: run .skills/doctor.sh
  assert False
   +  where False = PosixPath('.../.claude/hooks/skills-submodule-update.sh').exists()
1 failed, 443 passed, 18 deselected

Cause

7bf5988 (#16) added the second assertion in tests/test_skills_hook.py
test_the_hook_the_symlink_points_at_actually_exists, which requires a populated
skills-vendor/, not merely a correctly-shaped symlink. That is the right test: a dangling
link is a silent no-op, which is exactly what #16 existed to catch.

CI never populates it. .github/workflows/ci.yml line 85 is a bare
- uses: actions/checkout@v5 with no submodules: key, and actions/checkout does not
fetch submodules by default. So the symlink .claude/hooks/skills-submodule-update.sh -> ../../skills-vendor/gregoryfoster-skills/... resolves to nothing on the runner and
HOOK.exists() is False.

The first assertion in the same file passes, because is_symlink() and os.readlink() only
read the link itself. Only the one that dereferences it fails — which is why 443 of 444 pass
and the failure looks narrow.

Not the daily-refresh hold

Worth stating explicitly, because it is the intuitive read and it is wrong: this is
independent of 0f4fded (#22, suspend the daily skills refresh for the v1.2 hold). That
suspension governs a SessionStart hook that runs on a developer's machine. The CI job
never invokes it — the runner's skills-vendor/ is empty because checkout was never asked
to fetch it, not because a refresh was skipped. Lifting the v1.2 hold will leave main
red.

Fix

One key on the test job's checkout:

  test:
    name: test
    ...
    steps:
      - uses: actions/checkout@v5
        with:
          submodules: recursive

Both submodules are public HTTPS in .gitmodules
gregoryfoster/skills and obra/superpowers — so this needs no token, no WIF, and no
change to the existing wheelhouse auth.

The lint job (line 29) does not need it. Ruff never reads skills-vendor/, and
fetching two submodules to run a linter is cost for nothing. Scope the change to the one job
that dereferences the link.

Verification

  • Push the branch and confirm test goes green, i.e. 444 passed rather than 443.
  • Confirm the lint job is untouched and still green.
  • Confirm the added fetch does not materially lengthen the run — the last green test job was
    ~1m10s end to end.

Why it matters beyond the red badge

test_the_hook_the_symlink_points_at_actually_exists is currently a test that cannot pass
in CI
, only locally. A check that always fails is indistinguishable from a check that
started failing, so it trains everyone to merge past a red run — which is what happened three
times, including once knowingly (#26, merged per maintainer direction with the failure noted
in the merge body). Restoring the signal matters more than the individual test does.

Found while shipping #24.

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