feat(desktop): name commit and canary builds in the product display name

Commit builds now show 'Hermes Agent <sha7>' (e.g. Hermes Agent abc1234)
and canary builds 'Hermes Canary' / 'Hermes Light Canary' / 'Hermes Agent
Canary' as the OS-visible product name, so side-by-side installs and
per-commit artifacts are readable at a glance.

Display-only by design: appId, appNamePascal, and msixAppIdWithOrg are
unchanged, so a canary MSIX still updates in place over stable and
userData / single-instance sharing with the stable install is unaffected.
bundle-electron-main.mjs derives the commit from the install stamp
(source='commit-build') so the baked runtime identity matches the
packaging identity.
This commit is contained in:
ethernet
2026-09-11 14:58:11 -04:00
parent 4e45b78311
commit 162c5b92d5
3 changed files with 51 additions and 2 deletions

View File

@@ -17,6 +17,7 @@ beforeEach(() => {
afterEach(() => {
delete process.env.HERMES_DESKTOP_VARIANT
delete process.env.HERMES_PAYLOAD_TAG
delete process.env.HERMES_BUILD_COMMIT
vi.resetModules()
})
@@ -58,6 +59,38 @@ test('a canary payload tag moves BOTH variants onto their canary feed channel',
assert.equal(light.channel, 'light-canary')
})
test('a canary tag renames the DISPLAY name only, keeping machine identity intact', async () => {
const stable = await identityForVariant(undefined)
process.env.HERMES_PAYLOAD_TAG = 'v0.28.0-canary.20260818'
const canaryIdentity = await identityForVariant(undefined)
assert.equal(canaryIdentity.displayName, 'Hermes Canary')
// Machine identity is shared with stable so the canary MSIX still updates
// in place over stable and userData/single-instance stay shared.
assert.equal(canaryIdentity.appId, stable.appId)
assert.equal(canaryIdentity.appNamePascal, stable.appNamePascal)
assert.equal(canaryIdentity.msixAppIdWithOrg, stable.msixAppIdWithOrg)
})
test('a commit build names the SHA in the display name, machine identity intact', async () => {
process.env.HERMES_BUILD_COMMIT = 'abcdef1234567890abcdef1234567890abcdef12'
const full = await identityForVariant(undefined)
assert.equal(full.displayName, 'Hermes abcdef1')
const bundled = await identityForVariant('bundled')
assert.equal(bundled.displayName, 'Hermes Agent abcdef1')
delete process.env.HERMES_BUILD_COMMIT
const plain = await identityForVariant(undefined)
assert.equal(plain.displayName, 'Hermes')
// Malformed commit values must not leak into the name (commit builds
// validate the full SHA elsewhere; the display derivation stays total).
process.env.HERMES_BUILD_COMMIT = 'not-a-sha'
const malformed = await identityForVariant(undefined)
assert.equal(malformed.displayName, 'Hermes')
})
test('stable tags and tagless dev builds publish to the stable channels', async () => {
process.env.HERMES_PAYLOAD_TAG = 'v0.28.0'
assert.equal((await identityForVariant(undefined)).channel, 'latest')

View File

@@ -42,13 +42,26 @@ const name = variants[store ? 'bundled' : (variant || '')]
// never overwrite the stable feed file, and vice versa.
const canary = /-canary\.20\d{6}(?:\d{6})?$/.test(process.env.HERMES_PAYLOAD_TAG || '')
// Display-name qualifiers, display-only by design: appId/appNamePascal/
// msixAppIdWithOrg stay fixed so a canary MSIX still updates in place
// over stable and userData/single-instance sharing is unaffected. A
// commit build names the exact SHA it was built from (Hermes Agent abc1234);
// a canary tags itself so side-by-side installs are readable at a glance.
const buildCommitEnv = process.env.HERMES_BUILD_COMMIT || ''
const buildCommit = /^[a-f0-9]{40}$/.test(buildCommitEnv) ? buildCommitEnv.slice(0, 7) : null
const displayName = buildCommit
? `${name.display} ${buildCommit}`
: canary
? `${name.display} Canary`
: name.display
/** @typedef {import("./product-identity.d.cts")} ProductIdentity */
/** @type {ProductIdentity} */
const identity = {
store,
light,
displayName: name.display,
displayName,
appId: `com.nousresearch.${name.kebab}`,
// The store build never publishes to a release feed (the Store owns its
// updates); null means "no feed" for its publish config.

View File

@@ -18,7 +18,10 @@ function productIdentity(source, stamp) {
}
return execFileSync(process.execPath, ['-e', 'console.log(JSON.stringify(require(process.argv[1])))',
join(source, 'apps/desktop/product-identity.cjs')], {
env: { ...process.env, HERMES_DESKTOP_VARIANT: variant, HERMES_PAYLOAD_TAG: stamp.tag || '' },
env: { ...process.env, HERMES_DESKTOP_VARIANT: variant, HERMES_PAYLOAD_TAG: stamp.tag || '',
// Commit builds stamp source='commit-build'; the display name carries
// the short SHA (see product-identity.cjs). Tagged builds pass ''.
HERMES_BUILD_COMMIT: stamp.source === 'commit-build' ? (stamp.commit || '') : '' },
encoding: 'utf8',
}).trim()
}