Release version management: single VERSION file as source of truth
CI / Gateway plugin tests (push) Successful in 5m19s
CI / Kotlin tests (android host + desktop) (push) Successful in 7m0s

- VERSION at repo root (0.1.2); bump it to cut a release
- App: generated AppVersion.kt (config-cache-safe Gradle task with
  VERSION as declared input) shown in Settings; sent to the gateway
  via X-Iris-App-Version header on the SSE open
- Gateway: reports its own version in hello.ack server_caps.app_version
  (read from the repo-root VERSION via the plugin symlink); stores the
  app's version in the device registry caps (merge, not overwrite, so
  an old app reconnecting without the header doesn't wipe it)
- Settings: app + gateway version rows, mismatch hint, and a best-effort
  Gitea latest-release check (ReleaseCheck) with an 'update available' hint
- Release workflow: reads VERSION from the repo (no manual input), with
  a guard against an empty file
- Docs: frames.schema.json + 04-wire-protocol.md updated for app_version
This commit is contained in:
ARIA committed 2026-08-25 14:42:39 +02:00
1 parent f3f1b37221
commit fb980d12b4
17 files changed
+422 -37

No files matched your search

+4
View File
@@ -135,6 +135,7 @@ from .setup import (
validate_config,
)
from .tool_frames import ToolProgressHandlers
from .version import plugin_version
logger = logging.getLogger(__name__)
@@ -778,6 +779,9 @@ class IrisAdapter(
self._push.server_url if isinstance(self._push, NtfyBackend) else ""
),
"pickers": True, # picker.choice / picker.select (slash-command menus)
# Release version from the repo-root VERSION file (version.py);
# the app shows it in Settings and hints on app/gateway mismatch.
"app_version": plugin_version(),
}
+16 -2
View File
@@ -490,11 +490,20 @@ class HttpServer:
device_name = (handler.headers.get("X-Iris-Device-Name") or "").strip()[:120]
fcm_token = handler.headers.get("X-Iris-Fcm-Token") or None
ntfy_topic = handler.headers.get("X-Iris-Ntfy-Topic") or None
# App release version (the repo-root VERSION baked into the build);
# stored in the device registry's caps JSON so `hermes` can see which
# app version each device runs (old-version awareness). A missing
# header (old app build) must not wipe a previously stored version,
# so merge over the existing caps instead of replacing them.
app_version = (handler.headers.get("X-Iris-App-Version") or "").strip()[:40]
try:
existing_caps = dict(self._devices.get(device_id) or {}).get("caps") or {}
if app_version:
existing_caps["app_version"] = app_version
self._devices.upsert(
device_id,
device_name or device_id,
None,
existing_caps or None,
fcm_token,
ntfy_topic,
)
@@ -523,7 +532,12 @@ class HttpServer:
# lifecycle is the primary "is the device connected?" signal
# for debugging flaky links — a gap here is invisible at the
# gateway's default log level.
logger.info("iris: SSE stream opened: %s (cursor=%d)", device_id, cursor)
logger.info(
"iris: SSE stream opened: %s (cursor=%d, app_version=%s)",
device_id,
cursor,
app_version or "?",
)
# 1. Catch-up from the outbox (id = cursor; the envelope also
# carries the cursor for the app's push dedupe).
max_cursor = cursor
+26
View File
@@ -0,0 +1,26 @@
"""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).
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.
"""
from __future__ import annotations
from pathlib import Path
_FALLBACK = "unknown"
def plugin_version() -> str:
"""The release version from ``<repo>/VERSION``, or ``"unknown"``."""
candidate = Path(__file__).resolve().parent.parent / "VERSION"
try:
version = candidate.read_text().strip()
except OSError:
return _FALLBACK
return version or _FALLBACK