Skip to content

refactor: uv workspace — DRY pre-commit + CI matrix from single SoT#419

Merged
potiuk merged 3 commits into
apache:mainfrom
potiuk:refactor-uv-workspace-dry
May 31, 2026
Merged

refactor: uv workspace — DRY pre-commit + CI matrix from single SoT#419
potiuk merged 3 commits into
apache:mainfrom
potiuk:refactor-uv-workspace-dry

Conversation

@potiuk
Copy link
Copy Markdown
Member

@potiuk potiuk commented May 31, 2026

Why

Adding a new tool required editing five surfaces with the same per-project shape:

What

This refactors all of it to a single source of truth: the [tool.uv.workspace] members list in the root pyproject.toml. Adding a tool is now one line.

1. uv workspace

  • Root pyproject.toml declares every Python project under tools/ as a workspace member.
  • The shared dev toolchain (ruff, mypy, pytest) lives in root [dependency-groups] dev — members no longer duplicate it.
  • 16 per-member uv.lock files collapse to one root uv.lock. Dependency drift is caught at sync time.

2. Driver script

tools/dev/run-workspace-check.sh reads the workspace members list and runs the requested check in each via uv 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 runs

Members 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.yaml edits.

4. tests.yml matrix is dynamic

A new members job emits the workspace members list as JSON; the pytest matrix consumes fromJSON(needs.members.outputs.members). Adding a tool requires zero workflow edits.

5. One fix-along

tools/skill-evals/runner.py had a B905 (zip() without strict=) lint error that wasn't enforced because no pre-commit ruff hook existed for skill-evals. The auto-discovery driver picks it up; added strict=False.

6. Format pass

ruff format on tools/skill-evals/ and tools/spec-validator/ — they had [tool.ruff] configs but weren't enforced by the old hand-rolled hook list.

Coverage delta

Check Was Now
pytest matrix 9 entries 16 (every member with [tool.pytest.ini_options])
ruff hooks 11 tools 13 (every member with [tool.ruff])
mypy hooks 11 tools 12

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.yaml still requires only the tests-ok umbrella check, which needs: every matrix entry regardless of count or name.

Diff scale

+1,097 / -4,975 across 44 files. Most of the deletions are the 15 per-tool uv.lock files and the duplicated dev deps in member pyprojects.

Test plan

  • prek run --all-files — all 4 workspace hooks pass
  • tools/dev/run-workspace-check.sh ruff "ruff check" — 13 members pass
  • tools/dev/run-workspace-check.sh ruff-format "ruff format --check" — 13 members pass
  • tools/dev/run-workspace-check.sh mypy mypy — 12 members pass
  • tools/dev/run-workspace-check.sh pytest "pytest -q" — 16 members pass
  • YAML syntax check on edited workflows
  • uv sync at workspace root resolves cleanly (45 packages, 1 lock)
  • CI: tests-ok matrix expands to 16 entries; prek runs the 4 workspace hooks
  • CI: real-PR validation that branch protection still passes

🤖 Generated with Claude Code

potiuk added 3 commits May 31, 2026 17:19
…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.
@potiuk potiuk merged commit 46bee34 into apache:main May 31, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant