App addresses do not look like other sites: a workspace named like a domain loses that hyphen (flagon-io -> flagonio)
automation-lab-flagon-io.g1t.page read to Chrome as a look-alike of flagon.io, and Chrome told people who visit flagon.io the page looked fake. Any workspace named after its company's domain (acme-com) did the same to its own people. A workspace ending in a domain ending (com, io, dev, co-uk, ...) is now written without that hyphen in app addresses. Existing apps are found under the older name by the move sweep, built again under the new one from the same commit, and the old address redirects. The deployments guide says so.
4 files+73−100/4 viewed
| 21 | 21 | `pr-<number>` in place of the branch. A name too long for an address, or | |
| 22 | 22 | one another app already has, is shortened or given a short suffix. | |
| 23 | 23 | ||
| 24 | + | A workspace whose name ends like a domain is written without that last | |
| 25 | + | hyphen: `flagon-io` gives `https://web-flagonio.g1t.page`, not | |
| 26 | + | `web-flagon-io.g1t.page`. Browsers read `flagon-io` in an address as a | |
| 27 | + | copy of `flagon.io`, and warn people who visit flagon.io that the page | |
| 28 | + | looks fake. Apps made before this moved to the new address on their own; | |
| 29 | + | the old one redirects to it. | |
| 30 | + | ||
| 24 | 31 | When a workspace is [renamed](/guides/workspaces/#rename-a-workspace), or a | |
| 25 | 32 | repository is [transferred](/guides/transferring-repositories/#deployments) | |
| 26 | 33 | to another workspace or [renamed](/guides/managing-repositories/), its apps |
| 229 | 229 | assert.deepEqual(rows("SELECT workspace FROM settings"), [{ workspace: "flagon-io" }]); | |
| 230 | 230 | const queued = rows("SELECT script, kind, commit_sha FROM deployments WHERE status = 'queued' ORDER BY script"); | |
| 231 | 231 | assert.deepEqual(queued, [ | |
| 232 | − | { script: "lab-api-flagon-io", kind: "production", commit_sha: "8e500b1" }, | |
| 233 | − | { script: "lab-api-git-v2-flagon-io", kind: "preview", commit_sha: "pr1head" }, | |
| 232 | + | { script: "lab-api-flagonio", kind: "production", commit_sha: "8e500b1" }, | |
| 233 | + | { script: "lab-api-git-v2-flagonio", kind: "preview", commit_sha: "pr1head" }, | |
| 234 | 234 | ]); | |
| 235 | 235 | assert.equal(w.state.builds.length, 2); | |
| 236 | 236 | assert.ok(w.state.builds.every((b) => b.workspace === "flagon-io")); | |
| 253 | 253 | assert.equal(await deliver(transferred, 2), true); | |
| 254 | 254 | assert.deepEqual( | |
| 255 | 255 | rows("SELECT script FROM deployments WHERE status = 'queued' ORDER BY script").map((r) => r.script), | |
| 256 | − | ["lab-api-flagon-io", "lab-api-git-v2-flagon-io"], | |
| 256 | + | ["lab-api-flagonio", "lab-api-git-v2-flagonio"], | |
| 257 | 257 | ); | |
| 258 | 258 | }); | |
| 259 | 259 | ||
| 269 | 269 | assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 2); | |
| 270 | 270 | // syntaqx is over its limit, flagon-io is not: nothing of the project's is | |
| 271 | 271 | // paused for syntaqx's sake, and nothing new was paused at all. | |
| 272 | − | assert.ok(!w.state.cloudflare.some((call) => call.startsWith("PUT") && /scripts\/lab-api-flagon-io/.test(call))); | |
| 272 | + | assert.ok(!w.state.cloudflare.some((call) => call.startsWith("PUT") && /scripts\/lab-api-flagonio/.test(call))); | |
| 273 | 273 | }); | |
| 274 | 274 | ||
| 275 | 275 | test("a move waits, without refused deployments, while the new workspace is over its limit", async () => { | |
| 283 | 283 | ||
| 284 | 284 | test("once the new app is live, the old address redirects to it, paused or not", async () => { | |
| 285 | 285 | await deliver(transferred); | |
| 286 | − | const build = w.state.builds.find((b) => rows("SELECT script FROM deployments WHERE id = ?", b.deployId)[0].script === "lab-api-flagon-io")!; | |
| 286 | + | const build = w.state.builds.find((b) => rows("SELECT script FROM deployments WHERE id = ?", b.deployId)[0].script === "lab-api-flagonio")!; | |
| 287 | 287 | const finish = await worker.fetch( | |
| 288 | 288 | new Request(`https://deployments/jobs/${build.deployId}/finish`, { | |
| 289 | 289 | method: "POST", | |
| 295 | 295 | assert.deepEqual(await finish.json(), { ok: true, value: true }); | |
| 296 | 296 | assert.deepEqual( | |
| 297 | 297 | rows("SELECT script, workspace FROM apps WHERE kind = 'production'"), | |
| 298 | − | [{ script: "lab-api-flagon-io", workspace: "flagon-io" }], | |
| 298 | + | [{ script: "lab-api-flagonio", workspace: "flagon-io" }], | |
| 299 | 299 | ); | |
| 300 | 300 | const [redirect] = rows("SELECT script, target, workspace FROM redirects"); | |
| 301 | − | assert.deepEqual({ ...redirect }, { script: "lab-api-syntaqx", target: "lab-api-flagon-io.g1t.page", workspace: "flagon-io" }); | |
| 301 | + | assert.deepEqual({ ...redirect }, { script: "lab-api-syntaqx", target: "lab-api-flagonio.g1t.page", workspace: "flagon-io" }); | |
| 302 | 302 | // The dispatcher's entry, followed before the old (paused) script runs. | |
| 303 | 303 | const entry = w.state.kv.get("lab-api-syntaqx.g1t.page")!; | |
| 304 | − | assert.deepEqual(JSON.parse(entry.value), { script: "lab-api-syntaqx", redirect: "lab-api-flagon-io.g1t.page" }); | |
| 304 | + | assert.deepEqual(JSON.parse(entry.value), { script: "lab-api-syntaqx", redirect: "lab-api-flagonio.g1t.page" }); | |
| 305 | 305 | assert.ok(entry.expiration! > Date.now() / 1000 + 89 * 24 * 3600); | |
| 306 | 306 | // And the old script itself is the redirect too. | |
| 307 | 307 | assert.ok(w.state.cloudflare.some((call) => call === "PUT /client/v4/accounts/acct/workers/dispatch/namespaces/g1t-deployments/scripts/lab-api-syntaqx")); |
| 1 | + | import assert from "node:assert/strict"; | |
| 2 | + | import { registerHooks } from "node:module"; | |
| 3 | + | import { test } from "node:test"; | |
| 4 | + | ||
| 5 | + | // The contracts import their own modules without extensions. | |
| 6 | + | registerHooks({ | |
| 7 | + | resolve(specifier, context, next) { | |
| 8 | + | try { | |
| 9 | + | return next(specifier, context); | |
| 10 | + | } catch (error) { | |
| 11 | + | if (specifier.startsWith(".")) return next(`${specifier}.ts`, context); | |
| 12 | + | throw error; | |
| 13 | + | } | |
| 14 | + | }, | |
| 15 | + | }); | |
| 16 | + | ||
| 17 | + | const { hostWorkspace, label } = await import("./names.ts"); | |
| 18 | + | ||
| 19 | + | test("a workspace named like a domain loses that hyphen in hostnames", () => { | |
| 20 | + | assert.equal(hostWorkspace("flagon-io"), "flagonio"); | |
| 21 | + | assert.equal(hostWorkspace("acme-co-uk"), "acmecouk"); | |
| 22 | + | assert.equal(hostWorkspace("Big-Corp-COM"), "big-corpcom"); | |
| 23 | + | // Nothing that only looks like one. | |
| 24 | + | assert.equal(hostWorkspace("syntaqx"), "syntaqx"); | |
| 25 | + | assert.equal(hostWorkspace("io"), "io"); | |
| 26 | + | assert.equal(hostWorkspace("acme-labs"), "acme-labs"); | |
| 27 | + | }); | |
| 28 | + | ||
| 29 | + | test("an app's name uses that spelling, so no label reads as another site", async () => { | |
| 30 | + | assert.equal(await label("flagon-io", "automation-lab", null), "automation-lab-flagonio"); | |
| 31 | + | assert.equal(await label("flagon-io", "web", "fix/login"), "web-git-fix-login-flagonio"); | |
| 32 | + | assert.equal(await label("syntaqx", "hello", null), "hello-syntaqx"); | |
| 33 | + | }); |
| 4 | 4 | * `<project>-git-<branch>-<workspace>.g1t.page`. The first label is also | |
| 5 | 5 | * the app's script name in the dispatch namespace, so the dispatcher needs | |
| 6 | 6 | * nothing but the hostname to find it. The service makes sure no two apps | |
| 7 | − | * get the same name; this only spells them. | |
| 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. | |
| 8 | 10 | */ | |
| 9 | 11 | ||
| 10 | 12 | import { DEPLOYMENTS_DOMAIN } from "@g1t/contracts"; | |
| 21 | 23 | .replace(/^-|-$/g, ""); | |
| 22 | 24 | } | |
| 23 | 25 | ||
| 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 | + | ||
| 24 | 47 | /** A short, stable fingerprint of `text`. */ | |
| 25 | 48 | export async function fingerprint(text: string, bytes = 3): Promise<string> { | |
| 26 | 49 | const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)); | |
| 33 | 56 | * fingerprint of the whole keeps it unique. | |
| 34 | 57 | */ | |
| 35 | 58 | export async function label(workspace: string, project: string, branch: string | null): Promise<string> { | |
| 36 | − | const w = clean(workspace); | |
| 59 | + | const w = hostWorkspace(workspace); | |
| 37 | 60 | const p = clean(project); | |
| 38 | 61 | const b = branch == null ? null : clean(branch); | |
| 39 | 62 | const full = b == null ? `${p}-${w}` : `${p}-git-${b}-${w}`; |