From 38a9c6c1b7d8b5e321ff633cd7dbab5a4add401d Mon Sep 17 00:00:00 2001 From: Brandon Li Date: Sat, 12 Sep 2026 18:54:19 -0500 Subject: [PATCH] Report the running build's version to the master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The version is the checkout's HEAD commit (short hash + commit time), read once at startup — updates arrive as `git pull`, so there is no release number to bump. It rides on the socket hello, socket reports, and the HTTP report fallback; the field is optional in the protocol so older masters ignore it. Co-Authored-By: Claude Fable 5 --- README.md | 7 +++++++ lib/master-link.ts | 3 +++ lib/master-protocol.ts | 12 ++++++++++-- lib/reporter.ts | 2 ++ lib/version.ts | 43 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 lib/version.ts diff --git a/README.md b/README.md index 2bdebde..1d7489e 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,13 @@ instance does two things with them: it reports its firm and account counts every 30 seconds, and it holds a WebSocket open to `/ws/instance` so the master can drive it. +Every report also carries this checkout's version — the HEAD commit and its +time, since updates arrive as `git pull` and there is no release number to +bump. The master shows it in a **Version** column and flags any machine running +something older than the newest build in the fleet, which is how you check at a +glance that every machine took the last update. An instance showing *old build* +predates version reporting entirely. + What the master can trigger here: | | | diff --git a/lib/master-link.ts b/lib/master-link.ts index 31a4366..4a3d4bb 100644 --- a/lib/master-link.ts +++ b/lib/master-link.ts @@ -20,6 +20,7 @@ import { runTrade, startScheduler, stopScheduler, getSchedulerStatus } from './a import { FIRMS, automationKey } from './automations'; import { POINT_VALUES } from './trading-logic'; import { queueRun } from './queue-run'; +import { getBuildVersion } from './version'; import type { Command, InstanceCapabilities, @@ -161,6 +162,7 @@ export function pushReport(firms: unknown[], scheduler: unknown): boolean { firms: firms as never, scheduler: scheduler as never, capabilities: collectCapabilities(), + version: getBuildVersion() ?? undefined, }); } @@ -226,6 +228,7 @@ function connect() { instance, id: instanceId || undefined, capabilities: collectCapabilities(), + version: getBuildVersion() ?? undefined, } satisfies InstanceMessage)); }); diff --git a/lib/master-protocol.ts b/lib/master-protocol.ts index 875f034..5777b84 100644 --- a/lib/master-protocol.ts +++ b/lib/master-protocol.ts @@ -87,6 +87,14 @@ export interface SchedulerState { sessionOpenDelayMin?: number; } +/** The build this instance is running — its checkout's HEAD commit. The + * commit time is what orders two different versions. Optional in every + * message so an older instance or master simply ignores it. */ +export interface BuildVersion { + commit: string; + time: string; +} + export interface AutomationSummary { key: string; label: string; @@ -200,6 +208,6 @@ export type MasterMessage = | { type: 'command'; id: string; command: Command }; export type InstanceMessage = - | { type: 'hello'; instance: string; id?: string; capabilities: InstanceCapabilities } - | { type: 'report'; firms: FirmStats[]; scheduler: SchedulerState; capabilities?: InstanceCapabilities } + | { type: 'hello'; instance: string; id?: string; capabilities: InstanceCapabilities; version?: BuildVersion } + | { type: 'report'; firms: FirmStats[]; scheduler: SchedulerState; capabilities?: InstanceCapabilities; version?: BuildVersion } | { type: 'ack'; id: string; ok: boolean; result?: unknown; error?: string }; diff --git a/lib/reporter.ts b/lib/reporter.ts index 2c00211..2216566 100644 --- a/lib/reporter.ts +++ b/lib/reporter.ts @@ -13,6 +13,7 @@ import { getSetting } from './db'; import { pushReport } from './master-link'; +import { getBuildVersion } from './version'; import { getClients } from './clients'; import { getTradableFirms } from './firm-catalog'; import { getAccountConfig } from './account-type-resolver'; @@ -292,6 +293,7 @@ export async function sendReport(): Promise { id: getSetting('instance_id') || undefined, firms, scheduler, + version: getBuildVersion() ?? undefined, }), }); } catch { diff --git a/lib/version.ts b/lib/version.ts new file mode 100644 index 0000000..d265b81 --- /dev/null +++ b/lib/version.ts @@ -0,0 +1,43 @@ +/** + * The version of the running build. + * + * Updates arrive as `git pull` (scripts/update-check.mjs), so the checkout's + * HEAD commit *is* the version — there is no release number to bump and + * nothing for anyone to forget. The commit time rides along because hashes + * don't order: it is what lets the master say which of two different + * versions is the stale one. + * + * Read once and cached: the update task restarts the process after every + * pull, so HEAD cannot move under a running build — and when it briefly does + * (pull landed, restart pending), the cached value still describes the code + * that is actually executing. + */ + +import { execFileSync } from 'node:child_process'; + +export interface BuildVersion { + /** Short commit hash of HEAD, e.g. "c493796". */ + commit: string; + /** ISO commit time of HEAD — orders versions, which hashes cannot. */ + time: string; +} + +let cached: BuildVersion | null | undefined; + +/** null when this isn't a git checkout or git isn't installed — both mean + * "no version to report", and the master shows the machine as an old build. */ +export function getBuildVersion(): BuildVersion | null { + if (cached !== undefined) return cached; + try { + const out = execFileSync('git', ['log', '-1', '--format=%h %cI'], { + cwd: process.cwd(), + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + const [commit, time] = out.split(' '); + cached = commit && time ? { commit, time } : null; + } catch { + cached = null; + } + return cached; +}