fix: add authoritative native version information (#1009)

* fix: add authoritative native version information

* docs: explain native version support

* fix: pin patched brace expansion release

* fix: embed desktop release channel
This commit is contained in:
Brad Groux 2026-07-24 17:29:10 -05:00 committed by GitHub
parent edcd33c9f5
commit 2f18229ff2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 257 additions and 21 deletions

View file

@ -24,6 +24,7 @@ concurrency:
env:
NODE_VERSION: '22'
VERITAS_BUILD_SHA: ${{ github.sha }}
VERITAS_UPDATE_CHANNEL: ${{ github.event.inputs.channel || 'stable' }}
jobs:

View file

@ -27,6 +27,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
chat scrolling cannot move the application shell, wheel input cannot resize the
dock, and Close, Escape, Back, or Reset Layout always preserve a visible board
and restore focus (#1004).
- Added a native **About Veritas Kanban** panel and offline **Copy Version
Information** action using Electron's authoritative application version. The
same record now reports the embedded release commit, stable/beta/development
channel, OS, and architecture through About, clipboard support text, the
desktop bridge, and updater fallback without depending on the renderer or
network (#1005).
## [6.0.1] - 2026-07-24

View file

@ -6,6 +6,12 @@ const electronRuntimeExternal = ['electron', /^electron\/.+/];
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
define: {
__VERITAS_BUILD_SHA__: JSON.stringify(
process.env.VERITAS_BUILD_SHA ?? process.env.GITHUB_SHA ?? ''
),
__VERITAS_RELEASE_CHANNEL__: JSON.stringify(process.env.VERITAS_UPDATE_CHANNEL ?? ''),
},
build: {
// Vite 8 builds with Rolldown. Electron Vite 5 still places its built-in
// runtime externals under rollupOptions, which Rolldown does not consume.

View file

@ -138,6 +138,9 @@ describe('desktop bridge contracts', () => {
expect(bridgeHandlers.getAppInfo(undefined)).toMatchObject({
name: 'Veritas Kanban',
version: '6.0.1',
channel: 'stable',
arch: process.arch,
osVersion: expect.any(String),
packaged: true,
});
});

View file

@ -43,7 +43,8 @@ function updateStatus(state: DesktopUpdateStatus['state']): DesktopUpdateStatus
describe('desktop native menu', () => {
it('exposes common actions with keyboard shortcuts', () => {
const dispatch = vi.fn();
const template = createDesktopMenuTemplate({ status: status(), dispatch });
const copyVersionInfo = vi.fn();
const template = createDesktopMenuTemplate({ status: status(), dispatch, copyVersionInfo });
const labels = template.flatMap((item) =>
Array.isArray(item.submenu) ? item.submenu.map((child) => child.label) : []
);
@ -56,6 +57,14 @@ describe('desktop native menu', () => {
expect(labels).toContain('Restart Local Server');
expect(labels).toContain('Reset Window Layout');
const appMenu = template.find((item) => item.label === 'Veritas Kanban');
const appItems = Array.isArray(appMenu?.submenu) ? appMenu.submenu : [];
expect(appItems[0]).toMatchObject({ role: 'about', label: 'About Veritas Kanban' });
expect(appItems[1]).toMatchObject({ type: 'separator' });
const copyVersion = appItems.find((item) => item.label === 'Copy Version Information');
copyVersion?.click?.(undefined as never, undefined as never, undefined as never);
expect(copyVersionInfo).toHaveBeenCalledOnce();
const fileMenu = template.find((item) => item.label === 'File');
const newTask = Array.isArray(fileMenu?.submenu)
? fileMenu.submenu.find((item) => item.label === 'New Task')
@ -67,7 +76,11 @@ describe('desktop native menu', () => {
});
it('exposes the native edit menu so macOS text fields receive standard shortcuts', () => {
const template = createDesktopMenuTemplate({ status: status(), dispatch: vi.fn() });
const template = createDesktopMenuTemplate({
status: status(),
dispatch: vi.fn(),
copyVersionInfo: vi.fn(),
});
expect(template.some((item) => item.role === 'editMenu')).toBe(true);
});
@ -76,6 +89,7 @@ describe('desktop native menu', () => {
const desktopMenu = createDesktopMenuTemplate({
status: status('failed'),
dispatch: vi.fn(),
copyVersionInfo: vi.fn(),
}).find((item) => item.label === 'Desktop');
const externalTest = Array.isArray(desktopMenu?.submenu)
? desktopMenu.submenu.find((item) => item.label === 'Test External Delivery')
@ -89,6 +103,7 @@ describe('desktop native menu', () => {
status: status(),
updateStatus: updateStatus('available'),
dispatch: vi.fn(),
copyVersionInfo: vi.fn(),
}).find((item) => item.label === 'Veritas Kanban');
const downloadUpdate = Array.isArray(appMenu?.submenu)
? appMenu.submenu.find((item) => item.label === 'Download Update')

View file

@ -0,0 +1,80 @@
import { describe, expect, it } from 'vitest';
import {
createDesktopAboutPanelOptions,
createDesktopAppInfo,
formatDesktopVersionInfo,
normalizeBuildIdentity,
} from '../version-info.js';
describe('desktop version information', () => {
it('formats authoritative packaged version, build, channel, and platform details', () => {
const info = createDesktopAppInfo('6.0.2', true, {
platform: 'darwin',
arch: 'arm64',
osVersion: '15.5',
buildIdentity: 'abc1234',
});
expect(info).toMatchObject({
version: '6.0.2',
buildIdentity: 'abc1234',
channel: 'stable',
platform: 'darwin',
arch: 'arm64',
osVersion: '15.5',
packaged: true,
});
expect(formatDesktopVersionInfo(info)).toBe(
['Veritas Kanban 6.0.2', 'Build: abc1234', 'Channel: stable', 'macOS 15.5 · arm64'].join('\n')
);
});
it('labels prerelease and development builds without network access', () => {
expect(
createDesktopAppInfo('6.0.2-beta.1', true, {
platform: 'darwin',
arch: 'arm64',
osVersion: '15.5',
buildIdentity: null,
}).channel
).toBe('beta');
const development = createDesktopAppInfo('6.0.2', false, {
platform: 'darwin',
arch: 'arm64',
osVersion: '15.5',
buildIdentity: null,
});
expect(development.channel).toBe('dev');
expect(formatDesktopVersionInfo(development)).toContain('Build: development');
});
it('rejects path-like or unbounded build metadata from support output', () => {
expect(normalizeBuildIdentity('/Users/example/private/build')).toBeNull();
expect(normalizeBuildIdentity('a'.repeat(65))).toBeNull();
const info = createDesktopAppInfo('6.0.2', true, {
buildIdentity: '/Users/example/private/build',
osVersion: '15.5',
});
expect(info.buildIdentity).toBeNull();
expect(formatDesktopVersionInfo(info)).not.toContain('/Users/');
});
it('builds an offline native About panel from the same app information', () => {
const info = createDesktopAppInfo('6.0.2', true, {
platform: 'darwin',
arch: 'arm64',
osVersion: '15.5',
buildIdentity: 'abc1234',
});
expect(createDesktopAboutPanelOptions(info)).toMatchObject({
applicationName: 'Veritas Kanban',
applicationVersion: '6.0.2',
version: 'Build abc1234',
credits: expect.stringContaining('Channel: stable'),
});
});
});

View file

@ -2,11 +2,10 @@ import type { IpcMain, Shell } from 'electron';
import { lookup } from 'node:dns/promises';
import { blockedRemoteConnectionDestinationReason } from '@veritas-kanban/shared';
import { DESKTOP_APP_ID, DESKTOP_APP_NAME } from './app-metadata.js';
import type { DesktopCommandDispatcher } from './commands.js';
import type { DesktopAppInfo } from './types.js';
import type { DesktopRuntime } from './runtime.js';
import type { DesktopUpdateService } from './updates.js';
import { createDesktopAppInfo } from './version-info.js';
import {
createDesktopSetupDiagnostics,
createDesktopSupportSnapshot,
@ -209,13 +208,7 @@ export function createDesktopBridgeHandlers(
updateService?: DesktopUpdateService,
windowControls?: DesktopWindowControls
): DesktopBridgeHandlerMap {
const appInfo = (): DesktopAppInfo => ({
name: DESKTOP_APP_NAME,
appId: DESKTOP_APP_ID,
version: appVersion,
platform: process.platform,
packaged,
});
const appInfo = () => createDesktopAppInfo(appVersion, packaged);
return {
getAppInfo: appInfo,
@ -244,7 +237,7 @@ export function createDesktopBridgeHandlers(
updateService?.snapshot() ?? {
state: 'unsupported',
currentVersion: appInfo().version,
channel: packaged ? 'stable' : 'dev',
channel: appInfo().channel,
checkedAt: new Date().toISOString(),
detail: 'Updater service is not initialized.',
},
@ -324,8 +317,7 @@ export function registerDesktopBridge(
const definition = DESKTOP_BRIDGE_METHODS[method];
const handler = handlers[method] as (request: unknown) => MaybePromise<unknown>;
const validator = DESKTOP_BRIDGE_METHOD_VALIDATORS[method] as
| ((payload: unknown) => unknown)
| undefined;
((payload: unknown) => unknown) | undefined;
ipcMain.handle(definition.channel, async (_event, request: unknown) => {
try {

View file

@ -20,6 +20,11 @@ import {
ElectronAutoUpdaterAdapter,
resolveDesktopUpdateChannel,
} from './updates.js';
import {
createDesktopAboutPanelOptions,
createDesktopAppInfo,
formatDesktopVersionInfo,
} from './version-info.js';
import {
DESKTOP_BRIDGE_EVENTS,
redactDesktopBridgeValue,
@ -174,9 +179,11 @@ function refreshDesktopMenu(): void {
return;
}
const appInfo = createDesktopAppInfo(app.getVersion(), launchPackaged);
configureDesktopMenu({
status: runtime.snapshot(),
updateStatus: updateService?.snapshot(),
copyVersionInfo: () => clipboard.writeText(formatDesktopVersionInfo(appInfo)),
dispatch: (command) => {
if (commandDispatcher) {
dispatchDesktopMenuCommand(commandDispatcher, command);
@ -186,10 +193,11 @@ function refreshDesktopMenu(): void {
}
function updateServiceFallback(packaged: boolean): DesktopUpdateStatus {
const appInfo = createDesktopAppInfo(app.getVersion(), packaged);
return {
state: 'unsupported',
currentVersion: app.getVersion(),
channel: packaged ? 'stable' : 'dev',
currentVersion: appInfo.version,
channel: appInfo.channel,
checkedAt: new Date().toISOString(),
detail: 'Updater service is not initialized.',
};
@ -200,6 +208,8 @@ async function boot(): Promise<void> {
app.setAppUserModelId(DESKTOP_APP_ID);
const packaged = launchPackaged;
const appInfo = createDesktopAppInfo(app.getVersion(), packaged);
app.setAboutPanelOptions(createDesktopAboutPanelOptions(appInfo));
const repoRoot = launchRepoRoot;
const profile = launchProfile;
const workspace = launchWorkspace;

View file

@ -13,6 +13,7 @@ import type {
export interface ConfigureDesktopMenuOptions {
dispatch(command: DesktopCommandName): void;
copyVersionInfo(): void;
status: DesktopStatusSnapshot;
updateStatus?: DesktopUpdateStatus;
}
@ -38,6 +39,13 @@ export function createDesktopMenuTemplate(
{
label: 'Veritas Kanban',
submenu: [
{ role: 'about', label: 'About Veritas Kanban' },
{ type: 'separator' },
{
label: 'Copy Version Information',
click: () => options.copyVersionInfo(),
},
{ type: 'separator' },
command('open-onboarding'),
command('open-settings'),
command('communication-health'),

View file

@ -70,6 +70,10 @@ export interface DesktopAppInfo {
name: string;
appId: string;
version: string;
buildIdentity: string | null;
channel: 'dev' | 'beta' | 'stable';
platform: NodeJS.Platform;
arch: string;
osVersion: string;
packaged: boolean;
}

View file

@ -0,0 +1,102 @@
import os from 'node:os';
import { DESKTOP_APP_ID, DESKTOP_APP_NAME } from './app-metadata.js';
import type { DesktopAppInfo } from './types.js';
import { resolveDesktopUpdateChannel } from './updates.js';
declare const __VERITAS_BUILD_SHA__: string | undefined;
declare const __VERITAS_RELEASE_CHANNEL__: string | undefined;
export interface DesktopAppInfoOverrides {
platform?: NodeJS.Platform;
arch?: string;
osVersion?: string;
requestedChannel?: string;
buildIdentity?: string | null;
}
function embeddedBuildIdentity(): string | null {
const value = typeof __VERITAS_BUILD_SHA__ === 'string' ? __VERITAS_BUILD_SHA__ : undefined;
return normalizeBuildIdentity(value);
}
function embeddedReleaseChannel(): string | undefined {
return typeof __VERITAS_RELEASE_CHANNEL__ === 'string' && __VERITAS_RELEASE_CHANNEL__.trim()
? __VERITAS_RELEASE_CHANNEL__
: undefined;
}
export function normalizeBuildIdentity(value: string | undefined | null): string | null {
const normalized = value?.trim();
if (!normalized || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(normalized)) {
return null;
}
return normalized;
}
function resolveSystemVersion(platform: NodeJS.Platform): string {
if (platform === 'darwin') {
const electronProcess = process as NodeJS.Process & { getSystemVersion?: () => string };
const systemVersion = electronProcess.getSystemVersion?.();
if (systemVersion?.trim()) return systemVersion.trim();
}
return os.release();
}
export function createDesktopAppInfo(
version: string,
packaged: boolean,
overrides: DesktopAppInfoOverrides = {}
): DesktopAppInfo {
const platform = overrides.platform ?? process.platform;
const buildIdentity =
overrides.buildIdentity === undefined
? embeddedBuildIdentity()
: normalizeBuildIdentity(overrides.buildIdentity);
return {
name: DESKTOP_APP_NAME,
appId: DESKTOP_APP_ID,
version,
buildIdentity,
channel: resolveDesktopUpdateChannel(
overrides.requestedChannel ?? embeddedReleaseChannel() ?? process.env.VERITAS_UPDATE_CHANNEL,
version,
packaged
),
platform,
arch: overrides.arch ?? process.arch,
osVersion: overrides.osVersion ?? resolveSystemVersion(platform),
packaged,
};
}
function platformLabel(platform: NodeJS.Platform): string {
if (platform === 'darwin') return 'macOS';
if (platform === 'win32') return 'Windows';
if (platform === 'linux') return 'Linux';
return platform;
}
export function formatDesktopVersionInfo(info: DesktopAppInfo): string {
const lines = [`${info.name} ${info.version}`];
if (info.buildIdentity) {
lines.push(`Build: ${info.buildIdentity}`);
} else if (!info.packaged) {
lines.push('Build: development');
}
lines.push(
`Channel: ${info.channel}`,
`${platformLabel(info.platform)} ${info.osVersion} · ${info.arch}`
);
return lines.join('\n');
}
export function createDesktopAboutPanelOptions(info: DesktopAppInfo) {
const supportLines = formatDesktopVersionInfo(info).split('\n').slice(1);
return {
applicationName: info.name,
applicationVersion: info.version,
version: info.buildIdentity ? `Build ${info.buildIdentity}` : `Channel ${info.channel}`,
credits: supportLines.join('\n'),
};
}

View file

@ -118,6 +118,7 @@ The Kanban board is the central interface — a drag-and-drop workspace that ref
- **Dark/light mode** — Ships dark by default with a toggle in Settings → General → Appearance; persists to localStorage; inline script in `index.html` prevents flash of wrong theme on load
- **Filter bar** — Search tasks by text, filter by project and task type; filters persist in URL query params
- **Desktop shell controls** — Native-app-style toolbar with workspace selection, health state, view toggles, and bounded left/right/chat dock controls shared by the web and macOS app shells
- **Native version identity** — The macOS application menu opens an offline About panel and copies a redacted support string from the same authoritative Electron version, embedded release commit, release channel, OS, and architecture record exposed by the desktop bridge
- **Mobile shell controls** — Compact navigation uses bounded labels and full accessible names; Board Chat stays fixed above the bottom navigation and device safe area
- **Resizable Workbench** — Board Chat and Squad Chat open in a right dock by default, can switch to Bottom without losing the active conversation, and clamp their width or height to keep the application shell recoverable
- **Bulk operations** — Select multiple tasks to move, archive, or delete in batch; select-all toggle

View file

@ -223,3 +223,9 @@ runtime-manifest, support-profile, compatibility-matrix, launch-manifest,
approval, run-event, and completion evidence. Never paste raw private relay
events, provider output, credentials, or unrestricted support bundles into a
public issue.
In the packaged macOS app, choose **Veritas Kanban → About Veritas Kanban** for
the authoritative running version. **Copy Version Information** in the same
native menu produces a redacted offline support string with the build identity,
release channel, macOS version, and architecture even when the renderer or local
API is unavailable.

11
pnpm-lock.yaml generated
View file

@ -8,6 +8,7 @@ overrides:
'@babel/core@<7.29.1': 7.29.7
'@modelcontextprotocol/sdk>express-rate-limit': 8.5.2
'@xmldom/xmldom': 0.8.13
brace-expansion@<=5.0.7: 5.0.8
esbuild@<0.28.1: 0.28.1
fast-uri@<3.1.4: 3.1.4
form-data@<4.0.6: 4.0.6
@ -1969,9 +1970,9 @@ packages:
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
brace-expansion@5.0.7:
resolution: {integrity: sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==}
engines: {node: 18 || 20 || >=22}
brace-expansion@5.0.8:
resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==}
engines: {node: 20 || >=22}
browserslist@4.28.5:
resolution: {integrity: sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==}
@ -6922,7 +6923,7 @@ snapshots:
boolean@3.2.0:
optional: true
brace-expansion@5.0.7:
brace-expansion@5.0.8:
dependencies:
balanced-match: 4.0.4
@ -9176,7 +9177,7 @@ snapshots:
minimatch@10.2.5:
dependencies:
brace-expansion: 5.0.7
brace-expansion: 5.0.8
minimist@1.2.8: {}

View file

@ -17,6 +17,7 @@ overrides:
'@babel/core@<7.29.1': 7.29.7
'@modelcontextprotocol/sdk>express-rate-limit': 8.5.2
'@xmldom/xmldom': 0.8.13
'brace-expansion@<=5.0.7': 5.0.8
'esbuild@<0.28.1': 0.28.1
'fast-uri@<3.1.4': 3.1.4
'form-data@<4.0.6': 4.0.6