Fix gateway advertising 'unknown' version in production installs
hermes plugins install <repo>#gateway-plugin ships ONLY the gateway-plugin/ subdirectory into ~/.hermes/plugins/iris, so the repo-root VERSION file is not present there and plugin_version() fell back to 'unknown' — the app then showed a false 'App and gateway versions differ' warning. - version.py: resolution chain repo-root VERSION (dev/symlink install) -> plugin.yaml version field (production install) -> 'unknown'; stdlib-only parse, base= param for tests - plugin.yaml: bump stale version 0.1.0 -> 0.1.2 (matches VERSION) - scripts/check_version_sync.sh + pre-commit hook: fail commits where plugin.yaml drifts from the repo-root VERSION - tests: regression test simulating the production layout
This commit is contained in:
1 parent
ea375fd88c
commit
0f5b5a16ab
5 files changed
+146
-14
No files matched your search
@@ -13,3 +13,9 @@ repos:
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
- id: check-version-sync
|
||||
name: check gateway-plugin/plugin.yaml version == repo-root VERSION
|
||||
entry: scripts/check_version_sync.sh
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
@@ -1,7 +1,11 @@
|
||||
name: iris-platform
|
||||
label: Iris
|
||||
kind: platform
|
||||
version: 0.1.0
|
||||
# MUST match the repo-root VERSION file (checked by
|
||||
# scripts/check_version_sync.sh on commit). This field is the version the
|
||||
# gateway advertises in production installs, where only this plugin dir is
|
||||
# shipped (see version.py).
|
||||
version: 0.1.2
|
||||
description: >
|
||||
Native Android / Desktop client gateway adapter for Hermes Agent.
|
||||
Runs an HTTP server (optional TLS) inside the gateway; the app connects
|
||||
|
||||
+54
-13
@@ -1,26 +1,67 @@
|
||||
"""Version discovery -- the repo-root ``VERSION`` file is the single source
|
||||
of truth for the release version ("everything from here on out is vX.Y.Z"
|
||||
= bump ``VERSION`` and commit).
|
||||
"""Version discovery.
|
||||
|
||||
The plugin lives at ``<repo>/gateway-plugin`` (installed into
|
||||
``~/.hermes/plugins/iris`` as a symlink in production), so the ``VERSION``
|
||||
file is one directory up. The value is advertised to the app in
|
||||
``hello.ack`` (``server_caps.app_version``) so the app can show which
|
||||
gateway version it is talking to.
|
||||
The repo-root ``VERSION`` file is the single source of truth for the release
|
||||
version ("everything from here on out is vX.Y.Z" = bump ``VERSION`` and
|
||||
commit). It is advertised to the app in ``hello.ack``
|
||||
(``server_caps.app_version``) so the app can show which gateway version it is
|
||||
talking to.
|
||||
|
||||
Resolution order (first hit wins):
|
||||
|
||||
1. ``<repo>/VERSION`` — dev checkout / symlink install: the plugin lives at
|
||||
``<repo>/gateway-plugin``, so the ``VERSION`` file is one directory up.
|
||||
2. ``version:`` in the plugin's own ``plugin.yaml`` — production install:
|
||||
``hermes plugins install <repo>#gateway-plugin`` moves ONLY the
|
||||
``gateway-plugin/`` subdirectory into ``~/.hermes/plugins/iris``, so the
|
||||
repo-root ``VERSION`` is not present there. ``plugin.yaml`` ships with the
|
||||
plugin dir; a pre-commit hook (``scripts/check_version_sync.sh``) keeps its
|
||||
``version:`` field in sync with the repo-root ``VERSION``.
|
||||
3. ``"unknown"``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
_FALLBACK = "unknown"
|
||||
|
||||
_VERSION_RE = re.compile(r"^version:\s*[\"']?([^\"'\s]+)")
|
||||
|
||||
def plugin_version() -> str:
|
||||
"""The release version from ``<repo>/VERSION``, or ``"unknown"``."""
|
||||
candidate = Path(__file__).resolve().parent.parent / "VERSION"
|
||||
|
||||
def _read_version_file(path: Path) -> str:
|
||||
try:
|
||||
version = candidate.read_text().strip()
|
||||
return path.read_text().strip()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def _plugin_yaml_version(plugin_dir: Path) -> str:
|
||||
"""The top-level ``version:`` field of ``plugin.yaml`` (stdlib-only parse)."""
|
||||
try:
|
||||
text = (plugin_dir / "plugin.yaml").read_text()
|
||||
except OSError:
|
||||
return ""
|
||||
for line in text.splitlines():
|
||||
m = _VERSION_RE.match(line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
def plugin_version(base: Path | None = None) -> str:
|
||||
"""The release version, or ``"unknown"`` if it cannot be found.
|
||||
|
||||
``base`` overrides the plugin directory (tests); by default it is the
|
||||
directory containing this file.
|
||||
"""
|
||||
plugin_dir = base if base is not None else Path(__file__).resolve().parent
|
||||
# 1. Repo-root VERSION (dev checkout / symlink install).
|
||||
version = _read_version_file(plugin_dir.parent / "VERSION")
|
||||
if version:
|
||||
return version
|
||||
# 2. plugin.yaml (production install ships only the plugin dir).
|
||||
version = _plugin_yaml_version(plugin_dir)
|
||||
if version:
|
||||
return version
|
||||
return _FALLBACK
|
||||
return version or _FALLBACK
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env bash
|
||||
# Fail the commit if gateway-plugin/plugin.yaml's `version:` field drifts
|
||||
# from the repo-root VERSION file (the single source of truth).
|
||||
#
|
||||
# Why: `hermes plugins install <repo>#gateway-plugin` ships ONLY the
|
||||
# gateway-plugin/ subdirectory into ~/.hermes/plugins/iris, so in production
|
||||
# the gateway advertises the version from plugin.yaml (see
|
||||
# gateway-plugin/version.py). If the two drift, the app shows a false
|
||||
# "versions differ" warning.
|
||||
set -euo pipefail
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
version_file="$repo_root/VERSION"
|
||||
plugin_yaml="$repo_root/gateway-plugin/plugin.yaml"
|
||||
|
||||
[ -f "$version_file" ] || {
|
||||
echo "check_version_sync: missing $version_file" >&2
|
||||
exit 1
|
||||
}
|
||||
[ -f "$plugin_yaml" ] || {
|
||||
echo "check_version_sync: missing $plugin_yaml" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
root_version="$(tr -d '[:space:]' <"$version_file")"
|
||||
# Mirrors gateway-plugin/version.py's _VERSION_RE: optional single OR double
|
||||
# quote, at least one captured character.
|
||||
yaml_version="$(sed -n "s/^version:[[:space:]]*[\"']\{0,1\}\([^\"'[:space:]]\{1,\}\).*/\1/p" "$plugin_yaml" | head -n1)"
|
||||
|
||||
if [ -z "$yaml_version" ]; then
|
||||
echo "check_version_sync: no top-level 'version:' field in gateway-plugin/plugin.yaml" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$root_version" != "$yaml_version" ]; then
|
||||
echo "check_version_sync: version drift" >&2
|
||||
echo " VERSION (repo root) = $root_version" >&2
|
||||
echo " gateway-plugin/plugin.yaml = $yaml_version" >&2
|
||||
echo "Bump both to the same value (VERSION is the source of truth)." >&2
|
||||
exit 1
|
||||
fi
|
||||
@@ -486,6 +486,46 @@ async def test_hello_ack_advertises_app_version_and_sse_header_is_stored(
|
||||
assert device2["caps"].get("app_version") == "9.9.9"
|
||||
|
||||
|
||||
def test_plugin_version_falls_back_to_plugin_yaml_in_production_layout(plugin, tmp_path):
|
||||
"""Production install (``hermes plugins install <repo>#gateway-plugin``)
|
||||
ships ONLY the ``gateway-plugin/`` subdirectory into
|
||||
``~/.hermes/plugins/iris`` — the repo-root ``VERSION`` file is not
|
||||
present there. The advertised version must then come from
|
||||
``plugin.yaml``, not "unknown" (regression: the app showed
|
||||
'Gateway vunknown' against a production gateway)."""
|
||||
v = plugin.version
|
||||
|
||||
# Dev checkout / symlink install: repo-root VERSION is the source of truth.
|
||||
root_version = Path(v.__file__).resolve().parent.parent / "VERSION"
|
||||
root_value = root_version.read_text().strip() if root_version.is_file() else ""
|
||||
if root_value:
|
||||
assert v.plugin_version() == root_value
|
||||
|
||||
# Simulate the production layout: a plugin dir with a plugin.yaml but no
|
||||
# VERSION file one level up.
|
||||
prod_dir = tmp_path / "plugins" / "iris"
|
||||
prod_dir.mkdir(parents=True)
|
||||
(prod_dir / "plugin.yaml").write_text(
|
||||
(Path(v.__file__).parent / "plugin.yaml").read_text()
|
||||
)
|
||||
advertised = v.plugin_version(base=prod_dir)
|
||||
assert advertised != "unknown"
|
||||
# The pre-commit hook (scripts/check_version_sync.sh) keeps plugin.yaml in
|
||||
# sync with the repo-root VERSION, so the production fallback must
|
||||
# advertise the same real value.
|
||||
if root_value:
|
||||
assert advertised == root_value
|
||||
|
||||
# A VERSION file one level up still wins when present.
|
||||
(tmp_path / "plugins" / "VERSION").write_text("9.9.9\n")
|
||||
assert v.plugin_version(base=prod_dir) == "9.9.9"
|
||||
|
||||
# Nothing at all -> "unknown".
|
||||
empty = tmp_path / "empty"
|
||||
empty.mkdir()
|
||||
assert v.plugin_version(base=empty) == "unknown"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disconnect_broadcasts_status_restarting(adapter):
|
||||
"""Teardown broadcasts ``status{state=restarting}`` before closing the
|
||||
|
||||
Reference in new issue
Block a user