Commit

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.

syntaqxcommitted Parentf9e8c2dBrowse files
4 files+73−100/4 viewed
+7−0
2121 `pr-<number>` in place of the branch. A name too long for an address, or
2222 one another app already has, is shortened or given a short suffix.
2323
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+
2431 When a workspace is [renamed](/guides/workspaces/#rename-a-workspace), or a
2532 repository is [transferred](/guides/transferring-repositories/#deployments)
2633 to another workspace or [renamed](/guides/managing-repositories/), its apps
+8−8
229229 assert.deepEqual(rows("SELECT workspace FROM settings"), [{ workspace: "flagon-io" }]);
230230 const queued = rows("SELECT script, kind, commit_sha FROM deployments WHERE status = 'queued' ORDER BY script");
231231 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" },
234234 ]);
235235 assert.equal(w.state.builds.length, 2);
236236 assert.ok(w.state.builds.every((b) => b.workspace === "flagon-io"));
253253 assert.equal(await deliver(transferred, 2), true);
254254 assert.deepEqual(
255255 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"],
257257 );
258258 });
259259
269269 assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 2);
270270 // syntaqx is over its limit, flagon-io is not: nothing of the project's is
271271 // 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)));
273273 });
274274
275275 test("a move waits, without refused deployments, while the new workspace is over its limit", async () => {
283283
284284 test("once the new app is live, the old address redirects to it, paused or not", async () => {
285285 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")!;
287287 const finish = await worker.fetch(
288288 new Request(`https://deployments/jobs/${build.deployId}/finish`, {
289289 method: "POST",
295295 assert.deepEqual(await finish.json(), { ok: true, value: true });
296296 assert.deepEqual(
297297 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" }],
299299 );
300300 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" });
302302 // The dispatcher's entry, followed before the old (paused) script runs.
303303 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" });
305305 assert.ok(entry.expiration! > Date.now() / 1000 + 89 * 24 * 3600);
306306 // And the old script itself is the redirect too.
307307 assert.ok(w.state.cloudflare.some((call) => call === "PUT /client/v4/accounts/acct/workers/dispatch/namespaces/g1t-deployments/scripts/lab-api-syntaqx"));
+33−0
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+});
+25−2
44 * `<project>-git-<branch>-<workspace>.g1t.page`. The first label is also
55 * the app's script name in the dispatch namespace, so the dispatcher needs
66 * 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.
810 */
911
1012 import { DEPLOYMENTS_DOMAIN } from "@g1t/contracts";
2123 .replace(/^-|-$/g, "");
2224 }
2325
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+
2447 /** A short, stable fingerprint of `text`. */
2548 export async function fingerprint(text: string, bytes = 3): Promise<string> {
2649 const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
3356 * fingerprint of the whole keeps it unique.
3457 */
3558 export async function label(workspace: string, project: string, branch: string | null): Promise<string> {
36− const w = clean(workspace);
59+ const w = hostWorkspace(workspace);
3760 const p = clean(project);
3861 const b = branch == null ? null : clean(branch);
3962 const full = b == null ? `${p}-${w}` : `${p}-git-${b}-${w}`;