"""Rules about the image the app ships in, read from the Dockerfile. The base was pinned by digest alone, so Dependabot followed `latest`, which is the full Python image: 415 MB compressed against 43 MB, with compilers, git and curl in what ships. It went from slim to full three times, each through a pull request titled with nothing but two short digests. And with compilers in the test stage, a dependency that quietly started compiling from source still passed. """ from __future__ import annotations import re from pathlib import Path DOCKERFILE = (Path(__file__).resolve().parent.parent / "Dockerfile").read_text(encoding="utf-8") # Every image the build pulls: `FROM image` and `COPY --from=image`. PULLED = re.findall(r"^(?:FROM|COPY --from=)\s*(\S+)", DOCKERFILE, re.M) IMAGES = [ref for ref in PULLED if "/" in ref or "@" in ref or ":" in ref] def test_the_python_image_is_slim(): python = [ref for ref in IMAGES if "/python" in ref] assert python assert all(re.search(r"/python:3\.\d+-slim@sha256:[0-9a-f]{64}$", ref) for ref in python), python def test_every_pinned_image_names_the_tag_it_follows(): """A digest alone gives Dependabot nothing to follow but `latest`. The tag beside it is what an update keeps to, and what a reviewer reads.""" pinned = [ref for ref in IMAGES if "@sha256:" in ref] assert pinned assert [ref for ref in pinned if not re.search(r":[\w.-]+@sha256:", ref)] == [] def test_docker_hub_images_are_named_as_docker_hubs(): """Dependabot reads an image's tags from the registry its FROM line names, and mirror.gcr.io lists only the tags someone has already pulled through it. On 2026-11-01 Dependabot moved python:3.14-slim's digest but proposed no 3.15-slim, eleven days after 3.15.0 came out. Docker Hub's images are named as Docker Hub's; CI still pulls them through the mirror (see tests/test_workflows.py).""" assert IMAGES assert [ref for ref in IMAGES if ref.startswith("mirror.gcr.io/")] == [] def test_every_image_is_on_a_from_line(): """Dependabot reads FROM lines and nothing else. uv was copied straight out of its image, so it was never proposed an update and stayed at the version of the first commit.""" copied = re.findall(r"^COPY --from=(\S+)", DOCKERFILE, re.M) stages = re.findall(r"^FROM\s+\S+\s+AS\s+(\S+)", DOCKERFILE, re.M) assert copied assert [name for name in copied if name not in stages] == [] def _instructions(text: str) -> str: """The Dockerfile without its comments.""" return "\n".join(line for line in text.splitlines() if not line.lstrip().startswith("#")) SHIPPED = _instructions(DOCKERFILE[DOCKERFILE.index(" AS base"):]) def test_git_and_uv_stay_in_the_build_stages(): """Needed to make the virtualenv, not to run the app: git fetches appcore, uv installs from the lock. Neither reaches base, test or runtime.""" assert "/uv" not in SHIPPED and "uv sync" not in SHIPPED assert not re.search(r"apt-get install[^\n]*\bgit\b", SHIPPED) def test_the_app_cannot_rewrite_its_own_code(): """The code and the virtualenv were the runtime user's, so the process could change them. It owns what it writes, /data and /config, and no more; and never by a recursive chown, which once doubled the image. The test stage, which never ships, may give the suite its own files.""" base = DOCKERFILE[DOCKERFILE.index(" AS base"):DOCKERFILE.index(" AS test")] instructions = _instructions(base + DOCKERFILE[DOCKERFILE.index(" AS runtime"):]) assert "--chown" not in instructions assert "chown -R" not in instructions assert re.search(r"chown 1000:1000 /data /config\b", instructions) def test_the_running_image_says_whether_it_is_healthy(): runtime = _instructions(DOCKERFILE[DOCKERFILE.index(" AS runtime"):]) assert re.search(r"^HEALTHCHECK .*\n\s+CMD .*/healthz", runtime, re.M) assert re.search(r"^USER 1000:1000$", runtime, re.M) def test_the_build_is_not_sent_a_virtualenv(): ignored = (Path(__file__).resolve().parent.parent / ".dockerignore").read_text( encoding="utf-8").split() assert {".venv", ".git", "**/__pycache__"} <= set(ignored) def test_every_builder_image_a_workflow_uses_is_pinned(): """`binfmt:latest` and `buildkit:buildx-stable-1` floated: the image that builds the image changed underneath every release.""" workflows = Path(__file__).resolve().parent.parent / ".github" / "workflows" images = [match for path in sorted(workflows.glob("*.yml")) for match in re.findall(r"image[:=]\s*(\S*(?:binfmt|buildkit)\S*)", path.read_text(encoding="utf-8"))] assert len(images) == 6 assert [ref for ref in images if not re.search(r":[\w.-]+@sha256:[0-9a-f]{64}$", ref)] == [] def test_the_image_ships_what_the_migration_commands_need(): """docs/reference/cli.md runs `alembic -c alembic.ini` inside the container, and the image had no alembic.ini.""" base = _instructions(DOCKERFILE[DOCKERFILE.index(" AS base"):DOCKERFILE.index(" AS test")]) assert re.search(r"^COPY .*\balembic\.ini\b", base, re.M)