| author | |
| committer | |
| log | e64c19f1af672e4f1737d81fbe83906de3547f03 |
| tree | b118fbbcd2d140547ec93b1c3a88bc40dd06161c |
| parent | 6ab8f93a23465d0d92e55d2a9173307fa2295d9c |
| signature | Signed by PGP key B5690EEEBB952194 |
6 files changed, 41 insertions(+), 16 deletions(-)
.github/workflows/release.yml+10| ... | ... | @@ -625,6 +625,9 @@ jobs: |
| 625 | 625 | router_url="${T3CODE_WEB_ROUTER_URL:-https://app.t3.codes}" |
| 626 | 626 | latest_domain="${T3CODE_WEB_LATEST_DOMAIN:-latest.app.t3.codes}" |
| 627 | 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%%/*}" | |
| 628 | 631 | |
| 629 | 632 | if [[ "${{ needs.preflight.outputs.release_channel }}" == "stable" ]]; then |
| 630 | 633 | channel_domain="$latest_domain" |
| ... | ... | @@ -655,6 +658,13 @@ jobs: |
| 655 | 658 | --token "$VERCEL_TOKEN" \ |
| 656 | 659 | "${vercel_scope_args[@]}" |
| 657 | 660 | |
| 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 | 668 | finalize: |
| 659 | 669 | name: Finalize release |
| 660 | 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 | 42 | |
| 43 | 43 | expect(branding.HOSTED_APP_CHANNEL).toBe("nightly"); |
| 44 | 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 | }); |
| 46 | 48 | |
| 47 | 49 | it("ignores unknown hosted app channels", async () => { |
apps/web/src/branding.ts+7-5| ... | ... | @@ -11,13 +11,15 @@ function readInjectedDesktopAppBranding(): DesktopAppBranding | null { |
| 11 | 11 | const injectedDesktopAppBranding = readInjectedDesktopAppBranding(); |
| 12 | 12 | const hostedAppChannel = import.meta.env.VITE_HOSTED_APP_CHANNEL?.trim().toLowerCase(); |
| 13 | 13 | |
| 14 | export const HOSTED_APP_CHANNEL = | |
| 15 | hostedAppChannel === "latest" || hostedAppChannel === "nightly" ? hostedAppChannel : null; | |
| 16 | export const HOSTED_APP_CHANNEL_LABEL = | |
| 17 | HOSTED_APP_CHANNEL === "nightly" ? "Nightly" : HOSTED_APP_CHANNEL === "latest" ? "Latest" : null; | |
| 14 | 18 | export const APP_BASE_NAME = injectedDesktopAppBranding?.baseName ?? "T3 Code"; |
| 15 | 19 | export 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"); | |
| 17 | 23 | export const APP_DISPLAY_NAME = |
| 18 | 24 | injectedDesktopAppBranding?.displayName ?? `${APP_BASE_NAME} (${APP_STAGE_LABEL})`; |
| 19 | 25 | export const APP_VERSION = import.meta.env.APP_VERSION || "0.0.0"; |
| 20 | export const HOSTED_APP_CHANNEL = | |
| 21 | hostedAppChannel === "latest" || hostedAppChannel === "nightly" ? hostedAppChannel : null; | |
| 22 | export 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 | 892 | </SettingsSection> |
| 893 | 893 | |
| 894 | 894 | <SettingsSection title="About"> |
| 895 | {isElectron ? ( | |
| 895 | {isElectron || HOSTED_APP_CHANNEL ? ( | |
| 896 | 896 | <AboutVersionSection /> |
| 897 | 897 | ) : ( |
| 898 | 898 | <SettingsRow |
apps/web/vercel.ts+10-1| ... | ... | @@ -1,9 +1,16 @@ |
| 1 | import { matchers, routes, type VercelConfig } from "@vercel/config/v1"; | |
| 1 | import { matchers, routes, type Transform, type VercelConfig } from "@vercel/config/v1"; | |
| 2 | 2 | |
| 3 | 3 | const ROUTER_HOST = "app.t3.codes"; |
| 4 | 4 | const HOSTED_WEB_CHANNEL_COOKIE = "t3code_web_channel"; |
| 5 | 5 | const LATEST_ORIGIN = "https://latest.app.t3.codes"; |
| 6 | 6 | const NIGHTLY_ORIGIN = "https://nightly.app.t3.codes"; |
| 7 | const CLEAN_CHANNEL_QUERY_TRANSFORMS = [ | |
| 8 | { | |
| 9 | type: "request.query", | |
| 10 | op: "delete", | |
| 11 | target: { key: "channel" }, | |
| 12 | }, | |
| 13 | ] satisfies Transform[]; | |
| 7 | 14 | |
| 8 | 15 | function channelCookie(channel: "latest" | "nightly"): string { |
| 9 | 16 | return [ |
| ... | ... | @@ -28,6 +35,7 @@ export const config: VercelConfig = { |
| 28 | 35 | { |
| 29 | 36 | src: "/__t3code/channel", |
| 30 | 37 | has: [matchers.query("channel", "nightly")], |
| 38 | transforms: CLEAN_CHANNEL_QUERY_TRANSFORMS, | |
| 31 | 39 | headers: { |
| 32 | 40 | Location: "/", |
| 33 | 41 | "Set-Cookie": channelCookie("nightly"), |
| ... | ... | @@ -36,6 +44,7 @@ export const config: VercelConfig = { |
| 36 | 44 | }, |
| 37 | 45 | { |
| 38 | 46 | src: "/__t3code/channel", |
| 47 | transforms: CLEAN_CHANNEL_QUERY_TRANSFORMS, | |
| 39 | 48 | headers: { |
| 40 | 49 | Location: "/", |
| 41 | 50 | "Set-Cookie": channelCookie("latest"), |
docs/release.md+11-9| ... | ... | @@ -51,7 +51,7 @@ Optional GitHub Actions variables: |
| 51 | 51 | |
| 52 | 52 | Required Vercel domains: |
| 53 | 53 | |
| 54 | - `app.t3.codes`: the stable router domain users open. | |
| 54 | - `app.t3.codes`: the router domain users open, updated by stable releases. | |
| 55 | 55 | - `latest.app.t3.codes`: channel alias updated by stable releases. |
| 56 | 56 | - `nightly.app.t3.codes`: channel alias updated by nightly releases. |
| 57 | 57 | |
| ... | ... | @@ -62,11 +62,13 @@ visiting `/__t3code/channel?channel=latest` or |
| 62 | 62 | the matching channel alias. |
| 63 | 63 | |
| 64 | 64 | The release deploy job rewrites release package versions before upload so the |
| 65 | hosted app's About panel renders the release version. It also passes | |
| 66 | `VITE_HOSTED_APP_CHANNEL=latest|nightly`, which renders the hosted update track | |
| 67 | selector in the About panel. Changing the selector navigates through | |
| 68 | `/__t3code/channel` on the router domain so the user's channel cookie is updated | |
| 69 | before redirecting to the hosted app root. | |
| 65 | hosted app's About panel renders the release version. Stable deploys alias the | |
| 66 | same deployment to both the `latest` channel and the router domain so the router | |
| 67 | rules stay current. Nightly deploys only alias the `nightly` channel. The job | |
| 68 | also passes `VITE_HOSTED_APP_CHANNEL=latest|nightly`, which renders the hosted | |
| 69 | update track selector in the About panel. Changing the selector navigates | |
| 70 | through `/__t3code/channel` on the router domain so the user's channel cookie is | |
| 71 | updated before redirecting to the hosted app root. | |
| 70 | 72 | |
| 71 | 73 | One-time Vercel dashboard setup: |
| 72 | 74 | |
| ... | ... | @@ -75,9 +77,9 @@ One-time Vercel dashboard setup: |
| 75 | 77 | 3. Disable automatic Git deployments in the dashboard if desired; the committed |
| 76 | 78 | `vercel.ts` setting is the source-of-truth, but disconnecting Git in the |
| 77 | 79 | dashboard is also safe. |
| 78 | 4. 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. | |
| 80 | 4. Run one stable release deployment, or manually alias the current stable | |
| 81 | deployment, so `app.t3.codes` points at a deployment containing the router | |
| 82 | rules in `apps/web/vercel.ts`. Future stable releases keep this alias current. | |
| 81 | 83 | |
| 82 | 84 | ## Nightly builds |
| 83 | 85 |