refactor: uv workspace — DRY pre-commit + CI matrix from single SoT#419
Merged
Conversation
…ource of truth Adding a new tool used to require editing five surfaces with the same per-project shape: - 4 hook blocks in `.pre-commit-config.yaml` (ruff-check, ruff-format, mypy, pytest) - 1 matrix entry in `.github/workflows/tests.yml` - A standalone `uv.lock` in the tool dir - A duplicated `[dependency-groups] dev = [ruff, mypy, pytest, ...]` - Drift between pre-commit and tests.yml (3 tools had pre-commit coverage but no matrix entry; 0 tools had matrix entries with no pre-commit coverage; the privacy-llm renames made matching by name brittle) This refactors all of it to a single source of truth: the `[tool.uv.workspace] members` list in the root pyproject.toml. What changes: 1. **uv workspace.** Root pyproject.toml declares every Python project under `tools/` as a workspace member. The shared dev toolchain (`ruff`, `mypy`, `pytest`) moves to root `[dependency-groups] dev`; member pyproject's `dev` blocks are dropped. The 16 per-member `uv.lock` files collapse to one root `uv.lock`. Dependency resolution is now workspace-wide, which catches drift between tools' pins at sync time. 2. **Driver script.** `tools/dev/run-workspace-check.sh` lists workspace members from the root pyproject and runs the requested check in each via `uv run --directory`. Each member self-declares which checks apply via its own pyproject: - `[tool.ruff]` present → ruff and ruff-format run - `[tool.mypy]` present → mypy runs - `[tool.pytest.ini_options]` present → pytest runs Members can opt-out explicitly via `[tool.steward.checks] skip = [...]`. 3. **Pre-commit shrinks.** The ~340 lines of per-project hooks collapse to four workspace-level hooks calling the driver: `workspace-ruff-check`, `workspace-ruff-format`, `workspace-mypy`, `workspace-pytest`. Adding a tool requires ZERO `.pre-commit-config.yaml` edits. 4. **tests.yml matrix is dynamic.** A new `members` job emits the workspace members list as JSON (filtered to members with pytest); the `pytest` matrix consumes it via `fromJSON(needs.members.outputs.members)`. Adding a tool requires ZERO `.github/workflows/tests.yml` edits. 5. **One fix-along.** `tools/skill-evals/runner.py` had a B905 lint error (`zip()` without `strict=`) that wasn't enforced because no pre-commit ruff hook existed. The auto-discovery driver picks it up now; added `strict=False`. 6. **Format pass.** ruff format on `tools/skill-evals/` and `tools/spec-validator/` — they have `[tool.ruff]` configs but weren't enforced by the old hand-rolled pre-commit list. Net effect: `.pre-commit-config.yaml` 492 → 151 lines. `tests.yml` gains a discovery job and a `fromJSON` matrix, drops the 9-entry hand-list. Adding a workspace member is a one-line edit to the root pyproject. Coverage delta: - pytest: was 9 matrix entries; now 16 (every member with a `[tool.pytest.ini_options]` section runs in CI matrix). New: github-body-field, permission-audit, preflight-audit, pr-management-stats, skill-evals, spec-status-index, spec-validator. - ruff: was 11 per-project hooks; now 13 members (every with `[tool.ruff]`). New: skill-evals, spec-validator. - mypy: was 11; now 12. New: spec-status-index. The branch-protection contract is unchanged — `.asf.yaml` still requires only the `tests-ok` umbrella check, which `needs:` every matrix entry regardless of count or name.
…guard hook Three CI surfaces broke after the workspace refactor because the shared dev tools (ruff, mypy, pytest) now live in the root pyproject's `[dependency-groups] dev` instead of being duplicated in each member, and uv only installs them with an explicit `--group dev` sync. 1. `.github/workflows/tests.yml` — added a `uv sync --group dev` step before the per-matrix `uv run --directory <member> pytest`. The sync populates the shared workspace `.venv` with the dev tools so member runs find them. Dropped the `--group dev` flag from the per-member `uv run` since the group is no longer defined in the member's pyproject. 2. `.github/workflows/pre-commit.yml` — same sync step before `prek run`. The `workspace-*` hooks call the driver which `uv run --directory`s each member; without the prior sync the dev tools aren't in the venv. 3. `.github/workflows/sandbox-lint.yml` — switched from `uv run --project tools/sandbox-lint --group dev sandbox-lint` (fails: dev group not defined in member) to `uv sync --group dev` plus `uv run sandbox-lint` from workspace root. `sandbox-lint` is the member's `[project.scripts]` entry; the workspace sync puts it on PATH. Also adds the workspace-membership guard the user asked for: `tools/dev/check-workspace-members.py` walks `tools/*/pyproject.toml` and `tools/*/*/pyproject.toml` and compares against `[tool.uv.workspace] members` in the root pyproject. Exits non-zero with a diff if any on-disk project is missing from the list (silently skipped by every workspace surface) or any listed path no longer has a pyproject (stale entry). Wired in as a `check-workspace-members` prek hook that re-fires whenever any pyproject.toml at the relevant depth changes. Without this, a contributor adding `tools/<new>/pyproject.toml` and forgetting to extend the members list would get silent zero-coverage CI — the exact drift the workspace refactor was meant to eliminate.
…kspace venv sandbox-lint failed CI with 'Failed to spawn: sandbox-lint'. Root cause: `uv sync --group dev` from workspace root only installs the root project's deps, and the root has `package = false`. Member packages (and their `[project.scripts]` entry points like `sandbox-lint`) are skipped unless `--all-packages` is explicit. Locally this worked due to prior workspace state. Adding `--all-packages` to every workspace sync in tests.yml, pre-commit.yml, and sandbox-lint.yml.
5 tasks
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.
Why
Adding a new tool required editing five surfaces with the same per-project shape:
.pre-commit-config.yaml.github/workflows/tests.ymluv.lockper tool[dependency-groups] devin every member pyprojectWhat
This refactors all of it to a single source of truth: the
[tool.uv.workspace] memberslist in the rootpyproject.toml. Adding a tool is now one line.1. uv workspace
pyproject.tomldeclares every Python project undertools/as a workspace member.ruff,mypy,pytest) lives in root[dependency-groups] dev— members no longer duplicate it.uv.lockfiles collapse to one rootuv.lock. Dependency drift is caught at sync time.2. Driver script
tools/dev/run-workspace-check.shreads the workspace members list and runs the requested check in each viauv run --directory. Auto-discovery: each member's own pyproject tells the driver which checks apply:[tool.ruff]present → ruff + ruff-format run[tool.mypy]present → mypy runs[tool.pytest.ini_options]present → pytest runsMembers opt-out explicitly via
[tool.steward.checks] skip = [...].3. Pre-commit shrinks 492 → 151 lines
The ~340 lines of per-project hooks collapse to 4 workspace-level hooks calling the driver. Adding a tool requires zero
.pre-commit-config.yamledits.4. tests.yml matrix is dynamic
A new
membersjob emits the workspace members list as JSON; thepytestmatrix consumesfromJSON(needs.members.outputs.members). Adding a tool requires zero workflow edits.5. One fix-along
tools/skill-evals/runner.pyhad a B905 (zip()withoutstrict=) lint error that wasn't enforced because no pre-commit ruff hook existed for skill-evals. The auto-discovery driver picks it up; addedstrict=False.6. Format pass
ruff formatontools/skill-evals/andtools/spec-validator/— they had[tool.ruff]configs but weren't enforced by the old hand-rolled hook list.Coverage delta
[tool.pytest.ini_options])[tool.ruff])New tools picked up by pytest CI:
github-body-field,permission-audit,preflight-audit,pr-management-stats,skill-evals,spec-status-index,spec-validator.The branch-protection contract is unchanged —
.asf.yamlstill requires only thetests-okumbrella check, whichneeds:every matrix entry regardless of count or name.Diff scale
+1,097 / -4,975across 44 files. Most of the deletions are the 15 per-tooluv.lockfiles and the duplicateddevdeps in member pyprojects.Test plan
prek run --all-files— all 4 workspace hooks passtools/dev/run-workspace-check.sh ruff "ruff check"— 13 members passtools/dev/run-workspace-check.sh ruff-format "ruff format --check"— 13 members passtools/dev/run-workspace-check.sh mypy mypy— 12 members passtools/dev/run-workspace-check.sh pytest "pytest -q"— 16 members passuv syncat workspace root resolves cleanly (45 packages, 1 lock)tests-okmatrix expands to 16 entries;prekruns the 4 workspace hooks🤖 Generated with Claude Code