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.

An empty value is unset in the two string-valued layers: RHIZA_CI_OS_MATRIX= in .rhiza/.env, or an exported empty string, leaves the layer below it alone rather than resolving to "". That is make's $(or ...) rule, and the reusable workflows depend on it — rhiza_ci.yml exports one RHIZA_CI_OS_MATRIX for every caller and deliberately leaves it empty for consumers, whose own .rhiza/.env is meant to answer.

[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.2.tar.gz (79.0 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.2-py3-none-any.whl (38.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: rhiza_task-0.1.2.tar.gz
  • Upload date:
  • Size: 79.0 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.2.tar.gz
Algorithm Hash digest
SHA256 7b6a936630581305c85bbcfafab645826532ed92fb85dec2de370ab4dbf31ab3
MD5 a11f1e09ffddf1dc5a2ae3483e1d1ac1
BLAKE2b-256 359722c67b3e2f31f608125b7fd26b2d1c052e86aee2c9e719a608666230ff7c

See more details on using hashes here.

Provenance

The following attestation bundles were made for rhiza_task-0.1.2.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.2-py3-none-any.whl.

File metadata

  • Download URL: rhiza_task-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 38.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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 82226ed250d7686b7d17ae459cacd1a73cb687eff797cf9627d84076ea5a45f0
MD5 8166dcb88c01a809e4b46e436588fea3
BLAKE2b-256 3612cf364814f225fa9b6b3d706b1228b94ebd35d89651786c3763626cd12112

See more details on using hashes here.

Provenance

The following attestation bundles were made for rhiza_task-0.1.2-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

This release

0.1.2 This release

2 files

0.1.1

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