diff --git a/code-interpreter/app/main.py b/code-interpreter/app/main.py index 0ac7e1c..e9f4c32 100644 --- a/code-interpreter/app/main.py +++ b/code-interpreter/app/main.py @@ -5,6 +5,7 @@ import subprocess from collections.abc import AsyncGenerator from contextlib import asynccontextmanager, suppress +from importlib.metadata import version as _package_version from shutil import which from typing import Final @@ -25,6 +26,8 @@ logger = logging.getLogger(__name__) +SERVICE_VERSION: Final[str] = _package_version("code-interpreter") + def _ensure_docker_image_available() -> None: """Ensure the Docker executor image is available locally. @@ -123,7 +126,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: def create_app() -> FastAPI: app = FastAPI( title="Code Interpreter API", - version="0.1.0", + version=SERVICE_VERSION, docs_url="/docs", redoc_url="/redoc", openapi_url="/openapi.json", @@ -134,7 +137,11 @@ def create_app() -> FastAPI: def health() -> HealthResponse: """Health check that verifies the executor backend is operational.""" result = get_executor().check_health() - return HealthResponse(status=result.status, message=result.message) + return HealthResponse( + status=result.status, + message=result.message, + version=SERVICE_VERSION, + ) app.include_router(api_router, prefix="/v1") return app diff --git a/code-interpreter/app/models/schemas.py b/code-interpreter/app/models/schemas.py index 91ca319..85d360f 100644 --- a/code-interpreter/app/models/schemas.py +++ b/code-interpreter/app/models/schemas.py @@ -120,6 +120,13 @@ class ListFilesResponse(BaseModel): class HealthResponse(BaseModel): status: Literal["ok", "error"] message: StrictStr | None = None + version: StrictStr = Field( + ..., + description=( + "Semver of the running service. Clients can compare against a " + "required minimum to detect whether new functionality is available." + ), + ) DEFAULT_SESSION_TTL_SEC = 15 * 60 diff --git a/code-interpreter/pyproject.toml b/code-interpreter/pyproject.toml index 3da87c9..7f45ed6 100644 --- a/code-interpreter/pyproject.toml +++ b/code-interpreter/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "code-interpreter" -version = "0.1.0" +version = "0.3.3" description = "FastAPI service to execute Python code (sync, typed)" readme = "README.md" requires-python = ">=3.11,<3.12" diff --git a/code-interpreter/tests/integration_tests/test_health.py b/code-interpreter/tests/integration_tests/test_health.py index baa00b6..9b37243 100644 --- a/code-interpreter/tests/integration_tests/test_health.py +++ b/code-interpreter/tests/integration_tests/test_health.py @@ -1,17 +1,21 @@ from __future__ import annotations +import re import subprocess from collections.abc import Generator +from pathlib import Path from unittest.mock import patch import pytest from fastapi.testclient import TestClient -from app.main import create_app +from app.main import SERVICE_VERSION, create_app from app.services.executor_base import HealthCheck from app.services.executor_docker import DockerExecutor from app.services.executor_factory import get_executor +CHART_YAML = Path(__file__).resolve().parents[3] / "kubernetes" / "code-interpreter" / "Chart.yaml" + @pytest.fixture(autouse=True) def _clear_executor_cache() -> Generator[None, None, None]: @@ -29,6 +33,7 @@ def test_health_returns_ok_when_backend_healthy() -> None: body = response.json() assert body["status"] == "ok" assert body["message"] is None + assert body["version"] == SERVICE_VERSION def test_health_returns_error_when_backend_unhealthy() -> None: @@ -42,6 +47,32 @@ def test_health_returns_error_when_backend_unhealthy() -> None: body = response.json() assert body["status"] == "error" assert body["message"] == "daemon down" + assert body["version"] == SERVICE_VERSION + + +def test_health_version_matches_package_metadata() -> None: + """The version should come from the installed package, not be hardcoded.""" + from importlib.metadata import version as package_version + + assert package_version("code-interpreter") == SERVICE_VERSION + + +def test_service_version_matches_helm_chart_version() -> None: + """Guard against drift between the Python package and the Helm chart. + + A version mismatch means clients calling /health to gate on capabilities + would see one number while the deployment artifact reports another. + """ + assert CHART_YAML.is_file(), f"Chart.yaml not found at {CHART_YAML}" + text = CHART_YAML.read_text(encoding="utf-8") + match = re.search(r"^version:\s*(\S+)\s*$", text, re.MULTILINE) + assert match is not None, f"could not find a top-level 'version:' line in {CHART_YAML}" + chart_version = match.group(1).strip("\"'") + assert chart_version == SERVICE_VERSION, ( + f"Helm chart version {chart_version!r} != Python package version " + f"{SERVICE_VERSION!r}. Bump both together so /health and the deployed " + "chart report the same number." + ) def _make_completed(returncode: int, stderr: bytes = b"") -> subprocess.CompletedProcess[bytes]: diff --git a/code-interpreter/uv.lock b/code-interpreter/uv.lock index dbb947e..93cd6c0 100644 --- a/code-interpreter/uv.lock +++ b/code-interpreter/uv.lock @@ -98,7 +98,7 @@ wheels = [ [[package]] name = "code-interpreter" -version = "0.1.0" +version = "0.3.3" source = { editable = "." } dependencies = [ { name = "fastapi" }, diff --git a/executor/pyproject.toml b/executor/pyproject.toml index 4ee9fd3..e0c61a9 100644 --- a/executor/pyproject.toml +++ b/executor/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "executor" -version = "0.1.0" +version = "0.3.3" description = "Dependency bundle for the code interpreter executor image" requires-python = ">=3.11,<3.12" license = { text = "MIT" } diff --git a/executor/uv.lock b/executor/uv.lock index e25bc6a..ffca250 100644 --- a/executor/uv.lock +++ b/executor/uv.lock @@ -154,7 +154,7 @@ wheels = [ [[package]] name = "executor" -version = "0.1.0" +version = "0.3.3" source = { editable = "." } dependencies = [ { name = "matplotlib" },