From 17c7dfea8cb1d5391adf134c414fccc17272cc03 Mon Sep 17 00:00:00 2001 From: ethernet Date: Fri, 24 Jul 2026 17:48:39 -0400 Subject: [PATCH] feat(version): add commitDate to build stamp and version_info --- hermes_cli/dump.py | 24 +++++++--------- hermes_cli/version_info.py | 12 +++++++- nix/hermes-agent.nix | 3 +- nix/packages.nix | 1 + scripts/write_install_stamp.py | 16 +++++++++++ tests/hermes_cli/test_dump_git_commit.py | 35 +++++++++--------------- tests/hermes_cli/test_version_info.py | 4 ++- 7 files changed, 56 insertions(+), 39 deletions(-) diff --git a/hermes_cli/dump.py b/hermes_cli/dump.py index 1aa3a8572c2..c52a9420e3c 100644 --- a/hermes_cli/dump.py +++ b/hermes_cli/dump.py @@ -72,24 +72,20 @@ def _get_git_commit(project_root: Path) -> str: def _get_git_commit_date(project_root: Path) -> str: """Return the date the HEAD commit was authored (YYYY-MM-DD), or ''. - Resolves live via ``git log`` on source installs. The published Docker - image excludes ``.git``, so this returns '' there — the dump line simply - drops the date suffix in that case (the baked SHA still identifies the - build). + Uses ``version_info.get_version_info()`` which carries the commit date + as a Unix timestamp from the install stamp (Docker/Nix) or live git + (source installs). Formats as YYYY-MM-DD for display. """ try: - result = subprocess.run( - ["git", "log", "-1", "--format=%cd", "--date=short", "HEAD"], - capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=5, - cwd=str(project_root), - ) - if result.returncode == 0: - value = result.stdout.strip() - if value: - return value + from hermes_cli.version_info import get_version_info + + info = get_version_info() + if info.commit_date: + from datetime import datetime, timezone + + return datetime.fromtimestamp(info.commit_date, tz=timezone.utc).strftime("%Y-%m-%d") except Exception: pass - return "" diff --git a/hermes_cli/version_info.py b/hermes_cli/version_info.py index afc8378fd5d..32921d8c21e 100644 --- a/hermes_cli/version_info.py +++ b/hermes_cli/version_info.py @@ -33,6 +33,7 @@ class VersionInfo: branch: str | None source: Literal["git", "nix", "docker", "build", "unknown"] dirty: bool = False + commit_date: int | None = None def format_display_version(info: VersionInfo | None = None) -> str: @@ -137,6 +138,10 @@ def _stamp_version_info() -> VersionInfo | None: else: source = "build" + commit_date = data.get("commitDate") + if not isinstance(commit_date, int): + commit_date = None + return VersionInfo( base_version, display_version, @@ -145,6 +150,7 @@ def _stamp_version_info() -> VersionInfo | None: data.get("branch") or None, source, bool(data.get("dirty")), + commit_date, ) @@ -156,6 +162,10 @@ def _git_version_info(repo_dir: Path) -> VersionInfo: branch = _run_git(repo_dir, "branch", "--show-current") if not branch and commit: branch = commit[:8] + commit_date_raw = _run_git(repo_dir, "log", "-1", "--format=%ct", "HEAD") + commit_date: int | None = None + if commit_date_raw and commit_date_raw.isdigit(): + commit_date = int(commit_date_raw) try: dirty_result = subprocess.run( ["git", "status", "--porcelain"], @@ -179,7 +189,7 @@ def _git_version_info(repo_dir: Path) -> VersionInfo: break return VersionInfo( - __version__, _derived_version(__version__, distance, dirty), distance, commit, branch, "git", dirty + __version__, _derived_version(__version__, distance, dirty), distance, commit, branch, "git", dirty, commit_date ) diff --git a/nix/hermes-agent.nix b/nix/hermes-agent.nix index 8c2f8c580a6..228df8f24c5 100644 --- a/nix/hermes-agent.nix +++ b/nix/hermes-agent.nix @@ -37,6 +37,7 @@ revCount ? null, branch ? null, dirty ? false, + lastModified ? null, # Overridable parameters extraPythonPackages ? [ ], extraDependencyGroups ? [ ], @@ -205,7 +206,7 @@ stdenv.mkDerivation (finalAttrs: { # runtime instead of probing env vars or .git — one file, one source # of truth for the Python runtime (CLI, TUI). cat > $out/share/hermes-agent/.hermes_build_info.json < str | None: return branch if branch and branch != "HEAD" else None +def _resolve_commit_date_from_git() -> int | None: + """Return the commit timestamp (Unix epoch seconds) of HEAD, or None.""" + raw = _run_git("log", "-1", "--format=%ct", "HEAD") + if raw and raw.isdigit(): + return int(raw) + return None + + def _resolve_dirty_from_git() -> bool: status = _run_git("status", "--porcelain", "-uno") return status is not None and len(status) > 0 @@ -116,6 +124,7 @@ def build_stamp( dirty: bool | None = None, base_version: str | None = None, distance: int | None = None, + commit_date: int | None = None, source: str = "local", ) -> dict: """Build a stamp dict from explicit args, filling gaps from git/env. @@ -155,6 +164,10 @@ def build_stamp( if distance is None: distance = _compute_distance(base_version, _release_date) + # Commit date: explicit > git + if commit_date is None: + commit_date = _resolve_commit_date_from_git() + # Display version display_version = base_version or "" if distance is not None and distance > 0: @@ -165,6 +178,7 @@ def build_stamp( return { "schemaVersion": STAMP_SCHEMA_VERSION, "commit": commit, + "commitDate": commit_date, "branch": branch, "builtAt": datetime.now(timezone.utc).isoformat(), "dirty": dirty, @@ -192,6 +206,7 @@ def main() -> int: parser.add_argument("--dirty", action="store_true", default=None, help="Mark as dirty") parser.add_argument("--base-version", default=None, help="Override base version") parser.add_argument("--distance", type=int, default=None, help="Override commit distance") + parser.add_argument("--commit-date", type=int, default=None, help="Override commit timestamp (Unix epoch seconds)") parser.add_argument("--source", default="local", help="Stamp source label") args = parser.parse_args() @@ -202,6 +217,7 @@ def main() -> int: dirty=args.dirty, base_version=args.base_version, distance=args.distance, + commit_date=args.commit_date, source=args.source, ) diff --git a/tests/hermes_cli/test_dump_git_commit.py b/tests/hermes_cli/test_dump_git_commit.py index 9451f11c31b..76dcd6762bb 100644 --- a/tests/hermes_cli/test_dump_git_commit.py +++ b/tests/hermes_cli/test_dump_git_commit.py @@ -91,42 +91,33 @@ def test_get_git_commit_output_format_identical_between_sources(tmp_path): assert all(c in "0123456789abcdef" for c in live) -def test_get_git_commit_date_uses_live_git(tmp_path): - """Source install: ``git log -1 --format=%cd --date=short`` returns the date.""" +def test_get_git_commit_date_uses_version_info(tmp_path): + """Source install: version_info carries the commit date from live git.""" from hermes_cli import dump repo_dir = tmp_path / "repo" repo_dir.mkdir() - git_result = MagicMock(returncode=0, stdout="2026-06-17\n") - with patch("hermes_cli.dump.subprocess.run", return_value=git_result): + with patch("hermes_cli.version_info._resolve_stamp_file", lambda: None), \ + patch("hermes_cli.version_info._resolve_repo_dir", lambda: repo_dir), \ + patch("hermes_cli.version_info._git_version_info", + return_value=VersionInfo("0.19.0", "0.19.0+3", 3, "deadbeef" * 5, "main", "git", False, 1718662620)): + _reset_version_info_cache() date = dump._get_git_commit_date(repo_dir) - assert date == "2026-06-17" + assert date == "2024-06-17" -def test_get_git_commit_date_empty_when_git_fails(tmp_path): - """Docker image / pip wheel: no git → '' so the dump line drops the date.""" +def test_get_git_commit_date_empty_when_unknown(tmp_path): + """Docker/pip: no git, no stamp → '' so the dump line drops the date.""" from hermes_cli import dump repo_dir = tmp_path / "no-git-here" repo_dir.mkdir() - failed = MagicMock(returncode=128, stdout="") - with patch("hermes_cli.dump.subprocess.run", return_value=failed): - date = dump._get_git_commit_date(repo_dir) - - assert date == "" - - -def test_get_git_commit_date_empty_when_git_raises(tmp_path): - """git binary missing → '' (no crash, suffix simply omitted).""" - from hermes_cli import dump - - repo_dir = tmp_path / "repo" - repo_dir.mkdir() - - with patch("hermes_cli.dump.subprocess.run", side_effect=FileNotFoundError("git")): + with patch("hermes_cli.version_info._resolve_stamp_file", lambda: None), \ + patch("hermes_cli.version_info._resolve_repo_dir", lambda: None): + _reset_version_info_cache() date = dump._get_git_commit_date(repo_dir) assert date == "" diff --git a/tests/hermes_cli/test_version_info.py b/tests/hermes_cli/test_version_info.py index d564a2ed7ff..2b1cada5c57 100644 --- a/tests/hermes_cli/test_version_info.py +++ b/tests/hermes_cli/test_version_info.py @@ -103,13 +103,14 @@ def test_get_version_info_counts_commits_after_semver_tag(tmp_path, monkeypatch) ("git", "branch", "--show-current"): "feature/version", ("git", "status", "--porcelain"): "", ("git", "rev-list", "--count", "v0.19.0..HEAD"): "3", + ("git", "log", "-1", "--format=%ct", "HEAD"): "1718662620", }[tuple(command)] return MagicMock(returncode=0, stdout=f"{output}\n") with patch("hermes_cli.version_info.subprocess.run", side_effect=run): info = get_version_info() - assert info == VersionInfo("0.19.0", "0.19.0+3", 3, "b" * 40, "feature/version", "git") + assert info == VersionInfo("0.19.0", "0.19.0+3", 3, "b" * 40, "feature/version", "git", False, 1718662620) def test_get_version_info_falls_back_to_legacy_release_date_tag(tmp_path, monkeypatch): @@ -129,6 +130,7 @@ def test_get_version_info_falls_back_to_legacy_release_date_tag(tmp_path, monkey ("git", "branch", "--show-current"): "", ("git", "status", "--porcelain"): " M hermes_cli/version_info.py", ("git", "rev-list", "--count", "v2026.7.20..HEAD"): "2", + ("git", "log", "-1", "--format=%ct", "HEAD"): "1718662620", }[tuple(command)] return MagicMock(returncode=0, stdout=f"{output}\n")