diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 27e96f6..4da4819 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -18,3 +18,14 @@ if ! bash "$repo_root/utils/sync.sh" --check; then echo " run \`utils/sync.sh\` and \`git add\` the result, then retry." >&2 exit 1 fi + +# A PATH-injected mock must never delegate with `command ` — `command` +# bypasses functions and aliases but not PATH lookup, so the mock re-executes +# itself without bound. This is the cheap grep only; the full shellcheck pass +# (~8s) stays in CI and `lint-shell.sh` rather than on every commit. +if grep -rn --include='*.sh' -E '\)\s*command (git|gh|tea) ' "$repo_root/tests/" 2>/dev/null; then + echo "" + echo "pre-commit: mock delegates via \`command \` — this recurses through PATH." >&2 + echo " use write_mock_git from tests/lib/mock-git.sh instead." >&2 + exit 1 +fi diff --git a/.github/scripts/check-ci-parity.sh b/.github/scripts/check-ci-parity.sh new file mode 100755 index 0000000..6b61fcd --- /dev/null +++ b/.github/scripts/check-ci-parity.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# check-ci-parity.sh — assert validate-all.sh runs everything CI runs. +# +# validate-all.sh is documented as the single local equivalent of CI. That claim +# is only true if the two lists agree, and a comment asking humans to keep them +# in step is exactly the kind of thing that silently rots — CLAUDE.md claimed +# "four checks" while CI ran five, and the missing one had no local equivalent +# at all. This turns that convention into a check. +# +# Compares the check commands in .github/workflows/ci.yml against the +# run_check lines in validate-all.sh. Setup steps (installing jq, rumdl) are +# not checks and are ignored. +# +# Usage: bash .github/scripts/check-ci-parity.sh + +set -uo pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")/../.." + +WORKFLOW=".github/workflows/ci.yml" +VALIDATE_ALL=".github/scripts/validate-all.sh" + +for f in "$WORKFLOW" "$VALIDATE_ALL"; do + [[ -f "$f" ]] || { + echo "missing $f" >&2 + exit 1 + } +done + +# Check commands are single-line `run:` steps invoking a script or linter. +# Anything else (multi-line `run: |` install blocks) is setup, not a check. +mapfile -t ci_cmds < <( + grep -oE '^\s+run: (bash [^ ]+\.sh|rumdl check \.)$' "$WORKFLOW" | + sed -E 's/^\s+run: //' | sort -u +) + +if [[ ${#ci_cmds[@]} -eq 0 ]]; then + echo "no check commands found in $WORKFLOW — parsing likely broke" >&2 + exit 1 +fi + +missing=0 +for cmd in "${ci_cmds[@]}"; do + if grep -qF -- "$cmd" "$VALIDATE_ALL"; then + printf ' ok %s\n' "$cmd" + else + printf ' MISSING %s\n' "$cmd" + missing=1 + fi +done + +echo "" +if [[ $missing -eq 0 ]]; then + echo "CI parity: validate-all.sh covers all ${#ci_cmds[@]} CI check(s)." +else + echo "CI parity FAILED — $VALIDATE_ALL does not run every CI check." >&2 + echo "Add the missing command(s) to $VALIDATE_ALL so a local run matches CI." >&2 + exit 1 +fi diff --git a/.github/scripts/validate-all.sh b/.github/scripts/validate-all.sh index fd1be49..506b717 100755 --- a/.github/scripts/validate-all.sh +++ b/.github/scripts/validate-all.sh @@ -35,6 +35,7 @@ run_check() { echo "" } +run_check "ci parity" bash .github/scripts/check-ci-parity.sh run_check "plugin tests" bash test.sh run_check "plugin structure" bash .github/scripts/validate-plugins.sh run_check "frontmatter" bash .github/scripts/validate-frontmatter.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44b5c24..521664e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,8 @@ jobs: run: sudo apt-get update && sudo apt-get install -y jq - name: Validate plugins run: bash .github/scripts/validate-plugins.sh + - name: Check CI/validate-all parity + run: bash .github/scripts/check-ci-parity.sh test: name: Plugin tests @@ -28,7 +30,7 @@ jobs: sudo sh -c 'curl -sSL https://github.com/mvdan/sh/releases/download/v3.10.0/shfmt_v3.10.0_linux_amd64 > /usr/local/bin/shfmt' sudo chmod +x /usr/local/bin/shfmt - name: Run tests - run: bash tests/test.sh + run: bash test.sh validate-frontmatter: name: Validate frontmatter diff --git a/CLAUDE.md b/CLAUDE.md index bdffb17..1d3ad56 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -229,9 +229,18 @@ bash .github/scripts/validate-plugins.sh # plugin structure bash .github/scripts/validate-frontmatter.sh # command/skill frontmatter rumdl check . # markdown linting bash .github/scripts/lint-shell.sh # shellcheck + mock-recursion guard +bash .github/scripts/check-ci-parity.sh # CI and validate-all agree ``` -Adding a CI job means adding it to `validate-all.sh` too; the script says so. +Adding a CI check means adding it to `validate-all.sh` too — and +`check-ci-parity.sh` fails the build if you forget, so the two lists cannot +drift apart again. + +**Run `bash test.sh`, not `bash tests/test.sh`.** The top-level script is the +aggregator: it wraps the plugin suite and already parses pytest and cargo +output, so a second suite root added there is picked up automatically. CI used +to call the inner script directly, which would have silently skipped any such +addition. Run a single test suite directly: