mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(version): show provenance and distribution
This commit is contained in:
parent
f2aeea95e0
commit
3a3744a6b2
17 changed files with 117 additions and 25 deletions
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
|
|
@ -54,7 +54,7 @@ jobs:
|
|||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Write install stamp
|
||||
run: python3 scripts/write_install_stamp.py --output install-stamp.json
|
||||
run: python3 scripts/write_install_stamp.py --output install-stamp.json --distribution docker
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
|
|
@ -145,7 +145,7 @@ jobs:
|
|||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Write install stamp
|
||||
run: python3 scripts/write_install_stamp.py --output install-stamp.json
|
||||
run: python3 scripts/write_install_stamp.py --output install-stamp.json --distribution docker
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||
|
|
|
|||
|
|
@ -440,7 +440,7 @@ const SOURCE_REPO_ROOT = path.resolve(APP_ROOT, '../..')
|
|||
// Schema:
|
||||
// schema 1: { commit, branch, builtAt, dirty, source }
|
||||
// schema 2 adds immutable-package provenance: baseVersion, displayVersion,
|
||||
// distance, and installMethod.
|
||||
// distance, and distribution.
|
||||
const INSTALL_STAMP_SCHEMA_VERSION = 2
|
||||
|
||||
function loadInstallStamp() {
|
||||
|
|
@ -474,7 +474,7 @@ function loadInstallStamp() {
|
|||
baseVersion: typeof parsed.baseVersion === 'string' ? parsed.baseVersion : null,
|
||||
displayVersion: typeof parsed.displayVersion === 'string' ? parsed.displayVersion : null,
|
||||
distance: typeof parsed.distance === 'number' && parsed.distance >= 0 ? parsed.distance : null,
|
||||
installMethod: typeof parsed.installMethod === 'string' ? parsed.installMethod : null,
|
||||
distribution: typeof parsed.distribution === 'string' ? parsed.distribution : null,
|
||||
builtAt: parsed.builtAt || null,
|
||||
dirty: Boolean(parsed.dirty),
|
||||
source: parsed.source || null,
|
||||
|
|
@ -11071,7 +11071,7 @@ function resolveHermesVersionInfo() {
|
|||
commit: INSTALL_STAMP.commit,
|
||||
branch: INSTALL_STAMP.branch,
|
||||
source: INSTALL_STAMP.source ?? undefined,
|
||||
installMethod: INSTALL_STAMP.installMethod ?? undefined,
|
||||
distribution: INSTALL_STAMP.distribution ?? undefined,
|
||||
dirty: INSTALL_STAMP.dirty
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,4 +38,28 @@ describe('VersionDetails', () => {
|
|||
expect(screen.getByText('unknown')).toBeTruthy()
|
||||
expect(screen.queryByText('No branch information')).toBeNull()
|
||||
})
|
||||
|
||||
it('shows the Nix source and distribution from the stamp', () => {
|
||||
render(
|
||||
<I18nProvider configClient={null} initialLocale="en">
|
||||
<VersionDetails version={{ ...baseVersion, source: 'nix', distribution: 'nix' }} />
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
expect(screen.getByText('Source')).toBeTruthy()
|
||||
expect(screen.getAllByText('Nix')).toHaveLength(2)
|
||||
expect(screen.getByText('Distribution')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('distinguishes CI provenance from the Docker distribution', () => {
|
||||
render(
|
||||
<I18nProvider configClient={null} initialLocale="en">
|
||||
<VersionDetails version={{ ...baseVersion, source: 'ci', distribution: 'docker' }} />
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
expect(screen.getByText('CI')).toBeTruthy()
|
||||
expect(screen.getByText('Distribution')).toBeTruthy()
|
||||
expect(screen.getByText('Docker')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ export function VersionDetails({ version }: { version: DesktopVersionInfo }) {
|
|||
const { t } = useI18n()
|
||||
const u = t.updates
|
||||
const unknownDistance = version.dirty && version.distance == null
|
||||
const source = version.source === 'ci' ? 'CI' : version.source ? version.source[0].toUpperCase() + version.source.slice(1) : null
|
||||
const distribution = version.distribution === 'nix' ? 'Nix' : version.distribution === 'docker' ? 'Docker' : null
|
||||
|
||||
return (
|
||||
<dl className="grid gap-2 rounded-lg border border-border/70 bg-muted/20 px-3 py-3 text-sm">
|
||||
|
|
@ -40,6 +42,18 @@ export function VersionDetails({ version }: { version: DesktopVersionInfo }) {
|
|||
</ExternalLink>
|
||||
</div>
|
||||
)}
|
||||
{source && (
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-muted-foreground">{u.versionDetailsSource}</dt>
|
||||
<dd>{source}</dd>
|
||||
</div>
|
||||
)}
|
||||
{distribution && (
|
||||
<div className="flex justify-between gap-4">
|
||||
<dt className="text-muted-foreground">{u.versionDetailsDistribution}</dt>
|
||||
<dd>{distribution}</dd>
|
||||
</div>
|
||||
)}
|
||||
{version.dirty && (
|
||||
<div className="text-warning">{unknownDistance ? u.versionDetailsDirtyUnknown : u.versionDetailsDirty}</div>
|
||||
)}
|
||||
|
|
|
|||
2
apps/desktop/src/global.d.ts
vendored
2
apps/desktop/src/global.d.ts
vendored
|
|
@ -328,7 +328,7 @@ export interface DesktopVersionInfo {
|
|||
nodeVersion: string
|
||||
platform: string
|
||||
hermesRoot: string
|
||||
installMethod?: string
|
||||
distribution?: 'docker' | 'nix'
|
||||
source?: 'build' | 'ci' | 'docker' | 'fallback' | 'git' | 'local' | 'nix' | 'unknown'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2134,6 +2134,8 @@ export const en: Translations = {
|
|||
versionDetailsBranch: 'Branch',
|
||||
versionDetailsNoBranchInfo: 'No branch information',
|
||||
versionDetailsCommit: 'Commit',
|
||||
versionDetailsSource: 'Source',
|
||||
versionDetailsDistribution: 'Distribution',
|
||||
versionDetailsDirty: 'This package was built from a dirty source tree.',
|
||||
versionDetailsDirtyUnknown: 'Built from a dirty source tree. The number of changes since the last release is unknown.',
|
||||
connectionRetry: 'Check your connection and try again.',
|
||||
|
|
|
|||
|
|
@ -1994,6 +1994,8 @@ export const ja = defineLocale({
|
|||
versionDetailsBranch: 'ブランチ',
|
||||
versionDetailsNoBranchInfo: 'ブランチ情報なし',
|
||||
versionDetailsCommit: 'コミット',
|
||||
versionDetailsSource: 'ソース',
|
||||
versionDetailsDistribution: '配布形式',
|
||||
versionDetailsDirty: 'このパッケージは変更のあるソースツリーからビルドされました。',
|
||||
versionDetailsDirtyUnknown: '変更のあるソースツリーからビルドされました。最後のリリースからの変更数は不明です。',
|
||||
connectionRetry: '接続を確認してもう一度試してください。',
|
||||
|
|
|
|||
|
|
@ -1773,6 +1773,8 @@ export interface Translations {
|
|||
versionDetailsBranch: string
|
||||
versionDetailsNoBranchInfo: string
|
||||
versionDetailsCommit: string
|
||||
versionDetailsSource: string
|
||||
versionDetailsDistribution: string
|
||||
versionDetailsDirty: string
|
||||
versionDetailsDirtyUnknown: string
|
||||
connectionRetry: string
|
||||
|
|
|
|||
|
|
@ -1934,6 +1934,8 @@ export const zhHant = defineLocale({
|
|||
versionDetailsBranch: '分支',
|
||||
versionDetailsNoBranchInfo: '沒有分支資訊',
|
||||
versionDetailsCommit: '提交',
|
||||
versionDetailsSource: '來源',
|
||||
versionDetailsDistribution: '發行方式',
|
||||
versionDetailsDirty: '此套件從有未提交變更的來源樹建置。',
|
||||
versionDetailsDirtyUnknown: '從有未提交變更的來源樹建置。自上次發布以來的變更數量未知。',
|
||||
connectionRetry: '請檢查網路連線後重試。',
|
||||
|
|
|
|||
|
|
@ -2325,6 +2325,8 @@ export const zh: Translations = {
|
|||
versionDetailsBranch: '分支',
|
||||
versionDetailsNoBranchInfo: '无分支信息',
|
||||
versionDetailsCommit: '提交',
|
||||
versionDetailsSource: '来源',
|
||||
versionDetailsDistribution: '发行方式',
|
||||
versionDetailsDirty: '此软件包从有未提交更改的源代码树构建。',
|
||||
versionDetailsDirtyUnknown: '从有未提交更改的源代码树构建。自上次发布以来的更改数量未知。',
|
||||
connectionRetry: '请检查网络连接后重试。',
|
||||
|
|
|
|||
|
|
@ -4653,7 +4653,6 @@ def cmd_import(args):
|
|||
|
||||
|
||||
def _print_version_info(*, check_updates: bool = True) -> None:
|
||||
from hermes_cli.config import detect_install_method
|
||||
from hermes_cli.banner import format_banner_version_label
|
||||
from hermes_cli.version_info import get_version_info
|
||||
|
||||
|
|
@ -4665,8 +4664,10 @@ def _print_version_info(*, check_updates: bool = True) -> None:
|
|||
print(f"Commit: {version_info.commit}")
|
||||
if version_info.dirty is not None:
|
||||
print(f"Working tree: {'dirty' if version_info.dirty else 'clean'}")
|
||||
print(f"Source: {version_info.source}")
|
||||
if version_info.distribution:
|
||||
print(f"Distribution: {version_info.distribution}")
|
||||
print(f"Install directory: {PROJECT_ROOT}")
|
||||
print(f"Install method: {detect_install_method(PROJECT_ROOT)}")
|
||||
|
||||
# Show Python version
|
||||
print(f"Python: {sys.version.split()[0]}")
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import os
|
|||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
from typing import Literal, cast
|
||||
|
||||
from hermes_cli import __release_date__, __version__
|
||||
|
||||
|
|
@ -31,9 +31,10 @@ class VersionInfo:
|
|||
distance: int | None
|
||||
commit: str | None
|
||||
branch: str | None
|
||||
source: Literal["git", "nix", "docker", "build", "unknown"]
|
||||
source: Literal["build", "ci", "docker", "fallback", "git", "local", "nix", "unknown"]
|
||||
dirty: bool = False
|
||||
commit_date: int | None = None
|
||||
distribution: Literal["docker", "nix"] | None = None
|
||||
|
||||
|
||||
def format_display_version(info: VersionInfo | None = None) -> str:
|
||||
|
|
@ -125,18 +126,17 @@ def _stamp_version_info() -> VersionInfo | None:
|
|||
if isinstance(distance, str):
|
||||
distance = _parse_nonnegative(distance)
|
||||
|
||||
# Normalize source labels — the stamp's "source" field describes the
|
||||
# build environment (ci/local/docker/nix/fallback), not the runtime
|
||||
# provenance path. Map known packaged sources to their runtime label.
|
||||
# ``source`` describes build provenance, while ``distribution`` identifies
|
||||
# the package form users installed. Keep both facts intact for support.
|
||||
stamp_source = str(data.get("source") or "")
|
||||
if stamp_source in ("docker", "nix"):
|
||||
source: Literal["git", "nix", "docker", "build", "unknown"] = stamp_source
|
||||
elif stamp_source in ("ci", "local"):
|
||||
# CI/local stamps are still git-based provenance — the commit was
|
||||
# resolved from git at build time and baked into the stamp.
|
||||
source = "git"
|
||||
else:
|
||||
source = "build"
|
||||
source = (
|
||||
cast(Literal["build", "ci", "docker", "fallback", "git", "local", "nix", "unknown"], stamp_source)
|
||||
if stamp_source in {"ci", "docker", "fallback", "local", "nix"}
|
||||
else "build"
|
||||
)
|
||||
distribution = data.get("distribution")
|
||||
if distribution not in {"docker", "nix"}:
|
||||
distribution = None
|
||||
|
||||
commit_date = data.get("commitDate")
|
||||
if not isinstance(commit_date, int):
|
||||
|
|
@ -151,6 +151,7 @@ def _stamp_version_info() -> VersionInfo | None:
|
|||
source,
|
||||
bool(data.get("dirty")),
|
||||
commit_date,
|
||||
distribution,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -236,4 +237,6 @@ def format_version_details(info: VersionInfo | None = None) -> str:
|
|||
if info.commit:
|
||||
values.append(f"commit {info.commit}")
|
||||
values.append(f"source {info.source}")
|
||||
if info.distribution:
|
||||
values.append(f"distribution {info.distribution}")
|
||||
return " · ".join(values)
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ let
|
|||
cat > $out/install-stamp.json <<'EOF'
|
||||
{"schemaVersion":2,"commit":${builtins.toJSON rev},"branch":${builtins.toJSON branch},"baseVersion":"${version}","displayVersion":"${displayVersion}","distance":${builtins.toJSON distance},"dirty":${
|
||||
if dirty then "true" else "false"
|
||||
},"source":"nix","installMethod":"nix"}
|
||||
},"source":"nix","distribution":"nix"}
|
||||
EOF
|
||||
|
||||
cp -n apps/desktop/package.json $out/
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ Usage::
|
|||
# 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
|
||||
--base-version 0.19.0 --distance 42 --source nix --distribution 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
|
||||
--commit ${HERMES_GIT_SHA} --source ci --distribution docker
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -126,6 +126,7 @@ def build_stamp(
|
|||
distance: int | None = None,
|
||||
commit_date: int | None = None,
|
||||
source: str = "local",
|
||||
distribution: str | None = None,
|
||||
) -> dict:
|
||||
"""Build a stamp dict from explicit args, filling gaps from git/env.
|
||||
|
||||
|
|
@ -181,6 +182,7 @@ def build_stamp(
|
|||
"builtAt": datetime.now(timezone.utc).isoformat(),
|
||||
"dirty": dirty,
|
||||
"source": source,
|
||||
"distribution": distribution,
|
||||
"baseVersion": base_version,
|
||||
"displayVersion": display_version,
|
||||
"distance": distance,
|
||||
|
|
@ -206,6 +208,7 @@ def main() -> int:
|
|||
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")
|
||||
parser.add_argument("--distribution", choices=("docker", "nix"), help="Package distribution")
|
||||
args = parser.parse_args()
|
||||
|
||||
stamp = write_stamp(
|
||||
|
|
@ -217,6 +220,7 @@ def main() -> int:
|
|||
distance=args.distance,
|
||||
commit_date=args.commit_date,
|
||||
source=args.source,
|
||||
distribution=args.distribution,
|
||||
)
|
||||
|
||||
commit_short = stamp["commit"][:12]
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ def test_stamp_version_info_reads_nix_stamp(tmp_path, monkeypatch):
|
|||
"distance": 3,
|
||||
"dirty": False,
|
||||
"source": "nix",
|
||||
"distribution": "nix",
|
||||
}
|
||||
stamp_file = tmp_path / ".hermes_build_info.json"
|
||||
stamp_file.write_text(json.dumps(stamp))
|
||||
|
|
@ -44,7 +45,33 @@ def test_stamp_version_info_reads_nix_stamp(tmp_path, monkeypatch):
|
|||
|
||||
info = get_version_info()
|
||||
|
||||
assert info == VersionInfo("0.19.0", "0.19.0+3", 3, "a" * 40, "feature/version", "nix")
|
||||
assert info == VersionInfo("0.19.0", "0.19.0+3", 3, "a" * 40, "feature/version", "nix", distribution="nix")
|
||||
|
||||
|
||||
def test_stamp_version_info_preserves_ci_provenance_and_docker_distribution(tmp_path, monkeypatch):
|
||||
stamp = {"commit": "d" * 40, "source": "ci", "distribution": "docker"}
|
||||
stamp_file = tmp_path / ".hermes_build_info.json"
|
||||
stamp_file.write_text(json.dumps(stamp))
|
||||
monkeypatch.setattr("hermes_cli.version_info._resolve_stamp_file", lambda: stamp_file)
|
||||
|
||||
info = get_version_info()
|
||||
|
||||
assert info.source == "ci"
|
||||
assert info.distribution == "docker"
|
||||
|
||||
|
||||
def test_version_command_shows_provenance_and_distribution(monkeypatch, capsys):
|
||||
import hermes_cli.main as main
|
||||
|
||||
info = VersionInfo("0.19.0", "0.19.0", None, "a" * 40, "main", "ci", distribution="docker")
|
||||
monkeypatch.setattr("hermes_cli.banner.format_banner_version_label", lambda: "Hermes Agent v0.19.0")
|
||||
monkeypatch.setattr("hermes_cli.version_info.get_version_info", lambda: info)
|
||||
|
||||
main._print_version_info(check_updates=False)
|
||||
|
||||
output = capsys.readouterr().out
|
||||
assert "Source: ci" in output
|
||||
assert "Distribution: docker" in output
|
||||
|
||||
|
||||
def test_stamp_version_info_preserves_missing_branch(tmp_path, monkeypatch):
|
||||
|
|
|
|||
8
tests/scripts/test_write_install_stamp.py
Normal file
8
tests/scripts/test_write_install_stamp.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from scripts.write_install_stamp import build_stamp
|
||||
|
||||
|
||||
def test_build_stamp_keeps_provenance_separate_from_distribution():
|
||||
stamp = build_stamp(commit="a" * 40, source="ci", distribution="docker")
|
||||
|
||||
assert stamp["source"] == "ci"
|
||||
assert stamp["distribution"] == "docker"
|
||||
|
|
@ -4486,6 +4486,7 @@ def _session_info(agent, session: dict | None = None) -> dict:
|
|||
info["version_commit"] = version_info.commit or ""
|
||||
info["version_branch"] = version_info.branch or ""
|
||||
info["version_source"] = version_info.source
|
||||
info["version_distribution"] = version_info.distribution or ""
|
||||
info["version_dirty"] = version_info.dirty
|
||||
except Exception:
|
||||
pass
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue