authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-05-08 12:47:08-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-08 12:47:08-07:00
logaa8b9f2286fd8c4fcc36a240e34e8e89a3d793fa
tree6473fa52331efef3298e0ac83b301e5093e7d242
parent073eb389d3b831ea3622fbb1a98a9fea6454a6ba
signature Signed by PGP key B5690EEEBB952194

Deploy hosted web app from release workflow (#2507)

Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com>

14 files changed, 348 insertions(+), 44 deletions(-)

.github/workflows/release.yml+86-1
...@@ -567,6 +567,90 @@ jobs:...@@ -567,6 +567,90 @@ jobs:
567 fail_on_unmatched_files: true567 fail_on_unmatched_files: true
568 token: ${{ steps.app_token.outputs.token }}568 token: ${{ steps.app_token.outputs.token }}
569569
570 deploy_web:
571 name: Deploy hosted web app
572 needs: [preflight, release]
573 if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' }}
574 runs-on: blacksmith-8vcpu-ubuntu-2404
575 timeout-minutes: 10
576 env:
577 VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
578 VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
579 VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
580 T3CODE_WEB_ROUTER_URL: ${{ vars.T3CODE_WEB_ROUTER_URL }}
581 T3CODE_WEB_LATEST_DOMAIN: ${{ vars.T3CODE_WEB_LATEST_DOMAIN }}
582 T3CODE_WEB_NIGHTLY_DOMAIN: ${{ vars.T3CODE_WEB_NIGHTLY_DOMAIN }}
583 VERCEL_TEAM_SLUG: ${{ vars.VERCEL_TEAM_SLUG }}
584 steps:
585 - name: Checkout
586 uses: actions/checkout@v6
587 with:
588 ref: ${{ needs.preflight.outputs.ref }}
589
590 - name: Setup Bun
591 uses: oven-sh/setup-bun@v2
592 with:
593 bun-version-file: package.json
594
595 - name: Setup Node
596 uses: actions/setup-node@v6
597 with:
598 node-version-file: package.json
599
600 - name: Install release tooling dependencies
601 run: bun install --frozen-lockfile --filter=@t3tools/scripts --filter=@t3tools/web
602
603 - name: Align package versions to release version
604 run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}"
605
606 - name: Refresh release lockfile
607 run: bun install --lockfile-only --ignore-scripts
608
609 - name: Deploy and alias channel
610 shell: bash
611 run: |
612 set -euo pipefail
613
614 if [[ -z "${VERCEL_TOKEN:-}" || -z "${VERCEL_ORG_ID:-}" || -z "${VERCEL_PROJECT_ID:-}" ]]; then
615 echo "Missing one or more required Vercel secrets: VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID." >&2
616 exit 1
617 fi
618
619 router_url="${T3CODE_WEB_ROUTER_URL:-https://app.t3.codes}"
620 latest_domain="${T3CODE_WEB_LATEST_DOMAIN:-latest.app.t3.codes}"
621 nightly_domain="${T3CODE_WEB_NIGHTLY_DOMAIN:-nightly.app.t3.codes}"
622
623 if [[ "${{ needs.preflight.outputs.release_channel }}" == "stable" ]]; then
624 channel_domain="$latest_domain"
625 channel_name="latest"
626 else
627 channel_domain="$nightly_domain"
628 channel_name="nightly"
629 fi
630
631 vercel_scope_args=()
632 if [[ -n "${VERCEL_TEAM_SLUG:-}" ]]; then
633 vercel_scope_args=(--scope "$VERCEL_TEAM_SLUG")
634 fi
635
636 echo "Deploying hosted web app for $channel_name channel."
637 deployment_url="$(
638 bunx vercel@53.1.1 deploy apps/web \
639 --prod \
640 --skip-domain \
641 --yes \
642 --token "$VERCEL_TOKEN" \
643 "${vercel_scope_args[@]}" \
644 --build-env "APP_VERSION=${{ needs.preflight.outputs.version }}" \
645 --build-env "VITE_HOSTED_APP_URL=$router_url" \
646 --build-env "VITE_HOSTED_APP_CHANNEL=$channel_name"
647 )"
648
649 echo "Aliasing $deployment_url to $channel_domain."
650 bunx vercel@53.1.1 alias set "$deployment_url" "$channel_domain" \
651 --token "$VERCEL_TOKEN" \
652 "${vercel_scope_args[@]}"
653
570 finalize:654 finalize:
571 name: Finalize release655 name: Finalize release
572 if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }}656 if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }}
...@@ -651,8 +735,9 @@ jobs:...@@ -651,8 +735,9 @@ jobs:
651 always() && !cancelled() &&735 always() && !cancelled() &&
652 needs.preflight.result == 'success' &&736 needs.preflight.result == 'success' &&
653 needs.release.result == 'success' &&737 needs.release.result == 'success' &&
738 needs.deploy_web.result == 'success' &&
654 (needs.finalize.result == 'success' || needs.finalize.result == 'skipped')739 (needs.finalize.result == 'success' || needs.finalize.result == 'skipped')
655 needs: [preflight, release, finalize]740 needs: [preflight, release, deploy_web, finalize]
656 runs-on: blacksmith-8vcpu-ubuntu-2404741 runs-on: blacksmith-8vcpu-ubuntu-2404
657 timeout-minutes: 10742 timeout-minutes: 10
658 steps:743 steps:
apps/web/package.json+1
...@@ -50,6 +50,7 @@...@@ -50,6 +50,7 @@
50 "@types/babel__core": "^7.20.5",50 "@types/babel__core": "^7.20.5",
51 "@types/react": "^19.0.0",51 "@types/react": "^19.0.0",
52 "@types/react-dom": "^19.0.0",52 "@types/react-dom": "^19.0.0",
53 "@vercel/config": "^0.3.0",
53 "@vitejs/plugin-react": "^6.0.0",54 "@vitejs/plugin-react": "^6.0.0",
54 "@vitest/browser-playwright": "^4.0.18",55 "@vitest/browser-playwright": "^4.0.18",
55 "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",56 "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",
apps/web/src/branding.test.ts+18
...@@ -34,4 +34,22 @@ describe("branding", () => {...@@ -34,4 +34,22 @@ describe("branding", () => {
34 expect(branding.APP_STAGE_LABEL).toBe("Nightly");34 expect(branding.APP_STAGE_LABEL).toBe("Nightly");
35 expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)");35 expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)");
36 });36 });
37
38 it("normalizes hosted app channel metadata", async () => {
39 vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "nightly");
40
41 const branding = await import("./branding");
42
43 expect(branding.HOSTED_APP_CHANNEL).toBe("nightly");
44 expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Nightly");
45 });
46
47 it("ignores unknown hosted app channels", async () => {
48 vi.stubEnv("VITE_HOSTED_APP_CHANNEL", "preview");
49
50 const branding = await import("./branding");
51
52 expect(branding.HOSTED_APP_CHANNEL).toBeNull();
53 expect(branding.HOSTED_APP_CHANNEL_LABEL).toBeNull();
54 });
37});55});
apps/web/src/branding.ts+5
...@@ -9,6 +9,7 @@ function readInjectedDesktopAppBranding(): DesktopAppBranding | null {...@@ -9,6 +9,7 @@ function readInjectedDesktopAppBranding(): DesktopAppBranding | null {
9}9}
1010
11const injectedDesktopAppBranding = readInjectedDesktopAppBranding();11const injectedDesktopAppBranding = readInjectedDesktopAppBranding();
12const hostedAppChannel = import.meta.env.VITE_HOSTED_APP_CHANNEL?.trim().toLowerCase();
1213
13export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "T3 Code";14export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "T3 Code";
14export const APP_STAGE_LABEL =15export const APP_STAGE_LABEL =
...@@ -16,3 +17,7 @@ export const APP_STAGE_LABEL =...@@ -16,3 +17,7 @@ export const APP_STAGE_LABEL =
16export const APP_DISPLAY_NAME =17export const APP_DISPLAY_NAME =
17 injectedDesktopAppBranding?.displayName ?? `${APP_BASE_NAME} (${APP_STAGE_LABEL})`;18 injectedDesktopAppBranding?.displayName ?? `${APP_BASE_NAME} (${APP_STAGE_LABEL})`;
18export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0";19export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0";
20export const HOSTED_APP_CHANNEL =
21 hostedAppChannel === "latest" || hostedAppChannel === "nightly" ? hostedAppChannel : null;
22export const HOSTED_APP_CHANNEL_LABEL =
23 HOSTED_APP_CHANNEL === "nightly" ? "Nightly" : HOSTED_APP_CHANNEL === "latest" ? "Latest" : null;
apps/web/src/components/settings/SettingsPanels.tsx+62-30
...@@ -15,7 +15,7 @@ import { scopeThreadRef } from "@t3tools/client-runtime";...@@ -15,7 +15,7 @@ import { scopeThreadRef } from "@t3tools/client-runtime";
15import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";15import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";
16import { createModelSelection } from "@t3tools/shared/model";16import { createModelSelection } from "@t3tools/shared/model";
17import * as Equal from "effect/Equal";17import * as Equal from "effect/Equal";
18import { APP_VERSION } from "../../branding";18import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding";
19import {19import {
20 canCheckForUpdate,20 canCheckForUpdate,
21 getDesktopUpdateButtonTooltip,21 getDesktopUpdateButtonTooltip,
...@@ -26,6 +26,7 @@ import {...@@ -26,6 +26,7 @@ import {
26import { ProviderModelPicker } from "../chat/ProviderModelPicker";26import { ProviderModelPicker } from "../chat/ProviderModelPicker";
27import { TraitsPicker } from "../chat/TraitsPicker";27import { TraitsPicker } from "../chat/TraitsPicker";
28import { isElectron } from "../../env";28import { isElectron } from "../../env";
29import { buildHostedChannelSelectionUrl, type HostedAppChannel } from "../../hostedPairing";
29import { useTheme } from "../../hooks/useTheme";30import { useTheme } from "../../hooks/useTheme";
30import { useSettings, useUpdateSettings } from "../../hooks/useSettings";31import { useSettings, useUpdateSettings } from "../../hooks/useSettings";
31import { useThreadActions } from "../../hooks/useThreadActions";32import { useThreadActions } from "../../hooks/useThreadActions";
...@@ -162,6 +163,7 @@ function AboutVersionSection() {...@@ -162,6 +163,7 @@ function AboutVersionSection() {
162 const updateState = updateStateQuery.data ?? null;163 const updateState = updateStateQuery.data ?? null;
163 const hasDesktopBridge = typeof window !== "undefined" && Boolean(window.desktopBridge);164 const hasDesktopBridge = typeof window !== "undefined" && Boolean(window.desktopBridge);
164 const selectedUpdateChannel = updateState?.channel ?? "latest";165 const selectedUpdateChannel = updateState?.channel ?? "latest";
166 const selectedHostedAppChannel = hasDesktopBridge ? null : HOSTED_APP_CHANNEL;
165167
166 const handleUpdateChannelChange = useCallback(168 const handleUpdateChannelChange = useCallback(
167 (channel: DesktopUpdateChannel) => {169 (channel: DesktopUpdateChannel) => {
...@@ -314,36 +316,66 @@ function AboutVersionSection() {...@@ -314,36 +316,66 @@ function AboutVersionSection() {
314 </Tooltip>316 </Tooltip>
315 }317 }
316 />318 />
317 <SettingsRow319 {hasDesktopBridge ? (
318 title="Update track"320 <SettingsRow
319 description="Stable follows full releases. Nightly follows the nightly desktop channel and can switch back to stable immediately."321 title="Update track"
320 control={322 description="Stable follows full releases. Nightly follows the nightly desktop channel and can switch back to stable immediately."
321 <Select323 control={
322 value={selectedUpdateChannel}324 <Select
323 onValueChange={(value) => {325 value={selectedUpdateChannel}
324 handleUpdateChannelChange(value as DesktopUpdateChannel);326 onValueChange={(value) => {
325 }}327 handleUpdateChannelChange(value as DesktopUpdateChannel);
326 >328 }}
327 <SelectTrigger
328 className="w-full sm:w-40"
329 aria-label="Update track"
330 disabled={!hasDesktopBridge || isChangingUpdateChannel}
331 >329 >
332 <SelectValue>330 <SelectTrigger
333 {selectedUpdateChannel === "nightly" ? "Nightly" : "Stable"}331 className="w-full sm:w-40"
334 </SelectValue>332 aria-label="Update track"
335 </SelectTrigger>333 disabled={isChangingUpdateChannel}
336 <SelectPopup align="end" alignItemWithTrigger={false}>334 >
337 <SelectItem hideIndicator value="latest">335 <SelectValue>
338 Stable336 {selectedUpdateChannel === "nightly" ? "Nightly" : "Stable"}
339 </SelectItem>337 </SelectValue>
340 <SelectItem hideIndicator value="nightly">338 </SelectTrigger>
341 Nightly339 <SelectPopup align="end" alignItemWithTrigger={false}>
342 </SelectItem>340 <SelectItem hideIndicator value="latest">
343 </SelectPopup>341 Stable
344 </Select>342 </SelectItem>
345 }343 <SelectItem hideIndicator value="nightly">
346 />344 Nightly
345 </SelectItem>
346 </SelectPopup>
347 </Select>
348 }
349 />
350 ) : selectedHostedAppChannel ? (
351 <SettingsRow
352 title="Update track"
353 description="Switches the hosted app release channel."
354 control={
355 <Select
356 value={selectedHostedAppChannel}
357 onValueChange={(value) => {
358 if (value === selectedHostedAppChannel) return;
359 window.location.assign(
360 buildHostedChannelSelectionUrl({ channel: value as HostedAppChannel }),
361 );
362 }}
363 >
364 <SelectTrigger className="w-full sm:w-40" aria-label="Update track">
365 <SelectValue>{HOSTED_APP_CHANNEL_LABEL}</SelectValue>
366 </SelectTrigger>
367 <SelectPopup align="end" alignItemWithTrigger={false}>
368 <SelectItem hideIndicator value="latest">
369 Latest
370 </SelectItem>
371 <SelectItem hideIndicator value="nightly">
372 Nightly
373 </SelectItem>
374 </SelectPopup>
375 </Select>
376 }
377 />
378 ) : null}
347 </>379 </>
348 );380 );
349}381}
apps/web/src/hostedPairing.test.ts+16
...@@ -1,6 +1,7 @@...@@ -1,6 +1,7 @@
1import { afterEach, describe, expect, it, vi } from "vitest";1import { afterEach, describe, expect, it, vi } from "vitest";
22
3import {3import {
4 buildHostedChannelSelectionUrl,
4 buildHostedPairingUrl,5 buildHostedPairingUrl,
5 hasHostedPairingRequest,6 hasHostedPairingRequest,
6 isHostedStaticApp,7 isHostedStaticApp,
...@@ -42,6 +43,21 @@ describe("hostedPairing", () => {...@@ -42,6 +43,21 @@ describe("hostedPairing", () => {
42 expect(url.hash).toBe("#token=pairing-token");43 expect(url.hash).toBe("#token=pairing-token");
43 });44 });
4445
46 it("builds hosted channel selection URLs through the configured router origin", () => {
47 vi.stubEnv("VITE_HOSTED_APP_URL", "https://app.t3.codes");
48
49 const url = new URL(
50 buildHostedChannelSelectionUrl({
51 channel: "nightly",
52 }),
53 );
54
55 expect(url.origin).toBe("https://app.t3.codes");
56 expect(url.pathname).toBe("/__t3code/channel");
57 expect(url.searchParams.get("channel")).toBe("nightly");
58 expect(url.searchParams.has("next")).toBe(false);
59 });
60
45 it("ignores incomplete hosted pairing requests", () => {61 it("ignores incomplete hosted pairing requests", () => {
46 expect(62 expect(
47 hasHostedPairingRequest(new URL("https://app.t3.codes/pair?host=backend.example.com")),63 hasHostedPairingRequest(new URL("https://app.t3.codes/pair?host=backend.example.com")),
apps/web/src/hostedPairing.ts+10
...@@ -8,6 +8,8 @@ export interface HostedPairingRequest {...@@ -8,6 +8,8 @@ export interface HostedPairingRequest {
8 readonly label: string;8 readonly label: string;
9}9}
1010
11export type HostedAppChannel = "latest" | "nightly";
12
11function configuredHostedAppUrl(): string {13function configuredHostedAppUrl(): string {
12 return import.meta.env.VITE_HOSTED_APP_URL?.trim() || DEFAULT_HOSTED_APP_URL;14 return import.meta.env.VITE_HOSTED_APP_URL?.trim() || DEFAULT_HOSTED_APP_URL;
13}15}
...@@ -68,3 +70,11 @@ export function buildHostedPairingUrl(input: {...@@ -68,3 +70,11 @@ export function buildHostedPairingUrl(input: {
6870
69 return setPairingTokenOnUrl(url, input.token).toString();71 return setPairingTokenOnUrl(url, input.token).toString();
70}72}
73
74export function buildHostedChannelSelectionUrl(input: {
75 readonly channel: HostedAppChannel;
76}): string {
77 const url = new URL("/__t3code/channel", configuredHostedAppUrl());
78 url.searchParams.set("channel", input.channel);
79 return url.toString();
80}
apps/web/src/vite-env.d.ts+1
...@@ -6,6 +6,7 @@ interface ImportMetaEnv {...@@ -6,6 +6,7 @@ interface ImportMetaEnv {
6 readonly VITE_HTTP_URL: string;6 readonly VITE_HTTP_URL: string;
7 readonly VITE_WS_URL: string;7 readonly VITE_WS_URL: string;
8 readonly VITE_HOSTED_APP_URL: string;8 readonly VITE_HOSTED_APP_URL: string;
9 readonly VITE_HOSTED_APP_CHANNEL: string;
9 readonly APP_VERSION: string;10 readonly APP_VERSION: string;
10}11}
1112
apps/web/tsconfig.json+1-1
...@@ -35,5 +35,5 @@...@@ -35,5 +35,5 @@
35 }35 }
36 ]36 ]
37 },37 },
38 "include": ["src", "vite.config.ts", "test"]38 "include": ["src", "vite.config.ts", "vercel.ts", "test"]
39}39}
apps/web/vercel.json deleted-10
...@@ -1,10 +0,0 @@
1{
2 "buildCommand": "turbo build --filter @t3tools/web && bun ../../scripts/apply-web-brand-assets.ts production",
3 "installCommand": "bun add -g turbo && bun install --filter '@t3tools/contracts' --filter '@t3tools/client-runtime' --filter '@t3tools/scripts' --filter '@t3tools/web'",
4 "rewrites": [
5 {
6 "source": "/(.*)",
7 "destination": "/index.html"
8 }
9 ]
10}
apps/web/vercel.ts created+57
...@@ -0,0 +1,57 @@
1import { matchers, routes, type VercelConfig } from "@vercel/config/v1";
2
3const ROUTER_HOST = "app.t3.codes";
4const HOSTED_WEB_CHANNEL_COOKIE = "t3code_web_channel";
5const LATEST_ORIGIN = "https://latest.app.t3.codes";
6const NIGHTLY_ORIGIN = "https://nightly.app.t3.codes";
7
8function channelCookie(channel: "latest" | "nightly"): string {
9 return [
10 `${HOSTED_WEB_CHANNEL_COOKIE}=${channel}`,
11 "Path=/",
12 "Max-Age=31536000",
13 "HttpOnly",
14 "Secure",
15 "SameSite=Lax",
16 ].join("; ");
17}
18
19export const config: VercelConfig = {
20 buildCommand:
21 "turbo build --filter @t3tools/web && bun ../../scripts/apply-web-brand-assets.ts production",
22 git: {
23 deploymentEnabled: false,
24 },
25 installCommand:
26 "bun add -g turbo && bun install --filter '@t3tools/contracts' --filter '@t3tools/client-runtime' --filter '@t3tools/scripts' --filter '@t3tools/web'",
27 routes: [
28 {
29 src: "/__t3code/channel",
30 has: [matchers.query("channel", "nightly")],
31 headers: {
32 Location: "/",
33 "Set-Cookie": channelCookie("nightly"),
34 },
35 status: 302,
36 },
37 {
38 src: "/__t3code/channel",
39 headers: {
40 Location: "/",
41 "Set-Cookie": channelCookie("latest"),
42 },
43 status: 302,
44 },
45 {
46 src: "/(.*)",
47 has: [matchers.host(ROUTER_HOST), matchers.cookie(HOSTED_WEB_CHANNEL_COOKIE, "nightly")],
48 dest: `${NIGHTLY_ORIGIN}/$1`,
49 },
50 {
51 src: "/(.*)",
52 has: [matchers.host(ROUTER_HOST)],
53 dest: `${LATEST_ORIGIN}/$1`,
54 },
55 ],
56 rewrites: [routes.rewrite("/(.*)", "/index.html")],
57};
apps/web/vite.config.ts+9-2
...@@ -8,14 +8,20 @@ import pkg from "./package.json" with { type: "json" };...@@ -8,14 +8,20 @@ import pkg from "./package.json" with { type: "json" };
8const port = Number(process.env.PORT ?? 5733);8const port = Number(process.env.PORT ?? 5733);
9const host = process.env.HOST?.trim() || "localhost";9const host = process.env.HOST?.trim() || "localhost";
10const configuredWsUrl = process.env.VITE_WS_URL?.trim();10const configuredWsUrl = process.env.VITE_WS_URL?.trim();
11const configuredHostedAppChannel = process.env.VITE_HOSTED_APP_CHANNEL?.trim() || "";
12const configuredAppVersion = process.env.APP_VERSION?.trim() || pkg.version;
11const configuredHostedAppUrl = (() => {13const configuredHostedAppUrl = (() => {
14 const explicitHostedAppUrl = process.env.VITE_HOSTED_APP_URL?.trim();
15 if (explicitHostedAppUrl) {
16 return explicitHostedAppUrl;
17 }
12 if (process.env.VERCEL_ENV === "production" && process.env.VERCEL_PROJECT_PRODUCTION_URL) {18 if (process.env.VERCEL_ENV === "production" && process.env.VERCEL_PROJECT_PRODUCTION_URL) {
13 return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;19 return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`;
14 }20 }
15 if (process.env.VERCEL_URL) {21 if (process.env.VERCEL_URL) {
16 return `https://${process.env.VERCEL_URL}`;22 return `https://${process.env.VERCEL_URL}`;
17 }23 }
18 return process.env.VITE_HOSTED_APP_URL?.trim();24 return undefined;
19})();25})();
20const sourcemapEnv = process.env.T3CODE_WEB_SOURCEMAP?.trim().toLowerCase();26const sourcemapEnv = process.env.T3CODE_WEB_SOURCEMAP?.trim().toLowerCase();
2127
...@@ -76,7 +82,8 @@ export default defineConfig({...@@ -76,7 +82,8 @@ export default defineConfig({
76 // In dev mode, tell the web app where the WebSocket server lives82 // In dev mode, tell the web app where the WebSocket server lives
77 "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""),83 "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""),
78 "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""),84 "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""),
79 "import.meta.env.APP_VERSION": JSON.stringify(pkg.version),85 "import.meta.env.VITE_HOSTED_APP_CHANNEL": JSON.stringify(configuredHostedAppChannel),
86 "import.meta.env.APP_VERSION": JSON.stringify(configuredAppVersion),
80 },87 },
81 resolve: {88 resolve: {
82 tsconfigPaths: true,89 tsconfigPaths: true,
bun.lock+29
...@@ -124,6 +124,7 @@...@@ -124,6 +124,7 @@
124 "@types/babel__core": "^7.20.5",124 "@types/babel__core": "^7.20.5",
125 "@types/react": "^19.0.0",125 "@types/react": "^19.0.0",
126 "@types/react-dom": "^19.0.0",126 "@types/react-dom": "^19.0.0",
127 "@vercel/config": "^0.3.0",
127 "@vitejs/plugin-react": "^6.0.0",128 "@vitejs/plugin-react": "^6.0.0",
128 "@vitest/browser-playwright": "^4.0.18",129 "@vitest/browser-playwright": "^4.0.18",
129 "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",130 "babel-plugin-react-compiler": "^19.0.0-beta-e552027-20250112",
...@@ -927,6 +928,14 @@...@@ -927,6 +928,14 @@
927928
928 "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],929 "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
929930
931 "@vercel/config": ["@vercel/config@0.3.0", "", { "dependencies": { "@vercel/routing-utils": "6.2.0", "pretty-cache-header": "^1.0.0", "zod": "^3.22.0" }, "bin": { "config": "dist/cli.js" } }, "sha512-Tf5k5y2F478oTiQcU5R8Ntix1UejE6NdduZnI7aa1XXxjCtifX1XdRS/D2uTjiQAwIL3pLa1LSAN80ABbba+TQ=="],
932
933 "@vercel/functions": ["@vercel/functions@3.5.0", "", { "dependencies": { "@vercel/oidc": "3.4.0" }, "peerDependencies": { "@aws-sdk/credential-provider-web-identity": "*" }, "optionalPeers": ["@aws-sdk/credential-provider-web-identity"] }, "sha512-+RokZ+4gkYyOsKBuJ29cQ8iSZG123LLJbZfPry20kkTgrN9U0277La4feP4DnWVo3sGoYa4plCEKY9XKUYoX9g=="],
934
935 "@vercel/oidc": ["@vercel/oidc@3.4.0", "", {}, "sha512-p0sKfHkfRmMaqqDwNL4tjnX9TgRrLMlEtUjIxfrEns8pOxz1R9ztqOVI+ehqiq93/2/HnfPe/UBZkfAZwnx0UA=="],
936
937 "@vercel/routing-utils": ["@vercel/routing-utils@6.2.0", "", { "dependencies": { "path-to-regexp": "6.1.0", "path-to-regexp-updated": "npm:path-to-regexp@6.3.0" }, "optionalDependencies": { "ajv": "^6.12.3" } }, "sha512-YI2cGYZmJKEyGSEgf5Fw5rVuW7X1bTOQ7/pnLRNTFMH3YB3qqP5erCLCJGv2kIvNo1iQAPINHQRJj6ykEdGoPg=="],
938
930 "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],939 "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
931940
932 "@vitest/browser": ["@vitest/browser@4.1.0", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.0.3", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.0" } }, "sha512-tG/iOrgbiHQks0ew7CdelUyNEHkv8NLrt+CqdTivIuoSnXvO7scWMn4Kqo78/UGY1NJ6Hv+vp8BvRnED/bjFdQ=="],941 "@vitest/browser": ["@vitest/browser@4.1.0", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.0.3", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.0" } }, "sha512-tG/iOrgbiHQks0ew7CdelUyNEHkv8NLrt+CqdTivIuoSnXvO7scWMn4Kqo78/UGY1NJ6Hv+vp8BvRnED/bjFdQ=="],
...@@ -1241,6 +1250,8 @@...@@ -1241,6 +1250,8 @@
12411250
1242 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],1251 "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
12431252
1253 "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
1254
1244 "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],1255 "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
12451256
1246 "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],1257 "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
...@@ -1685,6 +1696,8 @@...@@ -1685,6 +1696,8 @@
16851696
1686 "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],1697 "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
16871698
1699 "path-to-regexp-updated": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
1700
1688 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],1701 "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
16891702
1690 "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],1703 "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
...@@ -1707,6 +1720,8 @@...@@ -1707,6 +1720,8 @@
17071720
1708 "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],1721 "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
17091722
1723 "pretty-cache-header": ["pretty-cache-header@1.0.0", "", { "dependencies": { "timestring": "^6.0.0" } }, "sha512-xtXazslu25CdnGnUkByU1RoOjK55TqwatJkjjJLg5ZAdz2Lngko/mmaUgeET36P2GMlNwh3fdM7FWBO717pNcw=="],
1724
1710 "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],1725 "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="],
17111726
1712 "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],1727 "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
...@@ -1717,6 +1732,8 @@...@@ -1717,6 +1732,8 @@
17171732
1718 "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],1733 "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
17191734
1735 "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
1736
1720 "pure-rand": ["pure-rand@8.1.0", "", {}, "sha512-53B3MB8wetRdD6JZ4W/0gDKaOvKwuXrEmV1auQc0hASWge8rieKV4PCCVNVbJ+i24miiubb4c/B+dg8Ho0ikYw=="],1737 "pure-rand": ["pure-rand@8.1.0", "", {}, "sha512-53B3MB8wetRdD6JZ4W/0gDKaOvKwuXrEmV1auQc0hASWge8rieKV4PCCVNVbJ+i24miiubb4c/B+dg8Ho0ikYw=="],
17211738
1722 "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],1739 "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="],
...@@ -1897,6 +1914,8 @@...@@ -1897,6 +1914,8 @@
18971914
1898 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],1915 "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
18991916
1917 "timestring": ["timestring@6.0.0", "", {}, "sha512-wMctrWD2HZZLuIlchlkE2dfXJh7J2KDI9Dwl+2abPYg0mswQHfOAyQW3jJg1pY5VfttSINZuKcXoB3FGypVklA=="],
1918
1900 "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],1919 "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="],
19011920
1902 "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],1921 "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
...@@ -2019,6 +2038,8 @@...@@ -2019,6 +2038,8 @@
20192038
2020 "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],2039 "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
20212040
2041 "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
2042
2022 "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],2043 "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
20232044
2024 "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],2045 "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="],
...@@ -2193,6 +2214,12 @@...@@ -2193,6 +2214,12 @@
21932214
2194 "@tanstack/router-utils/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],2215 "@tanstack/router-utils/@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
21952216
2217 "@vercel/config/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
2218
2219 "@vercel/routing-utils/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
2220
2221 "@vercel/routing-utils/path-to-regexp": ["path-to-regexp@6.1.0", "", {}, "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw=="],
2222
2196 "@vitest/browser/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],2223 "@vitest/browser/ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
21972224
2198 "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],2225 "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
...@@ -2317,6 +2344,8 @@...@@ -2317,6 +2344,8 @@
23172344
2318 "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],2345 "@tanstack/router-plugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
23192346
2347 "@vercel/routing-utils/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
2348
2320 "ast-kit/@babel/parser/@babel/types": ["@babel/types@8.0.0-rc.2", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.2", "@babel/helper-validator-identifier": "^8.0.0-rc.2" } }, "sha512-91gAaWRznDwSX4E2tZ1YjBuIfnQVOFDCQ2r0Toby0gu4XEbyF623kXLMA8d4ZbCu+fINcrudkmEcwSUHgDDkNw=="],2349 "ast-kit/@babel/parser/@babel/types": ["@babel/types@8.0.0-rc.2", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.2", "@babel/helper-validator-identifier": "^8.0.0-rc.2" } }, "sha512-91gAaWRznDwSX4E2tZ1YjBuIfnQVOFDCQ2r0Toby0gu4XEbyF623kXLMA8d4ZbCu+fINcrudkmEcwSUHgDDkNw=="],
23212350
2322 "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],2351 "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="],
docs/release.md+53
...@@ -24,8 +24,61 @@ This document covers the unified release workflow for stable and nightly desktop...@@ -24,8 +24,61 @@ This document covers the unified release workflow for stable and nightly desktop
24- Publishes the CLI package (`apps/server`, npm package `t3`) with OIDC trusted publishing from the same workflow file:24- Publishes the CLI package (`apps/server`, npm package `t3`) with OIDC trusted publishing from the same workflow file:
25 - stable releases publish npm dist-tag `latest`25 - stable releases publish npm dist-tag `latest`
26 - nightly releases publish npm dist-tag `nightly`26 - nightly releases publish npm dist-tag `nightly`
27- Deploys the hosted web app to Vercel only after a release is published:
28 - stable releases are aliased to the `latest` hosted app channel
29 - nightly releases are aliased to the `nightly` hosted app channel
27- Signing is optional and auto-detected per platform from secrets.30- Signing is optional and auto-detected per platform from secrets.
2831
32## Hosted web app release deployment
33
34The hosted app is intentionally not deployed by Vercel's Git integration. The
35web project disables automatic Git deployments in `apps/web/vercel.ts` via
36`git.deploymentEnabled: false`, and `.github/workflows/release.yml` deploys the
37web app with Vercel CLI after the GitHub Release succeeds.
38
39Required GitHub Actions secrets:
40
41- `VERCEL_TOKEN`
42- `VERCEL_ORG_ID`
43- `VERCEL_PROJECT_ID`
44
45Optional GitHub Actions variables:
46
47- `VERCEL_TEAM_SLUG`: required only if the token needs an explicit Vercel team scope.
48- `T3CODE_WEB_ROUTER_URL`: defaults to `https://app.t3.codes`.
49- `T3CODE_WEB_LATEST_DOMAIN`: defaults to `latest.app.t3.codes`.
50- `T3CODE_WEB_NIGHTLY_DOMAIN`: defaults to `nightly.app.t3.codes`.
51
52Required Vercel domains:
53
54- `app.t3.codes`: the stable router domain users open.
55- `latest.app.t3.codes`: channel alias updated by stable releases.
56- `nightly.app.t3.codes`: channel alias updated by nightly releases.
57
58The router domain uses `apps/web/vercel.ts` routes. Users opt into a channel by
59visiting `/__t3code/channel?channel=latest` or
60`/__t3code/channel?channel=nightly`; the router stores the
61`t3code_web_channel` cookie and rewrites future requests on `app.t3.codes` to
62the matching channel alias.
63
64The release deploy job rewrites release package versions before upload so the
65hosted app's About panel renders the release version. It also passes
66`VITE_HOSTED_APP_CHANNEL=latest|nightly`, which renders the hosted update track
67selector in the About panel. Changing the selector navigates through
68`/__t3code/channel` on the router domain so the user's channel cookie is updated
69before redirecting to the hosted app root.
70
71One-time Vercel dashboard setup:
72
731. Confirm the web project root directory remains `apps/web`.
742. Add the three domains above to the web project.
753. Disable automatic Git deployments in the dashboard if desired; the committed
76 `vercel.ts` setting is the source-of-truth, but disconnecting Git in the
77 dashboard is also safe.
784. Promote or alias one deployment containing the router rules in `apps/web/vercel.ts` to
79 `app.t3.codes` once. Future release jobs should only update the channel
80 aliases.
81
29## Nightly builds82## Nightly builds
3083
31- Workflow: `.github/workflows/release.yml`84- Workflow: `.github/workflows/release.yml`