feat(version): derive display versions from release tags

Resolve user-facing versions from SemVer release tags and available local
history while retaining raw package metadata for API identity. Stamp Desktop
provenance at build time, and expose generic version details for every
Desktop install method.
This commit is contained in:
ethernet 2026-07-24 16:14:56 -04:00
parent ad6df5eb95
commit 1a4c4eca0d
35 changed files with 827 additions and 293 deletions

View file

@ -6,12 +6,15 @@
*
* Schema (subject to bump via STAMP_SCHEMA_VERSION):
* {
* "schemaVersion": 1,
* "schemaVersion": 2,
* "commit": "<40-char SHA>",
* "branch": "<branch name>",
* "builtAt": "<ISO 8601 UTC timestamp>",
* "dirty": true|false,
* "source": "ci" | "local" | "fallback"
* "source": "ci" | "local" | "fallback",
* "baseVersion": "<SemVer release version, e.g. 0.19.0>" | null,
* "displayVersion":"<baseVersion, or baseVersion+distance>" | null,
* "distance": <commits since baseVersion's release tag> | null
* }
*
* Source preference order:
@ -26,13 +29,17 @@
* commit as unpinned and follows the branch instead of fetching a fake SHA.
*/
import { mkdirSync, writeFileSync } from "fs"
import { mkdirSync, readFileSync, writeFileSync } from "fs"
import { resolve, join, relative } from "path"
import { execSync } from "child_process"
import { isMain } from "./utils.mjs"
const STAMP_SCHEMA_VERSION = 1
const STAMP_SCHEMA_VERSION = 2
// 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.
const SEMVER_TAG = /^v(0|[1-9]\d{0,2})\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
/** All-zero placeholder used when no real commit can be resolved. */
export const FALLBACK_COMMIT = "0000000000000000000000000000000000000000"
@ -114,8 +121,68 @@ export function isFallbackCommit(commit) {
return typeof commit === "string" && /^0{7,40}$/.test(commit)
}
function parseReleaseMetadata(repoRoot, readFile = readFileSync) {
try {
const init = readFile(join(repoRoot, 'hermes_cli', '__init__.py'), 'utf8')
const baseVersion = init.match(/__version__\s*=\s*["']([^"']+)["']/)?.[1]
const releaseDate = init.match(/__release_date__\s*=\s*["']([^"']+)["']/)?.[1]
return { baseVersion: baseVersion || null, releaseDate: releaseDate || null }
} catch {
return { baseVersion: null, releaseDate: null }
}
}
function splitLines(value) {
return value ? value.split(/\r?\n/).map(line => line.trim()).filter(Boolean) : []
}
/**
* Add user-facing release topology to a build stamp. This runs while Git is
* available to the build, so Electron never needs to inspect a live checkout.
*/
export function deriveVersionMetadata(stamp, {
repoRoot = REPO_ROOT,
execFn = tryExec,
readFile = readFileSync
} = {}) {
const { baseVersion, releaseDate } = parseReleaseMetadata(repoRoot, readFile)
if (!baseVersion) {
return stamp
}
const mergedTags = splitLines(execFn('git tag --merged HEAD --format=%(refname:short)', { cwd: repoRoot }))
const candidates = mergedTags.filter(tag => SEMVER_TAG.test(tag))
// Hermes has historical CalVer tags. Keep the release-date tag as a
// temporary fallback until the first v0.x.y tag exists; never parse a
// broad v[0-9]* match as SemVer.
if (releaseDate) {
candidates.push(`v${releaseDate}`)
}
let distance = null
let releaseTag = null
for (const tag of candidates) {
const raw = execFn(`git rev-list --count ${tag}..HEAD`, { cwd: repoRoot })
const value = raw === null ? Number.NaN : Number(raw)
if (Number.isInteger(value) && value >= 0 && (distance === null || value < distance)) {
distance = value
releaseTag = tag
}
}
const semver = releaseTag?.match(SEMVER_TAG)
const taggedVersion = semver ? `${semver[1]}.${semver[2]}.${semver[3]}` : baseVersion
const displayVersion =
distance !== null && distance > 0 ? `${taggedVersion}+${distance}`
: stamp.dirty && distance === null ? `${taggedVersion}+?`
: taggedVersion
return { ...stamp, baseVersion: taggedVersion, displayVersion, distance }
}
function main() {
const stamp = resolveStamp()
const stamp = deriveVersionMetadata(resolveStamp())
if (!stamp || !stamp.commit) {
// Should not happen — fromFallback() always provides a commit.
console.error(
@ -155,7 +222,10 @@ function main() {
branch: stamp.branch,
builtAt: new Date().toISOString(),
dirty: stamp.dirty,
source: stamp.source
source: stamp.source,
baseVersion: stamp.baseVersion ?? null,
displayVersion: stamp.displayVersion ?? null,
distance: stamp.distance ?? null
}
mkdirSync(OUT_DIR, { recursive: true })

View file

@ -5,6 +5,7 @@ import {
FALLBACK_BRANCH,
FALLBACK_COMMIT,
fromCI,
deriveVersionMetadata,
fromFallback,
fromLocalGit,
isFallbackCommit,
@ -84,3 +85,70 @@ test('resolveStamp falls back when neither CI nor git is available', () => {
source: 'fallback'
})
})
test('deriveVersionMetadata prefers a strict SemVer tag over historical CalVer tags', () => {
const stamp = deriveVersionMetadata(
{ commit: 'a'.repeat(40), branch: 'feature', dirty: true, source: 'local' },
{
readFile: () => '__version__ = "0.20.0"\n__release_date__ = "2026.7.20"\n',
execFn: command => {
if (command.startsWith('git tag --merged')) return 'v2026.7.20\nv0.19.0\n'
if (command === 'git rev-list --count v0.19.0..HEAD') return '7'
if (command === 'git rev-list --count v2026.7.20..HEAD') return '20'
return null
}
}
)
assert.deepEqual(stamp, {
commit: 'a'.repeat(40), branch: 'feature', dirty: true, source: 'local',
baseVersion: '0.19.0', displayVersion: '0.19.0+7', distance: 7
})
})
test('deriveVersionMetadata uses the historical release tag only as a transition fallback', () => {
const stamp = deriveVersionMetadata(
{ commit: 'a'.repeat(40), branch: 'feature', dirty: false, source: 'local' },
{
readFile: () => '__version__ = "0.19.0"\n__release_date__ = "2026.7.20"\n',
execFn: command => command === 'git rev-list --count v2026.7.20..HEAD' ? '3' : ''
}
)
assert.equal(stamp.baseVersion, '0.19.0')
assert.equal(stamp.displayVersion, '0.19.0+3')
assert.equal(stamp.distance, 3)
})
test('deriveVersionMetadata accepts three-digit SemVer majors but rejects four-digit CalVer years', () => {
const stamp = deriveVersionMetadata(
{ commit: 'a'.repeat(40), branch: 'feature', dirty: false, source: 'local' },
{
readFile: () => '__version__ = "999.1.2"\n__release_date__ = "2026.7.20"\n',
execFn: command => {
if (command.startsWith('git tag --merged')) return 'v2026.7.20\nv999.1.2\n'
if (command === 'git rev-list --count v999.1.2..HEAD') return '4'
if (command === 'git rev-list --count v2026.7.20..HEAD') return '8'
return null
}
}
)
assert.equal(stamp.baseVersion, '999.1.2')
assert.equal(stamp.displayVersion, '999.1.2+4')
assert.equal(stamp.distance, 4)
})
test('deriveVersionMetadata shows +? for dirty builds with unknown distance', () => {
const stamp = deriveVersionMetadata(
{ commit: 'a'.repeat(40), branch: 'feature', dirty: true, source: 'local' },
{
readFile: () => '__version__ = "0.19.0"\n__release_date__ = "2026.7.20"\n',
execFn: () => null
}
)
assert.equal(stamp.baseVersion, '0.19.0')
assert.equal(stamp.displayVersion, '0.19.0+?')
assert.equal(stamp.distance, null)
})