From 13b287bd6a50de02dfb4f6e4d007726868765c01 Mon Sep 17 00:00:00 2001 From: Harald Nezbeda Date: Thu, 12 Mar 2026 20:10:19 +0100 Subject: [PATCH] Added a new CI job that validates all built-in default images exist --- .github/workflows/ci.yml | 20 ++++++++ scripts/check_default_images.py | 72 +++++++++++++++++++++++++++ src/vibepod/constants.py | 86 +++++++++++++++++++++------------ 3 files changed, 146 insertions(+), 32 deletions(-) create mode 100644 scripts/check_default_images.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d60f2b7..d4fd0d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: diff --git a/scripts/check_default_images.py b/scripts/check_default_images.py new file mode 100644 index 0000000..6281576 --- /dev/null +++ b/scripts/check_default_images.py @@ -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 + + +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()) diff --git a/src/vibepod/constants.py b/src/vibepod/constants.py index 8327eaf..79977ce 100644 --- a/src/vibepod/constants.py +++ b/src/vibepod/constants.py @@ -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", + ), + "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()},