Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tool>` — `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 <tool>\` — this recurses through PATH." >&2
echo " use write_mock_git from tests/lib/mock-git.sh instead." >&2
exit 1
fi
59 changes: 59 additions & 0 deletions .github/scripts/check-ci-parity.sh
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +33 to +34
)

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
1 change: 1 addition & 0 deletions .github/scripts/validate-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
11 changes: 10 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading