From 162c5b92d502f025fa4d27a27fa9668c7b290895 Mon Sep 17 00:00:00 2001 From: ethernet Date: Fri, 11 Sep 2026 14:58:11 -0400 Subject: [PATCH] feat(desktop): name commit and canary builds in the product display name Commit builds now show 'Hermes Agent ' (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. --- .../desktop/electron/product-identity.test.ts | 33 +++++++++++++++++++ apps/desktop/product-identity.cjs | 15 ++++++++- apps/desktop/scripts/bundle-electron-main.mjs | 5 ++- 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/apps/desktop/electron/product-identity.test.ts b/apps/desktop/electron/product-identity.test.ts index cb4dbc8b08..f9cbed7c88 100644 --- a/apps/desktop/electron/product-identity.test.ts +++ b/apps/desktop/electron/product-identity.test.ts @@ -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') diff --git a/apps/desktop/product-identity.cjs b/apps/desktop/product-identity.cjs index 3d36ab0f7c..1fb070733d 100644 --- a/apps/desktop/product-identity.cjs +++ b/apps/desktop/product-identity.cjs @@ -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. diff --git a/apps/desktop/scripts/bundle-electron-main.mjs b/apps/desktop/scripts/bundle-electron-main.mjs index cb8b17f25a..a8de104d56 100644 --- a/apps/desktop/scripts/bundle-electron-main.mjs +++ b/apps/desktop/scripts/bundle-electron-main.mjs @@ -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() }