From 77a593ea2291ef2f38a53efd916a6dd5669d2060 Mon Sep 17 00:00:00 2001 From: ethernet Date: Wed, 23 Sep 2026 18:43:33 -0400 Subject: [PATCH] fix(version): a checkout past a CalVer-only release reports that release's version Every release so far is tagged vYYYY.M.D only, and STABLE_TAG_RE rejects a 4-digit major on purpose, so a git checkout had no base and reported "unknown" (displayed git.). requires_hermes then compared plugins against "unknown" and let everything load. With no semver tag reachable, take the nearest CalVer tag (git describe) and the version its pyproject shipped, counting distance from that tag. A semver tag still wins when one exists; source stamps inherit this through _git_version_info. --- hermes_cli/version_info.py | 26 ++++++++++++++++++++++ tests/hermes_cli/test_version_info.py | 32 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/hermes_cli/version_info.py b/hermes_cli/version_info.py index d3b4005adc..6fb7d32200 100644 --- a/hermes_cli/version_info.py +++ b/hermes_cli/version_info.py @@ -13,6 +13,7 @@ from __future__ import annotations import json import subprocess +import tomllib from dataclasses import dataclass from pathlib import Path from typing import Literal, cast @@ -82,6 +83,29 @@ def _parse_nonnegative(value: str | None) -> int | None: return parsed if parsed >= 0 else None +def _calver_release_version(repo_dir: Path) -> tuple[str, int] | None: + """The version the nearest CalVer release shipped, and the commits since it. + + Releases before semver tags existed are tagged ``vYYYY.M.D`` only; the + version users actually run is in that tag's pyproject. Without this, a + checkout past such a release would compare as "unknown" against plugins' + ``requires_hermes``. + """ + described = _run_git(repo_dir, "describe", "--tags", "--long", "--match", "v2[0-9][0-9][0-9].*", "HEAD") + if not described: + return None + tag, count, _ = described.rsplit("-", 2) + distance = _parse_nonnegative(count) + try: + project = tomllib.loads(_run_git(repo_dir, "show", f"{tag}:pyproject.toml") or "").get("project", {}) + except tomllib.TOMLDecodeError: + return None + version = project.get("version") + if distance is None or not isinstance(version, str) or not STABLE_TAG_RE.fullmatch(f"v{version}"): + return None + return version, distance + + # --- Install stamp reader --------------------------------------------------- def _resolve_stamp_file() -> Path | None: @@ -212,6 +236,8 @@ def _git_version_info(repo_dir: Path, *, include_untracked: bool = False) -> Ver distance = _parse_nonnegative( _run_git(repo_dir, "rev-list", "--count", f"v{base_version}..HEAD") ) if releases else None + if not releases: + base_version, distance = _calver_release_version(repo_dir) or ("unknown", None) short_commit = _run_git(repo_dir, "rev-parse", "--short=7", "HEAD") if base_version == "unknown" and short_commit: display_version = f"git.{short_commit}{'.dirty' if dirty else ''}" diff --git a/tests/hermes_cli/test_version_info.py b/tests/hermes_cli/test_version_info.py index 033f0d7edd..4f166f706a 100644 --- a/tests/hermes_cli/test_version_info.py +++ b/tests/hermes_cli/test_version_info.py @@ -150,6 +150,38 @@ def test_get_version_info_derives_identity_from_reachable_release_tag(tmp_path, assert info.source == "git" +def test_get_version_info_takes_the_version_a_calver_only_release_shipped(tmp_path, monkeypatch): + """Releases tagged only vYYYY.M.D resolve to their pyproject version, not "unknown".""" + repo = tmp_path / "repo" + repo.mkdir() + + def git(*args: str) -> str: + result = subprocess.run( + ["git", *args], cwd=repo, text=True, capture_output=True, check=True, + env={"HOME": str(tmp_path), "PATH": __import__("os").environ["PATH"]}, + ) + return result.stdout.strip() + + git("init", "-q") + git("config", "user.name", "Hermes Test") + git("config", "user.email", "hermes@example.invalid") + (repo / "pyproject.toml").write_text('[project]\nname = "hermes-agent"\nversion = "0.21.4"\n', encoding="utf-8") + git("add", "pyproject.toml") + git("commit", "-qm", "release") + git("tag", "v2026.9.21") + (repo / "pyproject.toml").write_text('[project]\nname = "hermes-agent"\nversion = "0.0.0"\n', encoding="utf-8") + git("commit", "-qam", "next") + + monkeypatch.setattr("hermes_cli.version_info._resolve_stamp_file", lambda: None) + monkeypatch.setattr("hermes_cli.version_info._resolve_repo_dir", lambda: repo) + + info = get_version_info() + + assert info.base_version == "0.21.4" + assert info.distance == 1 + assert info.derived_version == f"0.21.4+1.g{git('rev-parse', '--short=7', 'HEAD')}" + + def test_resolve_stamp_file_honors_install_root(tmp_path, monkeypatch): """Sealed installs (the Nix wrapper) point HERMES_INSTALL_ROOT at the stamp dir.""" stamp = {"commit": "e" * 40, "source": "nix", "distribution": "nix", "updateMechanism": "external"}