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
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ on:
pull_request:

jobs:
default-images:
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"

- name: Validate default images
run: python scripts/check_default_images.py

tests:
runs-on: ubuntu-latest
strategy:
Expand Down
72 changes: 72 additions & 0 deletions scripts/check_default_images.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env python3
"""Validate that built-in default container images exist in registries."""

from __future__ import annotations

import os
import shutil
import subprocess
import sys

MANIFEST_CHECK_TIMEOUT_SECONDS = 30


def _unset_image_env_overrides(image_override_env_keys: tuple[str, ...]) -> None:
"""Force canonical built-in defaults by clearing image override env vars."""
for key in image_override_env_keys:
os.environ.pop(key, None)


def _check_image_exists(image: str) -> tuple[bool, str]:
try:
proc = subprocess.run(
["docker", "manifest", "inspect", image],
capture_output=True,
text=True,
check=False,
timeout=MANIFEST_CHECK_TIMEOUT_SECONDS,
)
except subprocess.TimeoutExpired:
return (
False,
(
"timed out after "
f"{MANIFEST_CHECK_TIMEOUT_SECONDS}s while checking docker manifest"
),
)
if proc.returncode == 0:
return True, ""
error = proc.stderr.strip() or proc.stdout.strip() or "unknown error"
return False, error
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def main() -> int:
if shutil.which("docker") is None:
print("Error: docker CLI is required but not found in PATH.", file=sys.stderr)
return 1

from vibepod.constants import IMAGE_OVERRIDE_ENV_KEYS, get_default_images

_unset_image_env_overrides(IMAGE_OVERRIDE_ENV_KEYS)
default_images = get_default_images()

failures: list[str] = []

for name in sorted(default_images):
image = default_images[name]
print(f"Checking default image for {name}: {image}")
ok, error = _check_image_exists(image)
if ok:
continue
failures.append(f"- {name}: {image}\n {error}")

if failures:
print("\nDefault image validation failed:\n" + "\n".join(failures), file=sys.stderr)
return 1

print("\nAll default images are resolvable.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
86 changes: 54 additions & 32 deletions src/vibepod/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,38 +38,60 @@
"x": "codex",
}

DEFAULT_IMAGES: dict[str, str] = {
"claude": os.environ.get(
"VP_IMAGE_CLAUDE",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'vibepod')}/claude:latest",
),
"gemini": os.environ.get(
"VP_IMAGE_GEMINI",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'nezhar')}/gemini-container:latest",
),
"opencode": os.environ.get(
"VP_IMAGE_OPENCODE", f"{os.environ.get('VP_IMAGE_NAMESPACE', 'nezhar')}/opencode-cli:latest"
),
"devstral": os.environ.get(
"VP_IMAGE_DEVSTRAL", f"{os.environ.get('VP_IMAGE_NAMESPACE', 'nezhar')}/devstral-cli:latest"
),
"auggie": os.environ.get(
"VP_IMAGE_AUGGIE", f"{os.environ.get('VP_IMAGE_NAMESPACE', 'nezhar')}/auggie-cli:latest"
),
"copilot": os.environ.get(
"VP_IMAGE_COPILOT",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'nezhar')}/copilot-cli:latest",
),
"codex": os.environ.get(
"VP_IMAGE_CODEX", f"{os.environ.get('VP_IMAGE_NAMESPACE', 'vibepod')}/codex:latest"
),
"datasette": os.environ.get(
"VP_DATASETTE_IMAGE", f"{os.environ.get('VP_IMAGE_NAMESPACE', 'vibepod')}/datasette:latest"
),
"proxy": os.environ.get(
"VP_PROXY_IMAGE", f"{os.environ.get('VP_IMAGE_NAMESPACE', 'vibepod')}/proxy:latest"
),
}
IMAGE_OVERRIDE_ENV_KEYS: tuple[str, ...] = (
"VP_IMAGE_NAMESPACE",
"VP_IMAGE_CLAUDE",
"VP_IMAGE_GEMINI",
"VP_IMAGE_OPENCODE",
"VP_IMAGE_DEVSTRAL",
"VP_IMAGE_AUGGIE",
"VP_IMAGE_COPILOT",
"VP_IMAGE_CODEX",
"VP_DATASETTE_IMAGE",
"VP_PROXY_IMAGE",
)


def get_default_images() -> dict[str, str]:
return {
"claude": os.environ.get(
"VP_IMAGE_CLAUDE",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'vibepod')}/claude:latest",
),
"gemini": os.environ.get(
"VP_IMAGE_GEMINI",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'nezhar')}/gemini-container:latest",
),
"opencode": os.environ.get(
"VP_IMAGE_OPENCODE",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'nezhar')}/opencode-cli:latest",
),
"devstral": os.environ.get(
"VP_IMAGE_DEVSTRAL",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'nezhar')}/devstral-cli:latest",
),
"auggie": os.environ.get(
"VP_IMAGE_AUGGIE",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'nezhar')}/auggie-cli:latest",
),
"copilot": os.environ.get(
"VP_IMAGE_COPILOT",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'nezhar')}/copilot-cli:latest",
),
"codex": os.environ.get(
"VP_IMAGE_CODEX", f"{os.environ.get('VP_IMAGE_NAMESPACE', 'vibepod')}/codex:latest"
),
"datasette": os.environ.get(
"VP_DATASETTE_IMAGE",
f"{os.environ.get('VP_IMAGE_NAMESPACE', 'vibepod')}/datasette:latest",
),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"proxy": os.environ.get(
"VP_PROXY_IMAGE", f"{os.environ.get('VP_IMAGE_NAMESPACE', 'vibepod')}/proxy:latest"
),
}


DEFAULT_IMAGES: dict[str, str] = get_default_images()

DEFAULT_ALIASES: dict[str, str] = {
**{shortcut: f"run {agent}" for shortcut, agent in AGENT_SHORTCUTS.items()},
Expand Down
Loading