From 449456ce97a0c4ee5e7a9cd0981318b262c27e9a Mon Sep 17 00:00:00 2001 From: Wolfgang Schoenberger <221313372+wolfiesch@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:25:36 -0700 Subject: [PATCH] fix(release): preserve canonical macOS app name --- README.md | 5 +++++ apps/site/src/docs/content.ts | 4 ++++ docs/CURRENT_RELEASE_NOTES.md | 7 +++++++ electron-builder.config.mjs | 2 +- scripts/inspect-macos-release.mjs | 15 +++++++++++++-- scripts/inspect-macos-release.test.mjs | 14 +++++++++++++- scripts/packaging.test.mjs | 3 +++ 7 files changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ec4be105..aed026eb 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,11 @@ chmod +x T4-Code-0.1.32-linux-x86_64.AppImage 3. Open T4 Code normally. The release workflow verifies the pinned publisher, hardened runtime, secure timestamp, Apple notarization, stapled ticket, and Gatekeeper acceptance before publication. 4. To start terminal or CMUX sessions that can hand off to T4 Code, open **Settings → Hosts** and choose **Install t4-omp**. This adds `~/.local/bin/t4-omp` and leaves any existing `omp` command unchanged. +Upgrade note for v0.1.32 only: its DMG named the bundle `t4-code.app`. Before installing a +newer release, quit T4 Code and move `/Applications/t4-code.app` to the Trash, then copy +`T4 Code.app` into `/Applications`. Removing the old application bundle does not remove +your T4 Code settings or sessions. + ## What the app does - **Sessions.** Browse sessions grouped by their working folder, create new ones, and switch between them. Rename, terminate a stuck runtime, archive, restore, or permanently delete a session from its menu. Recently used sessions stay warm, so switching back is instant and nothing is replayed twice. diff --git a/apps/site/src/docs/content.ts b/apps/site/src/docs/content.ts index 9f9d4675..8c7e14a8 100644 --- a/apps/site/src/docs/content.ts +++ b/apps/site/src/docs/content.ts @@ -100,6 +100,10 @@ const install: DocTopic = { kind: "note", text: `The v${RELEASE_VERSION} macOS build is signed with the project's pinned Developer ID identity and notarized by Apple. The release workflow verifies both downloadable formats with Gatekeeper before publication.`, }, + { + kind: "note", + text: "Upgrade note for v0.1.32 only: its DMG named the bundle `t4-code.app`. Before installing a newer release, quit T4 Code and move `/Applications/t4-code.app` to the Trash, then copy `T4 Code.app` into Applications. Removing the old application bundle does not remove your T4 Code settings or sessions.", + }, { kind: "h3", id: "install-gatekeeper", text: "First launch on macOS" }, { kind: "p", text: "After copying the app into Applications, open T4 Code normally." }, { diff --git a/docs/CURRENT_RELEASE_NOTES.md b/docs/CURRENT_RELEASE_NOTES.md index 73471139..18d71759 100644 --- a/docs/CURRENT_RELEASE_NOTES.md +++ b/docs/CURRENT_RELEASE_NOTES.md @@ -20,6 +20,13 @@ the session lock. Pull-request CI now treats client runtime changes as bridge-boundary changes and runs both the current-runtime and pinned-legacy continuity gates. +## macOS application identity + +The v0.1.32 DMG accidentally named its bundle `t4-code.app` instead of the documented +`T4 Code.app`. Before installing a newer release, quit T4 Code and move +`/Applications/t4-code.app` to the Trash, then copy the new `T4 Code.app` into Applications. +Removing the old application bundle does not remove T4 Code settings or sessions. + ## A session rail built for large libraries T4 Code v0.1.32 makes a large session library easier to navigate. The rail now supports text search, activity filters, newest/oldest sorting, grouped and flat layouts, collapsible project folders, and saved display preferences. Those controls follow the Codex desktop organization model while keeping OMP as the source of truth. diff --git a/electron-builder.config.mjs b/electron-builder.config.mjs index feb83f41..e3bdef64 100644 --- a/electron-builder.config.mjs +++ b/electron-builder.config.mjs @@ -16,7 +16,6 @@ export const linuxUpdatePublish = { const config = { appId: "com.lycaonsolutions.t4code", productName: "T4 Code", - executableName: "t4-code", electronVersion: "41.5.0", artifactName: "T4-Code-${version}-${os}-${arch}.${ext}", directories: { @@ -37,6 +36,7 @@ const config = { ], protocols: [{ name: "T4 Code", schemes: ["t4-code"] }], linux: { + executableName: "t4-code", category: "Development", icon: "apps/desktop/build/icons", publish: [linuxUpdatePublish], diff --git a/scripts/inspect-macos-release.mjs b/scripts/inspect-macos-release.mjs index 4e9838df..d333c3ee 100644 --- a/scripts/inspect-macos-release.mjs +++ b/scripts/inspect-macos-release.mjs @@ -4,12 +4,13 @@ import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { mkdtempSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync } from "node:fs"; import { tmpdir } from "node:os"; -import { extname, join, resolve } from "node:path"; +import { basename, extname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const SHA256_PATTERN = /^[0-9a-f]{64}$/u; const TEAM_ID_PATTERN = /^[A-Z0-9]{10}$/u; const TAG_PATTERN = /^v\d+\.\d+\.\d+$/u; +const MACOS_APP_BUNDLE_NAME = "T4 Code.app"; function requireString(value, label) { if (typeof value !== "string" || value.length === 0) { @@ -132,13 +133,23 @@ function run(command, args, options = {}) { return `${result.stdout ?? ""}${result.stderr ?? ""}`; } +export function validateMacosAppBundleName(appPath) { + const actualName = basename(appPath); + if (actualName !== MACOS_APP_BUNDLE_NAME) { + throw new Error( + `macOS release app must be named ${MACOS_APP_BUNDLE_NAME}; found ${actualName}`, + ); + } + return appPath; +} + function findSingleApp(directory) { const apps = readdirSync(directory, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && extname(entry.name).toLowerCase() === ".app") .map((entry) => join(directory, entry.name)); if (apps.length !== 1) throw new Error(`expected exactly one top-level macOS app; found ${apps.length}`); - return apps[0]; + return validateMacosAppBundleName(apps[0]); } function inspectApp(appPath, contract, certificatePrefix) { diff --git a/scripts/inspect-macos-release.test.mjs b/scripts/inspect-macos-release.test.mjs index 0e2acded..be704cfc 100644 --- a/scripts/inspect-macos-release.test.mjs +++ b/scripts/inspect-macos-release.test.mjs @@ -5,6 +5,7 @@ import { test } from "node:test"; import { parseCodesignDisplay, validateMacosLibraryValidationBoundary, + validateMacosAppBundleName, validateMacosIdentityContract, validateMacosSignatureReport, } from "./inspect-macos-release.mjs"; @@ -14,7 +15,7 @@ const identity = JSON.parse( ); const displayFixture = ` -Executable=/Applications/T4 Code.app/Contents/MacOS/t4-code +Executable=/Applications/T4 Code.app/Contents/MacOS/T4 Code Identifier=com.lycaonsolutions.t4code Format=app bundle with Mach-O thin (arm64) CodeDirectory v=20500 size=640 flags=0x10000(runtime) hashes=10+7 location=embedded @@ -25,6 +26,17 @@ Timestamp=Jul 18, 2026 at 8:30:00 PM TeamIdentifier=WJLM3D3DK6 `; +test("macOS release archives preserve the canonical application bundle name", () => { + assert.equal( + validateMacosAppBundleName("/Volumes/T4 Code/T4 Code.app"), + "/Volumes/T4 Code/T4 Code.app", + ); + assert.throws( + () => validateMacosAppBundleName("/Volumes/T4 Code/t4-code.app"), + /must be named T4 Code\.app; found t4-code\.app/u, + ); +}); + test("macOS release identity pins the public Developer ID contract", () => { assert.doesNotThrow(() => validateMacosIdentityContract(identity)); assert.equal(identity.firstSignedReleaseTag, "v0.1.24"); diff --git a/scripts/packaging.test.mjs b/scripts/packaging.test.mjs index e46ceb72..9eb83dde 100644 --- a/scripts/packaging.test.mjs +++ b/scripts/packaging.test.mjs @@ -87,6 +87,9 @@ function androidReleaseFixture(overrides = {}) { test("builder config keeps release contract", () => { assert.equal(config.appId, "com.lycaonsolutions.t4code"); assert.equal(config.productName, "T4 Code"); + assert.equal(config.executableName, undefined); + assert.equal(config.linux.executableName, "t4-code"); + assert.equal(config.mac.executableName, undefined); assert.equal(config.asar, true); assert.deepEqual(config.protocols[0].schemes, ["t4-code"]); assert.equal(config.linux.category, "Development");