Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ updates:
- "/src/agent_env/env/gateway"
- "/src/agent_env/env/envs/service_db"
- "/src/agent_env/env/envs/website_browser"
- "/src/agent_env/agents/agents/claude-code"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
Expand Down
14 changes: 9 additions & 5 deletions .github/scripts/infra_build.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""What the release workflow's jobs that hold a token to push images or publish to PyPI run for the infra images. It uses
only the standard library and the docker CLI, so those jobs install nothing; what they're handed by the jobs that run
agent-env (``infra_images.py``) is checked against the images agent-env ships before it's built, pushed or recorded.
"""What the release workflow's jobs that hold a token to push images or publish to PyPI run for the images agent-env
publishes, its infra images and the Claude Code agent's. It uses only the standard library and the docker CLI, so those
jobs install nothing; what they're handed by the jobs that run agent-env (``infra_images.py``) is checked against the
images agent-env ships before it's built, pushed or recorded.

``check ENTRY`` fails unless ENTRY, a JSON object as ``infra_images.py plan`` lists an image, names an image agent-env
ships, with its Dockerfile, context, repository and a source tag. ``exists REF`` exits 0 when the registry has REF, 1
Expand Down Expand Up @@ -29,6 +30,7 @@
"agentenv-db-web": "src/agent_env/env/envs/service_db/Dockerfile.db-web",
"agentenv-db-mcp": "src/agent_env/env/envs/service_db/Dockerfile.db-mcp",
"agentenv-website-browser": "src/agent_env/env/envs/website_browser/Dockerfile",
"agentenv-claude-code": "src/agent_env/agents/agents/claude-code/Dockerfile",
}
_TAG = re.compile(r"^src-[0-9a-f]{64}$")
_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
Expand All @@ -42,15 +44,17 @@
# How long to wait before each try again, so a burst of builds starting together spreads out.
_RATE_LIMIT_WAITS_SECONDS = (30, 60, 120)

# Each image's check, beyond its build: the gateway imports its server, Postgres turns healthy, and the sidecars and the
# browser answer their own commands.
# Each image's check, beyond its build: the gateway imports its server, Postgres turns healthy, the sidecars and the
# browser answer their own commands, and Claude Code's agent runs its CLI and imports its server.
_SMOKES = {
"agentenv-gateway": ["docker", "run", "--rm", "--entrypoint", "python", "{image}", "-c", "import gateway.entrypoint"],
"agentenv-db-web": ["docker", "run", "--rm", "--entrypoint", "/usr/bin/pgweb", "{image}", "--version"],
"agentenv-db-mcp": ["docker", "run", "--rm", "{image}", "--help"],
"agentenv-website-browser": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c",
'npx --no-install @playwright/mcp@"$PLAYWRIGHT_MCP_VERSION" --help >/dev/null'
" && ls -d /root/.cache/ms-playwright/chromium-*"],
"agentenv-claude-code": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c",
'claude --version && python -c "import a2a_server"'],
}


Expand Down
20 changes: 13 additions & 7 deletions .github/scripts/infra_images.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""The infra images agent-env publishes (``agent_env.env.published_images``), for the release workflow's jobs that only
read: this imports agent-env, so it runs where agent-env and its dependencies are installed, and no job that holds a
token to push images or publish to PyPI runs it (those run ``infra_build.py``).
"""The images agent-env publishes, its infra images and the Claude Code agent's (``agent_env.env.published_images``), for
the release workflow's jobs that only read: this imports agent-env, so it runs where agent-env and its dependencies are
installed, and no job that holds a token to push images or publish to PyPI runs it (those run ``infra_build.py``).

``plan`` prints each image's name, repository, tag, Dockerfile and context as JSON, ``--missing`` only those the
registry hasn't published: it has no image under the tag, or refuses to show one, as GHCR does for a package that
doesn't exist yet, so the first publish can start. ``write PATH`` records the digest the registry serves under every
doesn't exist yet, so the first publish can start. ``--only infra`` lists only the infra images, and ``--only agents``
only the agents' images, which install the agentenv-framework-protocol a release ships with, so a release builds them
after it publishes the protocol. ``write PATH`` records the digest the registry serves under every
image's tag as a release wheel carries them, and fails if any is missing. ``public`` warns about each image a user, who
pulls anonymously, can't. The registry is read with ``GHCR_TOKEN`` (as ``GITHUB_ACTOR``) when it's set, anonymously
otherwise, and always anonymously by ``public``.
Expand All @@ -19,22 +21,26 @@
from pathlib import Path

from agent_env.env.bootstrap import PUBLISHED_IMAGES
from agent_env.env.published_images import REGISTRY, PublishedImage, write_release_digests
from agent_env.env.published_images import AGENT_IMAGES, REGISTRY, PublishedImage, write_release_digests
from agent_env.store.image_store.oci_registry_credentials import RegistryAuth
from agent_env.store.image_store.registry_api import NoSuchImageError, RegistryRefusedError, pin_digest

REPO = Path(__file__).resolve().parents[2]
_GROUPS = {"infra": tuple(image for image in PUBLISHED_IMAGES if image not in AGENT_IMAGES), "agents": AGENT_IMAGES}


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("plan").add_argument("--missing", action="store_true")
plan = commands.add_parser("plan")
plan.add_argument("--missing", action="store_true")
plan.add_argument("--only", choices=sorted(_GROUPS))
commands.add_parser("write").add_argument("path", type=Path)
commands.add_parser("public")
args = parser.parse_args()
if args.command == "plan":
print(json.dumps([_described(image) for image in PUBLISHED_IMAGES
images = _GROUPS[args.only] if args.only else PUBLISHED_IMAGES
print(json.dumps([_described(image) for image in images
if not args.missing or _published_digest(image) is None]))
return 0
if args.command == "public":
Expand Down
52 changes: 49 additions & 3 deletions .github/scripts/shipped_bundles.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
"""The bundles agentenv-framework ships, from its ``agent_env.bundles`` entry points: ``<name> = "<package>"`` ships the
folder ``src/<package as a path>/<name>/``. Run, it prints each shipped env's, agent's and artifact's requirements.txt,
the files a release pins agentenv-framework-protocol in."""
folder ``src/<package as a path>/<name>/``.

``pin`` pins agentenv-framework-protocol, keeping its extras, in each shipped env's, agent's and artifact's
requirements.txt to the version ``packages/agentenv-protocol`` is at, which a release ships them with. Every release job
that reads those folders runs it first, so an image's tag, its build and the wheel all see the same sources. It uses only
the standard library, so the jobs that push images can run it.
"""

from __future__ import annotations

import argparse
import re
import sys
import tomllib
from pathlib import Path

REPO = Path(__file__).resolve().parents[2]
BUNDLES = "agent_env.bundles"
PROTOCOL = "agentenv-framework-protocol"
# A requirement on the protocol, with its extras, and not on a package whose name starts with the protocol's.
_PROTOCOL_REQUIREMENT = re.compile(rf"^{PROTOCOL}(\[[^\]]*\])?(?![\w.-])")


def bundle_roots(repo: Path = REPO) -> dict[str, Path]:
Expand All @@ -23,5 +34,40 @@ def requirements_files(repo: Path = REPO) -> list[Path]:
return sorted(path for root in bundle_roots(repo).values() for path in root.glob("*/*/requirements.txt"))


def protocol_version(repo: Path = REPO) -> str:
return tomllib.loads((repo / "packages" / "agentenv-protocol" / "pyproject.toml").read_text())["project"]["version"]


def pin(repo: Path = REPO) -> list[Path]:
"""Pin the protocol in each shipped requirements file that names it, and return those files."""
files = requirements_files(repo)
if not files:
raise ValueError(f"the bundles {repo / 'pyproject.toml'} declares hold no requirements.txt")
version, pinned = protocol_version(repo), []
for path in files:
lines = path.read_text().splitlines(keepends=True)
if any(_PROTOCOL_REQUIREMENT.match(line) for line in lines):
path.write_text("".join(_pinned(line, version) for line in lines))
pinned.append(path)
return pinned


def _pinned(line: str, version: str) -> str:
if not (match := _PROTOCOL_REQUIREMENT.match(line)):
return line
ending = line[len(line.rstrip("\r\n")):]
return f"{PROTOCOL}{match.group(1) or ''}=={version}{ending}"


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_subparsers(dest="command", required=True).add_parser("pin")
parser.parse_args()
version = protocol_version()
for path in pin():
print(f"pinned {PROTOCOL}=={version} in {path.relative_to(REPO).as_posix()}")
return 0


if __name__ == "__main__":
print("\n".join(path.relative_to(REPO).as_posix() for path in requirements_files()))
sys.exit(main())
Loading
Loading