fix(version): preserve unavailable Nix branch metadata

Stamp absent branches as JSON null instead of the ambiguous "unknown" sentinel. Show an explicit no-branch-information value in Desktop version details.
This commit is contained in:
ethernet 2026-07-28 10:13:58 -04:00
parent cad2864f8c
commit 6cbd3a6ac3
14 changed files with 61 additions and 22 deletions

View file

@ -11069,7 +11069,7 @@ function resolveHermesVersionInfo() {
baseVersion: INSTALL_STAMP.baseVersion ?? undefined,
distance: INSTALL_STAMP.distance ?? undefined,
commit: INSTALL_STAMP.commit,
branch: INSTALL_STAMP.branch ?? undefined,
branch: INSTALL_STAMP.branch,
source: INSTALL_STAMP.source ?? undefined,
installMethod: INSTALL_STAMP.installMethod ?? undefined,
dirty: INSTALL_STAMP.dirty

View file

@ -0,0 +1,41 @@
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it } from 'vitest'
import type { DesktopVersionInfo } from '@/global'
import { I18nProvider } from '@/i18n'
import { VersionDetails } from './version-details'
const baseVersion: DesktopVersionInfo = {
appVersion: '0.19.0',
electronVersion: '37.0.0',
hermesRoot: '/tmp/hermes',
nodeVersion: '22.0.0',
platform: 'linux'
}
afterEach(cleanup)
describe('VersionDetails', () => {
it('labels a missing branch explicitly', () => {
render(
<I18nProvider configClient={null} initialLocale="en">
<VersionDetails version={{ ...baseVersion, branch: null }} />
</I18nProvider>
)
expect(screen.getByText('Branch')).toBeTruthy()
expect(screen.getByText('No branch information')).toBeTruthy()
})
it('preserves a literal branch named unknown', () => {
render(
<I18nProvider configClient={null} initialLocale="en">
<VersionDetails version={{ ...baseVersion, branch: 'unknown' }} />
</I18nProvider>
)
expect(screen.getByText('unknown')).toBeTruthy()
expect(screen.queryByText('No branch information')).toBeNull()
})
})

View file

@ -25,12 +25,10 @@ export function VersionDetails({ version }: { version: DesktopVersionInfo }) {
<dd>{version.baseVersion}</dd>
</div>
)}
{version.branch && (
<div className="flex justify-between gap-4">
<dt className="text-muted-foreground">{u.versionDetailsBranch}</dt>
<dd className="break-all text-right">{version.branch}</dd>
</div>
)}
<div className="flex justify-between gap-4">
<dt className="text-muted-foreground">{u.versionDetailsBranch}</dt>
<dd className="break-all text-right">{version.branch ?? u.versionDetailsNoBranchInfo}</dd>
</div>
{version.commit && (
<div className="flex justify-between gap-4">
<dt className="text-muted-foreground">{u.versionDetailsCommit}</dt>

View file

@ -320,7 +320,7 @@ export interface HermesTerminalExit {
export interface DesktopVersionInfo {
appVersion: string
baseVersion?: string
branch?: string
branch?: string | null
commit?: string
distance?: number
dirty?: boolean

View file

@ -2132,6 +2132,7 @@ export const en: Translations = {
versionDetailsVersion: 'Version',
versionDetailsBaseVersion: 'Release version',
versionDetailsBranch: 'Branch',
versionDetailsNoBranchInfo: 'No branch information',
versionDetailsCommit: 'Commit',
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.',

View file

@ -1992,6 +1992,7 @@ export const ja = defineLocale({
versionDetailsVersion: 'バージョン',
versionDetailsBaseVersion: 'リリースバージョン',
versionDetailsBranch: 'ブランチ',
versionDetailsNoBranchInfo: 'ブランチ情報なし',
versionDetailsCommit: 'コミット',
versionDetailsDirty: 'このパッケージは変更のあるソースツリーからビルドされました。',
versionDetailsDirtyUnknown: '変更のあるソースツリーからビルドされました。最後のリリースからの変更数は不明です。',

View file

@ -1771,6 +1771,7 @@ export interface Translations {
versionDetailsVersion: string
versionDetailsBaseVersion: string
versionDetailsBranch: string
versionDetailsNoBranchInfo: string
versionDetailsCommit: string
versionDetailsDirty: string
versionDetailsDirtyUnknown: string

View file

@ -1932,6 +1932,7 @@ export const zhHant = defineLocale({
versionDetailsVersion: '版本',
versionDetailsBaseVersion: '發行版本',
versionDetailsBranch: '分支',
versionDetailsNoBranchInfo: '沒有分支資訊',
versionDetailsCommit: '提交',
versionDetailsDirty: '此套件從有未提交變更的來源樹建置。',
versionDetailsDirtyUnknown: '從有未提交變更的來源樹建置。自上次發布以來的變更數量未知。',

View file

@ -2323,6 +2323,7 @@ export const zh: Translations = {
versionDetailsVersion: '版本',
versionDetailsBaseVersion: '发布版本',
versionDetailsBranch: '分支',
versionDetailsNoBranchInfo: '无分支信息',
versionDetailsCommit: '提交',
versionDetailsDirty: '此软件包从有未提交更改的源代码树构建。',
versionDetailsDirtyUnknown: '从有未提交更改的源代码树构建。自上次发布以来的更改数量未知。',

View file

@ -44,10 +44,6 @@ let
if distance != null && distance > 0 then "${version}+${toString distance}"
else if dirty && distance == null then "${version}+?"
else version;
# On a dirty tree, sourceInfo.ref is null (flakes can't determine the ref).
# Show "unknown" rather than null so the desktop doesn't fall back to its
# self-update default branch ("main") when the real branch is unavailable.
branchLabel = if branch != null then branch else "unknown";
electronHeaders = pkgs.fetchurl {
url = "https://artifacts.electronjs.org/headers/dist/v${electron.version}/node-v${electron.version}-headers.tar.gz";
@ -166,7 +162,7 @@ let
cp -rn apps/desktop/dist $out/
cat > $out/install-stamp.json <<'EOF'
{"schemaVersion":2,"commit":${builtins.toJSON rev},"branch":${builtins.toJSON branchLabel},"baseVersion":"${version}","displayVersion":"${displayVersion}","distance":${builtins.toJSON distance},"dirty":${
{"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"}
EOF

View file

@ -56,7 +56,6 @@ let
if stampDistance != null && stampDistance > 0 then "${version}+${toString stampDistance}"
else if dirty && stampDistance == null then "${version}+?"
else version;
stampBranch = if branch != null then branch else "unknown";
nodejs = nodejs_22;
mkHermesVenv =
@ -206,7 +205,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 <<STAMP
{"schemaVersion":2,"commit":${builtins.toJSON rev},"commitDate":${builtins.toJSON lastModified},"branch":${builtins.toJSON stampBranch},"baseVersion":"${version}","displayVersion":"${stampDisplayVersion}","distance":${builtins.toJSON stampDistance},"dirty":${if dirty then "true" else "false"},"source":"nix"}
{"schemaVersion":2,"commit":${builtins.toJSON rev},"commitDate":${builtins.toJSON lastModified},"branch":${builtins.toJSON branch},"baseVersion":"${version}","displayVersion":"${stampDisplayVersion}","distance":${builtins.toJSON stampDistance},"dirty":${if dirty then "true" else "false"},"source":"nix"}
STAMP
${lib.concatMapStringsSep "\n"

View file

@ -12,8 +12,10 @@
dirtyRevision = inputs.self.dirtyRev or null;
# inputs.self.sourceInfo is a string (store path) in current Nix, not a
# record. The metadata fields are available as top-level attributes on
# inputs.self directly: rev, dirtyRev, lastModified, ref, revCount.
# On dirty trees ref/rev/revCount are null; dirtyRev is set.
# inputs.self directly. A flake URL's requested ref is intentionally not
# exposed as self.ref after Nix resolves it to an immutable source, so
# branch is normally null for remote flakes. Preserve null rather than
# inventing a sentinel that could be a real branch name.
rev = inputs.self.rev or (if dirtyRevision != null then builtins.substring 0 40 dirtyRevision else null);
revCount = inputs.self.revCount or null;
rawRef = inputs.self.ref or null;

View file

@ -153,8 +153,6 @@ def build_stamp(
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:

View file

@ -47,11 +47,11 @@ def test_stamp_version_info_reads_nix_stamp(tmp_path, monkeypatch):
assert info == VersionInfo("0.19.0", "0.19.0+3", 3, "a" * 40, "feature/version", "nix")
def test_stamp_version_info_reads_docker_stamp_with_unknown_distance(tmp_path, monkeypatch):
def test_stamp_version_info_preserves_missing_branch(tmp_path, monkeypatch):
stamp = {
"schemaVersion": 2,
"commit": "b" * 40,
"branch": "unknown",
"branch": None,
"baseVersion": "0.19.0",
"displayVersion": "0.19.0+?",
"distance": None,
@ -64,7 +64,7 @@ def test_stamp_version_info_reads_docker_stamp_with_unknown_distance(tmp_path, m
info = get_version_info()
assert info == VersionInfo("0.19.0", "0.19.0+?", None, "b" * 40, "unknown", "docker", True)
assert info == VersionInfo("0.19.0", "0.19.0+?", None, "b" * 40, None, "docker", True)
def test_stamp_version_info_ignores_fallback_commit(tmp_path, monkeypatch):