From c653b2f859e18fc123850f77e08df7285fa135b3 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Thu, 29 Oct 2026 22:35:07 -0700 Subject: [PATCH 01/13] Pin the Validator's uvx ruff to a Dependabot-Managed Manifest (#3640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Implements #3616 as the maintainer decided on it: a Dependabot-managed ruff pin with no fleet canary. - **The manifest.** A new `python-tool-pins` composite action carries `requirements.txt` with `ruff==0.17.3`, the current PyPI release and the version the hub's own code already passes. Its one step reads that pin from `$GITHUB_ACTION_PATH` and fails, naming the file, unless exactly one `ruff==` line is there. It does so without `mapfile`, so the test also runs under macOS's stock bash 3.2. - **The validator.** `validate-task.yml` reaches the action as `$/.github/actions/python-tool-pins`, so it resolves at the reusable workflow's own commit. A carrier calling the workflow at a hub pin therefore runs that pin's ruff rather than anything in its own tree. Both lint-only `uvx` calls, `ruff check` and `ruff format --check`, now run `uvx "ruff@$RUFF_VERSION"`. A declared pip directory whose venv installs ruff, and a uv directory, keep their own versions as before. - **Dependabot.** The hub's `.github/dependabot.yml` gains a `pip` pair, main and develop, on `/.github/actions/python-tool-pins`, so the next ruff release opens a hub pull request. The hub's own CI calls the workflow locally, so that bump is linted against the hub's code before it merges. It then reaches carriers at a named hub release. - **Hub-local runs.** The hub's `.husky/pre-commit`, the `OPERATIONS.md` local block, and its own `.vscode/tasks.json` ruff tasks run `uvx --with-requirements .github/actions/python-tool-pins/requirements.txt ruff ...`. That constrains ruff to the pin, so a local run here still matches CI. A probe with a manifest pinning `ruff==0.15.22` resolved 0.15.22 that way. - **Docs and spec.** `spec/project-types.json` `python.scripts.uvx`, the python-codestyle skill (`SKILL.md` and `references/profiles.md`), and `docs/reusable-workflows.md` now say CI's uvx ruff runs the hub's pin. mypy, pyright, and coverage still run `@latest`. A carrier's local tools (the catalog's VS Code and pre-commit snippets, the skill's local commands) still run ruff `@latest`, since #3616 covers the validator only. The skill now names the skew that leaves, and says it ends when the caller's hub pin moves to a release carrying the new version. The change adds comment lines, so it carries the `comments` label. Closes on promotion: #3616 ## Verification - `tests/test_release_guards.py` gains `test_tool_pins_read_exactly_one_ruff_pin`. It runs the action's step against the hub's own manifest and against constructed manifests: one with a comment, no pin, a range, and two pins. It also gains `test_tool_pins_manifest_is_dependabot_tracked_on_both_branches`. The existing `test_ruff_prefers_a_declared_pip_directory_venv` now expects `uvx ruff@`. - The full unittest suite ran 3105 tests, OK (skipped=7). - `uvx ruff@0.17.3 check .` and `format --check .` are clean on the hub. mypy, `repo_gate.py` (composite-actions included), `spec/validate.py`, `build_dist.py --check`, actionlint, and the prose gate with `--allow-comments` are clean too. - The full unittest suite ran again on the final head: 3105 tests, OK (skipped=7). The hub's own pre-commit hook ran the pinned ruff on each commit. - Recorded `local-strict-review` passes ran before each push. - The first raised two `introduced` findings: `docs/reusable-workflows.md` and the python-codestyle `SKILL.md` still described the fallback as `ruff@latest`. Both are fixed in ee4ed5b7. - The second raised four, all fixed in 7180e2ff. They were the `mapfile` read under bash 3.2, a hand-rolled `step_script`, the hub's hook and `OPERATIONS.md` claiming to match CI while running `@latest`, and the skill naming the wrong release as the end of the skew. - The third raised the redundant `skipUnless(shutil.which("bash"))` decorator, which Copilot raised too. It is fixed in 853ba386. - The passes on the fix push raised two more, both fixed. One was the hub's own VS Code ruff tasks still running `@latest`, fixed in b248df8d. The other was the spec and profile text then misdescribing the hub, plus the hook's and `OPERATIONS.md`'s separate sed readers, fixed in 633642ce. - The last pass raised one design finding, declined below on evidence. ## Declined findings - `.github/actions/python-tool-pins/action.yml` read step, `introduced`, design: the finding says the multi-line half of the one-pin check guards a failure that is already loud, since a second unkeyed line in `$GITHUB_OUTPUT` fails the step. Declined. It is one more condition in the `if` the empty-pin check needs anyway, and the finding itself says that check stays. Its error names the manifest and the expected `ruff==` shape, which a runner refusal of a malformed output line does not. The `two pins` subtest of `test_tool_pins_read_exactly_one_ruff_pin` pins that message. The finding's alternative, `uvx --with-requirements` in the validator, would float silently to latest if the manifest lost its ruff line. So it would need the same presence check. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 --- .agents/skills/python-codestyle/SKILL.md | 4 +- .../python-codestyle/references/profiles.md | 22 +++--- .../.source-digests/python-codestyle | 2 +- .../skills/python-codestyle/SKILL.md | 4 +- .../python-codestyle/references/profiles.md | 22 +++--- .github/actions/python-tool-pins/action.yml | 25 ++++++ .../actions/python-tool-pins/requirements.txt | 3 + .github/dependabot.yml | 15 +++- .github/skills/python-codestyle/SKILL.md | 4 +- .../python-codestyle/references/profiles.md | 22 +++--- .github/workflows/validate-task.yml | 19 +++-- .husky/pre-commit | 5 +- .vscode/tasks.json | 8 +- OPERATIONS.md | 4 +- docs/reusable-workflows.md | 2 +- spec/project-types.json | 2 +- tests/test_release_guards.py | 79 +++++++++++++++++-- 17 files changed, 189 insertions(+), 53 deletions(-) create mode 100644 .github/actions/python-tool-pins/action.yml create mode 100644 .github/actions/python-tool-pins/requirements.txt diff --git a/.agents/skills/python-codestyle/SKILL.md b/.agents/skills/python-codestyle/SKILL.md index 90f526ae3..72a80d368 100644 --- a/.agents/skills/python-codestyle/SKILL.md +++ b/.agents/skills/python-codestyle/SKILL.md @@ -163,7 +163,9 @@ form that the fleet baseline expects. Its header names the pip-form and lint-onl `dependsOn`-only. Neither chains with `&&`, so the mirror runs the same on any task shell. CI runs these same commands as the authoritative backstop in every directory the hub validator's -`python-directories` input declares. A repo declaring none has its root gated only where it tracks +`python-directories` input declares. Its `uvx` ruff runs at the version the hub's `python-tool-pins` +action pins rather than at `@latest`. So a ruff release can make a local run and CI disagree until +the caller's hub pin moves to a release carrying the new version. A repo declaring none has its root gated only where it tracks a root `pyproject.toml`, per `WORKFLOW.md` D1.2. That undeclared root is gated more loosely. CI reads its type-checker configuration only from the `[tool.mypy]` or `[tool.pyright]` section of its `pyproject.toml`. It skips the type check where neither is there, diff --git a/.agents/skills/python-codestyle/references/profiles.md b/.agents/skills/python-codestyle/references/profiles.md index d71f1b617..6fe393d5f 100644 --- a/.agents/skills/python-codestyle/references/profiles.md +++ b/.agents/skills/python-codestyle/references/profiles.md @@ -47,8 +47,8 @@ Tool configuration alone marks the lint-only one: pytest from it as `.venv/bin/python -m pytest`. A directory declared in the hub validator's `python-directories` input runs ruff and the type checker from that environment where it installs them. It otherwise runs them through `uvx`, with the checker pointed at that - environment. The undeclared default root runs `uvx ruff@latest` and a bare - `uvx @latest`, with nothing installed. + environment. The undeclared default root runs ruff through `uvx` at the pinned version the + lint-only profile below names, and a bare `uvx @latest`, with nothing installed. - **Scripts** (the `lint-only` profile): stdlib-only utility scripts (e.g. a Python tooling subtree of a `csharp` app). Run the tools with `uvx` (no project install, no lockfile). The `pyproject.toml` carries only tool config (`[tool.ruff]`, `[tool.mypy]`, and @@ -56,12 +56,16 @@ Tool configuration alone marks the lint-only one: `uv.lock` (that metadata would misrepresent it as a shippable package). mypy is the type-check gate (there is no first-party package for pyright strict to anchor on). A `[tool.pyright]` block in standard mode keeps Pylance quiet in the editor, the same mypy-gate/pyright-editor - split the build profile uses. There is no lockfile, and a `uvx @` pin in a `run:` - step is not something Dependabot tracks. So CI runs `uvx ruff@latest` / `uvx mypy@latest` rather - than a manual pin that would silently go stale. The fleet rule is to pin only what Dependabot - auto-updates (SHA-pinned actions, package deps) and otherwise run latest. So the VS Code tasks, - README, and CI all run the unpinned latest here. `.py` files follow the repo's LF line-ending - default (per GOVERNANCE.md's "Line Endings" section). There is no pytest suite, and `unittest` is + split the build profile uses. There is no lockfile, and a `uvx @` literal in a `run:` + step is not something Dependabot tracks. The fleet rule is to pin only what Dependabot + auto-updates (SHA-pinned actions, package deps) and otherwise run latest. A manual pin would + silently go stale. So CI runs ruff at the version the hub validator's + `python-tool-pins` action reads from the `requirements.txt` beside it, which the hub's + Dependabot pip ecosystem bumps. A caller reaches that version at the hub commit its workflow + pins, so a ruff release reaches its lint only through a hub release. CI runs `uvx mypy@latest`, + and a carrier's VS Code tasks and README run the unpinned latest of every tool. The hub's own + tasks and hook take that `requirements.txt` as a requirement instead. `.py` files follow the + repo's LF line-ending default (per GOVERNANCE.md's "Line Endings" section). There is no pytest suite, and `unittest` is the runner instead. The directory still owes tests, written with the standard library's `unittest` so they need no test runner installed. Each is a `test_