Skip to main content

rhiza-task

License: MIT

The rhiza developer tasks as a pinned CLI rather than a synced make layer.

uvx rhiza-task@0.1.0 test

Sibling to pytest-rhiza, which did the same thing for .rhiza/tests.

Why

The pain was never make's syntax — it was distribution by copying, which make structurally cannot fix, because include cannot reach a remote file. Every consumer got a full copy at a template tag, and everything downstream was damage control.

before, per consumer repo after
.rhiza/rhiza.mk — 200 lines, synced gone
.rhiza/make.d/*.mk — 823 lines in 10 files, synced gone
exclude: entries in template.yml, because "a deletion alone is undone by the next sync" not needed
targets shadowed in the repo Makefile, make printing overriding commands for target as the mechanism working [tool.rhiza-task]
~40 lines of GNU-make guard and Windows POSIX-shell probe gone — no make, no shell
install-uv curling astral.sh/uv/install.sh into ./bin gone — running under uvx means uv exists
Makefile 6-line shim, generated by rhiza-task shim

Version pinning becomes a dependency pin, which is a real mechanism instead of "copy files at tag v1.3.3 and hope nobody edited them."

Install

Nothing to install. uvx provisions it per invocation:

uvx rhiza-task@0.1.0 list          # what is available
uvx rhiza-task@0.1.0 all           # every gate, as CI runs them
uvx rhiza-task@0.1.0 test --strict # fail rather than skip when a gate measures nothing

For a consumer repository, generate the shim once:

uvx rhiza-task@0.1.0 shim > Makefile

make test, make book and the rest keep working — the shim forwards every target to the pinned CLI, so a consumer still on an older reusable workflow needs no change.

Tasks

section tasks
Python install test typecheck security deps license docs-coverage all
Quality fmt semgrep rhiza-test test-pyproject todos
Testing extras benchmark hypothesis-test stress mutation
Book book serve marimo marimo-validate
Dev doctor clean

Not ported: github.mk's seven gh wrappers — gh pr list is shorter than make view-prs and always current — and install-uv, which the uvx entry point makes unnecessary.

Design

Reading all ten make fragments back to back, every recipe has the same three parts: a guard on a folder existing, a provision via uv run --with or uvx, and a long, mostly static argument list. So the model is declarative, with an escape hatch for the four recipes that genuinely are not:

  • test — retry once on pytest exit 3 (xdist teardown race), never on 1/2/4
  • mutation — run/html/move/results, reporting the first status
  • doctor — semantic version comparison, formerly an awk function inside a make recipe
  • book — aggregate gates, copy reports, export notebooks, build, badge
module what
spec.py Task, Guard, Skip/Failed, the @task registry
config.py five-layer resolution, replacing ?= and +=
uv.py the three ways rhiza reaches a tool
runner.py prerequisite dedup, guards, outcome bookkeeping
cli.py Typer app, generated from the registry
tasks/*.py the gates themselves, loaded by entry point

Configuration

Five layers, lowest precedence first: dataclass defaults → .rhiza/.env (kept unchanged) → [tool.rhiza-task] in pyproject.tomlRHIZA_* or bare make-style environment variables → command-line flags.

[tool.rhiza-task]
source_folder = "src"
typechecker = "ty"
coverage_fail_under = 95
license_ignore_packages = ["docutils"]

The += accumulators (DEPTRY_FOLDERS, LICENSE_IGNORE_PACKAGES, RHIZA_CHECKS) have no successor and need none: each was a bundle contributing something it owned, which a task body now derives by asking whether the contributing task is registered. See deps and license in tasks/python.py.

Three things that fall out for free

  1. Double-colon rules disappear. book.mk declares test:: ; @: no-op stubs so book can depend on gates the tests bundle may not have contributed. Here that question is "test" in REGISTRY — four stubs and the whole :: mechanism gone.
  2. Skip is a first-class outcome. jointview's own Makefile complains that an excluded folder leaves "a green gate measuring nothing". --strict turns every skip into a failure, so CI can assert a gate actually measured something.
  3. Help stops being a parser. rhiza.mk runs awk over $(MAKEFILE_LIST) hunting ## and ##@ comments. Typer has descriptions natively, from the same registry the runner uses, so they cannot drift.

Adding a task

Register a module under the rhiza_task.tasks entry-point group — the same mechanism the built-ins use, so a project's own task is a first-class citizen rather than an override. That replaces -include local.mk. Repo-specific one-offs can also just stay in the Makefile, where an explicit rule beats the shim's catch-all.

from rhiza_task.spec import Guard, task
from rhiza_task.uv import uvx


@task("audit", "run the in-house audit", section="Quality", needs=("install",), guards=(Guard("source_folder"),))
def audit(cfg):
    """Audit the source tree."""
    uvx("my-auditor", cfg.source_folder, cwd=cfg.root)

Why not a Taskfile (or just)

Considered and rejected. go-task is a genuinely better make — real deps:, desc: giving task --list for free, and preconditions:/status: that express Guard declaratively. Its remote includes would even attack the same root problem.

Two reasons against. First, that feature is experimental and env-var-gated, and it would be the single load-bearing dependency of the whole multi-repo task layer, whereas uvx pkg@version is boring, stable and already used ~15 times per repo. Second, the four recipes listed above are procedural; in YAML they stay embedded shell, which improves the syntax around the mess without removing it — and embedded shell keeps the Windows problem too.

just and poe don't apply: a Justfile or a noxfile still has to be copied into every repo, which is the problem being deleted.

Migration

Make target names are the interface between the reusable workflows and the consumer checkout — rhiza_ci.yml alone calls make test, typecheck, deps, fmt, docs-coverage, security, license, rhiza-test. Consumers pin @v1.3.3, so old pins keep calling make forever. Hence the shim, and hence task names identical to the retired target names.

  1. Ship this package; consumers replace the synced make layer with rhiza-task shim.
  2. template.yml excludes .rhiza/make.d and .rhiza/rhiza.mk, exactly as it already excludes .rhiza/tests.
  3. Bump the reusable workflows to invoke uvx rhiza-task directly, with one astral-sh/setup-uv step in place of install-uv.
  4. Second pass: retire github.mk and fold doctor into the release checklist.

Open questions

  • Rust and Go layers. rust.mk/go.mk ship the same target names with different recipes and today need only make. Shelling out to cargo from here works, but it makes Python a prerequisite for a Rust repo. This is the one place the Taskfile argument stays strong.
  • Nested uv cost. uvx rhiza-task test then internally uv run --with pytest .... Cached this should be milliseconds; measure before rolling out widely.

Development

uv sync --all-groups
uv run pytest

No test in the suite runs uv. Every task test patches the three entry points in uv.py and asserts on the argument vector that would have been executed — which is exactly what the make recipes expressed in $$-escaped shell, and could not assert.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rhiza_task-0.1.1.tar.gz (77.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rhiza_task-0.1.1-py3-none-any.whl (37.7 kB view details)

Uploaded Python 3

File details

Details for the file rhiza_task-0.1.1.tar.gz.

File metadata

  • Download URL: rhiza_task-0.1.1.tar.gz
  • Upload date:
  • Size: 77.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for rhiza_task-0.1.1.tar.gz
Algorithm Hash digest
SHA256 29d5f002c6ff9ce6560a36c6aeb00bc31806159dc0427bf848a82d7be076d230
MD5 ddb5f16ee41534dc610b45d1aeab337a
BLAKE2b-256 4d3dde0e593e5b2fe0bbfc6c518feeeff1e4c904be400ba1d57301429ba4e833

See more details on using hashes here.

Provenance

The following attestation bundles were made for rhiza_task-0.1.1.tar.gz:

Publisher: rhiza_release.yml on Jebel-Quant/rhiza-task

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rhiza_task-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: rhiza_task-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 37.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for rhiza_task-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 18089690903224f5b2b65e1a1f093ce7d56f2e43803f816b65017ea2a99fe431
MD5 6f5955e28801cebaadd36f36b6f2bb20
BLAKE2b-256 0473efe681ad1acf9c1c154d6828391a2424cdf5cb459f2ac1c2f821b49b9ff4

See more details on using hashes here.

Provenance

The following attestation bundles were made for rhiza_task-0.1.1-py3-none-any.whl:

Publisher: rhiza_release.yml on Jebel-Quant/rhiza-task

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.4.0

2 files

1.3.1

2 files

1.3.0

2 files

1.2.0

2 files

1.1.0

2 files

1.0.0

2 files

0.3.1

2 files

0.3.0

2 files

0.2.0

2 files

0.1.2

2 files

This release

0.1.1 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page