diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 4fb965b..a25235b 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -373,6 +373,21 @@ jobs: -t svcforge/${{ matrix.svc }}:ci \ . + - name: import smoke test + # Import the service's entrypoint module INSIDE the built image, which every unit and + # integration test that passes cannot do: they import from the source tree, where + # every file exists. The image is a different filesystem — each Dockerfile copies only + # its own `services//`, so a shared module added at the `services/` root, or any + # dependency the Dockerfile forgets, is present in the tests and absent in the image. + # + # That gap shipped a reconciler that crashed on boot with + # `ModuleNotFoundError: No module named 'services._runtime'` while every gate was + # green. Importing main here loads the whole transitive graph and fails the build + # before the digest is pushed, instead of after ArgoCD has rolled it out. + run: | + docker run --rm --entrypoint python svcforge/${{ matrix.svc }}:ci \ + -c "import services.${{ matrix.svc }}.main" + - name: trivy # Run trivy directly rather than via aquasecurity/trivy-action, for the same reason # gitleaks is run directly above: the command is the documented one, pinned by diff --git a/services/_runtime.py b/libs/svcforge_core/svcforge_core/runtime.py similarity index 100% rename from services/_runtime.py rename to libs/svcforge_core/svcforge_core/runtime.py diff --git a/services/reconciler/main.py b/services/reconciler/main.py index b60b28f..8a94620 100644 --- a/services/reconciler/main.py +++ b/services/reconciler/main.py @@ -34,7 +34,6 @@ from dataclasses import dataclass import typer -from services._runtime import install_stop_signals, sleep_or_stop from svcforge_core.adapters.clock import Clock, SystemClock from svcforge_core.adapters.helm import HelmProvisioner, Provisioner from svcforge_core.adapters.notify import LogNotifier, Notifier @@ -54,6 +53,7 @@ from svcforge_core.repo.db import DictPool, make_pool from svcforge_core.repo.instances import InstanceRepo from svcforge_core.repo.reconcile import ReconcileRepo from svcforge_core.repo.tasks import TaskRepo +from svcforge_core.runtime import install_stop_signals, sleep_or_stop from svcforge_core.settings import Settings, load_settings log = get_logger("svcforge.reconciler") diff --git a/services/worker/main.py b/services/worker/main.py index f855ab4..1733627 100644 --- a/services/worker/main.py +++ b/services/worker/main.py @@ -15,7 +15,6 @@ from collections.abc import Awaitable from opentelemetry import trace -from services._runtime import install_stop_signals, sleep_or_stop from services.worker.deps import WorkerDeps from services.worker.handlers import HANDLERS from svcforge_core import obs @@ -27,6 +26,7 @@ from svcforge_core.domain.models import Task, TaskKind from svcforge_core.repo.db import make_pool from svcforge_core.repo.instances import InstanceRepo from svcforge_core.repo.tasks import TaskRepo +from svcforge_core.runtime import install_stop_signals, sleep_or_stop from svcforge_core.settings import Settings, load_settings log = obs.get_logger("svcforge.worker")