Coverage for src/rhiza_task/tasks/docker.py: 100%
26 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:13 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 04:13 +0000
1"""The container tasks: docker.mk, as tasks.
3Three wrappers over the docker CLI, and the shortest of the five fragments. The only
4thing worth stating is what the image is called: docker.mk defaults it to
5``$(shell basename $(CURDIR))``, so an unset :attr:`~rhiza_task.config.Config.docker_image`
6resolves to the repository directory's name here too -- moving a checkout would rename the
7image, which is surprising but is the behaviour consumers already have.
9``docker-build`` skips rather than fails on a missing Dockerfile, as the fragment does.
10That is not the same judgement as the tool guard's: a repository with no ``docker/``
11folder has adopted the bundle and not used it yet, whereas a machine with no docker
12cannot answer the question at all. Both are a skip, and ``--strict`` fails both.
13"""
15from __future__ import annotations
17from ..config import Config
18from ..spec import Guard, Skip, task
19from ..uv import tool
21SECTION = "Docker"
23HAVE_DOCKER = Guard(tool="docker", reason="docker not found; install from https://docs.docker.com/get-docker/")
26def image_name(cfg: Config) -> str:
27 """Return the tag to build and run.
29 Args:
30 cfg: The resolved config.
32 Returns:
33 The configured image name, or the repository directory's name.
34 """
35 return cfg.docker_image or cfg.root.name
38@task("docker-build", "build the Docker image", section=SECTION, guards=(HAVE_DOCKER,))
39def docker_build(cfg: Config) -> None:
40 """Build ``<docker_folder>/Dockerfile`` with the repository root as the context.
42 ``PYTHON_VERSION`` is passed as a build argument whatever the layer, as docker.mk
43 does. A Dockerfile that declares no such ``ARG`` gets a warning from docker and
44 nothing else, which is cheaper than making the flag conditional on a language.
46 Args:
47 cfg: The resolved config.
49 Raises:
50 Skip: When the folder holds no Dockerfile.
51 """
52 dockerfile = cfg.root / cfg.docker_folder / "Dockerfile"
53 if not dockerfile.is_file():
54 raise Skip(f"no {cfg.docker_folder}/Dockerfile")
56 tag = f"{image_name(cfg)}:latest"
57 print(f"[INFO] building {tag} with Python {cfg.python_version}")
58 tool(
59 "docker",
60 "buildx",
61 "build",
62 "--file",
63 f"{cfg.docker_folder}/Dockerfile",
64 "--build-arg",
65 f"PYTHON_VERSION={cfg.python_version}",
66 "--tag",
67 tag,
68 "--load",
69 ".",
70 cwd=cfg.root,
71 )
74@task("docker-run", "run the Docker container", section=SECTION, needs=("docker-build",), guards=(HAVE_DOCKER,))
75def docker_run(cfg: Config) -> None:
76 """Run the built image interactively, removing the container on exit.
78 Args:
79 cfg: The resolved config.
80 """
81 tag = f"{image_name(cfg)}:latest"
82 print(f"[INFO] running {tag}")
83 tool("docker", "run", "--rm", "-it", tag, cwd=cfg.root)
86@task("docker-clean", "remove the Docker image", section=SECTION, guards=(HAVE_DOCKER,))
87def docker_clean(cfg: Config) -> None:
88 """Delete the image, tolerating its absence.
90 ``check=False`` is docker.mk's ``2>/dev/null || true``: removing an image that was
91 never built is the expected state of a clean target, not a failure.
93 Args:
94 cfg: The resolved config.
95 """
96 tag = f"{image_name(cfg)}:latest"
97 print(f"[INFO] removing {tag}")
98 tool("docker", "rmi", tag, cwd=cfg.root, check=False)