mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(deploy): run RoboCo from pre-built registry images
The orchestrator spawned agents only by bare image names and built any
missing image from source on the host, so a deployment had to carry the
build context and a toolchain — there was no way to just pull and run the
images the release workflow publishes.
Add two settings (default empty = unchanged local-build behavior):
ROBOCO_AGENT_IMAGE_REGISTRY and ROBOCO_AGENT_IMAGE_TAG. When a registry is
set, the orchestrator spawns and ensures {registry}/roboco-agent-*[:tag] and
pulls (never builds) any image it lacks. Also adds the previously-missing
agent-secretary image to the lazy-build map.
Ship docker-compose.registry.yml: a standalone compose that pulls every
published image (GHCR or Docker Hub, pinnable version) and wires the
orchestrator to spawn the matching pre-built agent images. The existing
build compose files are unchanged.
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
# ============================================================================
|
||||
# RoboCo — pre-built (registry) deployment
|
||||
# ============================================================================
|
||||
# This is the "pull and run" compose for USERS: it runs the images the release
|
||||
# workflow publishes (GHCR + Docker Hub) instead of building from source, so a
|
||||
# host needs neither the repo's build context nor a build toolchain.
|
||||
#
|
||||
# 1. Copy `.env.example` to `.env` and fill in the required secrets.
|
||||
# 2. docker compose -f docker-compose.registry.yml pull
|
||||
# 3. docker compose -f docker-compose.registry.yml up -d
|
||||
#
|
||||
# Pick the registry + version with two env vars (defaults shown):
|
||||
# ROBOCO_REGISTRY=ghcr.io/rennf93 # or docker.io/renzof93
|
||||
# ROBOCO_VERSION=latest # or a pinned release, e.g. 0.5.0
|
||||
#
|
||||
# The orchestrator spawns agent containers itself; ROBOCO_AGENT_IMAGE_REGISTRY
|
||||
# + ROBOCO_AGENT_IMAGE_TAG below tell it to spawn the SAME pre-built agent
|
||||
# images (it pulls any it doesn't already have). The one-shot agent-*-image
|
||||
# services exist only so `docker compose pull` fetches every agent image up
|
||||
# front; they pull then exit.
|
||||
#
|
||||
# NOTE: this file is the registry counterpart of docker-compose.yml — when you
|
||||
# add or change a service there, mirror it here. The infra services
|
||||
# (postgres/redis/ollama/nginx) are byte-identical to the build compose.
|
||||
# ============================================================================
|
||||
services:
|
||||
postgres:
|
||||
image: pgvector/pgvector:pg16
|
||||
container_name: roboco-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: roboco
|
||||
POSTGRES_PASSWORD: roboco
|
||||
POSTGRES_DB: roboco
|
||||
ports:
|
||||
- "15432:5432"
|
||||
volumes:
|
||||
- ${ROBOCO_DATA_DIR:-./data}/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U roboco -d roboco"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
container_name: roboco-redis
|
||||
restart: unless-stopped
|
||||
command: redis-server --appendonly yes
|
||||
ports:
|
||||
- "16379:6379"
|
||||
volumes:
|
||||
- ${ROBOCO_DATA_DIR:-./data}/redis:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
container_name: roboco-ollama
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
|
||||
ports:
|
||||
- "11435:11434"
|
||||
volumes:
|
||||
- ${ROBOCO_DATA_DIR:-./data}/ollama:/root/.ollama
|
||||
healthcheck:
|
||||
test: ["CMD", "ollama", "list"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
ollama-init:
|
||||
image: curlimages/curl:latest
|
||||
container_name: roboco-ollama-init
|
||||
depends_on:
|
||||
ollama:
|
||||
condition: service_healthy
|
||||
restart: "no"
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command:
|
||||
- |
|
||||
set -e
|
||||
echo "=== Pulling embedding model (qwen3-embedding:0.6b) ==="
|
||||
curl -sN http://ollama:11434/api/pull -d '{"name":"qwen3-embedding:0.6b"}' | while read -r line; do
|
||||
status=$$(echo "$$line" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
|
||||
[ -n "$$status" ] && echo " $$status"
|
||||
done
|
||||
echo "=== Pulling LLM model (glm-5:cloud) ==="
|
||||
curl -sN http://ollama:11434/api/pull -d '{"name":"glm-5:cloud"}' | while read -r line; do
|
||||
status=$$(echo "$$line" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
|
||||
[ -n "$$status" ] && echo " $$status"
|
||||
done
|
||||
echo "=== Verifying models are available ==="
|
||||
curl -sf http://ollama:11434/api/tags | grep -q "qwen3-embedding" && echo " qwen3-embedding: OK"
|
||||
curl -sf http://ollama:11434/api/tags | grep -q "glm-5" && echo " glm-5: OK"
|
||||
echo "=== All models ready! ==="
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Agent image pre-pull (one-shot). Each pulls its published image then exits,
|
||||
# so `docker compose pull` fetches every agent image up front. The
|
||||
# orchestrator spawns these same images at runtime.
|
||||
# --------------------------------------------------------------------------
|
||||
agent-base-image:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-agent-base:${ROBOCO_VERSION:-latest}
|
||||
entrypoint: ["/bin/sh", "-c", "echo 'agent-base image present'"]
|
||||
restart: "no"
|
||||
|
||||
agent-pm-image:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-agent-pm:${ROBOCO_VERSION:-latest}
|
||||
entrypoint: ["/bin/sh", "-c", "echo 'agent-pm image present'"]
|
||||
restart: "no"
|
||||
|
||||
agent-dev-be-image:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-agent-dev-be:${ROBOCO_VERSION:-latest}
|
||||
entrypoint: ["/bin/sh", "-c", "echo 'agent-dev-be image present'"]
|
||||
restart: "no"
|
||||
|
||||
agent-dev-fe-image:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-agent-dev-fe:${ROBOCO_VERSION:-latest}
|
||||
entrypoint: ["/bin/sh", "-c", "echo 'agent-dev-fe image present'"]
|
||||
restart: "no"
|
||||
|
||||
agent-qa-be-image:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-agent-qa-be:${ROBOCO_VERSION:-latest}
|
||||
entrypoint: ["/bin/sh", "-c", "echo 'agent-qa-be image present'"]
|
||||
restart: "no"
|
||||
|
||||
agent-qa-fe-image:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-agent-qa-fe:${ROBOCO_VERSION:-latest}
|
||||
entrypoint: ["/bin/sh", "-c", "echo 'agent-qa-fe image present'"]
|
||||
restart: "no"
|
||||
|
||||
agent-ux-image:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-agent-ux:${ROBOCO_VERSION:-latest}
|
||||
entrypoint: ["/bin/sh", "-c", "echo 'agent-ux image present'"]
|
||||
restart: "no"
|
||||
|
||||
agent-doc-image:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-agent-doc:${ROBOCO_VERSION:-latest}
|
||||
entrypoint: ["/bin/sh", "-c", "echo 'agent-doc image present'"]
|
||||
restart: "no"
|
||||
|
||||
agent-prompter-image:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-agent-prompter:${ROBOCO_VERSION:-latest}
|
||||
entrypoint: ["/bin/sh", "-c", "echo 'agent-prompter image present'"]
|
||||
restart: "no"
|
||||
|
||||
agent-secretary-image:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-agent-secretary:${ROBOCO_VERSION:-latest}
|
||||
entrypoint: ["/bin/sh", "-c", "echo 'agent-secretary image present'"]
|
||||
restart: "no"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Orchestrator — API server + agent spawner
|
||||
# --------------------------------------------------------------------------
|
||||
orchestrator:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-orchestrator:${ROBOCO_VERSION:-latest}
|
||||
container_name: roboco-orchestrator
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
ROBOCO_DATABASE_HOST: roboco-postgres
|
||||
ROBOCO_DATABASE_PORT: 5432
|
||||
ROBOCO_DATABASE_USER: roboco
|
||||
ROBOCO_DATABASE_PASSWORD: roboco
|
||||
ROBOCO_DATABASE_NAME: roboco
|
||||
ROBOCO_REDIS_HOST: roboco-redis
|
||||
ROBOCO_REDIS_PORT: 6379
|
||||
ROBOCO_HOST: 0.0.0.0
|
||||
ROBOCO_PORT: 8000
|
||||
ROBOCO_ENCRYPTION_KEY: ${ROBOCO_ENCRYPTION_KEY:?ROBOCO_ENCRYPTION_KEY is required}
|
||||
ROBOCO_AGENT_AUTH_SECRET: ${ROBOCO_AGENT_AUTH_SECRET:?ROBOCO_AGENT_AUTH_SECRET is required}
|
||||
ROBOCO_AGENT_AUTH_REQUIRED: ${ROBOCO_AGENT_AUTH_REQUIRED:-false}
|
||||
ROBOCO_LOCAL_LLM_BASE_URL: http://roboco-ollama:11434/v1
|
||||
ROBOCO_LOCAL_LLM_MODEL: glm-5:cloud
|
||||
ROBOCO_DEFAULT_EMBEDDING_MODEL: qwen3-embedding:0.6b
|
||||
ROBOCO_OLLAMA_BASE_URL: http://roboco-ollama:11434
|
||||
# Spawn the PRE-BUILT agent images from the same registry instead of
|
||||
# building them from source (the orchestrator pulls any it lacks).
|
||||
ROBOCO_AGENT_IMAGE_REGISTRY: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}
|
||||
ROBOCO_AGENT_IMAGE_TAG: ${ROBOCO_VERSION:-latest}
|
||||
# Host paths for spawning agent containers (Docker-in-Docker). MUST be
|
||||
# absolute paths on the host. Default to this compose project's ./data.
|
||||
ROBOCO_HOST_PROJECT_DIR: ${ROBOCO_HOST_PROJECT_DIR:-/opt/roboco}
|
||||
ROBOCO_HOST_CLAUDE_DIR: ${ROBOCO_HOST_CLAUDE_DIR:-${HOME}/.claude}
|
||||
ROBOCO_HOST_DATA_DIR: ${ROBOCO_HOST_DATA_DIR:-/opt/roboco/data}
|
||||
# Reachable base URL for commit-trailer links — set to your host's LAN
|
||||
# address or domain so the links in commit bodies resolve.
|
||||
ROBOCO_PUBLIC_BASE_URL: ${ROBOCO_PUBLIC_BASE_URL:-http://localhost:8000}
|
||||
ROBOCO_ENVIRONMENT: production
|
||||
ROBOCO_CLAIM_STALE_SECONDS: "1800"
|
||||
ROBOCO_STALE_CLAIM_REAP_SECONDS: "1800"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ${CLAUDE_AUTH_DIR:-${HOME}/.claude}:/root/.claude
|
||||
- ${ROBOCO_DATA_DIR:-./data}/mcp-configs:/app/mcp-configs
|
||||
- ${ROBOCO_DATA_DIR:-./data}/prompts-generated:/app/prompts-generated
|
||||
- ${ROBOCO_DATA_DIR:-./data}/agent-settings:/app/agent-settings
|
||||
- ${ROBOCO_DATA_DIR:-./data}/workspaces:/data/workspaces
|
||||
- ${ROBOCO_DATA_DIR:-./data}/logs:/data/logs
|
||||
- ${ROBOCO_DATA_DIR:-./data}/briefings:/app/briefings
|
||||
- ${ROBOCO_DATA_DIR:-./data}/manifests:/app/manifests
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
ollama:
|
||||
condition: service_healthy
|
||||
ollama-init:
|
||||
condition: service_completed_successfully
|
||||
agent-base-image:
|
||||
condition: service_completed_successfully
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Next.js control panel (fronted by nginx; not exposed directly)
|
||||
# --------------------------------------------------------------------------
|
||||
panel:
|
||||
image: ${ROBOCO_REGISTRY:-ghcr.io/rennf93}/roboco-panel:${ROBOCO_VERSION:-latest}
|
||||
container_name: roboco-panel
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "3000"
|
||||
depends_on:
|
||||
- orchestrator
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Nginx — single entry point on port 3000
|
||||
# --------------------------------------------------------------------------
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
container_name: roboco-nginx
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:80"
|
||||
environment:
|
||||
ROBOCO_PANEL_AGENT_TOKEN: ${ROBOCO_PANEL_AGENT_TOKEN:-}
|
||||
NGINX_ENVSUBST_FILTER: "^ROBOCO_"
|
||||
volumes:
|
||||
- ./docker/nginx.conf:/etc/nginx/templates/default.conf.template:ro
|
||||
depends_on:
|
||||
- panel
|
||||
- orchestrator
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: roboco_default
|
||||
@@ -393,6 +393,32 @@ class Settings(BaseSettings):
|
||||
),
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# Agent container images (spawn source)
|
||||
# ==========================================================================
|
||||
# By default the orchestrator builds each specialized agent image locally
|
||||
# from docker/agent-*.Dockerfile the first time it spawns that role (the
|
||||
# build/test flow). Set a registry to run the PRE-BUILT images the release
|
||||
# workflow publishes instead — the orchestrator then pulls
|
||||
# `{registry}/roboco-agent-*[:tag]` rather than building, so a deployment
|
||||
# never needs the source tree or a build toolchain. Empty = local build
|
||||
# (unchanged behavior).
|
||||
agent_image_registry: str = Field(
|
||||
default="",
|
||||
description=(
|
||||
"Registry namespace for pre-built agent images, e.g. "
|
||||
"'ghcr.io/rennf93' or 'docker.io/renzof93'. Empty builds locally."
|
||||
),
|
||||
)
|
||||
agent_image_tag: str = Field(
|
||||
default="",
|
||||
description=(
|
||||
"Tag for pre-built agent images (e.g. 'latest' or '0.5.0'). Empty "
|
||||
"leaves the tag implicit (':latest'); only meaningful with "
|
||||
"agent_image_registry set."
|
||||
),
|
||||
)
|
||||
|
||||
# ==========================================================================
|
||||
# Transcript retention (agent Claude Code transcripts under ~/.claude)
|
||||
# ==========================================================================
|
||||
|
||||
@@ -130,9 +130,23 @@ AGENT_IMAGES: dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
def _qualify_agent_image(bare: str) -> str:
|
||||
"""Apply the configured registry namespace + tag to a bare agent image.
|
||||
|
||||
Default (no ``agent_image_registry``, no ``agent_image_tag``) returns the
|
||||
bare name unchanged — the local build flow. With a registry set the
|
||||
orchestrator spawns (and ensures) ``{registry}/roboco-agent-*[:tag]``, the
|
||||
pre-built images the release workflow publishes, instead of building.
|
||||
"""
|
||||
registry = settings.agent_image_registry.rstrip("/")
|
||||
name = f"{registry}/{bare}" if registry else bare
|
||||
tag = settings.agent_image_tag
|
||||
return f"{name}:{tag}" if tag else name
|
||||
|
||||
|
||||
def get_agent_image(agent_id: str) -> str:
|
||||
"""Get the Docker image for an agent."""
|
||||
return AGENT_IMAGES.get(agent_id, AGENT_BASE_IMAGE)
|
||||
"""Get the Docker image for an agent (registry-qualified when configured)."""
|
||||
return _qualify_agent_image(AGENT_IMAGES.get(agent_id, AGENT_BASE_IMAGE))
|
||||
|
||||
|
||||
# When running in a container, we need host paths for volume mounts.
|
||||
@@ -731,9 +745,12 @@ class AgentOrchestrator:
|
||||
logger.info("Orchestrator stopped")
|
||||
|
||||
async def _ensure_agent_image(self, agent_id: str | None = None) -> None:
|
||||
"""Ensure the agent Docker images are built.
|
||||
"""Ensure the agent Docker images are present.
|
||||
|
||||
Builds base image first, then specialized image if agent_id provided.
|
||||
Local mode (no ``agent_image_registry``) builds the base image first,
|
||||
then the role-specialized image, from ``docker/agent-*.Dockerfile``.
|
||||
Registry mode pulls the pre-built images instead. Idempotent — skips
|
||||
anything already present locally.
|
||||
"""
|
||||
# Determine build context
|
||||
if PROJECT_HOST_PATH:
|
||||
@@ -744,17 +761,17 @@ class AgentOrchestrator:
|
||||
docker_dir = str(self.project_root / "docker")
|
||||
|
||||
# Always ensure base image exists
|
||||
await self._build_image_if_missing(
|
||||
await self._ensure_image_present(
|
||||
AGENT_BASE_IMAGE,
|
||||
f"{docker_dir}/agent-base.Dockerfile",
|
||||
build_context,
|
||||
)
|
||||
|
||||
# Build specialized image if agent specified
|
||||
# Ensure the role-specialized image if this agent uses one
|
||||
if agent_id:
|
||||
image = get_agent_image(agent_id)
|
||||
if image != AGENT_BASE_IMAGE:
|
||||
# Map image name to dockerfile
|
||||
bare = AGENT_IMAGES.get(agent_id, AGENT_BASE_IMAGE)
|
||||
if bare != AGENT_BASE_IMAGE:
|
||||
# Map the bare image name to its dockerfile
|
||||
dockerfile_map = {
|
||||
"roboco-agent-pm": "agent-pm.Dockerfile",
|
||||
"roboco-agent-dev-be": "agent-dev-be.Dockerfile",
|
||||
@@ -764,49 +781,71 @@ class AgentOrchestrator:
|
||||
"roboco-agent-doc": "agent-doc.Dockerfile",
|
||||
"roboco-agent-ux": "agent-ux.Dockerfile",
|
||||
"roboco-agent-prompter": "agent-prompter.Dockerfile",
|
||||
"roboco-agent-secretary": "agent-secretary.Dockerfile",
|
||||
}
|
||||
dockerfile = dockerfile_map.get(image)
|
||||
dockerfile = dockerfile_map.get(bare)
|
||||
if dockerfile:
|
||||
await self._build_image_if_missing(
|
||||
image,
|
||||
await self._ensure_image_present(
|
||||
bare,
|
||||
f"{docker_dir}/{dockerfile}",
|
||||
build_context,
|
||||
)
|
||||
|
||||
async def _build_image_if_missing(
|
||||
self, image_name: str, dockerfile_path: str, build_context: str
|
||||
async def _ensure_image_present(
|
||||
self, bare_image: str, dockerfile_path: str, build_context: str
|
||||
) -> None:
|
||||
"""Build a Docker image if it doesn't exist."""
|
||||
"""Ensure one agent image is present locally.
|
||||
|
||||
Pulls it (registry mode) or builds it from its Dockerfile (local mode)
|
||||
when missing; no-op if already present.
|
||||
"""
|
||||
image = _qualify_agent_image(bare_image)
|
||||
# Check if image exists
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"docker",
|
||||
"image",
|
||||
"inspect",
|
||||
image_name,
|
||||
image,
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
await proc.wait()
|
||||
if proc.returncode == 0:
|
||||
return
|
||||
|
||||
if proc.returncode != 0:
|
||||
logger.info("Building Docker image...", image=image_name)
|
||||
if settings.agent_image_registry:
|
||||
# Registry mode: pull the pre-built image; never build from source
|
||||
# (a deployment running pre-built images has no build context).
|
||||
logger.info("Pulling agent image...", image=image)
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"docker",
|
||||
"build",
|
||||
"-t",
|
||||
image_name,
|
||||
"-f",
|
||||
dockerfile_path,
|
||||
build_context,
|
||||
"pull",
|
||||
image,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"Failed to build image {image_name}: {stderr.decode()}"
|
||||
)
|
||||
logger.info("Docker image built successfully", image=image_name)
|
||||
raise RuntimeError(f"Failed to pull image {image}: {stderr.decode()}")
|
||||
logger.info("Agent image pulled", image=image)
|
||||
return
|
||||
|
||||
logger.info("Building Docker image...", image=image)
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
"docker",
|
||||
"build",
|
||||
"-t",
|
||||
image,
|
||||
"-f",
|
||||
dockerfile_path,
|
||||
build_context,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
_, stderr = await proc.communicate()
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"Failed to build image {image}: {stderr.decode()}")
|
||||
logger.info("Docker image built successfully", image=image)
|
||||
|
||||
# =========================================================================
|
||||
# PER-AGENT SETTINGS GENERATION
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Agent image resolution — local build vs. pre-built registry images.
|
||||
|
||||
``_qualify_agent_image`` decides what image name the orchestrator spawns (and
|
||||
ensures). Empty registry/tag MUST return the bare name unchanged so the local
|
||||
build flow and existing NAS deployment are untouched; a configured registry
|
||||
switches every agent to the pre-built ``{registry}/roboco-agent-*[:tag]``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from roboco.runtime import orchestrator as orch
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("registry", "tag", "bare", "expected"),
|
||||
[
|
||||
# Default: no registry, no tag -> bare name unchanged (local build).
|
||||
("", "", "roboco-agent-pm", "roboco-agent-pm"),
|
||||
# Registry only -> qualified, implicit :latest.
|
||||
("ghcr.io/rennf93", "", "roboco-agent-pm", "ghcr.io/rennf93/roboco-agent-pm"),
|
||||
# Trailing slash on the registry is tolerated.
|
||||
(
|
||||
"ghcr.io/rennf93/",
|
||||
"latest",
|
||||
"roboco-agent-pm",
|
||||
"ghcr.io/rennf93/roboco-agent-pm:latest",
|
||||
),
|
||||
# Docker Hub namespace + pinned version.
|
||||
(
|
||||
"docker.io/renzof93",
|
||||
"0.5.0",
|
||||
"roboco-agent-base",
|
||||
"docker.io/renzof93/roboco-agent-base:0.5.0",
|
||||
),
|
||||
# Tag without registry is still applied (edge case, valid).
|
||||
("", "latest", "roboco-agent-pm", "roboco-agent-pm:latest"),
|
||||
],
|
||||
)
|
||||
def test_qualify_agent_image(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
registry: str,
|
||||
tag: str,
|
||||
bare: str,
|
||||
expected: str,
|
||||
) -> None:
|
||||
monkeypatch.setattr(orch.settings, "agent_image_registry", registry)
|
||||
monkeypatch.setattr(orch.settings, "agent_image_tag", tag)
|
||||
assert orch._qualify_agent_image(bare) == expected
|
||||
|
||||
|
||||
def test_get_agent_image_local_default(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(orch.settings, "agent_image_registry", "")
|
||||
monkeypatch.setattr(orch.settings, "agent_image_tag", "")
|
||||
assert orch.get_agent_image("be-dev-1") == "roboco-agent-dev-be"
|
||||
# Unknown agent id falls back to the base image.
|
||||
assert orch.get_agent_image("pr-reviewer-1") == "roboco-agent-base"
|
||||
|
||||
|
||||
def test_get_agent_image_registry_mode(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr(orch.settings, "agent_image_registry", "ghcr.io/rennf93")
|
||||
monkeypatch.setattr(orch.settings, "agent_image_tag", "0.5.0")
|
||||
assert (
|
||||
orch.get_agent_image("be-dev-1")
|
||||
== "ghcr.io/rennf93/roboco-agent-dev-be:0.5.0"
|
||||
)
|
||||
assert (
|
||||
orch.get_agent_image("pr-reviewer-1")
|
||||
== "ghcr.io/rennf93/roboco-agent-base:0.5.0"
|
||||
)
|
||||
Reference in New Issue
Block a user