mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
refactor(version): centralize build provenance on install-stamp.json
Replace the three separate provenance paths (Nix env vars, Docker .hermes_build_sha, and live git probes) with a single install-stamp.json file read by version_info.py. All packagers (Docker, Nix, desktop) now call the shared scripts/write_install_stamp.py to produce the same canonical stamp. - Add scripts/write_install_stamp.py: shared stamp builder for all packagers - Refactor version_info.py: read stamp file first, fall back to live git - Delete hermes_cli/build_info.py (superseded by the stamp) - Refactor dump.py to use version_info instead of its own git/build_sha path - Refactor banner.py check_for_updates() to use stamp commit instead of HERMES_REVISION env vars - Dockerfile: write .hermes_build_info.json via the shared script - nix/hermes-agent.nix: write .hermes_build_info.json instead of setting HERMES_REVISION_* env vars via makeWrapper - Desktop build (package.json): call the shared Python script instead of the JS-only write-build-stamp.mjs for stamp generation - Update all tests to mock the stamp file instead of env vars
This commit is contained in:
parent
1a4c4eca0d
commit
813f3a0020
11 changed files with 465 additions and 263 deletions
217
scripts/write_install_stamp.py
Normal file
217
scripts/write_install_stamp.py
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
"""Generate canonical install-stamp.json for packaged Hermes builds.
|
||||
|
||||
All packagers (Docker, Nix, desktop) call this script to produce the same
|
||||
``install-stamp.json`` file. Runtime surfaces (CLI, TUI, desktop) read the
|
||||
stamp through ``hermes_cli.version_info`` — no env vars, no separate
|
||||
docker/nix code paths.
|
||||
|
||||
Usage::
|
||||
|
||||
# From a repo root with .git available (dev/CI builds):
|
||||
python scripts/write_install_stamp.py --output /path/to/install-stamp.json
|
||||
|
||||
# Override provenance for reproducible/packaged builds:
|
||||
python scripts/write_install_stamp.py --output ... \\
|
||||
--commit <sha> --branch <name> --dirty \\
|
||||
--base-version 0.19.0 --distance 42 --source nix
|
||||
|
||||
# Docker (no .git, commit known from build arg):
|
||||
python scripts/write_install_stamp.py --output /opt/hermes/.hermes_build_info.json \\
|
||||
--commit ${HERMES_GIT_SHA} --source docker
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
STAMP_SCHEMA_VERSION = 2
|
||||
_REPO_ROOT = Path(__file__).parent.parent.resolve()
|
||||
|
||||
# Hermes's historical tags use a four-digit calendar year as their major
|
||||
# component (for example v2026.7.20). Restrict release majors to three digits
|
||||
# so these date tags cannot masquerade as the v0.x.y SemVer boundaries.
|
||||
_SEMVER_TAG_RE = re.compile(r"^v(0|[1-9]\d{0,2})\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$")
|
||||
_LEGACY_CALVER_TAG_RE = re.compile(r"^v20\d{2}\.\d+\.\d+(?:\.\d+)?$")
|
||||
|
||||
FALLBACK_COMMIT = "0" * 40
|
||||
|
||||
|
||||
def _run_git(*args: str, cwd: str | Path = _REPO_ROOT) -> str | None:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", *args], capture_output=True, text=True, timeout=5, cwd=str(cwd)
|
||||
)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return None
|
||||
value = (result.stdout or "").strip()
|
||||
return value if result.returncode == 0 and value else None
|
||||
|
||||
|
||||
def _parse_release_metadata() -> tuple[str | None, str | None]:
|
||||
"""Read __version__ and __release_date__ from hermes_cli/__init__.py."""
|
||||
try:
|
||||
text = (_REPO_ROOT / "hermes_cli" / "__init__.py").read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return None, None
|
||||
version = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', text)
|
||||
date = re.search(r'__release_date__\s*=\s*["\']([^"\']+)["\']', text)
|
||||
return (version.group(1) if version else None, date.group(1) if date else None)
|
||||
|
||||
|
||||
def _resolve_commit_from_env() -> str | None:
|
||||
"""CI builds pass the commit via $GITHUB_SHA."""
|
||||
return os.environ.get("GITHUB_SHA") or None
|
||||
|
||||
|
||||
def _resolve_commit_from_git() -> str | None:
|
||||
return _run_git("rev-parse", "HEAD")
|
||||
|
||||
|
||||
def _resolve_branch_from_env() -> str | None:
|
||||
return os.environ.get("GITHUB_REF_NAME") or os.environ.get("GITHUB_HEAD_REF") or None
|
||||
|
||||
|
||||
def _resolve_branch_from_git() -> str | None:
|
||||
branch = _run_git("rev-parse", "--abbrev-ref", "HEAD")
|
||||
return branch if branch and branch != "HEAD" else None
|
||||
|
||||
|
||||
def _resolve_dirty_from_git() -> bool:
|
||||
status = _run_git("status", "--porcelain", "-uno")
|
||||
return status is not None and len(status) > 0
|
||||
|
||||
|
||||
def _compute_distance(base_version: str | None, release_date: str | None) -> int | None:
|
||||
"""Count commits since the release tag, trying SemVer then CalVer fallback."""
|
||||
if not base_version:
|
||||
return None
|
||||
|
||||
# Try SemVer tag first, then legacy CalVer tag.
|
||||
for tag in (f"v{base_version}", f"v{release_date}" if release_date else None):
|
||||
if not tag:
|
||||
continue
|
||||
raw = _run_git("rev-list", "--count", f"{tag}..HEAD")
|
||||
if raw is None:
|
||||
continue
|
||||
try:
|
||||
count = int(raw)
|
||||
except ValueError:
|
||||
continue
|
||||
if count >= 0:
|
||||
return count
|
||||
return None
|
||||
|
||||
|
||||
def build_stamp(
|
||||
*,
|
||||
commit: str | None = None,
|
||||
branch: str | None = None,
|
||||
dirty: bool | None = None,
|
||||
base_version: str | None = None,
|
||||
distance: int | None = None,
|
||||
source: str = "local",
|
||||
) -> dict:
|
||||
"""Build a stamp dict from explicit args, filling gaps from git/env.
|
||||
|
||||
Args override detection — if you pass ``commit``, it's used directly.
|
||||
``source`` identifies where the stamp came from (``ci``, ``local``,
|
||||
``docker``, ``nix``, ``fallback``).
|
||||
"""
|
||||
_base_version, _release_date = _parse_release_metadata()
|
||||
if base_version is None:
|
||||
base_version = _base_version
|
||||
|
||||
# Commit: explicit > CI env > git
|
||||
if commit is None:
|
||||
commit = _resolve_commit_from_env()
|
||||
source = "ci" if commit else source
|
||||
if commit is None:
|
||||
commit = _resolve_commit_from_git()
|
||||
source = "local" if commit else source
|
||||
if not commit:
|
||||
commit = FALLBACK_COMMIT
|
||||
source = "fallback"
|
||||
|
||||
# Branch: explicit > CI env > git
|
||||
if branch is None:
|
||||
branch = _resolve_branch_from_env()
|
||||
if branch is None:
|
||||
branch = _resolve_branch_from_git()
|
||||
if branch is None:
|
||||
branch = "unknown"
|
||||
|
||||
# Dirty: explicit > git
|
||||
if dirty is None:
|
||||
dirty = _resolve_dirty_from_git()
|
||||
|
||||
# Distance: explicit > computed from git
|
||||
if distance is None:
|
||||
distance = _compute_distance(base_version, _release_date)
|
||||
|
||||
# Display version
|
||||
display_version = base_version or ""
|
||||
if distance is not None and distance > 0:
|
||||
display_version = f"{display_version}+{distance}"
|
||||
elif dirty and distance is None:
|
||||
display_version = f"{display_version}+?"
|
||||
|
||||
return {
|
||||
"schemaVersion": STAMP_SCHEMA_VERSION,
|
||||
"commit": commit,
|
||||
"branch": branch,
|
||||
"builtAt": datetime.now(timezone.utc).isoformat(),
|
||||
"dirty": dirty,
|
||||
"source": source,
|
||||
"baseVersion": base_version,
|
||||
"displayVersion": display_version,
|
||||
"distance": distance,
|
||||
}
|
||||
|
||||
|
||||
def write_stamp(output: str | Path, **kwargs) -> dict:
|
||||
"""Build and write an install-stamp.json to ``output``. Returns the stamp."""
|
||||
stamp = build_stamp(**kwargs)
|
||||
out_path = Path(output)
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(stamp, indent=2) + "\n", encoding="utf-8")
|
||||
return stamp
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Write install-stamp.json")
|
||||
parser.add_argument("--output", "-o", required=True, help="Output file path")
|
||||
parser.add_argument("--commit", default=None, help="Override commit SHA")
|
||||
parser.add_argument("--branch", default=None, help="Override branch name")
|
||||
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("--source", default="local", help="Stamp source label")
|
||||
args = parser.parse_args()
|
||||
|
||||
stamp = write_stamp(
|
||||
args.output,
|
||||
commit=args.commit,
|
||||
branch=args.branch,
|
||||
dirty=args.dirty,
|
||||
base_version=args.base_version,
|
||||
distance=args.distance,
|
||||
source=args.source,
|
||||
)
|
||||
|
||||
commit_short = stamp["commit"][:12]
|
||||
branch_str = f" ({stamp['branch']})" if stamp["branch"] else ""
|
||||
dirty_str = " [DIRTY]" if stamp["dirty"] else ""
|
||||
fallback_str = " [FALLBACK]" if stamp["source"] == "fallback" else ""
|
||||
print(f"[write_install_stamp] wrote {args.output} -> {commit_short}{branch_str}{dirty_str}{fallback_str}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue