The delivery and image-automation gates now enforce whichever state the chat StatefulSet is actually in: with no hux sidecar they require zero partial HUX wiring (no containers, volumes, PVC, or HUX_* env); with the sidecar staged they enforce the full strict boundary. This lets the reviewed source chain merge and build before the activation topology lands, without ever waiving an activated assertion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BvMSXH8VH2tMWXanb8SJdf
128 lines
5.3 KiB
Python
128 lines
5.3 KiB
Python
"""Contracts for automatic deployment of validated Hermes image releases."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import re
|
|
|
|
import yaml
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SERVICE = ROOT / "services/hermes"
|
|
APPLICATIONS = ROOT / "clusters/atlas/flux-system/applications"
|
|
|
|
|
|
def test_image_policies_observe_only_validated_release_tags() -> None:
|
|
"""Candidates remain invisible until Jenkins publishes the release suffix."""
|
|
documents = list(
|
|
yaml.safe_load_all((SERVICE / "image.yaml").read_text(encoding="utf-8"))
|
|
)
|
|
repositories = {
|
|
item["metadata"]["name"]: item
|
|
for item in documents
|
|
if item["kind"] == "ImageRepository"
|
|
}
|
|
policies = {
|
|
item["metadata"]["name"]: item
|
|
for item in documents
|
|
if item["kind"] == "ImagePolicy"
|
|
}
|
|
assert set(repositories) == {
|
|
"hermes-agent-release",
|
|
"hermes-chat-router-release",
|
|
"hermes-webui-release",
|
|
"hermes-stt-release",
|
|
"hermes-tts-release",
|
|
}
|
|
assert set(policies) == set(repositories)
|
|
for name, policy in policies.items():
|
|
assert policy["metadata"]["namespace"] == "hermes"
|
|
assert policy["spec"]["imageRepositoryRef"]["name"] == name
|
|
assert policy["spec"]["filterTags"] == {
|
|
"pattern": ("^git-[0-9a-f]{40}-build-" "(?P<build>[1-9][0-9]*)-release$"),
|
|
"extract": "$build",
|
|
}
|
|
assert policy["spec"]["policy"] == {"numerical": {"order": "asc"}}
|
|
assert policy["spec"]["digestReflectionPolicy"] == "Always"
|
|
|
|
|
|
def test_flux_updates_only_the_reviewed_hermes_image_digests() -> None:
|
|
"""Flux persists selected digests to Git and rolls all matching workloads."""
|
|
service_kustomization = (SERVICE / "kustomization.yaml").read_text(encoding="utf-8")
|
|
applications_kustomization = (APPLICATIONS / "kustomization.yaml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
automation = yaml.safe_load(
|
|
(APPLICATIONS / "hermes/image-automation.yaml").read_text(encoding="utf-8")
|
|
)
|
|
agent = (SERVICE / "kustomization.yaml").read_text(encoding="utf-8")
|
|
chat = (SERVICE / "chat-statefulset.yaml").read_text(encoding="utf-8")
|
|
dashboard = (SERVICE / "deployment.yaml").read_text(encoding="utf-8")
|
|
voice = (SERVICE / "voice-deployment.yaml").read_text(encoding="utf-8")
|
|
router = (SERVICE / "chat-router.yaml").read_text(encoding="utf-8")
|
|
|
|
assert " - image.yaml" in service_kustomization
|
|
assert " - hermes/image-automation.yaml" in applications_kustomization
|
|
assert automation["spec"]["git"]["checkout"]["ref"]["branch"] == "main"
|
|
assert automation["spec"]["git"]["push"]["branch"] == "main"
|
|
assert automation["spec"]["update"] == {
|
|
"strategy": "Setters",
|
|
"path": "services/hermes",
|
|
}
|
|
assert agent.count('"$imagepolicy": "hermes:hermes-agent-release:digest"') == 1
|
|
webui_marker = '"$imagepolicy": "hermes:hermes-webui-release"'
|
|
chat_object = yaml.safe_load(chat)
|
|
hux = next(
|
|
(
|
|
item
|
|
for item in chat_object["spec"]["template"]["spec"]["containers"]
|
|
if item["name"] == "hux"
|
|
),
|
|
None,
|
|
)
|
|
# Before activation the chat StatefulSet carries exactly one WebUI
|
|
# consumer and no HUX setters; after activation the HUX sidecar is the
|
|
# second consumer of the very same reviewed image line, with tag and
|
|
# digest setters binding its build metadata.
|
|
assert chat.count(webui_marker) == (2 if hux else 1)
|
|
assert dashboard.count(webui_marker) == 1
|
|
assert chat.count(
|
|
'"$imagepolicy": "hermes:hermes-webui-release:tag"'
|
|
) == (1 if hux else 0)
|
|
assert chat.count(
|
|
'"$imagepolicy": "hermes:hermes-webui-release:digest"'
|
|
) == (1 if hux else 0)
|
|
if hux:
|
|
hux_env = {
|
|
item["name"]: item["value"] for item in hux["env"] if "value" in item
|
|
}
|
|
release = re.fullmatch(
|
|
r"git-([0-9a-f]{40})-build-[1-9][0-9]*-release",
|
|
hux_env["HUX_IMAGE_TAG"],
|
|
)
|
|
assert release is not None
|
|
assert hux["image"].split(":git-", 1)[1].split("@", 1)[0] == hux_env[
|
|
"HUX_IMAGE_TAG"
|
|
].removeprefix("git-")
|
|
assert hux["image"].endswith("@" + hux_env["HUX_IMAGE_DIGEST"])
|
|
# A digest-only setter replaces the complete YAML scalar with ``sha256:...``.
|
|
# Whole-image setters must retain the registry and repository in pod specs.
|
|
for workload in (chat, dashboard):
|
|
marked_line = next(
|
|
line for line in workload.splitlines() if webui_marker in line
|
|
)
|
|
assert "registry.bstein.dev/bstein/hermes-webui:" in marked_line
|
|
assert "@sha256:" in marked_line
|
|
for component in ("stt", "tts"):
|
|
marker = f'"$imagepolicy": "hermes:hermes-{component}-release"'
|
|
assert voice.count(marker) == 1
|
|
marked_line = next(line for line in voice.splitlines() if marker in line)
|
|
assert f"registry.bstein.dev/bstein/hermes-jetson-{component}" in marked_line
|
|
assert "@sha256:" in marked_line
|
|
router_marker = '"$imagepolicy": "hermes:hermes-chat-router-release"'
|
|
assert router.count(router_marker) == 1
|
|
marked_line = next(line for line in router.splitlines() if router_marker in line)
|
|
assert "registry.bstein.dev/bstein/hermes-chat-router" in marked_line
|
|
assert "@sha256:" in marked_line
|