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
10 changes: 10 additions & 0 deletions docker-compose.registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,16 @@ services:
NGINX_ENVSUBST_FILTER: "^ROBOCO_"
volumes:
- ./deploy/nginx.conf:/etc/nginx/templates/default.conf.template:ro
# The active-color include is mounted via its DIRECTORY, not as a file
# bind: a file bind pins the inode, so a host-side rewrite (or the
# deploy script's atomic tmp+mv swap) never reaches the running
# container and nginx keeps testing/serving the PREVIOUS color
# (2026-09-17 green flip: "host not found in upstream
# roboco-panel-blue" after the include already said green).
# Registry installs clone the repo, so ./front ships with it; missing
# this mount made fresh `make quickstart` crash-loop nginx on the
# include (#1111).
- ./front:/etc/nginx/front:ro
depends_on:
- panel
- orchestrator
Expand Down
4 changes: 4 additions & 0 deletions roboco/llm/providers/openrouter_cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,10 @@ def main(argv: list[str] | None = None) -> int:
base_url = os.environ.get("OPENROUTER_BASE_URL", settings.openrouter_base_url)

config = render_config(role, model, base_url, mcp_path)
# The image does not pre-create opencode's global config home (#1110):
# without this, every OpenRouter-routed spawn died on FileNotFoundError
# before the CLI ever started.
OPENCODE_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
OPENCODE_CONFIG_PATH.write_text(json.dumps(config, indent=2), encoding="utf-8")
write_bash_guard_plugin()
return 0
Expand Down
27 changes: 27 additions & 0 deletions tests/unit/llm/providers/test_openrouter_cli_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,33 @@ def test_main_writes_opencode_json(
assert "tool.execute.before" in plugin_path.read_text(encoding="utf-8")


def test_main_creates_missing_config_dir(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""#1110: the agent image does not pre-create opencode's global config
home, so the render step must mkdir the parent itself. Regression: every
OpenRouter-routed spawn exited 1 on FileNotFoundError before the CLI
started. The plugin write already mkdir'd its parents; the config write
did not."""
mcp_path = tmp_path / "mcp-config.json"
mcp_path.write_text(json.dumps(_SAMPLE_MCP), encoding="utf-8")
config_path = tmp_path / ".config" / "opencode" / "opencode.json"
plugin_path = tmp_path / ".config" / "opencode" / "plugins" / "guard.js"
system_prompt = tmp_path / "system-prompt.md"
system_prompt.write_text("blueprint", encoding="utf-8")

monkeypatch.setattr(oc, "OPENCODE_CONFIG_PATH", config_path)
monkeypatch.setattr(oc, "OPENCODE_PLUGIN_PATH", plugin_path)
monkeypatch.setattr(oc, "SYSTEM_PROMPT_PATH", system_prompt)
monkeypatch.setenv("ROBOCO_AGENT_ID", "be-dev-1")
monkeypatch.setenv("ROBOCO_MCP_CONFIG", str(mcp_path))
monkeypatch.setenv("ROBOCO_AGENT_MODEL", "anthropic/claude-sonnet-4")

assert oc.main([]) == 0
assert config_path.exists()
json.loads(config_path.read_text(encoding="utf-8"))


def test_main_check_flag_passes_when_key_set(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/test_compose_roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,33 @@ def test_indexer_services_have_no_http_healthcheck() -> None:
f"{name}: an indexer service serves no HTTP, a healthcheck "
"can never pass"
)


def test_nginx_mounts_front_include_dir_in_every_compose() -> None:
"""#1111: deploy/nginx.conf includes /etc/nginx/front/active-upstreams.conf
(the blue-green color switch), so EVERY compose file must mount the repo's
./front directory at /etc/nginx/front. 2026-09-21 the registry compose
shipped without it and fresh `make quickstart` crash-looped nginx on the
missing include. The mount must be the DIRECTORY, not a file bind: a file
bind pins the inode and the deploy script's atomic tmp+mv swap would never
reach the running container."""
for name in _COMPOSE_FILES:
compose = yaml.safe_load((_REPO_ROOT / name).read_text())
nginx = compose["services"].get("nginx")
assert nginx is not None, f"{name}: no nginx service found"
volumes = [str(v) for v in (nginx.get("volumes") or [])]
dir_mount = any(
v.startswith("./front:") and v.endswith(":/etc/nginx/front:ro")
for v in volumes
)
assert dir_mount, (
f"{name}: nginx must mount ./front:/etc/nginx/front:ro (directory "
"bind, not a file bind) or the active-upstreams include in "
"deploy/nginx.conf crash-loops the container on a fresh install"
)
# And the included file must actually exist in the repo, so the mount
# is never empty on a clean clone.
assert (_REPO_ROOT / "front" / "active-upstreams.conf").is_file(), (
"front/active-upstreams.conf is missing from the repo; the nginx "
"include would 404 it and crash-loop roboco-nginx"
)
Loading