Skip to content

Commit 92d033b

Browse files
TMHSDigitalclaude
andauthored
fix(ci): one requirements file and one command for every local check (#432)
A contributor could not reproduce Validate without reading the YAML: test deps were pinned inline (`pip install PyYAML==6.0.2`, `numpy==2.2.6 Pillow==11.2.1`), invisible to Dependabot, and three checks existed only as heredoc Python inside validate.yml. Pillow 11.2.1 also predates the 11.3.0 fix for CVE-2025-48379. - requirements-dev.txt pins PyYAML, numpy and Pillow 11.3.0 (plus the site build's Jinja2); Validate installs from it and Dependabot watches it. - tests/run_all.py runs every tests/check_*.py and test_*.py, the smoke harness tests, the generator --check modes, the landing build the link check needs, the gallery-drift check, and the inline validate.yml heredoc checks, extracted from the workflow at run time so there is still one copy of each. CONTRIBUTING documents it. - .gitattributes keeps *.sh LF so Windows checkouts run under WSL bash. Closes #364 Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 99a3dc8 commit 92d033b

6 files changed

Lines changed: 136 additions & 2 deletions

File tree

‎.gitattributes‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*.html text eol=lf
88
*.css text eol=lf
99
*.py text eol=lf
10+
# Shell scripts run under bash in CI and Git Bash/WSL locally; CRLF breaks WSL.
11+
*.sh text eol=lf
1012

1113
# Binary. Gallery stills and any future .blend fixtures.
1214
*.webp binary

‎.github/dependabot.yml‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,14 @@ updates:
2222
site-build:
2323
patterns:
2424
- "*"
25+
- package-ecosystem: "pip"
26+
directory: "/"
27+
schedule:
28+
interval: "weekly"
29+
target-branch: "main"
30+
commit-message:
31+
prefix: "chore(deps)"
32+
groups:
33+
dev-tools:
34+
patterns:
35+
- "*"

‎.github/workflows/validate.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
3939
- name: Validate skill and rule frontmatter (parsed YAML, first block only)
4040
run: |
41-
pip install PyYAML==6.0.2
41+
pip install -r requirements-dev.txt
4242
python3 tests/check_frontmatter.py
4343
4444
- name: Validate snippet Python syntax
@@ -434,5 +434,5 @@ jobs:
434434

435435
- name: Hero-drift tool exit-code tests
436436
run: |
437-
pip install numpy==2.2.6 Pillow==11.2.1
437+
pip install -r requirements-dev.txt
438438
python3 tests/test_measure_hero_drift.py -v

‎CONTRIBUTING.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ see the end of this file). Examples and showcase pieces have more gates
3535
git checkout -b your-feature-name
3636
```
3737

38+
4. **Run the checks** before you open a pull request. One command runs every
39+
Validate check that does not need Blender (the frontmatter, catalog,
40+
exit-code, gallery, link and packaging gates, plus the tooling tests):
41+
42+
```bash
43+
pip install -r requirements-dev.txt
44+
python tests/run_all.py # or: python tests/run_all.py -k catalog
45+
```
46+
47+
Blender smoke runs in CI on 5.2 LTS and 4.5 LTS; to run an example locally,
48+
see `tests/smoke/run_example.py`.
49+
3850
## Repository Structure
3951

4052
This repo is a content collection (skills, rules, snippets, templates, examples, and showcase pieces) for Blender Python development. There is no runtime and no MCP server. Headless checks run through `tests/smoke/run_example.py`; CI validates frontmatter, syntax, and aggregate counts.

‎requirements-dev.txt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Everything the Validate workflow's Python checks need, pinned. One file so a
2+
# fresh clone can run them all (`python tests/run_all.py`) and Dependabot can
3+
# propose bumps (#364). Blender itself is not a pip package; see CLAUDE.md
4+
# "Blender Runtime Discovery" for the smoke tests.
5+
-r scripts/site/requirements.txt
6+
PyYAML==6.0.2
7+
numpy==2.2.6
8+
Pillow==11.3.0

‎tests/run_all.py‎

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
"""Run every Validate check locally in one command (#364). No Blender needed.
3+
4+
pip install -r requirements-dev.txt
5+
python tests/run_all.py # all checks, summary at the end
6+
python tests/run_all.py -k gallery # only checks whose name contains "gallery"
7+
8+
It runs every tests/check_*.py and tests/test_*.py, the smoke-harness tests,
9+
the generator --check modes, and the Python heredoc checks that live inline in
10+
.github/workflows/validate.yml (extracted from the workflow at run time, so
11+
there is one copy of each). It does not run Blender, `gh`, or the site build.
12+
Exit 0 only if everything passed.
13+
"""
14+
from __future__ import annotations
15+
16+
import argparse
17+
import subprocess
18+
import sys
19+
import tempfile
20+
import textwrap
21+
from pathlib import Path
22+
23+
ROOT = Path(__file__).resolve().parent.parent
24+
25+
26+
def inline_checks() -> list[tuple[str, str]]:
27+
"""(name, python source) for each `python3 << 'PYEOF'` step in validate.yml."""
28+
import yaml
29+
30+
wf = yaml.safe_load((ROOT / ".github" / "workflows" / "validate.yml").read_text(encoding="utf-8"))
31+
out = []
32+
for job in wf["jobs"].values():
33+
for step in job.get("steps", []):
34+
run = step.get("run") or ""
35+
if "<< 'PYEOF'" in run:
36+
body = run.split("<< 'PYEOF'\n", 1)[1].rsplit("PYEOF", 1)[0]
37+
out.append((f"validate.yml: {step.get('name', '?')}", textwrap.dedent(body)))
38+
return out
39+
40+
41+
GALLERY_DRIFT = """
42+
import subprocess, sys
43+
subprocess.run([sys.executable, "scripts/build_gallery.py"], check=True, capture_output=True)
44+
diff = subprocess.run(["git", "status", "--porcelain", "docs/gallery"], capture_output=True, text=True).stdout
45+
if diff.strip():
46+
sys.exit("docs/gallery/ is stale; commit the regenerated pages: " + diff)
47+
"""
48+
49+
# check_site_links reads the built landing page, which is not committed.
50+
SITE_BUILD = """
51+
import subprocess, sys
52+
subprocess.run([sys.executable, "scripts/site/build_site.py", "--repo-root", ".", "--out", "docs"],
53+
check=True)
54+
"""
55+
56+
57+
def commands() -> list[tuple[str, list[str] | str]]:
58+
py = sys.executable
59+
cmds: list[tuple[str, list[str] | str]] = [
60+
("build landing page (for check_site_links)", SITE_BUILD),
61+
("committed gallery matches its generator", GALLERY_DRIFT),
62+
]
63+
for path in sorted((ROOT / "tests").glob("check_*.py")):
64+
cmds.append((path.name, [py, str(path)]))
65+
for path in [*sorted((ROOT / "tests").glob("test_*.py")), ROOT / "tests" / "smoke" / "test_harness.py"]:
66+
cmds.append((path.name, [py, str(path)]))
67+
cmds.append(("build_claude_rules --check", [py, str(ROOT / "scripts" / "build_claude_rules.py"), "--check"]))
68+
cmds.append(("build_plugin_dist --check", [py, str(ROOT / "scripts" / "build_plugin_dist.py"), "--check"]))
69+
cmds += inline_checks()
70+
return cmds
71+
72+
73+
def run(name: str, cmd: list[str] | str) -> bool:
74+
if isinstance(cmd, str): # inline heredoc source
75+
with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False, encoding="utf-8") as fh:
76+
fh.write(cmd)
77+
argv = [sys.executable, fh.name]
78+
else:
79+
argv = cmd
80+
proc = subprocess.run(argv, cwd=ROOT, capture_output=True, text=True,
81+
encoding="utf-8", errors="replace")
82+
ok = proc.returncode == 0
83+
print(f"{'PASS' if ok else 'FAIL'} {name}", flush=True)
84+
if not ok:
85+
tail = (proc.stdout + proc.stderr).strip().splitlines()[-15:]
86+
print("\n".join(" " + line for line in tail), flush=True)
87+
return ok
88+
89+
90+
def main(argv=None) -> int:
91+
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
92+
ap.add_argument("-k", default="", help="only run checks whose name contains this")
93+
args = ap.parse_args(argv)
94+
todo = [(n, c) for n, c in commands() if args.k.lower() in n.lower()]
95+
failed = [n for n, c in todo if not run(n, c)]
96+
print(f"\n{len(todo) - len(failed)} passed, {len(failed)} failed")
97+
return 1 if failed else 0
98+
99+
100+
if __name__ == "__main__":
101+
sys.exit(main())

0 commit comments

Comments
 (0)