feat(release): pin first-party agent images
lint / lint (push) Successful in 1m15s
test / image-input-builds (pull_request) Successful in 1m12s
test / integration-docker (pull_request) Failing after 2m47s
tracker-policy-pr / check-pr (pull_request) Failing after 11m49s
test / unit (pull_request) Failing after 12m1s
prd-number-check / require-numbered-prds (pull_request) Failing after 12m6s
test / coverage (pull_request) Has been skipped
lint / lint (push) Successful in 1m15s
test / image-input-builds (pull_request) Successful in 1m12s
test / integration-docker (pull_request) Failing after 2m47s
tracker-policy-pr / check-pr (pull_request) Failing after 11m49s
test / unit (pull_request) Failing after 12m1s
prd-number-check / require-numbered-prds (pull_request) Failing after 12m6s
test / coverage (pull_request) Has been skipped
This commit is contained in:
@@ -13,6 +13,15 @@ on:
|
||||
gateway_image:
|
||||
description: Digest-pinned gateway OCI reference
|
||||
required: true
|
||||
agent_claude_image:
|
||||
description: Digest-pinned Claude agent OCI reference
|
||||
required: true
|
||||
agent_codex_image:
|
||||
description: Digest-pinned Codex agent OCI reference
|
||||
required: true
|
||||
agent_pi_image:
|
||||
description: Digest-pinned Pi agent OCI reference
|
||||
required: true
|
||||
firecracker_orchestrator_version:
|
||||
description: Published orchestrator rootfs package version
|
||||
required: true
|
||||
@@ -42,6 +51,9 @@ jobs:
|
||||
--source-commit "$GITHUB_SHA" \
|
||||
--orchestrator-image '${{ inputs.orchestrator_image }}' \
|
||||
--gateway-image '${{ inputs.gateway_image }}' \
|
||||
--agent-claude-image '${{ inputs.agent_claude_image }}' \
|
||||
--agent-codex-image '${{ inputs.agent_codex_image }}' \
|
||||
--agent-pi-image '${{ inputs.agent_pi_image }}' \
|
||||
--firecracker-orchestrator-version '${{ inputs.firecracker_orchestrator_version }}' \
|
||||
--firecracker-orchestrator-sha256 '${{ inputs.firecracker_orchestrator_sha256 }}' \
|
||||
--firecracker-gateway-version '${{ inputs.firecracker_gateway_version }}' \
|
||||
|
||||
@@ -84,6 +84,15 @@ def build_or_load_images(plan: DockerBottlePlan) -> BottleImages:
|
||||
)
|
||||
info(f"using cached agent image {plan.image!r}")
|
||||
return BottleImages(agent=plan.image)
|
||||
if "@sha256:" in plan.image:
|
||||
pulled = docker_mod.run_docker(["docker", "pull", plan.image])
|
||||
if pulled.returncode != 0:
|
||||
die(f"pulling packaged agent image {plan.image!r} failed: "
|
||||
f"{pulled.stderr.strip()}")
|
||||
docker_mod.verify_agent_image(
|
||||
plan.image, runtime_for(plan.agent_provider_template).smoke_test,
|
||||
)
|
||||
return BottleImages(agent=plan.image)
|
||||
docker_mod.build_image(plan.image, str(resources.build_root()), dockerfile=plan.dockerfile_path)
|
||||
docker_mod.verify_agent_image(
|
||||
plan.image, runtime_for(plan.agent_provider_template).smoke_test,
|
||||
|
||||
@@ -38,6 +38,7 @@ import subprocess
|
||||
from contextlib import ExitStack, contextmanager
|
||||
from typing import Callable, Generator
|
||||
|
||||
from ...agent_provider import runtime_for
|
||||
from ...bottle_state import (
|
||||
egress_state_dir,
|
||||
git_gate_state_dir,
|
||||
@@ -93,7 +94,14 @@ def _agent_image(plan: MacosContainerBottlePlan) -> str:
|
||||
)
|
||||
info(f"using cached agent image {plan.image!r}")
|
||||
return plan.image
|
||||
container_mod.build_image(plan.image, str(resources.build_root()), dockerfile=plan.dockerfile_path)
|
||||
if "@sha256:" in plan.image:
|
||||
container_mod.pull_image(plan.image)
|
||||
container_mod.verify_agent_image(
|
||||
plan.image, runtime_for(plan.agent_provider_template).smoke_test,
|
||||
)
|
||||
return plan.image
|
||||
container_mod.build_image(
|
||||
plan.image, str(resources.build_root()), dockerfile=plan.dockerfile_path)
|
||||
return plan.image
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ backend-specific final resolution.
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
from ..agent_provider import AgentProvisionPlan, build_agent_provision_plan, get_provider
|
||||
@@ -17,6 +17,7 @@ from ..egress import EgressPlan
|
||||
from ..env import ResolvedEnv, resolve_env
|
||||
from ..git_gate import GitGate, GitGatePlan
|
||||
from ..manifest import Manifest
|
||||
from ..release_manifest import oci_image
|
||||
from ..supervisor.plan import SupervisePlan
|
||||
from ..workspace import workspace_plan
|
||||
from .resolve_common import (
|
||||
@@ -112,6 +113,11 @@ class BottlePreparationPlanner:
|
||||
color=spec.color,
|
||||
provider_settings=provider_config.settings,
|
||||
)
|
||||
if not provider_config.dockerfile:
|
||||
image, local = oci_image(
|
||||
f"agent_{provider_config.template}", provision.image)
|
||||
if not local:
|
||||
provision = replace(provision, image=image)
|
||||
provision = merge_provision_env_vars(provision)
|
||||
return PreparedBottle(
|
||||
manifest=manifest,
|
||||
|
||||
@@ -51,6 +51,10 @@ def build_bundle_index(
|
||||
"oci": {
|
||||
"orchestrator": manifest.orchestrator_image,
|
||||
"gateway": manifest.gateway_image,
|
||||
**{
|
||||
f"agent_{role}": reference
|
||||
for role, reference in manifest.agent_images.items()
|
||||
},
|
||||
},
|
||||
"firecracker": {
|
||||
"orchestrator": {
|
||||
|
||||
@@ -32,6 +32,7 @@ class ReleaseManifest:
|
||||
source_commit: str
|
||||
orchestrator_image: str
|
||||
gateway_image: str
|
||||
agent_images: dict[str, str]
|
||||
firecracker_orchestrator: FirecrackerArtifact
|
||||
firecracker_gateway: FirecrackerArtifact
|
||||
|
||||
@@ -66,7 +67,7 @@ def parse_manifest(data: Any) -> ReleaseManifest:
|
||||
if not isinstance(oci, dict):
|
||||
raise ReleaseManifestError("release manifest oci must be an object")
|
||||
images: dict[str, str] = {}
|
||||
for role in ("orchestrator", "gateway"):
|
||||
for role in ("orchestrator", "gateway", "agent_claude", "agent_codex", "agent_pi"):
|
||||
ref = oci.get(role)
|
||||
if not isinstance(ref, str) or _OCI_RE.fullmatch(ref) is None:
|
||||
raise ReleaseManifestError(
|
||||
@@ -79,6 +80,9 @@ def parse_manifest(data: Any) -> ReleaseManifest:
|
||||
source_commit=commit,
|
||||
orchestrator_image=images["orchestrator"],
|
||||
gateway_image=images["gateway"],
|
||||
agent_images={
|
||||
role: images[f"agent_{role}"] for role in ("claude", "codex", "pi")
|
||||
},
|
||||
firecracker_orchestrator=_artifact(
|
||||
firecracker.get("orchestrator"), "orchestrator"),
|
||||
firecracker_gateway=_artifact(firecracker.get("gateway"), "gateway"),
|
||||
@@ -132,7 +136,8 @@ def oci_image(role: str, local_default: str) -> tuple[str, bool]:
|
||||
assert manifest is not None
|
||||
return (
|
||||
manifest.orchestrator_image if role == "orchestrator"
|
||||
else manifest.gateway_image,
|
||||
else manifest.gateway_image if role == "gateway"
|
||||
else manifest.agent_images[role.removeprefix("agent_")],
|
||||
False,
|
||||
)
|
||||
|
||||
|
||||
@@ -49,6 +49,8 @@ supply-chain boundary between release production and runtime.
|
||||
immutable orchestrator and gateway identities.
|
||||
- Docker and Apple Container acquire and run the manifest's digest-pinned OCI
|
||||
images without invoking `docker build` or `container build`.
|
||||
- The shipped Claude, Codex, and Pi agent images are built, smoke-tested,
|
||||
published, and selected by digest as part of the same commit bundle.
|
||||
- Firecracker acquires the manifest's versioned, checksum-verified
|
||||
orchestrator and gateway rootfs artifacts rather than deriving versions at
|
||||
launch.
|
||||
@@ -75,7 +77,7 @@ supply-chain boundary between release production and runtime.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Changing agent/provider image builds. User Dockerfiles continue to build
|
||||
- Prebuilding user-supplied Dockerfiles. Custom agent images continue to build
|
||||
through the backend-specific agent-image paths.
|
||||
- Installing Docker, Apple Container, Firecracker, or other host prerequisites.
|
||||
- Replacing Gitea's OCI or generic-package registries.
|
||||
@@ -101,7 +103,10 @@ shipped as package data:
|
||||
"source_commit": "<40 lowercase hex>",
|
||||
"oci": {
|
||||
"orchestrator": "gitea.dideric.is/didericis/bot-bottle-orchestrator@sha256:<64 hex>",
|
||||
"gateway": "gitea.dideric.is/didericis/bot-bottle-gateway@sha256:<64 hex>"
|
||||
"gateway": "gitea.dideric.is/didericis/bot-bottle-gateway@sha256:<64 hex>",
|
||||
"agent_claude": "gitea.dideric.is/didericis/bot-bottle-claude@sha256:<64 hex>",
|
||||
"agent_codex": "gitea.dideric.is/didericis/bot-bottle-codex@sha256:<64 hex>",
|
||||
"agent_pi": "gitea.dideric.is/didericis/bot-bottle-pi@sha256:<64 hex>"
|
||||
},
|
||||
"firecracker": {
|
||||
"orchestrator": {
|
||||
@@ -131,7 +136,8 @@ it refuses mutable or incomplete release inputs.
|
||||
|
||||
The release job operates on one tested commit:
|
||||
|
||||
1. Build the multi-architecture orchestrator and gateway OCI images.
|
||||
1. Build and smoke-test the multi-architecture orchestrator, gateway, Claude,
|
||||
Codex, and Pi OCI images.
|
||||
2. Push them and resolve their registry manifest digests.
|
||||
3. Build Firecracker's orchestrator and gateway rootfs artifacts from the same
|
||||
source and pinned OCI bases.
|
||||
@@ -158,6 +164,7 @@ immutable external bundle index under that SHA. The index contains:
|
||||
- schema version and source SHA;
|
||||
- wheel URL and SHA-256;
|
||||
- orchestrator and gateway OCI digest references;
|
||||
- Claude, Codex, and Pi agent OCI digest references;
|
||||
- Firecracker package versions and compressed-file SHA-256 values;
|
||||
- producing workflow/run identity and publication timestamp; and
|
||||
- qualification records, if any, naming the test workflow and release tag.
|
||||
|
||||
@@ -14,6 +14,8 @@ def parser() -> argparse.ArgumentParser:
|
||||
result.add_argument("--source-commit", required=True)
|
||||
result.add_argument("--orchestrator-image", required=True)
|
||||
result.add_argument("--gateway-image", required=True)
|
||||
for provider in ("claude", "codex", "pi"):
|
||||
result.add_argument(f"--agent-{provider}-image", required=True)
|
||||
for role in ("orchestrator", "gateway"):
|
||||
result.add_argument(f"--firecracker-{role}-version", required=True)
|
||||
result.add_argument(f"--firecracker-{role}-sha256", required=True)
|
||||
@@ -29,6 +31,10 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"oci": {
|
||||
"orchestrator": args.orchestrator_image,
|
||||
"gateway": args.gateway_image,
|
||||
**{
|
||||
f"agent_{provider}": getattr(args, f"agent_{provider}_image")
|
||||
for provider in ("claude", "codex", "pi")
|
||||
},
|
||||
},
|
||||
"firecracker": {
|
||||
role: {
|
||||
|
||||
@@ -17,6 +17,9 @@ class TestGenerateReleaseManifest(unittest.TestCase):
|
||||
"--orchestrator-image",
|
||||
"registry/orchestrator@sha256:" + "a" * 64,
|
||||
"--gateway-image", "registry/gateway@sha256:" + "b" * 64,
|
||||
"--agent-claude-image", "registry/claude@sha256:" + "e" * 64,
|
||||
"--agent-codex-image", "registry/codex@sha256:" + "f" * 64,
|
||||
"--agent-pi-image", "registry/pi@sha256:" + "0" * 64,
|
||||
"--firecracker-orchestrator-version", "orch-v1",
|
||||
"--firecracker-orchestrator-sha256", "c" * 64,
|
||||
"--firecracker-gateway-version", "gateway-v1",
|
||||
|
||||
@@ -18,6 +18,9 @@ def manifest() -> dict[str, object]:
|
||||
"oci": {
|
||||
"orchestrator": "registry/orchestrator@sha256:" + "a" * 64,
|
||||
"gateway": "registry/gateway@sha256:" + "b" * 64,
|
||||
"agent_claude": "registry/agent-claude@sha256:" + "e" * 64,
|
||||
"agent_codex": "registry/agent-codex@sha256:" + "f" * 64,
|
||||
"agent_pi": "registry/agent-pi@sha256:" + "0" * 64,
|
||||
},
|
||||
"firecracker": {
|
||||
"orchestrator": {"version": "orch-v1", "sha256": "c" * 64},
|
||||
@@ -31,6 +34,7 @@ class TestParseManifest(unittest.TestCase):
|
||||
parsed = parse_manifest(manifest())
|
||||
self.assertTrue(parsed.orchestrator_image.endswith("a" * 64))
|
||||
self.assertEqual("orch-v1", parsed.firecracker_orchestrator.version)
|
||||
self.assertTrue(parsed.agent_images["codex"].endswith("f" * 64))
|
||||
|
||||
def test_rejects_mutable_oci_reference(self) -> None:
|
||||
data = manifest()
|
||||
@@ -100,6 +104,17 @@ class TestImageSelection(unittest.TestCase):
|
||||
self.assertEqual(parsed.orchestrator_image, ref)
|
||||
self.assertFalse(local)
|
||||
|
||||
def test_packaged_install_selects_agent_reference(self) -> None:
|
||||
parsed = parse_manifest(manifest())
|
||||
with patch(
|
||||
"bot_bottle.release_manifest.packaged_manifest", return_value=parsed,
|
||||
), patch(
|
||||
"bot_bottle.release_manifest.local_build_requested", return_value=False,
|
||||
), patch.dict("os.environ", {}, clear=True):
|
||||
ref, local = oci_image("agent_codex", "local:latest")
|
||||
self.assertEqual(parsed.agent_images["codex"], ref)
|
||||
self.assertFalse(local)
|
||||
|
||||
def test_mutable_override_fails_outside_local_mode(self) -> None:
|
||||
parsed = parse_manifest(manifest())
|
||||
with patch(
|
||||
|
||||
Reference in New Issue
Block a user