From a68568d0c9d8a25c31196f7b1128e57812b115ab Mon Sep 17 00:00:00 2001 From: jenkins Date: Sun, 23 Aug 2026 19:22:26 -0300 Subject: [PATCH] fix(hermes): version chat release assets --- ci/Jenkinsfile.hermes-webui-image | 1 + dockerfiles/Dockerfile.hermes-webui | 7 +- dockerfiles/hermes-webui-brand-patch.py | 2 +- dockerfiles/hermes-webui-release-patch.py | 25 +++++++ testing/tests/test_hermes_webui_brand.py | 77 +++++++++++++++++++++- testing/tests/test_hermes_webui_release.py | 6 ++ 6 files changed, 114 insertions(+), 4 deletions(-) create mode 100644 dockerfiles/hermes-webui-release-patch.py diff --git a/ci/Jenkinsfile.hermes-webui-image b/ci/Jenkinsfile.hermes-webui-image index 18932c36..3173e7b4 100644 --- a/ci/Jenkinsfile.hermes-webui-image +++ b/ci/Jenkinsfile.hermes-webui-image @@ -226,6 +226,7 @@ spec: --context="dir://${WORKSPACE}" \ --dockerfile="${WORKSPACE}/dockerfiles/Dockerfile.hermes-webui" \ --destination="${destination}" \ + --build-arg="HERMES_WEBUI_RELEASE_ID=git-${source_revision}-build-${BUILD_NUMBER}" \ --digest-file="${WORKSPACE}/build/hermes-webui.digest" \ --image-name-tag-with-digest-file="${WORKSPACE}/build/hermes-webui.image" \ --label="org.opencontainers.image.revision=${source_revision}" \ diff --git a/dockerfiles/Dockerfile.hermes-webui b/dockerfiles/Dockerfile.hermes-webui index be26f5de..44e13b8b 100644 --- a/dockerfiles/Dockerfile.hermes-webui +++ b/dockerfiles/Dockerfile.hermes-webui @@ -4,6 +4,8 @@ FROM ghcr.io/nesquena/hermes-webui@sha256:a83a3893111dcb250e7aa7aa657d3d6f4570b0 FROM registry.bstein.dev/bstein/hermes-agent@sha256:81970563e542f0720773e72297810b3a844b83e381e278f25c0916c78d930107 +ARG HERMES_WEBUI_RELEASE_ID + USER root # 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-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-release-patch.py /tmp/hermes-webui-release-patch.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-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-router-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' \ && grep -Fq 'VALID_REASONING_EFFORTS = ("minimal", "low", "medium", "high", "xhigh")' \ diff --git a/dockerfiles/hermes-webui-brand-patch.py b/dockerfiles/hermes-webui-brand-patch.py index c4c0a70b..80753614 100644 --- a/dockerfiles/hermes-webui-brand-patch.py +++ b/dockerfiles/hermes-webui-brand-patch.py @@ -98,7 +98,7 @@ replace_between_exact( '

', ' \n', ) diff --git a/dockerfiles/hermes-webui-release-patch.py b/dockerfiles/hermes-webui-release-patch.py new file mode 100644 index 00000000..a0ba2698 --- /dev/null +++ b/dockerfiles/hermes-webui-release-patch.py @@ -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", + ) diff --git a/testing/tests/test_hermes_webui_brand.py b/testing/tests/test_hermes_webui_brand.py index 053fb49d..ecbd52fb 100644 --- a/testing/tests/test_hermes_webui_brand.py +++ b/testing/tests/test_hermes_webui_brand.py @@ -7,6 +7,7 @@ import importlib.util import json import os from pathlib import Path +import re import shutil import struct import subprocess @@ -22,6 +23,7 @@ AGENT_FIXTURE = ROOT / "testing/fixtures/hermes-agent" ATLAS_PATCHER = DOCKERFILES / "hermes-webui-atlas-patch.py" BRAND_PATCHER = DOCKERFILES / "hermes-webui-brand-patch.py" MANIFEST_PATCHER = DOCKERFILES / "hermes-webui-manifest-patch.py" +RELEASE_PATCHER = DOCKERFILES / "hermes-webui-release-patch.py" SMOKE = DOCKERFILES / "hermes-webui-smoke.py" ASSETS = DOCKERFILES / "hermes-webui-assets" MANIFEST = DOCKERFILES / "hermes-webui-manifest.json" @@ -144,7 +146,7 @@ def test_production_patchers_apply_title_icons_theme_and_cache_contract( assert '' in index assert ( 'Hermes Agent' + 'src="static/hermes-agent-512.png" alt="Hermes Agent">' ) in index assert 'aria-label="Hermes caduceus"' 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('
', 1)[1] empty_state = empty_state.split('
', 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: """The immutable image, not a runtime coordinator path, owns PWA assets.""" 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 "COPY dockerfiles/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 ( "COPY dockerfiles/hermes-webui-manifest.json /tmp/hermes-webui-manifest.json" in dockerfile diff --git a/testing/tests/test_hermes_webui_release.py b/testing/tests/test_hermes_webui_release.py index 6b8f9f2b..a33c22af 100644 --- a/testing/tests/test_hermes_webui_release.py +++ b/testing/tests/test_hermes_webui_release.py @@ -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-brand-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-stt-patch.py" in source assert "hermes-webui-atlas-voice.js" 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 "urljoin(manifest_url, source)" in ( ROOT / "dockerfiles/hermes-webui-smoke.py"