g1t/services/deployments/src/names.ts
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Deployments: a preview for every pull request, production on g1t.page | 1 | /** |
| Projects: what a workspace builds and runs, first on every page | 2 | * Where an app is served, the way Vercel names them: production at |
| 3 | * `<project>-<workspace>.g1t.page`, and a branch's preview at | |
| 4 | * `<project>-git-<branch>-<workspace>.g1t.page`. The first label is also | |
| 5 | * the app's script name in the dispatch namespace, so the dispatcher needs | |
| 6 | * nothing but the hostname to find it. The service makes sure no two apps | |
| App addresses do not look like other sites: a workspace named like a domain loses that hyphen (flagon-io -> flagonio) | 7 | * get the same name; this only spells them. A workspace ending in a domain |
| 8 | * ending is written without that hyphen (`hostWorkspace`); an app under the | |
| 9 | * older spelling is moved to the new one, and its old name redirects. | |
| Deployments: a preview for every pull request, production on g1t.page | 10 | */ |
| 11 | ||
| Projects: what a workspace builds and runs, first on every page | 12 | import { DEPLOYMENTS_DOMAIN } from "@g1t/contracts"; |
| Deployments: a preview for every pull request, production on g1t.page | 13 | |
| 14 | /** A hostname label is at most this long. */ | |
| 15 | const MAX_LABEL = 63; | |
| 16 | ||
| 17 | /** Lowercase letters, digits and single hyphens, as a label allows. */ | |
| Projects: what a workspace builds and runs, first on every page | 18 | export function clean(text: string): string { |
| Deployments: a preview for every pull request, production on g1t.page | 19 | return text |
| 20 | .toLowerCase() | |
| 21 | .replace(/[^a-z0-9]+/g, "-") | |
| 22 | .replace(/-{2,}/g, "-") | |
| 23 | .replace(/^-|-$/g, ""); | |
| 24 | } | |
| 25 | ||
| App addresses do not look like other sites: a workspace named like a domain loses that hyphen (flagon-io -> flagonio) | 26 | /** |
| 27 | * Domain endings a workspace's name often finishes with, as a company's | |
| 28 | * domain does (`acme-com`, `flagon-io`). In a hostname they lose their | |
| 29 | * hyphen: `app-acme-com.g1t.page` reads to browsers as a look-alike of | |
| 30 | * acme.com, and Chrome warns the people most likely to open it, those who | |
| 31 | * visit acme.com, that the page "looks fake". | |
| 32 | */ | |
| 33 | const DOMAIN_ENDINGS = new Set([ | |
| 34 | "com", "net", "org", "io", "co", "dev", "app", "ai", "sh", "xyz", "tech", "cloud", "so", "gg", "me", | |
| 35 | "us", "uk", "de", "ca", "eu", "fr", "nl", "au", "in", "jp", "site", "page", "tools", "studio", "inc", | |
| 36 | ]); | |
| 37 | ||
| 38 | /** The workspace as a hostname writes it: `flagon-io` as `flagonio`, `acme-co-uk` as `acmecouk`. */ | |
| 39 | export function hostWorkspace(workspace: string): string { | |
| 40 | const parts = clean(workspace).split("-"); | |
| 41 | let at = parts.length; | |
| 42 | while (at > 1 && DOMAIN_ENDINGS.has(parts[at - 1])) at--; | |
| 43 | if (at === parts.length) return parts.join("-"); | |
| 44 | return `${parts.slice(0, at - 1).concat(parts.slice(at - 1).join("")).join("-")}`; | |
| 45 | } | |
| 46 | ||
| Projects: what a workspace builds and runs, first on every page | 47 | /** A short, stable fingerprint of `text`. */ |
| 48 | export async function fingerprint(text: string, bytes = 3): Promise<string> { | |
| Deployments: a preview for every pull request, production on g1t.page | 49 | const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); |
| Projects: what a workspace builds and runs, first on every page | 50 | return [...new Uint8Array(digest).slice(0, bytes)].map((b) => b.toString(16).padStart(2, "0")).join(""); |
| Deployments: a preview for every pull request, production on g1t.page | 51 | } |
| 52 | ||
| 53 | /** | |
| Projects: what a workspace builds and runs, first on every page | 54 | * The label for production (`branch` null) or a branch's preview. Too long |
| 55 | * for a label, the branch and then the project are shortened, and a | |
| 56 | * fingerprint of the whole keeps it unique. | |
| Deployments: a preview for every pull request, production on g1t.page | 57 | */ |
| Projects: what a workspace builds and runs, first on every page | 58 | export async function label(workspace: string, project: string, branch: string | null): Promise<string> { |
| App addresses do not look like other sites: a workspace named like a domain loses that hyphen (flagon-io -> flagonio) | 59 | const w = hostWorkspace(workspace); |
| Projects: what a workspace builds and runs, first on every page | 60 | const p = clean(project); |
| 61 | const b = branch == null ? null : clean(branch); | |
| 62 | const full = b == null ? `${p}-${w}` : `${p}-git-${b}-${w}`; | |
| Deployments: a preview for every pull request, production on g1t.page | 63 | if (full.length <= MAX_LABEL) return full; |
| Projects: what a workspace builds and runs, first on every page | 64 | const tail = `-${await fingerprint(`${workspace}/${project}/${branch ?? ""}`)}-${w}`.slice(0, 40); |
| 65 | const room = MAX_LABEL - tail.length; | |
| 66 | const head = b == null ? p : `${p.slice(0, Math.max(8, Math.floor(room / 2)))}-git-${b}`; | |
| 67 | return `${head.slice(0, room).replace(/-+$/, "")}${tail}`; | |
| 68 | } | |
| 69 | ||
| 70 | /** The same label made unique, when another app already holds it. */ | |
| 71 | export async function uniqueLabel(base: string, key: string): Promise<string> { | |
| 72 | const suffix = `-${await fingerprint(key, 2)}`; | |
| 73 | return `${base.slice(0, MAX_LABEL - suffix.length).replace(/-+$/, "")}${suffix}`; | |
| Deployments: a preview for every pull request, production on g1t.page | 74 | } |
| 75 | ||
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 76 | export function appHost(script: string): string { |
| 77 | return `${script}.${DEPLOYMENTS_DOMAIN}`; | |
| 78 | } | |
| 79 | ||
| Deployments: a preview for every pull request, production on g1t.page | 80 | export function appUrl(script: string): string { |
| Agents and memory, checks and conflicts, profiles, slug renames, custom domains | 81 | return `https://${appHost(script)}`; |
| Deployments: a preview for every pull request, production on g1t.page | 82 | } |