fix(hermes): version chat release assets
This commit is contained in:
parent
55e21c9e08
commit
a68568d0c9
@ -226,6 +226,7 @@ spec:
|
|||||||
--context="dir://${WORKSPACE}" \
|
--context="dir://${WORKSPACE}" \
|
||||||
--dockerfile="${WORKSPACE}/dockerfiles/Dockerfile.hermes-webui" \
|
--dockerfile="${WORKSPACE}/dockerfiles/Dockerfile.hermes-webui" \
|
||||||
--destination="${destination}" \
|
--destination="${destination}" \
|
||||||
|
--build-arg="HERMES_WEBUI_RELEASE_ID=git-${source_revision}-build-${BUILD_NUMBER}" \
|
||||||
--digest-file="${WORKSPACE}/build/hermes-webui.digest" \
|
--digest-file="${WORKSPACE}/build/hermes-webui.digest" \
|
||||||
--image-name-tag-with-digest-file="${WORKSPACE}/build/hermes-webui.image" \
|
--image-name-tag-with-digest-file="${WORKSPACE}/build/hermes-webui.image" \
|
||||||
--label="org.opencontainers.image.revision=${source_revision}" \
|
--label="org.opencontainers.image.revision=${source_revision}" \
|
||||||
|
|||||||
@ -4,6 +4,8 @@ FROM ghcr.io/nesquena/hermes-webui@sha256:a83a3893111dcb250e7aa7aa657d3d6f4570b0
|
|||||||
|
|
||||||
FROM registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
FROM registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107
|
||||||
|
|
||||||
|
ARG HERMES_WEBUI_RELEASE_ID
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
# Keep WebUI and Hermes pinned together. The WebUI imports Hermes internals,
|
# Keep WebUI and Hermes pinned together. The WebUI imports Hermes internals,
|
||||||
@ -23,6 +25,7 @@ COPY dockerfiles/hermes-webui-router-patch.py /tmp/hermes-webui-router-patch.py
|
|||||||
COPY dockerfiles/hermes-webui-router.js /opt/hermes-webui/static/atlas-router.js
|
COPY dockerfiles/hermes-webui-router.js /opt/hermes-webui/static/atlas-router.js
|
||||||
COPY dockerfiles/hermes-webui-brand-patch.py /tmp/hermes-webui-brand-patch.py
|
COPY dockerfiles/hermes-webui-brand-patch.py /tmp/hermes-webui-brand-patch.py
|
||||||
COPY dockerfiles/hermes-webui-manifest-patch.py /tmp/hermes-webui-manifest-patch.py
|
COPY dockerfiles/hermes-webui-manifest-patch.py /tmp/hermes-webui-manifest-patch.py
|
||||||
|
COPY dockerfiles/hermes-webui-release-patch.py /tmp/hermes-webui-release-patch.py
|
||||||
COPY dockerfiles/hermes-webui-smoke.py /tmp/hermes-webui-smoke.py
|
COPY dockerfiles/hermes-webui-smoke.py /tmp/hermes-webui-smoke.py
|
||||||
COPY dockerfiles/hermes-webui-brand.css /opt/hermes-webui/static/hermes-brand.css
|
COPY dockerfiles/hermes-webui-brand.css /opt/hermes-webui/static/hermes-brand.css
|
||||||
COPY dockerfiles/hermes-webui-manifest.json /tmp/hermes-webui-manifest.json
|
COPY dockerfiles/hermes-webui-manifest.json /tmp/hermes-webui-manifest.json
|
||||||
@ -35,7 +38,9 @@ RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-base-patch.py \
|
|||||||
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-telegram-project-patch.py \
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-telegram-project-patch.py \
|
||||||
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-router-patch.py \
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-router-patch.py \
|
||||||
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-brand-patch.py \
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-brand-patch.py \
|
||||||
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-manifest-patch.py
|
&& /opt/hermes/.venv/bin/python /tmp/hermes-webui-manifest-patch.py \
|
||||||
|
&& HERMES_WEBUI_RELEASE_ID="${HERMES_WEBUI_RELEASE_ID}" \
|
||||||
|
/opt/hermes/.venv/bin/python /tmp/hermes-webui-release-patch.py
|
||||||
|
|
||||||
RUN /opt/hermes/.venv/bin/python -c 'import cryptography, yaml' \
|
RUN /opt/hermes/.venv/bin/python -c 'import cryptography, yaml' \
|
||||||
&& grep -Fq 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")' \
|
&& grep -Fq 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")' \
|
||||||
|
|||||||
@ -98,7 +98,7 @@ replace_between_exact(
|
|||||||
' <h2 data-i18n="empty_title">',
|
' <h2 data-i18n="empty_title">',
|
||||||
' <div class="empty-logo">\n'
|
' <div class="empty-logo">\n'
|
||||||
' <img class="hermes-agent-portrait" '
|
' <img class="hermes-agent-portrait" '
|
||||||
'src="static/hermes-agent-192.png" alt="Hermes Agent">\n'
|
'src="static/hermes-agent-512.png" alt="Hermes Agent">\n'
|
||||||
' </div>\n',
|
' </div>\n',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
25
dockerfiles/hermes-webui-release-patch.py
Normal file
25
dockerfiles/hermes-webui-release-patch.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stamp Atlas WebUI shell URLs with the immutable image release identity."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(os.environ.get("HERMES_WEBUI_PATCH_ROOT", "/opt/hermes-webui"))
|
||||||
|
RELEASE_ID = os.environ.get("HERMES_WEBUI_RELEASE_ID", "").strip()
|
||||||
|
|
||||||
|
if not re.fullmatch(r"[a-z0-9][a-z0-9.-]{0,127}", RELEASE_ID):
|
||||||
|
raise SystemExit("HERMES_WEBUI_RELEASE_ID must be a safe immutable release token")
|
||||||
|
|
||||||
|
for relative in ("static/index.html", "static/sw.js"):
|
||||||
|
path = ROOT / relative
|
||||||
|
source = path.read_text(encoding="utf-8")
|
||||||
|
if "__WEBUI_VERSION__" not in source:
|
||||||
|
raise SystemExit(f"Hermes release patch context changed in {path}")
|
||||||
|
path.write_text(
|
||||||
|
source.replace("__WEBUI_VERSION__", f"__WEBUI_VERSION__-{RELEASE_ID}"),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
@ -7,6 +7,7 @@ import importlib.util
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -22,6 +23,7 @@ AGENT_FIXTURE = ROOT / "testing/fixtures/hermes-agent"
|
|||||||
ATLAS_PATCHER = DOCKERFILES / "hermes-webui-atlas-patch.py"
|
ATLAS_PATCHER = DOCKERFILES / "hermes-webui-atlas-patch.py"
|
||||||
BRAND_PATCHER = DOCKERFILES / "hermes-webui-brand-patch.py"
|
BRAND_PATCHER = DOCKERFILES / "hermes-webui-brand-patch.py"
|
||||||
MANIFEST_PATCHER = DOCKERFILES / "hermes-webui-manifest-patch.py"
|
MANIFEST_PATCHER = DOCKERFILES / "hermes-webui-manifest-patch.py"
|
||||||
|
RELEASE_PATCHER = DOCKERFILES / "hermes-webui-release-patch.py"
|
||||||
SMOKE = DOCKERFILES / "hermes-webui-smoke.py"
|
SMOKE = DOCKERFILES / "hermes-webui-smoke.py"
|
||||||
ASSETS = DOCKERFILES / "hermes-webui-assets"
|
ASSETS = DOCKERFILES / "hermes-webui-assets"
|
||||||
MANIFEST = DOCKERFILES / "hermes-webui-manifest.json"
|
MANIFEST = DOCKERFILES / "hermes-webui-manifest.json"
|
||||||
@ -144,7 +146,7 @@ def test_production_patchers_apply_title_icons_theme_and_cache_contract(
|
|||||||
assert '<img src="static/hermes-agent-192.png" alt="">' in index
|
assert '<img src="static/hermes-agent-192.png" alt="">' in index
|
||||||
assert (
|
assert (
|
||||||
'<img class="hermes-agent-portrait" '
|
'<img class="hermes-agent-portrait" '
|
||||||
'src="static/hermes-agent-192.png" alt="Hermes Agent">'
|
'src="static/hermes-agent-512.png" alt="Hermes Agent">'
|
||||||
) in index
|
) in index
|
||||||
assert 'aria-label="Hermes caduceus"' not in index
|
assert 'aria-label="Hermes caduceus"' not in index
|
||||||
assert "favicon.svg" not in index
|
assert "favicon.svg" not in index
|
||||||
@ -321,10 +323,79 @@ def test_empty_state_uses_canonical_character_without_inline_staff(
|
|||||||
empty_state = index.split('<div class="empty-state" id="emptyState">', 1)[1]
|
empty_state = index.split('<div class="empty-state" id="emptyState">', 1)[1]
|
||||||
empty_state = empty_state.split('<div class="messages-inner"', 1)[0]
|
empty_state = empty_state.split('<div class="messages-inner"', 1)[0]
|
||||||
assert empty_state.count('class="hermes-agent-portrait"') == 1
|
assert empty_state.count('class="hermes-agent-portrait"') == 1
|
||||||
assert empty_state.count('src="static/hermes-agent-192.png"') == 1
|
assert empty_state.count('src="static/hermes-agent-512.png"') == 1
|
||||||
assert "<svg" not in empty_state.split('<h2 data-i18n="empty_title">', 1)[0]
|
assert "<svg" not in empty_state.split('<h2 data-i18n="empty_title">', 1)[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_release_patch_gives_every_shell_url_an_immutable_build_token(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Atlas image releases never share a browser cache key."""
|
||||||
|
target = _patched_fixture(tmp_path)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["HERMES_WEBUI_PATCH_ROOT"] = str(target)
|
||||||
|
env["HERMES_WEBUI_RELEASE_ID"] = "git-0123456789abcdef-build-11"
|
||||||
|
subprocess.run(
|
||||||
|
[sys.executable, str(RELEASE_PATCHER)],
|
||||||
|
cwd=ROOT,
|
||||||
|
env=env,
|
||||||
|
check=True,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
index = (target / "static/index.html").read_text(encoding="utf-8")
|
||||||
|
worker = (target / "static/sw.js").read_text(encoding="utf-8")
|
||||||
|
token = "__WEBUI_VERSION__-git-0123456789abcdef-build-11"
|
||||||
|
assert token in index
|
||||||
|
assert token in worker
|
||||||
|
assert not re.search(r"__WEBUI_VERSION__(?!-git-0123456789abcdef-build-11)", index)
|
||||||
|
assert not re.search(r"__WEBUI_VERSION__(?!-git-0123456789abcdef-build-11)", worker)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("release_id", ["", "../escape", "UPPERCASE", "a" * 129])
|
||||||
|
def test_release_patch_rejects_missing_or_unsafe_tokens(
|
||||||
|
tmp_path: Path, release_id: str
|
||||||
|
) -> None:
|
||||||
|
"""A malformed build token cannot silently collapse browser cache keys."""
|
||||||
|
target = _patched_fixture(tmp_path)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["HERMES_WEBUI_PATCH_ROOT"] = str(target)
|
||||||
|
env["HERMES_WEBUI_RELEASE_ID"] = release_id
|
||||||
|
completed = subprocess.run(
|
||||||
|
[sys.executable, str(RELEASE_PATCHER)],
|
||||||
|
cwd=ROOT,
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert completed.returncode != 0
|
||||||
|
assert "safe immutable release token" in completed.stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_release_patch_rejects_upstream_without_version_markers(
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Pinned upstream drift fails the image build before publication."""
|
||||||
|
target = _patched_fixture(tmp_path)
|
||||||
|
index = target / "static/index.html"
|
||||||
|
index.write_text(
|
||||||
|
index.read_text(encoding="utf-8").replace("__WEBUI_VERSION__", "fixed"),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["HERMES_WEBUI_PATCH_ROOT"] = str(target)
|
||||||
|
env["HERMES_WEBUI_RELEASE_ID"] = "git-0123456789abcdef-build-11"
|
||||||
|
completed = subprocess.run(
|
||||||
|
[sys.executable, str(RELEASE_PATCHER)],
|
||||||
|
cwd=ROOT,
|
||||||
|
env=env,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
assert completed.returncode != 0
|
||||||
|
assert "release patch context changed" in completed.stderr
|
||||||
|
|
||||||
|
|
||||||
def test_dockerfile_copies_and_verifies_every_tracked_brand_asset() -> None:
|
def test_dockerfile_copies_and_verifies_every_tracked_brand_asset() -> None:
|
||||||
"""The immutable image, not a runtime coordinator path, owns PWA assets."""
|
"""The immutable image, not a runtime coordinator path, owns PWA assets."""
|
||||||
dockerfile = (DOCKERFILES / "Dockerfile.hermes-webui").read_text(encoding="utf-8")
|
dockerfile = (DOCKERFILES / "Dockerfile.hermes-webui").read_text(encoding="utf-8")
|
||||||
@ -333,6 +404,8 @@ def test_dockerfile_copies_and_verifies_every_tracked_brand_asset() -> None:
|
|||||||
assert "python /tmp/hermes-webui-brand-patch.py" in dockerfile
|
assert "python /tmp/hermes-webui-brand-patch.py" in dockerfile
|
||||||
assert "COPY dockerfiles/hermes-webui-manifest-patch.py" in dockerfile
|
assert "COPY dockerfiles/hermes-webui-manifest-patch.py" in dockerfile
|
||||||
assert "python /tmp/hermes-webui-manifest-patch.py" in dockerfile
|
assert "python /tmp/hermes-webui-manifest-patch.py" in dockerfile
|
||||||
|
assert "COPY dockerfiles/hermes-webui-release-patch.py" in dockerfile
|
||||||
|
assert "python /tmp/hermes-webui-release-patch.py" in dockerfile
|
||||||
assert (
|
assert (
|
||||||
"COPY dockerfiles/hermes-webui-manifest.json /tmp/hermes-webui-manifest.json"
|
"COPY dockerfiles/hermes-webui-manifest.json /tmp/hermes-webui-manifest.json"
|
||||||
in dockerfile
|
in dockerfile
|
||||||
|
|||||||
@ -139,10 +139,16 @@ def test_webui_dockerfile_is_kaniko_safe_and_uses_reviewed_repo_source() -> None
|
|||||||
assert "hermes-webui-base-patch.py" in source
|
assert "hermes-webui-base-patch.py" in source
|
||||||
assert "hermes-webui-brand-patch.py" in source
|
assert "hermes-webui-brand-patch.py" in source
|
||||||
assert "hermes-webui-manifest-patch.py" in source
|
assert "hermes-webui-manifest-patch.py" in source
|
||||||
|
assert "hermes-webui-release-patch.py" in source
|
||||||
assert "hermes-webui-smoke.py" in source
|
assert "hermes-webui-smoke.py" in source
|
||||||
assert "hermes-webui-stt-patch.py" in source
|
assert "hermes-webui-stt-patch.py" in source
|
||||||
assert "hermes-webui-atlas-voice.js" in source
|
assert "hermes-webui-atlas-voice.js" in source
|
||||||
assert "hermes-webui-manifest.json" in source
|
assert "hermes-webui-manifest.json" in source
|
||||||
|
assert (
|
||||||
|
'--build-arg="HERMES_WEBUI_RELEASE_ID='
|
||||||
|
'git-${source_revision}-build-${BUILD_NUMBER}"'
|
||||||
|
in PIPELINE.read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
assert source.count("RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-") == 1
|
assert source.count("RUN /opt/hermes/.venv/bin/python /tmp/hermes-webui-") == 1
|
||||||
assert "urljoin(manifest_url, source)" in (
|
assert "urljoin(manifest_url, source)" in (
|
||||||
ROOT / "dockerfiles/hermes-webui-smoke.py"
|
ROOT / "dockerfiles/hermes-webui-smoke.py"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user