rhiza-task
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/4mutation— run/html/move/results, reporting the first statusdoctor— semantic version comparison, formerly an awk function inside a make recipebook— 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.toml → RHIZA_* 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
- Double-colon rules disappear. book.mk declares
test:: ; @:no-op stubs sobookcan depend on gates thetestsbundle may not have contributed. Here that question is"test" in REGISTRY— four stubs and the whole::mechanism gone. - Skip is a first-class outcome. jointview's own Makefile complains that an excluded
folder leaves "a green gate measuring nothing".
--strictturns every skip into a failure, so CI can assert a gate actually measured something. - 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.
- Ship this package; consumers replace the synced make layer with
rhiza-task shim. template.ymlexcludes.rhiza/make.dand.rhiza/rhiza.mk, exactly as it already excludes.rhiza/tests.- Bump the reusable workflows to invoke
uvx rhiza-taskdirectly, with oneastral-sh/setup-uvstep in place ofinstall-uv. - Second pass: retire
github.mkand folddoctorinto the release checklist.
Open questions
- Rust and Go layers.
rust.mk/go.mkship the same target names with different recipes and today need only make. Shelling out tocargofrom 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 testthen internallyuv 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7b6a936630581305c85bbcfafab645826532ed92fb85dec2de370ab4dbf31ab3
|
|
| MD5 |
a11f1e09ffddf1dc5a2ae3483e1d1ac1
|
|
| BLAKE2b-256 |
359722c67b3e2f31f608125b7fd26b2d1c052e86aee2c9e719a608666230ff7c
|
Provenance
The following attestation bundles were made for rhiza_task-0.1.2.tar.gz:
Publisher:
rhiza_release.yml on Jebel-Quant/rhiza-task
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rhiza_task-0.1.2.tar.gz -
Subject digest:
7b6a936630581305c85bbcfafab645826532ed92fb85dec2de370ab4dbf31ab3 - Sigstore transparency entry: 2501998698
- Sigstore integration time:
-
Permalink:
Jebel-Quant/rhiza-task@64b6afe3f9fcaf184bfa553c93f7ffb132f4d8c6 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/Jebel-Quant
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
rhiza_release.yml@64b6afe3f9fcaf184bfa553c93f7ffb132f4d8c6 -
Trigger Event:
push
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82226ed250d7686b7d17ae459cacd1a73cb687eff797cf9627d84076ea5a45f0
|
|
| MD5 |
8166dcb88c01a809e4b46e436588fea3
|
|
| BLAKE2b-256 |
3612cf364814f225fa9b6b3d706b1228b94ebd35d89651786c3763626cd12112
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
rhiza_task-0.1.2-py3-none-any.whl -
Subject digest:
82226ed250d7686b7d17ae459cacd1a73cb687eff797cf9627d84076ea5a45f0 - Sigstore transparency entry: 2501998709
- Sigstore integration time:
-
Permalink:
Jebel-Quant/rhiza-task@64b6afe3f9fcaf184bfa553c93f7ffb132f4d8c6 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/Jebel-Quant
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
rhiza_release.yml@64b6afe3f9fcaf184bfa553c93f7ffb132f4d8c6 -
Trigger Event:
push
-
Statement type: