fix: smoke-test agent images after build, add start --no-cache
npm treats optionalDependencies failures as non-fatal, so a transient network blip fetching claude-code's platform-native binary during `npm install -g` left a stub CLI in an image that still "built" successfully — then got baked into the Docker/Container layer cache until forced to rebuild. Post-build smoke test (provider-declared argv, run in a throwaway container of the freshly built image) fails the launch loudly instead of shipping a broken image; --no-cache gives an escape hatch to force a from-scratch rebuild. Closes #353.
This commit is contained in:
@@ -36,6 +36,7 @@ from contextlib import ExitStack, contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Callable, Generator
|
||||
|
||||
from ...agent_provider import runtime_for
|
||||
from ...egress import egress_resolve_token_values
|
||||
from ...git_gate import (
|
||||
provision_git_gate_dynamic_keys,
|
||||
@@ -110,6 +111,9 @@ def launch(
|
||||
plan.image, _REPO_DIR,
|
||||
dockerfile=plan.dockerfile_path,
|
||||
)
|
||||
docker_mod.verify_agent_image(
|
||||
plan.image, runtime_for(plan.agent_provider_template).smoke_test,
|
||||
)
|
||||
|
||||
# Step 2: mint the git-gate dynamic (gitea) deploy keys, if any, before
|
||||
# provisioning the bottle's repos into the shared gateway.
|
||||
|
||||
@@ -4,6 +4,7 @@ existence, and building images."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -108,15 +109,45 @@ def build_image(ref: str, context: str, *, dockerfile: str = "") -> None:
|
||||
|
||||
`dockerfile` is an optional path (relative to `context`, or
|
||||
absolute) for callers that need to build from a non-default
|
||||
Dockerfile in the same context — e.g. `Dockerfile.git-gate`."""
|
||||
Dockerfile in the same context — e.g. `Dockerfile.git-gate`.
|
||||
|
||||
Set `BOT_BOTTLE_NO_CACHE=1` (the `start --no-cache` flag) to force
|
||||
`--no-cache`. The npm/curl installers some provider Dockerfiles
|
||||
shell out to can silently no-op on a transient network failure —
|
||||
e.g. an `optionalDependencies` fetch for a platform-native binary —
|
||||
and Docker will then cache that broken layer indefinitely."""
|
||||
info(f"building image {ref} from {context} (layer cache keeps repeat builds fast)")
|
||||
args = ["docker", "build", "-t", ref]
|
||||
if os.environ.get("BOT_BOTTLE_NO_CACHE") == "1":
|
||||
args.append("--no-cache")
|
||||
if dockerfile:
|
||||
args.extend(["-f", dockerfile])
|
||||
args.append(context)
|
||||
subprocess.run(args, check=True)
|
||||
|
||||
|
||||
def verify_agent_image(image: str, argv: tuple[str, ...]) -> None:
|
||||
"""Run `argv` inside a throwaway container of a freshly built agent
|
||||
image and die loudly if it fails, instead of shipping an image
|
||||
whose CLI only breaks at first real use. No-op when the provider
|
||||
hasn't declared a smoke test (`AgentProviderRuntime.smoke_test`)."""
|
||||
if not argv:
|
||||
return
|
||||
result = subprocess.run(
|
||||
["docker", "run", "--rm", "--entrypoint", argv[0], image, *argv[1:]],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
detail = (result.stderr or result.stdout or "").strip()
|
||||
die(
|
||||
f"agent image {image!r} failed its post-build smoke test "
|
||||
f"({' '.join(argv)}): {detail}\n"
|
||||
f"Try rebuilding from scratch: bot-bottle start --no-cache"
|
||||
)
|
||||
|
||||
|
||||
# def build_image_with_cwd(
|
||||
# derived: str,
|
||||
# base: str,
|
||||
|
||||
Reference in New Issue
Block a user