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.<sha>). 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.
This commit is contained in:
ethernet
2026-09-23 18:43:33 -04:00
parent babbec1c4c
commit 77a593ea22
2 changed files with 58 additions and 0 deletions

View File

@@ -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 ''}"

View File

@@ -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"}