From b7e321e8f1c00f16fb600e86f6b2cdc769092c6c Mon Sep 17 00:00:00 2001 From: collissimo <148455189+collissimo@users.noreply.github.com> Date: Fri, 13 Nov 2026 20:54:17 +0100 Subject: [PATCH] fix: fork builds update from their own releases, and the update capsule keeps its label A server built in a fork asked upstream's releases for its fork-only version and got a 404 on SHA256SUMS, unless T3CODE_RELEASE_BASE_URL was set by hand on that machine. Server, desktop and web builds now bake in the repository they were built in (GITHUB_REPOSITORY, or T3CODE_DESKTOP_UPDATE_REPOSITORY), the same source the desktop updater already uses, so release lookups and archive downloads default to it. Builds without one still use upstream. In a narrow sidebar the update capsule had no room for its label beside the footer icons and showed only an off-center "Retry". It now wraps onto its own row above the icons when it cannot fit its label. Co-Authored-By: Claude Fable 5.2 --- apps/desktop/vite.config.ts | 3 ++- apps/server/vite.config.ts | 3 ++- .../web/src/components/desktopUpdate.logic.ts | 18 ++------------ .../src/components/sidebar/SidebarChrome.tsx | 2 +- .../components/sidebar/SidebarUpdatePill.tsx | 3 ++- apps/web/vite.config.ts | 8 +++---- packages/shared/src/cliRelease.test.ts | 14 ++++++++++- packages/shared/src/cliRelease.ts | 24 ++++++++++++++++++- scripts/lib/public-config.ts | 5 ++++ 9 files changed, 53 insertions(+), 27 deletions(-) diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index ba37576815097..de67fbcd0f256 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -2,7 +2,7 @@ import "vite-plus/test/config"; import { defaultInclude, defineConfig } from "vite-plus"; import { isDesktopRuntimeExternalDependency } from "../../scripts/lib/desktop-external-packages.ts"; -import { loadRepoEnv } from "../../scripts/lib/public-config.ts"; +import { buildReleaseRepository, loadRepoEnv } from "../../scripts/lib/public-config.ts"; const repoEnv = loadRepoEnv(); @@ -11,6 +11,7 @@ const isMainProcessExternal = (id: string) => id === "electron" || id.startsWith("electron/") || isDesktopRuntimeExternalDependency(id); const shouldLaunchElectronAfterPack = process.env.T3CODE_DESKTOP_DEV === "1"; const publicConfigDefine = { + __T3CODE_BUILD_RELEASE_REPOSITORY__: JSON.stringify(buildReleaseRepository()), __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: JSON.stringify( repoEnv.T3CODE_CLERK_PUBLISHABLE_KEY?.trim() ?? "", ), diff --git a/apps/server/vite.config.ts b/apps/server/vite.config.ts index a48b4afe3fbe7..6de3aa6afac59 100644 --- a/apps/server/vite.config.ts +++ b/apps/server/vite.config.ts @@ -4,7 +4,7 @@ import * as NodePath from "node:path"; import { defineConfig, mergeConfig } from "vite-plus"; import baseConfig from "../../vite.config.ts"; -import { loadRepoEnv } from "../../scripts/lib/public-config.ts"; +import { buildReleaseRepository, loadRepoEnv } from "../../scripts/lib/public-config.ts"; import rootPackageJson from "../../package.json" with { type: "json" }; import packageJson from "./package.json" with { type: "json" }; import { WeightedShardSequencer } from "./src/testUtils/weightedShardSequencer.ts"; @@ -128,6 +128,7 @@ export default mergeConfig( define: { __T3CODE_BUILD_CHANNEL__: JSON.stringify(cliBuildChannel), __T3CODE_TELEMETRY_CHANNEL__: JSON.stringify(telemetryChannel), + __T3CODE_BUILD_RELEASE_REPOSITORY__: JSON.stringify(buildReleaseRepository()), __T3CODE_BUILD_RELAY_URL__: JSON.stringify(repoEnv.T3CODE_RELAY_URL?.trim() ?? ""), __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: JSON.stringify( repoEnv.T3CODE_CLERK_PUBLISHABLE_KEY?.trim() ?? "", diff --git a/apps/web/src/components/desktopUpdate.logic.ts b/apps/web/src/components/desktopUpdate.logic.ts index 51b2b92b0a8ba..b9e4cca76e23c 100644 --- a/apps/web/src/components/desktopUpdate.logic.ts +++ b/apps/web/src/components/desktopUpdate.logic.ts @@ -1,24 +1,10 @@ import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts"; +import { githubReleaseRepository } from "@t3tools/shared/cliRelease"; export type DesktopUpdateButtonAction = "download" | "install" | "none"; -const DEFAULT_DESKTOP_RELEASE_REPOSITORY = "pingdotgg/t3code"; -const GITHUB_REPOSITORY_SEGMENT = /^[A-Za-z0-9._-]+$/u; - -function isValidRepositorySegment(value: string | undefined): value is string { - return ( - value !== undefined && value !== "." && value !== ".." && GITHUB_REPOSITORY_SEGMENT.test(value) - ); -} - function desktopReleaseRepositoryUrl(repository: string | undefined): string { - const configuredRepository = repository?.trim() ?? ""; - const [owner, name, ...extra] = configuredRepository.split("/"); - const releaseRepository = - isValidRepositorySegment(owner) && isValidRepositorySegment(name) && extra.length === 0 - ? configuredRepository - : DEFAULT_DESKTOP_RELEASE_REPOSITORY; - return `https://github.com/${releaseRepository}/releases`; + return `https://github.com/${githubReleaseRepository(repository)}/releases`; } /** The download RPC can beat the updater's downloadedVersion event; fall back to the requested version. */ diff --git a/apps/web/src/components/sidebar/SidebarChrome.tsx b/apps/web/src/components/sidebar/SidebarChrome.tsx index 6a047624f8210..1a4afacdee7fa 100644 --- a/apps/web/src/components/sidebar/SidebarChrome.tsx +++ b/apps/web/src/components/sidebar/SidebarChrome.tsx @@ -277,7 +277,7 @@ const SidebarUtilityMenu = memo(function SidebarUtilityMenu() { }; return ( - + {isOnUtilityPage ? ( diff --git a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx index 3c18b2c7a927b..cd0db079e9842 100644 --- a/apps/web/src/components/sidebar/SidebarUpdatePill.tsx +++ b/apps/web/src/components/sidebar/SidebarUpdatePill.tsx @@ -432,7 +432,8 @@ function SidebarUpdateControl() { ? { status: "downloading", percent: state.downloadPercent } : { status: "waiting" }; return ( - + // Too narrow for its label beside the footer icons, the capsule wraps onto its own row. + { const explicitHostedAppUrl = process.env.VITE_HOSTED_APP_URL?.trim(); @@ -317,6 +314,7 @@ export default defineConfig(() => { "import.meta.env.T3CODE_DESKTOP_UPDATE_REPOSITORY": JSON.stringify( configuredDesktopUpdateRepository, ), + __T3CODE_BUILD_RELEASE_REPOSITORY__: JSON.stringify(configuredDesktopUpdateRepository), "import.meta.env.T3CODE_FEATURE_SECRETS": JSON.stringify(configuredFeatureSecrets), "import.meta.env.VITE_RELEASE_CHANNEL": JSON.stringify(configuredReleaseChannel), "import.meta.env.APP_VERSION": JSON.stringify(configuredAppVersion), diff --git a/packages/shared/src/cliRelease.test.ts b/packages/shared/src/cliRelease.test.ts index b0aa61765e45a..2cdabde959aea 100644 --- a/packages/shared/src/cliRelease.test.ts +++ b/packages/shared/src/cliRelease.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from "vite-plus/test"; -import { cliArchiveTarCommand, newestCliReleaseVersion, parseChecksums } from "./cliRelease.ts"; +import { + cliArchiveTarCommand, + githubReleaseRepository, + newestCliReleaseVersion, + parseChecksums, +} from "./cliRelease.ts"; describe("cliRelease", () => { it("parses sha256sum output including binary-mode markers", () => { @@ -17,6 +22,13 @@ describe("cliRelease", () => { expect(checksums.size).toBe(2); }); + it("updates from the repository a build was made in, and from upstream without one", () => { + expect(githubReleaseRepository(" fork-owner/t3code ")).toBe("fork-owner/t3code"); + expect(githubReleaseRepository("")).toBe("pingdotgg/t3code"); + expect(githubReleaseRepository(undefined)).toBe("pingdotgg/t3code"); + expect(githubReleaseRepository("fork-owner/../t3code")).toBe("pingdotgg/t3code"); + }); + it("extracts with the System32 bsdtar on Windows and plain tar elsewhere", () => { expect(cliArchiveTarCommand("linux", {})).toBe("tar"); expect(cliArchiveTarCommand("win32", { SystemRoot: "D:\\Win" })).toBe( diff --git a/packages/shared/src/cliRelease.ts b/packages/shared/src/cliRelease.ts index 80eeec4397545..dbedc444d7cca 100644 --- a/packages/shared/src/cliRelease.ts +++ b/packages/shared/src/cliRelease.ts @@ -3,7 +3,29 @@ import * as Schema from "effect/Schema"; import { compareSemverVersions } from "./semver.ts"; -const CLI_RELEASE_REPOSITORY = "pingdotgg/t3code"; +const UPSTREAM_RELEASE_REPOSITORY = "pingdotgg/t3code"; +const GITHUB_REPOSITORY_SEGMENT = /^[A-Za-z0-9._-]+$/u; + +/** `configured` when it is a GitHub `owner/name`, otherwise the upstream repository. */ +export function githubReleaseRepository(configured: string | undefined): string { + const repository = configured?.trim() ?? ""; + const segments = repository.split("/"); + return segments.length === 2 && + segments.every( + (segment) => segment !== "." && segment !== ".." && GITHUB_REPOSITORY_SEGMENT.test(segment), + ) + ? repository + : UPSTREAM_RELEASE_REPOSITORY; +} + +// Server, desktop and web builds bake in the repository they were built in, as the desktop updater +// does, so a fork build updates from its fork's releases. Unset in tests and mobile. +declare const __T3CODE_BUILD_RELEASE_REPOSITORY__: string | undefined; +const CLI_RELEASE_REPOSITORY = githubReleaseRepository( + typeof __T3CODE_BUILD_RELEASE_REPOSITORY__ === "undefined" + ? undefined + : __T3CODE_BUILD_RELEASE_REPOSITORY__, +); export const CLI_RELEASES_PAGE_URL = `https://github.com/${CLI_RELEASE_REPOSITORY}/releases`; export const CLI_RELEASE_CHECKSUMS_FILE = "SHA256SUMS"; export const CLI_RELEASE_BASE_URL_ENV = "T3CODE_RELEASE_BASE_URL"; diff --git a/scripts/lib/public-config.ts b/scripts/lib/public-config.ts index 7fe8d05386cd1..d0b2443de2fc5 100644 --- a/scripts/lib/public-config.ts +++ b/scripts/lib/public-config.ts @@ -101,6 +101,11 @@ export function loadRepoEnv({ }; } +/** The GitHub `owner/name` whose releases this build updates from: CI's own repository unless overridden. */ +export function buildReleaseRepository(env: Environment = process.env): string { + return env.T3CODE_DESKTOP_UPDATE_REPOSITORY?.trim() || env.GITHUB_REPOSITORY?.trim() || ""; +} + function resolvePublicConfig(...sources: readonly Environment[]): T3CodePublicConfig { return { clerkPublishableKey: firstNonEmpty(