authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-02 18:05:17-08:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-02 18:53:21-08:00
loge7790490f16592d9b0a5418201921a58b1fb5fe7
tree07c71df0aa755ac6220f304eabd1d8b6c4659ad0
parenta638978b75d42e78daad25170098efdec7d76dd4

Show desktop commit hash in About panel

- embed `t3codeCommitHash` into staged desktop package metadata during builds - resolve commit hash from env/package/git and display it as About panel version

2 files changed, 79 insertions(+), 1 deletions(-)

apps/desktop/src/main.ts+59-1
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import { spawn, type ChildProcess } from "node:child_process";1import { execFileSync, spawn, type ChildProcess } from "node:child_process";
2import { randomBytes } from "node:crypto";2import { randomBytes } from "node:crypto";
3import fs from "node:fs";3import fs from "node:fs";
4import net from "node:net";4import net from "node:net";
...@@ -26,6 +26,8 @@ const ROOT_DIR = path.resolve(__dirname, "../../..");...@@ -26,6 +26,8 @@ const ROOT_DIR = path.resolve(__dirname, "../../..");
26const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);26const isDevelopment = Boolean(process.env.VITE_DEV_SERVER_URL);
27const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";27const APP_DISPLAY_NAME = isDevelopment ? "T3 Code (Dev)" : "T3 Code (Alpha)";
28const APP_USER_MODEL_ID = "com.t3tools.t3code";28const APP_USER_MODEL_ID = "com.t3tools.t3code";
29const COMMIT_HASH_PATTERN = /^[0-9a-f]{7,40}$/i;
30const COMMIT_HASH_DISPLAY_LENGTH = 12;
2931
30let mainWindow: BrowserWindow | null = null;32let mainWindow: BrowserWindow | null = null;
31let backendProcess: ChildProcess | null = null;33let backendProcess: ChildProcess | null = null;
...@@ -36,6 +38,7 @@ let restartAttempt = 0;...@@ -36,6 +38,7 @@ let restartAttempt = 0;
36let restartTimer: ReturnType<typeof setTimeout> | null = null;38let restartTimer: ReturnType<typeof setTimeout> | null = null;
37let isQuitting = false;39let isQuitting = false;
38let desktopProtocolRegistered = false;40let desktopProtocolRegistered = false;
41let aboutCommitHashCache: string | null | undefined;
3942
40let destructiveMenuIconCache: Electron.NativeImage | null | undefined;43let destructiveMenuIconCache: Electron.NativeImage | null | undefined;
4144
...@@ -81,6 +84,58 @@ function resolveAppRoot(): string {...@@ -81,6 +84,58 @@ function resolveAppRoot(): string {
81 return app.getAppPath();84 return app.getAppPath();
82}85}
8386
87function normalizeCommitHash(value: unknown): string | null {
88 if (typeof value !== "string") {
89 return null;
90 }
91 const trimmed = value.trim();
92 if (!COMMIT_HASH_PATTERN.test(trimmed)) {
93 return null;
94 }
95 return trimmed.slice(0, COMMIT_HASH_DISPLAY_LENGTH).toLowerCase();
96}
97
98function resolveEmbeddedCommitHash(): string | null {
99 const packageJsonPath = path.join(resolveAppRoot(), "package.json");
100 if (!fs.existsSync(packageJsonPath)) {
101 return null;
102 }
103
104 try {
105 const raw = fs.readFileSync(packageJsonPath, "utf8");
106 const parsed = JSON.parse(raw) as { t3codeCommitHash?: unknown };
107 return normalizeCommitHash(parsed.t3codeCommitHash);
108 } catch {
109 return null;
110 }
111}
112
113function resolveGitCommitHash(): string | null {
114 try {
115 const stdout = execFileSync("git", ["rev-parse", `--short=${COMMIT_HASH_DISPLAY_LENGTH}`, "HEAD"], {
116 cwd: ROOT_DIR,
117 encoding: "utf8",
118 stdio: ["ignore", "pipe", "ignore"],
119 });
120 return normalizeCommitHash(stdout);
121 } catch {
122 return null;
123 }
124}
125
126function resolveAboutCommitHash(): string | null {
127 if (aboutCommitHashCache !== undefined) {
128 return aboutCommitHashCache;
129 }
130
131 aboutCommitHashCache =
132 normalizeCommitHash(process.env.T3CODE_COMMIT_HASH) ??
133 resolveEmbeddedCommitHash() ??
134 resolveGitCommitHash();
135
136 return aboutCommitHashCache;
137}
138
84function resolveBackendEntry(): string {139function resolveBackendEntry(): string {
85 return path.join(resolveAppRoot(), "apps/server/dist/index.mjs");140 return path.join(resolveAppRoot(), "apps/server/dist/index.mjs");
86}141}
...@@ -277,8 +332,11 @@ function resolveIconPath(ext: "ico" | "icns" | "png"): string | null {...@@ -277,8 +332,11 @@ function resolveIconPath(ext: "ico" | "icns" | "png"): string | null {
277332
278function configureAppIdentity(): void {333function configureAppIdentity(): void {
279 app.setName(APP_DISPLAY_NAME);334 app.setName(APP_DISPLAY_NAME);
335 const commitHash = resolveAboutCommitHash();
280 app.setAboutPanelOptions({336 app.setAboutPanelOptions({
281 applicationName: APP_DISPLAY_NAME,337 applicationName: APP_DISPLAY_NAME,
338 applicationVersion: app.getVersion(),
339 version: commitHash ?? "unknown",
282 });340 });
283341
284 if (process.platform === "win32") {342 if (process.platform === "win32") {
scripts/build-desktop-artifact.ts+20
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1#!/usr/bin/env node1#!/usr/bin/env node
22
3import { spawnSync } from "node:child_process";
4
3import rootPackageJson from "../package.json" with { type: "json" };5import rootPackageJson from "../package.json" with { type: "json" };
4import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };6import desktopPackageJson from "../apps/desktop/package.json" with { type: "json" };
5import serverPackageJson from "../apps/server/package.json" with { type: "json" };7import serverPackageJson from "../apps/server/package.json" with { type: "json" };
...@@ -84,6 +86,21 @@ class BuildScriptError extends Data.TaggedError("BuildScriptError")<{...@@ -84,6 +86,21 @@ class BuildScriptError extends Data.TaggedError("BuildScriptError")<{
84 readonly cause?: unknown;86 readonly cause?: unknown;
85}> {}87}> {}
8688
89function resolveGitCommitHash(repoRoot: string): string {
90 const result = spawnSync("git", ["rev-parse", "--short=12", "HEAD"], {
91 cwd: repoRoot,
92 encoding: "utf8",
93 });
94 if (result.status !== 0) {
95 return "unknown";
96 }
97 const hash = result.stdout.trim();
98 if (!/^[0-9a-f]{7,40}$/i.test(hash)) {
99 return "unknown";
100 }
101 return hash.toLowerCase();
102}
103
87interface ResolvedBuildOptions {104interface ResolvedBuildOptions {
88 readonly platform: typeof BuildPlatform.Type;105 readonly platform: typeof BuildPlatform.Type;
89 readonly target: string;106 readonly target: string;
...@@ -100,6 +117,7 @@ interface StagePackageJson {...@@ -100,6 +117,7 @@ interface StagePackageJson {
100 readonly name: string;117 readonly name: string;
101 readonly version: string;118 readonly version: string;
102 readonly buildVersion: string;119 readonly buildVersion: string;
120 readonly t3codeCommitHash: string;
103 readonly private: true;121 readonly private: true;
104 readonly description: string;122 readonly description: string;
105 readonly author: string;123 readonly author: string;
...@@ -436,6 +454,7 @@ const buildDesktopArtifact = Effect.fn(function* (options: ResolvedBuildOptions)...@@ -436,6 +454,7 @@ const buildDesktopArtifact = Effect.fn(function* (options: ResolvedBuildOptions)
436 });454 });
437455
438 const appVersion = options.version ?? serverPackageJson.version;456 const appVersion = options.version ?? serverPackageJson.version;
457 const commitHash = resolveGitCommitHash(repoRoot);
439 const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped;458 const mkdir = options.keepStage ? fs.makeTempDirectory : fs.makeTempDirectoryScoped;
440 const stageRoot = yield* mkdir({459 const stageRoot = yield* mkdir({
441 prefix: `t3code-desktop-${options.platform}-stage-`,460 prefix: `t3code-desktop-${options.platform}-stage-`,
...@@ -490,6 +509,7 @@ const buildDesktopArtifact = Effect.fn(function* (options: ResolvedBuildOptions)...@@ -490,6 +509,7 @@ const buildDesktopArtifact = Effect.fn(function* (options: ResolvedBuildOptions)
490 name: "t3-code-desktop",509 name: "t3-code-desktop",
491 version: appVersion,510 version: appVersion,
492 buildVersion: appVersion,511 buildVersion: appVersion,
512 t3codeCommitHash: commitHash,
493 private: true,513 private: true,
494 description: "T3 Code desktop build",514 description: "T3 Code desktop build",
495 author: "T3 Tools",515 author: "T3 Tools",