diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..168e627f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +# Pre-commit hooks for the query library pipeline. +# +# One-time setup per clone: +# pip install pre-commit +# pre-commit install +# +# Hooks run only when staged files touch the library sources or the +# committed artifacts. Both use language: python with the pipeline's +# dependencies (keep additional_dependencies in sync with +# query-library/scripts/requirements.txt), so no local pip install of the +# requirements is needed. CI enforces the same checks either way; the hooks +# just fail faster. +repos: + - repo: local + hooks: + - id: query-library-validate + name: query library structural validation + entry: python query-library/scripts/validate.py + language: python + additional_dependencies: ["PyYAML>=6.0", "jsonschema>=4.17"] + files: ^query-library/ + pass_filenames: false + - id: query-library-artifacts + name: query library artifacts freshness + entry: python query-library/scripts/precommit-artifacts.py + language: python + additional_dependencies: ["PyYAML>=6.0", "jsonschema>=4.17"] + files: ^(query-library/|static/docs/query-library/) + pass_filenames: false diff --git a/CLAUDE.md b/CLAUDE.md index 910a60e7..a2c624a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -222,6 +222,10 @@ silently diverge from the committed artifact. python query-library/scripts/validate.py python query-library/scripts/build-artifacts.py +# Optional commit-time enforcement of the two commands above +# (.pre-commit-config.yaml; per-clone opt-in) +pip install pre-commit && pre-commit install + # Site (no env vars needed) yarn start # dev server - no postBuild AEO outputs, no JSON-LD yarn build # full production build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5e17cdd0..bfddbe75 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,6 +105,23 @@ python query-library/scripts/build-artifacts.py Commit the regenerated files under `static/docs/query-library/` together with your entry. CI fails the PR if the committed artifacts do not match the sources. +### Pre-commit hooks (optional but recommended) + +The repo ships a `.pre-commit-config.yaml` that runs the same validation and +artifact-freshness checks at commit time, so you find out in seconds instead +of at CI. One-time setup per clone: + +```bash +pip install pre-commit +pre-commit install +``` + +The hooks only fire when a commit touches `query-library/` or the committed +artifacts. On a freshness failure the hook has already regenerated the +artifacts in your working tree - review them, `git add` them, and commit +again. The hooks install their own Python dependencies, and CI enforces the +same checks regardless, so skipping this setup just means slower feedback. + ## What CI checks Per PR: diff --git a/query-library/scripts/nightly-verify.py b/query-library/scripts/nightly-verify.py index 090ef54a..06ed4d5f 100644 --- a/query-library/scripts/nightly-verify.py +++ b/query-library/scripts/nightly-verify.py @@ -106,7 +106,8 @@ def main() -> int: args = parser.parse_args() try: - overrides = json.loads(args.params_json) + # An unset secret reaches us as an empty env var; treat blank as no overrides. + overrides = json.loads(args.params_json.strip() or "{}") except json.JSONDecodeError as e: print(f"nightly-verify: bad params JSON: {e}", file=sys.stderr) return 1 diff --git a/query-library/scripts/precommit-artifacts.py b/query-library/scripts/precommit-artifacts.py new file mode 100644 index 00000000..2f45a604 --- /dev/null +++ b/query-library/scripts/precommit-artifacts.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Pre-commit gate: regenerate the committed artifacts and fail if the result +does not match what is staged. + +Runs build-artifacts.py (which validates entries first), then checks git +status for the generated outputs: static/docs/query-library/ and the +per-family .mdx stubs under query-library/. Any diff means the staged +sources and the staged artifacts are out of sync - the same condition the +CI freshness gate fails on. The rebuild is idempotent (build_id is a +content hash; manifest timestamps only change when it does), so a clean +tree passes with no side effects. + +pre-commit stashes unstaged changes before running hooks, so a diff here +always means "regenerated artifacts were not staged", never "you have +unrelated local edits". +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +GENERATED_PATHSPECS = ["static/docs/query-library", "query-library/*.mdx"] +HANDWRITTEN = {"query-library/index.mdx"} + + +def main() -> int: + build = REPO_ROOT / "query-library" / "scripts" / "build-artifacts.py" + if subprocess.run([sys.executable, str(build)], cwd=REPO_ROOT).returncode != 0: + return 1 + + status = subprocess.run( + ["git", "status", "--porcelain", "--", *GENERATED_PATHSPECS], + cwd=REPO_ROOT, + capture_output=True, + text=True, + ) + if status.returncode != 0: + print(status.stderr, file=sys.stderr) + return 1 + + stale = [ + line + for line in status.stdout.splitlines() + if line.strip() and line[3:].strip('"') not in HANDWRITTEN + ] + if stale: + print("Regenerated artifacts differ from what is staged:", file=sys.stderr) + for line in stale: + print(f" {line}", file=sys.stderr) + print( + "\nThe build has refreshed these files in your working tree.\n" + "Review them, then: git add static/docs/query-library query-library/*.mdx\n" + "and commit again.", + file=sys.stderr, + ) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/static/docs/query-library/manifest.json b/static/docs/query-library/manifest.json index 47ff79aa..233532d6 100644 --- a/static/docs/query-library/manifest.json +++ b/static/docs/query-library/manifest.json @@ -1,6 +1,6 @@ { "build_id": "ql-ea4d2cbf5ec44568", - "generated_at": "2026-07-31T03:10:30Z", - "library_commit": "3a2c3d9b", + "generated_at": "2026-07-31T21:03:45Z", + "library_commit": "30430712", "entry_count": 47 }