authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-05-11 01:41:32-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-11 10:41:32+02:00
loge64c19f1af672e4f1737d81fbe83906de3547f03
treeb118fbbcd2d140547ec93b1c3a88bc40dd06161c
parent6ab8f93a23465d0d92e55d2a9173307fa2295d9c
signature Signed by PGP key B5690EEEBB952194

Alias stable releases to the router domain (#2636)


6 files changed, 41 insertions(+), 16 deletions(-)

.github/workflows/release.yml+10
...@@ -625,6 +625,9 @@ jobs:...@@ -625,6 +625,9 @@ jobs:
625 router_url="${T3CODE_WEB_ROUTER_URL:-https://app.t3.codes}"625 router_url="${T3CODE_WEB_ROUTER_URL:-https://app.t3.codes}"
626 latest_domain="${T3CODE_WEB_LATEST_DOMAIN:-latest.app.t3.codes}"626 latest_domain="${T3CODE_WEB_LATEST_DOMAIN:-latest.app.t3.codes}"
627 nightly_domain="${T3CODE_WEB_NIGHTLY_DOMAIN:-nightly.app.t3.codes}"627 nightly_domain="${T3CODE_WEB_NIGHTLY_DOMAIN:-nightly.app.t3.codes}"
628 router_domain="${router_url#http://}"
629 router_domain="${router_domain#https://}"
630 router_domain="${router_domain%%/*}"
628631
629 if [[ "${{ needs.preflight.outputs.release_channel }}" == "stable" ]]; then632 if [[ "${{ needs.preflight.outputs.release_channel }}" == "stable" ]]; then
630 channel_domain="$latest_domain"633 channel_domain="$latest_domain"
...@@ -655,6 +658,13 @@ jobs:...@@ -655,6 +658,13 @@ jobs:
655 --token "$VERCEL_TOKEN" \658 --token "$VERCEL_TOKEN" \
656 "${vercel_scope_args[@]}"659 "${vercel_scope_args[@]}"
657660
661 if [[ "$channel_name" == "latest" && -n "$router_domain" && "$router_domain" != "$channel_domain" ]]; then
662 echo "Aliasing $deployment_url to router domain $router_domain."
663 bunx vercel@53.1.1 alias set "$deployment_url" "$router_domain" \
664 --token "$VERCEL_TOKEN" \
665 "${vercel_scope_args[@]}"
666 fi
667
658 finalize:668 finalize:
659 name: Finalize release669 name: Finalize release
660 if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }}670 if: ${{ !failure() && !cancelled() && needs.preflight.result == 'success' && needs.release.result == 'success' && needs.preflight.outputs.release_channel == 'stable' }}
apps/web/src/branding.test.ts+2
...@@ -42,6 +42,8 @@ describe("branding", () => {...@@ -42,6 +42,8 @@ describe("branding", () => {
4242
43 expect(branding.HOSTED_APP_CHANNEL).toBe("nightly");43 expect(branding.HOSTED_APP_CHANNEL).toBe("nightly");
44 expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Nightly");44 expect(branding.HOSTED_APP_CHANNEL_LABEL).toBe("Nightly");
45 expect(branding.APP_STAGE_LABEL).toBe("Nightly");
46 expect(branding.APP_DISPLAY_NAME).toBe("T3 Code (Nightly)");
45 });47 });
4648
47 it("ignores unknown hosted app channels", async () => {49 it("ignores unknown hosted app channels", async () => {
apps/web/src/branding.ts+7-5
...@@ -11,13 +11,15 @@ function readInjectedDesktopAppBranding(): DesktopAppBranding | null {...@@ -11,13 +11,15 @@ function readInjectedDesktopAppBranding(): DesktopAppBranding | null {
11const injectedDesktopAppBranding = readInjectedDesktopAppBranding();11const injectedDesktopAppBranding = readInjectedDesktopAppBranding();
12const hostedAppChannel = import.meta.env.VITE_HOSTED_APP_CHANNEL?.trim().toLowerCase();12const hostedAppChannel = import.meta.env.VITE_HOSTED_APP_CHANNEL?.trim().toLowerCase();
1313
14export const HOSTED_APP_CHANNEL =
15 hostedAppChannel === "latest" || hostedAppChannel === "nightly" ? hostedAppChannel : null;
16export const HOSTED_APP_CHANNEL_LABEL =
17 HOSTED_APP_CHANNEL === "nightly" ? "Nightly" : HOSTED_APP_CHANNEL === "latest" ? "Latest" : null;
14export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "T3 Code";18export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "T3 Code";
15export const APP_STAGE_LABEL =19export const APP_STAGE_LABEL =
16 injectedDesktopAppBranding?.stageLabel ?? (import.meta.env.DEV ? "Dev" : "Alpha");20 injectedDesktopAppBranding?.stageLabel ??
21 HOSTED_APP_CHANNEL_LABEL ??
22 (import.meta.env.DEV ? "Dev" : "Alpha");
17export const APP_DISPLAY_NAME =23export const APP_DISPLAY_NAME =
18 injectedDesktopAppBranding?.displayName ?? `${APP_BASE_NAME} (${APP_STAGE_LABEL})`;24 injectedDesktopAppBranding?.displayName ?? `${APP_BASE_NAME} (${APP_STAGE_LABEL})`;
19export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0";25export 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+1-1
...@@ -892,7 +892,7 @@ export function GeneralSettingsPanel() {...@@ -892,7 +892,7 @@ export function GeneralSettingsPanel() {
892 </SettingsSection>892 </SettingsSection>
893893
894 <SettingsSection title="About">894 <SettingsSection title="About">
895 {isElectron ? (895 {isElectron || HOSTED_APP_CHANNEL ? (
896 <AboutVersionSection />896 <AboutVersionSection />
897 ) : (897 ) : (
898 <SettingsRow898 <SettingsRow
apps/web/vercel.ts+10-1
...@@ -1,9 +1,16 @@...@@ -1,9 +1,16 @@
1import { matchers, routes, type VercelConfig } from "@vercel/config/v1";1import { matchers, routes, type Transform, type VercelConfig } from "@vercel/config/v1";
22
3const ROUTER_HOST = "app.t3.codes";3const ROUTER_HOST = "app.t3.codes";
4const HOSTED_WEB_CHANNEL_COOKIE = "t3code_web_channel";4const HOSTED_WEB_CHANNEL_COOKIE = "t3code_web_channel";
5const LATEST_ORIGIN = "https://latest.app.t3.codes";5const LATEST_ORIGIN = "https://latest.app.t3.codes";
6const NIGHTLY_ORIGIN = "https://nightly.app.t3.codes";6const NIGHTLY_ORIGIN = "https://nightly.app.t3.codes";
7const CLEAN_CHANNEL_QUERY_TRANSFORMS = [
8 {
9 type: "request.query",
10 op: "delete",
11 target: { key: "channel" },
12 },
13] satisfies Transform[];
714
8function channelCookie(channel: "latest" | "nightly"): string {15function channelCookie(channel: "latest" | "nightly"): string {
9 return [16 return [
...@@ -28,6 +35,7 @@ export const config: VercelConfig = {...@@ -28,6 +35,7 @@ export const config: VercelConfig = {
28 {35 {
29 src: "/__t3code/channel",36 src: "/__t3code/channel",
30 has: [matchers.query("channel", "nightly")],37 has: [matchers.query("channel", "nightly")],
38 transforms: CLEAN_CHANNEL_QUERY_TRANSFORMS,
31 headers: {39 headers: {
32 Location: "/",40 Location: "/",
33 "Set-Cookie": channelCookie("nightly"),41 "Set-Cookie": channelCookie("nightly"),
...@@ -36,6 +44,7 @@ export const config: VercelConfig = {...@@ -36,6 +44,7 @@ export const config: VercelConfig = {
36 },44 },
37 {45 {
38 src: "/__t3code/channel",46 src: "/__t3code/channel",
47 transforms: CLEAN_CHANNEL_QUERY_TRANSFORMS,
39 headers: {48 headers: {
40 Location: "/",49 Location: "/",
41 "Set-Cookie": channelCookie("latest"),50 "Set-Cookie": channelCookie("latest"),
docs/release.md+11-9
...@@ -51,7 +51,7 @@ Optional GitHub Actions variables:...@@ -51,7 +51,7 @@ Optional GitHub Actions variables:
5151
52Required Vercel domains:52Required Vercel domains:
5353
54- `app.t3.codes`: the stable router domain users open.54- `app.t3.codes`: the router domain users open, updated by stable releases.
55- `latest.app.t3.codes`: channel alias updated by stable releases.55- `latest.app.t3.codes`: channel alias updated by stable releases.
56- `nightly.app.t3.codes`: channel alias updated by nightly releases.56- `nightly.app.t3.codes`: channel alias updated by nightly releases.
5757
...@@ -62,11 +62,13 @@ visiting `/__t3code/channel?channel=latest` or...@@ -62,11 +62,13 @@ visiting `/__t3code/channel?channel=latest` or
62the matching channel alias.62the matching channel alias.
6363
64The release deploy job rewrites release package versions before upload so the64The release deploy job rewrites release package versions before upload so the
65hosted app's About panel renders the release version. It also passes65hosted app's About panel renders the release version. Stable deploys alias the
66`VITE_HOSTED_APP_CHANNEL=latest|nightly`, which renders the hosted update track66same deployment to both the `latest` channel and the router domain so the router
67selector in the About panel. Changing the selector navigates through67rules stay current. Nightly deploys only alias the `nightly` channel. The job
68`/__t3code/channel` on the router domain so the user's channel cookie is updated68also passes `VITE_HOSTED_APP_CHANNEL=latest|nightly`, which renders the hosted
69before redirecting to the hosted app root.69update track selector in the About panel. Changing the selector navigates
70through `/__t3code/channel` on the router domain so the user's channel cookie is
71updated before redirecting to the hosted app root.
7072
71One-time Vercel dashboard setup:73One-time Vercel dashboard setup:
7274
...@@ -75,9 +77,9 @@ One-time Vercel dashboard setup:...@@ -75,9 +77,9 @@ One-time Vercel dashboard setup:
753. Disable automatic Git deployments in the dashboard if desired; the committed773. Disable automatic Git deployments in the dashboard if desired; the committed
76 `vercel.ts` setting is the source-of-truth, but disconnecting Git in the78 `vercel.ts` setting is the source-of-truth, but disconnecting Git in the
77 dashboard is also safe.79 dashboard is also safe.
784. Promote or alias one deployment containing the router rules in `apps/web/vercel.ts` to804. Run one stable release deployment, or manually alias the current stable
79 `app.t3.codes` once. Future release jobs should only update the channel81 deployment, so `app.t3.codes` points at a deployment containing the router
80 aliases.82 rules in `apps/web/vercel.ts`. Future stable releases keep this alias current.
8183
82## Nightly builds84## Nightly builds
8385