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
41 changes: 18 additions & 23 deletions docs/developer-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,30 +254,26 @@ is useful for debugging the dispatcher or previewing action output.

## Pre-commit Hooks

Pre-commit runs on every `git commit`. Run all hooks manually before
pushing:
After `scripts/ci-local.sh --install-hooks`, Git runs `.githooks/pre-commit`
on every `git commit`. From the repository root, the hook runs these steps
in order and stops at the first failure:

```sh
pre-commit run --all-files
scripts/check_no_secrets.sh --staged
cargo fmt --all --check
cargo clippy --workspace --all-features --all-targets --locked -- -D warnings
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --workspace --locked
scripts/test_baseline.sh
```

Hooks included:
These check staged secrets, Rust formatting, Clippy warnings, rustdoc warnings,
and the workspace test baseline. `scripts/test_baseline.sh` runs
`cargo nextest run --workspace --locked` and checks the measured test count.
Run `bash .githooks/pre-commit` to invoke the same gate manually.

| Hook | What it checks |
|---|---|
| trailing-whitespace | Removes trailing spaces |
| end-of-file-fixer | Ensures files end with a newline |
| check-yaml / check-toml / check-json | Syntax validity |
| no-commit-to-branch | Blocks direct commits to `main` |
| gitleaks | Detects hardcoded secrets |
| cargo fmt | Rust formatting (`--check` mode) |
| cargo check | Workspace compilation |
| tsc --noEmit | TypeScript type checking |
| markdownlint-cli2 | Markdown style |
| yamllint | YAML style |

Intentionally excluded from pre-commit (they run in CI instead):
`cargo clippy` (20–30 s), `cargo nextest run` (minutes), `vitest` (minutes).
Vitest is not part of this pre-commit hook; it runs in CI.
`.pre-commit-config.yaml` is an unused remnant of the earlier framework setup,
not the active Git hook configuration.

## Configuration

Expand Down Expand Up @@ -425,13 +421,12 @@ the script exits non-zero only if something in the summary actually failed.
failure. Bypass a single push with `git push --no-verify`; undo the hook
entirely with `git config --unset core.hooksPath`.

`.githooks/` already ships a `pre-commit` hook (`cargo fmt --all --check` +
`cargo nextest run --workspace --locked`) alongside the new `pre-push` one.
`.githooks/` also ships the `pre-commit` hook with the ordered checks listed
under [Pre-commit Hooks](#pre-commit-hooks).
Both are opt-in via the same `core.hooksPath` setting. Note that
`core.hooksPath` is a single switch: pointing it at `.githooks` means Git
stops looking in `.git/hooks`, so it supersedes hooks installed by the
`pre-commit` framework (see [Pre-commit Hooks](#pre-commit-hooks) above) —
use one mechanism or the other, not both, per clone.
`pre-commit` framework. Use the repository's `.githooks` setup, not the framework.

### Full workflow replay

Expand Down
23 changes: 23 additions & 0 deletions scripts/check_evidence_claims.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,28 @@ def load_test_baseline(root: Path) -> dict:
return baseline


def check_pre_commit_commands(root: Path, guide: str) -> list[str]:
"""Compare the documented gate with the executable hook, in order."""
hook = root / ".githooks/pre-commit"
if not hook.exists():
raise Failure(".githooks/pre-commit is missing")
commands = [
line.strip()
for line in hook.read_text(encoding="utf-8").splitlines()
if line.strip()
and not line.lstrip().startswith(("#", "set ", "cd ", "echo "))
]
section = guide.split("## Pre-commit Hooks\n", 1)[-1].split("\n## ", 1)[0]
block = re.search(r"```sh\n(.*?)\n```", section, re.S)
documented = block.group(1).splitlines() if block else []
if documented == commands:
return []
return [
"docs/developer-guide.md: pre-commit steps differ from .githooks/pre-commit; "
f"expected {commands!r}; documented {documented!r}"
]


def main() -> int:
root = Path(sys.argv[1] if len(sys.argv) > 1 else ".").resolve()
try:
Expand All @@ -815,6 +837,7 @@ def main() -> int:
baseline = load_test_baseline(root)

problems = []
problems += check_pre_commit_commands(root, texts["docs/developer-guide.md"])
problems += check_figure(texts, "Rust tests", baseline["tests"])
problems += check_figure(texts, "frontend tests", baseline["frontend_tests"])
problems += check_figure(texts, "typed actions", count_actions(root))
Expand Down
9 changes: 9 additions & 0 deletions tests/release/public-claims.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ PYEOF

fixture_files=(
"${claim_files_from_checker[@]}"
".githooks/pre-commit"
"assets/demo/mcp-flow-mock.sh"
# Evidence the numeric claims derive from, and the source the action count is
# counted out of. Without these the checker aborts on its own input check and
Expand Down Expand Up @@ -94,6 +95,14 @@ assert_rejected_with_diagnostic() {
done
}

# Hook/prose drift must name the changed command, not merely fail on a fixture.
printf '\nnode --version\n' >> "$fixture/.githooks/pre-commit"
assert_rejected_with_diagnostic 'undocumented sixth hook step' 'pre-commit steps differ' 'node --version'
cp "$repo_root/.githooks/pre-commit" "$fixture/.githooks/pre-commit"
sed '/^scripts\/test_baseline.sh$/d' "$repo_root/.githooks/pre-commit" > "$fixture/.githooks/pre-commit"
assert_rejected_with_diagnostic 'documented step removed from hook' 'pre-commit steps differ' 'scripts/test_baseline.sh'
cp "$repo_root/.githooks/pre-commit" "$fixture/.githooks/pre-commit"

# The story coverage sentence is a derived claim, not a second catalogue. Mutate
# its published All-family figure without repeating today's value in this test.
read -r derived_all published_all < <(
Expand Down
Loading