Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions apps/web/src/components/desktopUpdate.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,37 @@ describe("getDesktopUpdateActionError", () => {

describe("desktop update UI helpers", () => {
it("builds the stable release URL for a downloaded version", () => {
expect(getDesktopUpdateReleaseUrl("0.0.30")).toBe(
expect(getDesktopUpdateReleaseUrl("0.0.30", "pingdotgg/t3code")).toBe(
"https://github.com/pingdotgg/t3code/releases/tag/v0.0.30",
);
});

it("uses the configured desktop update repository for release URLs", () => {
expect(getDesktopUpdateReleaseUrl("1.2.3", "JustYannicc/t3code")).toBe(
"https://github.com/JustYannicc/t3code/releases/tag/v1.2.3",
);
expect(getDesktopUpdateReleaseHistoryUrl("JustYannicc/t3code")).toBe(
"https://github.com/JustYannicc/t3code/releases",
);
});

it.each([
"owner/repo/extra",
"owner#notes/repo",
"owner/repo#notes",
"owner?tab=notes/repo",
"owner/repo?tab=notes",
])("falls back to upstream for an invalid configured repository: %s", (repository) => {
expect(getDesktopUpdateReleaseHistoryUrl(repository)).toBe(
"https://github.com/pingdotgg/t3code/releases",
);
expect(getDesktopUpdateReleaseUrl("1.2.3", repository)).toBe(
"https://github.com/pingdotgg/t3code/releases/tag/v1.2.3",
);
});

it("builds the nightly release URL without dropping its version suffix", () => {
expect(getDesktopUpdateReleaseUrl("0.0.30-nightly.20260728.931")).toBe(
expect(getDesktopUpdateReleaseUrl("0.0.30-nightly.20260728.931", "pingdotgg/t3code")).toBe(
"https://github.com/pingdotgg/t3code/releases/tag/v0.0.30-nightly.20260728.931",
);
});
Expand All @@ -218,7 +242,7 @@ describe("desktop update UI helpers", () => {
});

it("builds the release history URL", () => {
expect(getDesktopUpdateReleaseHistoryUrl()).toBe(
expect(getDesktopUpdateReleaseHistoryUrl("pingdotgg/t3code")).toBe(
"https://github.com/pingdotgg/t3code/releases",
);
});
Expand Down
33 changes: 27 additions & 6 deletions apps/web/src/components/desktopUpdate.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,24 @@ import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/con

export type DesktopUpdateButtonAction = "download" | "install" | "none";

const DESKTOP_RELEASE_HISTORY_URL = "https://github.com/pingdotgg/t3code/releases";
const DESKTOP_RELEASE_TAG_URL = `${DESKTOP_RELEASE_HISTORY_URL}/tag`;
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`;
}

/**
* The main process fills `downloadedVersion` from the updater's `update-downloaded`
Expand All @@ -14,14 +30,19 @@ export function getDesktopUpdateDownloadedVersion(state: DesktopUpdateState): st
return state.downloadedVersion ?? state.availableVersion;
}

export function getDesktopUpdateReleaseUrl(version: string | null): string | null {
export function getDesktopUpdateReleaseUrl(
version: string | null,
repository: string | undefined = import.meta.env.T3CODE_DESKTOP_UPDATE_REPOSITORY,
): string | null {
const normalizedVersion = version?.trim();
if (!normalizedVersion) return null;
return `${DESKTOP_RELEASE_TAG_URL}/v${encodeURIComponent(normalizedVersion)}`;
return `${desktopReleaseRepositoryUrl(repository)}/tag/v${encodeURIComponent(normalizedVersion)}`;
}

export function getDesktopUpdateReleaseHistoryUrl(): string {
return DESKTOP_RELEASE_HISTORY_URL;
export function getDesktopUpdateReleaseHistoryUrl(
repository: string | undefined = import.meta.env.T3CODE_DESKTOP_UPDATE_REPOSITORY,
): string {
return desktopReleaseRepositoryUrl(repository);
}

export function resolveDesktopUpdateButtonAction(
Expand Down
20 changes: 19 additions & 1 deletion apps/web/src/components/desktopUpdate.toast.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isValidElement, type ReactElement, type ReactNode } from "react";
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
import type { DesktopUpdateState } from "@t3tools/contracts";

const testState = vi.hoisted(() => ({
Expand Down Expand Up @@ -64,6 +64,24 @@ describe("showDesktopUpdateDownloadedToast", () => {
testState.addToast.mockReset();
});

afterEach(() => {
vi.unstubAllEnvs();
});

it("opens release notes from the configured repository", async () => {
vi.stubEnv("T3CODE_DESKTOP_UPDATE_REPOSITORY", "JustYannicc/t3code");
const openExternal = vi.fn().mockResolvedValue(true);

showDesktopUpdateDownloadedToast({ openExternal }, downloadedState());
findReleaseNotesLink(getDescription())?.props.onClick?.();

await vi.waitFor(() => {
expect(openExternal).toHaveBeenCalledWith(
"https://github.com/JustYannicc/t3code/releases/tag/v0.0.30",
);
});
});

it("opens the downloaded version's release notes", async () => {
const openExternal = vi.fn().mockResolvedValue(true);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { DesktopUpdateState } from "@t3tools/contracts";
import { isValidElement, type MouseEvent, type ReactElement, type ReactNode } from "react";
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";

const testState = vi.hoisted(() => ({
addToast: vi.fn(),
Expand Down Expand Up @@ -74,6 +74,26 @@ describe("SidebarUpdateReleaseNotes", () => {
testState.addToast.mockReset();
});

afterEach(() => {
vi.unstubAllEnvs();
});

it("uses the configured repository for release links", () => {
vi.stubEnv("T3CODE_DESKTOP_UPDATE_REPOSITORY", "JustYannicc/t3code");
const anchors = collectAnchors(
renderNotes({
...baseState,
releaseNotes: [{ version: "0.0.36-nightly.3", items: ["Change 3"], totalItems: 1 }],
omittedReleaseCount: 1,
}),
);

expect(anchors.map(({ props }) => props.href)).toEqual([
"https://github.com/JustYannicc/t3code/releases/tag/v0.0.36-nightly.3",
"https://github.com/JustYannicc/t3code/releases",
]);
});

it("links each preview to its exact release and labels hidden changes", () => {
const anchors = collectAnchors(
renderNotes({
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/vite-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface ImportMetaEnv {
readonly VITE_RELAY_OTLP_TRACES_URL: string;
readonly VITE_RELAY_OTLP_TRACES_DATASET: string;
readonly VITE_RELAY_OTLP_TRACES_TOKEN: string;
readonly T3CODE_DESKTOP_UPDATE_REPOSITORY: string;
readonly APP_VERSION: string;
}

Expand Down
7 changes: 7 additions & 0 deletions apps/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ const configuredRelayTracingDataset = repoEnv.VITE_RELAY_OTLP_TRACES_DATASET?.tr
const configuredRelayTracingToken = repoEnv.VITE_RELAY_OTLP_TRACES_TOKEN?.trim() || "";
const configuredHostedAppChannel = process.env.VITE_HOSTED_APP_CHANNEL?.trim() || "";
const configuredAppVersion = process.env.APP_VERSION?.trim() || pkg.version;
const configuredDesktopUpdateRepository =
process.env.T3CODE_DESKTOP_UPDATE_REPOSITORY?.trim() ||
process.env.GITHUB_REPOSITORY?.trim() ||
"";
const configuredHostedAppUrl = (() => {
const explicitHostedAppUrl = process.env.VITE_HOSTED_APP_URL?.trim();
if (explicitHostedAppUrl) {
Expand Down Expand Up @@ -215,6 +219,9 @@ export default defineConfig(() => {
"import.meta.env.VITE_RELAY_OTLP_TRACES_TOKEN": JSON.stringify(configuredRelayTracingToken),
"import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""),
"import.meta.env.VITE_HOSTED_APP_CHANNEL": JSON.stringify(configuredHostedAppChannel),
"import.meta.env.T3CODE_DESKTOP_UPDATE_REPOSITORY": JSON.stringify(
configuredDesktopUpdateRepository,
),
"import.meta.env.APP_VERSION": JSON.stringify(configuredAppVersion),
},
resolve: {
Expand Down
Loading