mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
`hermes dump` and the startup banner both call `git rev-parse HEAD` to
report the running commit, but `.dockerignore` line 2 excludes `.git` —
so inside the published image `hermes dump` shows
`version: ... [(unknown)]` and the banner drops its `· upstream <sha>`
suffix entirely. That makes support triage from container bug reports
impossible: we can't tell which commit the user is actually running.
Fix: thread the build-time SHA through as a Docker build-arg, write it
to `/opt/hermes/.hermes_build_sha` in the image, and have a new
`hermes_cli/build_info.get_build_sha()` read it as a fallback after the
existing live-git lookup fails. Output format is unchanged in both
callsites — same 8-char short SHA whether resolved live or baked.
Wiring:
- Dockerfile: `ARG HERMES_GIT_SHA=` + write-file step after the source
copy. Empty/missing arg → no file written → callers fall through to
live git (so local `docker build` without --build-arg is unchanged).
- docker-publish.yml: passes `HERMES_GIT_SHA=${{ github.sha }}` on all
four build-push-action steps (amd64/arm64, smoke-test + final push).
- dump.py:_get_git_commit() / banner.py:get_git_banner_state(): try
live git first, fall back to baked SHA, then to legacy `(unknown)`
/ None. Banner returns `upstream == local, ahead=0` because a built
image is by definition pinned to one commit.
Coverage:
- Unit tests cover build_info (file present/absent/empty/error,
truncation, whitespace), dump (live-git wins, both fallbacks,
identical output-format regression guard), and banner (no-repo +
baked, no-repo + no-sha, shallow-clone fallback).
- tests/docker/test_dump_build_sha.py is an integration regression
guard that runs against the real image, reads
`/opt/hermes/.hermes_build_sha`, and asserts `hermes dump` surfaces
its content (or stays at `(unknown)` if no file).
- Verified end-to-end: `docker build --build-arg HERMES_GIT_SHA=abc...`
→ `docker run ... dump` reports `[abc12345]`; without the build-arg
it reports `[(unknown)]` as before.
118 lines
4.3 KiB
Python
118 lines
4.3 KiB
Python
"""Tests for hermes_cli.dump._get_git_commit — git SHA resolution for ``hermes dump``.
|
|
|
|
``hermes dump`` prints the running commit so support bug reports identify the
|
|
exact version. Source installs resolve it live via ``git rev-parse``; the
|
|
published Docker image excludes ``.git`` and falls back to the baked SHA
|
|
written by the Dockerfile's ``HERMES_GIT_SHA`` build-arg.
|
|
|
|
These tests cover both paths plus the failure modes (no git, no baked file).
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
|
|
def test_get_git_commit_uses_live_git_when_available(tmp_path):
|
|
"""Source install: ``git rev-parse --short=8 HEAD`` wins; no fallback."""
|
|
from hermes_cli import dump
|
|
|
|
repo_dir = tmp_path / "repo"
|
|
repo_dir.mkdir()
|
|
|
|
git_result = MagicMock(returncode=0, stdout="deadbeef\n")
|
|
# build_info should NOT be consulted when live git succeeds.
|
|
with patch("hermes_cli.dump.subprocess.run", return_value=git_result) as mock_run, \
|
|
patch("hermes_cli.build_info.get_build_sha") as mock_build:
|
|
commit = dump._get_git_commit(repo_dir)
|
|
|
|
assert commit == "deadbeef"
|
|
mock_run.assert_called_once()
|
|
mock_build.assert_not_called()
|
|
|
|
|
|
def test_get_git_commit_falls_back_to_build_sha_when_live_git_fails(tmp_path):
|
|
"""Docker image case: live git returns non-zero → use baked SHA."""
|
|
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), \
|
|
patch("hermes_cli.build_info.get_build_sha", return_value="cafef00d"):
|
|
commit = dump._get_git_commit(repo_dir)
|
|
|
|
assert commit == "cafef00d"
|
|
|
|
|
|
def test_get_git_commit_falls_back_when_git_returns_empty_stdout(tmp_path):
|
|
"""Edge case: git exits 0 but prints nothing — still try the baked SHA."""
|
|
from hermes_cli import dump
|
|
|
|
repo_dir = tmp_path / "repo"
|
|
repo_dir.mkdir()
|
|
|
|
empty = MagicMock(returncode=0, stdout="\n")
|
|
with patch("hermes_cli.dump.subprocess.run", return_value=empty), \
|
|
patch("hermes_cli.build_info.get_build_sha", return_value="abcdef12"):
|
|
commit = dump._get_git_commit(repo_dir)
|
|
|
|
assert commit == "abcdef12"
|
|
|
|
|
|
def test_get_git_commit_falls_back_when_git_raises(tmp_path):
|
|
"""git binary missing (e.g. minimal container w/o git) → baked SHA path."""
|
|
from hermes_cli import dump
|
|
|
|
repo_dir = tmp_path / "repo"
|
|
repo_dir.mkdir()
|
|
|
|
with patch("hermes_cli.dump.subprocess.run", side_effect=FileNotFoundError("git")), \
|
|
patch("hermes_cli.build_info.get_build_sha", return_value="feedface"):
|
|
commit = dump._get_git_commit(repo_dir)
|
|
|
|
assert commit == "feedface"
|
|
|
|
|
|
def test_get_git_commit_returns_unknown_when_neither_source_available(tmp_path):
|
|
"""Pip-installed wheel: no git, no baked SHA → '(unknown)' (legacy contract)."""
|
|
from hermes_cli import dump
|
|
|
|
repo_dir = tmp_path / "repo"
|
|
repo_dir.mkdir()
|
|
|
|
failed = MagicMock(returncode=128, stdout="")
|
|
with patch("hermes_cli.dump.subprocess.run", return_value=failed), \
|
|
patch("hermes_cli.build_info.get_build_sha", return_value=None):
|
|
commit = dump._get_git_commit(repo_dir)
|
|
|
|
assert commit == "(unknown)"
|
|
|
|
|
|
def test_get_git_commit_output_format_identical_between_sources(tmp_path):
|
|
"""Regression guard: live-git and baked-SHA outputs share the same shape.
|
|
|
|
Ben explicitly asked for identical output between Docker and source installs
|
|
so support tooling that parses ``hermes dump`` doesn't have to special-case
|
|
container builds. Both paths must return a bare 8-char SHA — no prefix,
|
|
no suffix, no annotation.
|
|
"""
|
|
from hermes_cli import dump
|
|
|
|
repo_dir = tmp_path / "repo"
|
|
repo_dir.mkdir()
|
|
|
|
# Live-git path.
|
|
git_result = MagicMock(returncode=0, stdout="b2f477a3\n")
|
|
with patch("hermes_cli.dump.subprocess.run", return_value=git_result):
|
|
live = dump._get_git_commit(repo_dir)
|
|
|
|
# Baked-SHA path.
|
|
failed = MagicMock(returncode=128, stdout="")
|
|
with patch("hermes_cli.dump.subprocess.run", return_value=failed), \
|
|
patch("hermes_cli.build_info.get_build_sha", return_value="b2f477a3"):
|
|
baked = dump._get_git_commit(repo_dir)
|
|
|
|
assert live == baked == "b2f477a3"
|
|
# Same length, same charset — no decoration in either branch.
|
|
assert len(live) == 8
|
|
assert all(c in "0123456789abcdef" for c in live)
|