diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7d8755c2..cab509e8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -31,6 +31,19 @@ updates: groups: npm-minor-and-patch: update-types: ["minor", "patch"] + # The infra images the release publishes. Each base is pinned by digest, and a release rebuilds an image only when its + # sources change, so a bump here is what picks up a base's fixes: the next release builds and publishes the image. + - package-ecosystem: "docker" + directories: + - "/src/agent_env/env/gateway" + - "/src/agent_env/env/envs/service_db" + - "/src/agent_env/env/envs/website_browser" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + groups: + infra-base-images: + patterns: ["*"] - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/AGENTS.md b/AGENTS.md index 7a6e8d2b..2be17214 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -281,7 +281,8 @@ package to PyPI. Run by hand on `main`, the workflow publishes the images alone. Its jobs that run agent-env (`.github/scripts/infra_images.py`) hold no token that writes; the jobs that push images or publish to PyPI install nothing and check what they're handed (`.github/scripts/infra_build.py`, standard library and the docker CLI only). -Actions are pinned to commit SHAs; Dependabot updates them, the pinned uv, `uv.lock` and the UI's npm packages. +Actions are pinned to commit SHAs; Dependabot updates them, the pinned uv, `uv.lock`, the UI's npm packages and +the infra images' base images, each pinned by digest so that a bump rebuilds and republishes the image. ## Conventions diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee5848fc..cfd53839 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,6 +91,8 @@ the previous release, so a clear pull request title is also the release note. A A release also publishes the infra images agent-env ships, once the `PUBLISH_INFRA_IMAGES` repository variable is `true`: one per Dockerfile under `src/agent_env/env`, at `ghcr.io/scaleapi/agentenv-:src-` for linux/amd64 and linux/arm64, where the tag is the build context's source digest, so an unchanged Dockerfile is not built again. +Each of them pins its base images by digest, and Dependabot bumps those pins, so a base's fixes reach the published +image: a bump changes the sources, and the next release builds it again. Installs pull them instead of building the infra images: a run on local stores registers a kind's images when GHCR has every one for the sources installed, by the digest the wheel records or, with no record, the one GHCR serves, and builds them otherwise; a sandbox that can't pull one builds it from its build context. The wheel and the release notes diff --git a/src/agent_env/env/envs/service_db/Dockerfile b/src/agent_env/env/envs/service_db/Dockerfile index b0f0aff8..4ae33d5a 100644 --- a/src/agent_env/env/envs/service_db/Dockerfile +++ b/src/agent_env/env/envs/service_db/Dockerfile @@ -1,4 +1,4 @@ -FROM public.ecr.aws/docker/library/postgres:16-alpine +FROM public.ecr.aws/docker/library/postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea # ServiceDBEnv - PostgreSQL database for MCP services # diff --git a/src/agent_env/env/envs/service_db/Dockerfile.db-mcp b/src/agent_env/env/envs/service_db/Dockerfile.db-mcp index 5b71e411..2ee43e67 100644 --- a/src/agent_env/env/envs/service_db/Dockerfile.db-mcp +++ b/src/agent_env/env/envs/service_db/Dockerfile.db-mcp @@ -2,7 +2,7 @@ # The Docker Hub image (crystaldba/postgres-mcp:latest) ships MCP 1.6.0, # which only supports stdio and SSE transports. -FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder +FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim@sha256:e5b65587bce7de595f299855d7385fe7fca39b8a74baa261ba1b7147afa78e58 AS builder ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy ENV UV_PYTHON_DOWNLOADS=0 @@ -24,7 +24,7 @@ RUN sed -i 's/mcp = FastMCP("postgres-mcp")/from mcp.server.transport_security i RUN uv sync --frozen --no-dev -FROM public.ecr.aws/docker/library/python:3.12-slim-bookworm +FROM public.ecr.aws/docker/library/python:3.12-slim-bookworm@sha256:6a0e06f5ba0b373d051aabd3fffb9b873f27f9a2a3a14bcbe37dee6f0e3bbc70 RUN groupadd -r app && useradd -r -g app app COPY --from=builder --chown=app:app /app /app diff --git a/tst/unit/test_release_infra_images.py b/tst/unit/test_release_infra_images.py index 64aada90..9e90f873 100644 --- a/tst/unit/test_release_infra_images.py +++ b/tst/unit/test_release_infra_images.py @@ -9,6 +9,7 @@ import pytest import yaml +from agent_env.env import bootstrap from agent_env.env.published_images import RELEASE_FILE REPO = Path(__file__).resolve().parents[2] @@ -135,3 +136,18 @@ def test_what_a_registry_can_rate_limit_is_tried_again(): assert 'infra_build.py retry docker push "$BUILT"' in build assert "infra_build.py retry docker buildx imagetools create" in publish assert JOBS["infra-build"]["strategy"]["max-parallel"] <= 4 + + +def test_every_infra_image_pins_its_bases_by_digest_and_dependabot_bumps_them(): + """A release rebuilds an image only when its sources change, so a base behind a bare tag never picks up its + fixes; pinned, the pin is a source, and Dependabot bumping it is what rebuilds and republishes the image.""" + dependabot = yaml.safe_load((REPO / ".github" / "dependabot.yml").read_text()) + (docker,) = [update for update in dependabot["updates"] if update["package-ecosystem"] == "docker"] + + for image in bootstrap.PUBLISHED_IMAGES: + text = image.dockerfile.read_text() + stages = set(re.findall(r"^FROM\s+\S+\s+AS\s+(\S+)", text, re.IGNORECASE | re.MULTILINE)) + bases = [ref for ref in re.findall(r"^FROM\s+(?:--\S+\s+)*(\S+)", text, re.IGNORECASE | re.MULTILINE) + if ref not in stages] + assert bases and all(re.search(r"@sha256:[0-9a-f]{64}$", ref) for ref in bases), (image.name, bases) + assert f"/{image.dockerfile.parent.relative_to(REPO)}" in docker["directories"], image.name