mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-10-20 05:15:31 +00:00
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:
parent
ad6df5eb95
commit
1a4c4eca0d
35 changed files with 827 additions and 293 deletions
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue