feat(ci): adopt canonical Rust+Python CI gates (zackees/zccache#835)#1
Merged
Conversation
Apply the issue's full ruleset to the canonical hybrid template so
every future `gh repo create --template zackees/template-python-rust-cmd`
inherits the practices by construction.
What lands:
- `ci.sh` + `ci.py` PEP 723 dispatcher. `--no-project --script` lives
in one place so bare `uv run` never triggers the maturin auto-build
trap.
- `ci/gates/{loc,fmt,clippy,ruff,build,test,action_yaml,action_surface}.py`.
One `def run() -> int` per gate; canonical ordering owned by
`ci.py::GATE_ORDER`. `build` is the only fatal gate.
- `ci/hooks/{tool_guard,readme_guard,loc_guard,check-on-start}.py`.
`tool_guard` extended to ban `uv run` without `--no-project --script`
outside named build entry points. README floor at 50 lines.
- `.github/workflows/ci.yml` rewritten as thin orchestration: 8-platform
matrix, every step is `./ci.sh <gate>`, continue-past-failure with
a final `report-failures` summary.
- `.claude/settings.json` binds all four hooks to their events.
- `action.yml` + `action/cleanup/action.yml` make the repo a real
composite action. Both validated by `action_yaml` (structural) +
`action_surface` (subcommand-vs-binary match).
- `tests/test_gates.py` locks the `def run() -> int` contract across
all 8 gates (32 parametrized cases).
- READMEs added/expanded to >=50 lines for every directory in the
tree per the readme_guard floor.
- Untracks built artifacts (`_native*.pyd`, `dist/*`, `__pycache__/`).
Local validation:
- ./ci.sh --list, loc, fmt, ruff, build, action_yaml all green.
- tool_guard correctly denies bare `cargo`, accepts ./ci.sh / ./test /
uv run with protective flags / named build entry points.
- 32/32 gate contract tests pass.
Rule 10 (action gates) addressed by adding a real composite action shape;
the gates are wired against actual `action.yml` + `action/cleanup/
action.yml`, not stubs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
actions/checkout preserves the index mode of tracked files; ci.sh and
the other root entry points were committed with mode 100644, so the
runners hit `Permission denied` before any gate ran. `git update-index
--chmod=+x` on ci.sh, ci.py, install, lint, test, publish.
Also drop the dead for-loop in the report-failures step that interpolated
`${{ join(steps.*.outcome, ' ') }}` into a discarded variable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three blocking failures from the last run: 1. ruff format: 5 files would be reformatted on Linux runners — CRLF line endings from Windows commits. Add `.gitattributes` with `* text=auto eol=lf` plus explicit per-extension entries so the tree stays LF everywhere. `git add --renormalize` applied to the existing files. 2. tests/test_gates.py: `import ci.gates.action_yaml` raised `ModuleNotFoundError: No module named 'yaml'` because the gate's `import yaml` ran at module load time and pytest's venv doesn't install pyyaml. Wrap the import in try/except so the module stays importable; `run()` fails fast with a clear message if pyyaml wasn't provisioned by the ci.py dispatcher. 3. action_surface: failed when no `template-cli` binary existed. The `build` gate is `cargo check` (no link), so the binary never gets produced in CI. Downgrade missing-binary to a skip + warning since the structural contract is already covered by `action_yaml`; the runtime check is best-effort against whatever build is locally available. Local re-validation: ./ci.sh ruff ok (25 files already formatted) ./ci.sh action_yaml ok ./ci.sh action_surface ok (skipped, no binary) pytest tests/test_gates.py — 32/32 (without pyyaml installed) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`>=0.8` resolved to different ruff versions between local and CI, which produced different `ruff format --check` verdicts on the same tree. CI keeps reporting 5 files would be reformatted while local ruff (0.15.18) reports clean. Pin to ruff==0.15.18 so the gate is deterministic. Bumping is now a one-line change with explicit intent.
`uv run` was silently reusing a cached older ruff locally despite the ==0.15.18 pin, masking the format diff. The real issue: ruff walked up to a parent-directory `pyproject.toml` and inherited a `[tool.ruff]` block that doesn't belong to this repo. Local saw config A, CI saw default config — different verdicts on the same file. `--isolated` makes ruff ignore ancestor pyproject.toml entirely, so the gate evaluates the same bytes everywhere. Apply the format pass that the unconfigured ruff actually wants. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
macos-14 (and likely other future runners) defaults to Python 3.14,
which pyo3-ffi 0.23 rejects: "configured Python interpreter version
(3.14) is newer than PyO3's maximum supported version (3.13)".
Pin python3 -> 3.13 in two ways:
- setup-uv `python-version: 3.13` so uv operations use it.
- explicit `uv python install 3.13` + GITHUB_PATH + PYO3_PYTHON env
so pyo3's build.rs finds 3.13 even though it isn't a uv
invocation.
Drop both once the workspace bumps pyo3 past 0.24.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Applies the full ruleset from zackees/zccache#835 to the canonical hybrid Rust+Python template so every future
gh repo create --template zackees/template-python-rust-cmdconsumer inherits the practices by construction. This is the template-first leg of the rollout — backfilling existing repos (zackees/clud,zackees/soldr,zackees/running-process, etc.) comes next, but the template fix is the multiplier.What landed
ci.sh+ci.pyPEP 723 dispatcher. Thin bash wrapper + Python script with inline deps. Loadsuv run --no-project --scriptexactly once so every gate avoids the maturin auto-build trap (a bareuv runon a maturin-backedpyproject.tomltriggers a full wheel build before the script starts; the flags suppress that).ci/gates/{loc,fmt,clippy,ruff,build,test,action_yaml,action_surface}.py. One file per gate, each exposingdef run() -> int. Canonical ordering owned byci.py::GATE_ORDER.buildis the only fatal gate — see rule 7.ci/hooks/{tool_guard,readme_guard,loc_guard,check-on-start}.py.tool_guardextended per rule 4 to banuv runwithout--no-project --scriptoutside named build entry points (./test,./build,ci/build_wheel.py, etc.).readme_guardenforces the 50-line floor per rule 8..github/workflows/ci.ymlrewritten as thin orchestration. 8-platform matrix (linux-x86, linux-x86-musl, linux-arm, linux-arm-musl, mac-x86, mac-arm, windows-x86, windows-arm), every step is./ci.sh <gate>,continue-on-error: trueeverywhere exceptbuild, finalreport-failuresstep collects step outcomes..claude/settings.jsonbinds all four hooks to PreToolUse / PostToolUse / SessionStart events.action.yml+action/cleanup/action.ymlmake the repo a real composite action. Both validated byaction_yaml(structural) +action_surface(subcommand-vs-binary surface match).tests/test_gates.pylocks thedef run() -> intcontract across all 8 gates — 32 parametrized cases..cargo/,.claude/,.github/,.github/workflows/,action/,action/cleanup/,ci/,ci/gates/,ci/hooks/,crates/,crates/template-{core,cli,py}/,docs/,src/,src/template_python_rust_cmd/,tests/)..gitignoreextended for Python build artifacts; built_native*.pydanddist/*untracked.Rule-by-rule mapping
ci/gates/loc.pyci.sh/ci.pyci.shuv runshape via hookci/hooks/tool_guard.pyuv runfor explicit build entry pointsBUILD_ENTRY_POINTSintool_guard.py.github/workflows/ci.ymlci.ymlmatrix +report-failuresstepci/hooks/readme_guard.pyci/gates/vsci/hooks/ci/gates/action_yaml.py,ci/gates/action_surface.pyLocal validation
tool_guard hook self-tests:
cargo build→ DENY (exit 2, structured permission decision payload)./ci.sh fmt→ PASS./test→ PASS (named build entry)uv run --no-project --script ci/build_wheel.py→ PASSTest plan
action_yamlgate passes against the newaction.yml+action/cleanup/action.yml.action_surfacegate passes — verifiestemplate-cli --helpcovers everythingaction.ymlshells out to.tests/test_gates.pypasses (32 cases).gh repo create --template zackees/template-python-rust-cmd ...inherits all the above intact.Notes
ci/lint.pyandci/test.pyshims were removed;./lintand./testnow route directly through./ci.sh.ci/build_wheel.pyandci/publish.pykept as-is — they're the documented opt-in to the full maturin context (per rule 5).🤖 Generated with Claude Code