g1t/services/pages/src/index.ts

211 lines8,685 bytesCodeBlame

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.page1/**
2 * The dispatcher for g1t.page: every app deployed by g1t is served here.
3 *
4 * The hostname's first label is the app's script name in the Workers for
Projects live: fixes from walking them end to end5 * Platforms namespace (`web-git-fix-login-acme.g1t.page` is the fix-login
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas6 * branch's preview of acme's web project), so the app is fetched by name
7 * and runs only for as long as it answers. An app no one visits runs
8 * nothing and costs nothing. The one lookup is for an address an app
9 * had before its project moved: `DOMAINS` holds a redirect under the old
10 * hostname for as long as the old name is held, and it is followed before
11 * anything the old script would answer (such as a paused notice).
Deployments: a preview for every pull request, production on g1t.page12 *
Agents and memory, checks and conflicts, profiles, slug renames, custom domains13 * Any other hostname is a project's custom domain, reaching here through
14 * Cloudflare for SaaS: the `DOMAINS` KV namespace, written by the
15 * deployments service, names its app, or the hostname it redirects to.
16 *
Deployments: a preview for every pull request, production on g1t.page17 * Kept apart from g1t.sh, so apps share no cookies or origin with the site
18 * people sign in to.
19 */
20
Agents and memory, checks and conflicts, profiles, slug renames, custom domains21import { DOMAIN, FALLBACK, parseEntry, redirectTo, route, type DomainEntry } from "./route.ts";
22
Deployments: a preview for every pull request, production on g1t.page23type Env = {
24 APPS: DispatchNamespace;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas25 /** Custom hostname, or an app's old g1t.page hostname, to `{ script, redirect }`. */
Agents and memory, checks and conflicts, profiles, slug renames, custom domains26 DOMAINS?: KVNamespace;
Deployments: a preview for every pull request, production on g1t.page27};
28
Agents and memory, checks and conflicts, profiles, slug renames, custom domains29/** How long a custom hostname's entry is kept in this isolate and at the edge. */
30const ENTRY_TTL_MS = 60 * 1000;
31const entries = new Map<string, { at: number; entry: DomainEntry | null }>();
Deployments: a preview for every pull request, production on g1t.page32
Agents and memory, checks and conflicts, profiles, slug renames, custom domains33function page(status: number, title: string, body: string, site = DOMAIN): Response {
Deployments: a preview for every pull request, production on g1t.page34 const html = `<!doctype html>
35<html lang="en">
36<head>
37<meta charset="utf-8">
38<meta name="viewport" content="width=device-width, initial-scale=1">
39<meta name="robots" content="noindex">
Agents and memory, checks and conflicts, profiles, slug renames, custom domains40<title>${escape(title)} · ${escape(site)}</title>
Deployments: a preview for every pull request, production on g1t.page41<style>
42 :root { color-scheme: dark; --bg: #121214; --fg: #ececf1; --muted: #9a9aa6; --accent: #b4a7f5; }
43 body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: var(--bg); color: var(--fg);
44 font: 16px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; padding: 0 16px; }
45 main { max-width: 32rem; }
46 h1 { font-size: 1.5rem; margin: 0 0 .5rem; letter-spacing: -.01em; }
47 p { color: var(--muted); margin: .5rem 0; }
48 a { color: var(--accent); }
49 code { font: .9em ui-monospace, SFMono-Regular, Menlo, monospace; color: var(--fg); }
50</style>
51</head>
Agents and memory, checks and conflicts, profiles, slug renames, custom domains52<body><main><h1>${escape(title)}</h1>${body}</main></body>
Deployments: a preview for every pull request, production on g1t.page53</html>`;
54 return new Response(html, {
55 status,
Agents and memory, checks and conflicts, profiles, slug renames, custom domains56 headers: { "content-type": "text/html; charset=utf-8", "x-robots-tag": "noindex", "cache-control": "no-store" },
Deployments: a preview for every pull request, production on g1t.page57 });
58}
59
60function escape(text: string): string {
61 return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
62}
63
Agents and memory, checks and conflicts, profiles, slug renames, custom domains64/** A custom hostname's entry: from this isolate, else KV (cached at the edge too). */
65async function lookup(env: Env, hostname: string): Promise<DomainEntry | null> {
66 const cached = entries.get(hostname);
67 if (cached && Date.now() - cached.at < ENTRY_TTL_MS) return cached.entry;
68 const value = env.DOMAINS ? await env.DOMAINS.get(hostname, { type: "json", cacheTtl: ENTRY_TTL_MS / 1000 }) : null;
69 const entry = parseEntry(value);
70 entries.set(hostname, { at: Date.now(), entry });
71 if (entries.size > 5000) entries.delete(entries.keys().next().value!);
72 return entry;
73}
74
75/** Runs the app `script` for the request; `shown` is the address named in its error pages. */
76async function dispatch(env: Env, request: Request, script: string, shown: string, missing: () => Response): Promise<Response> {
77 let app: Fetcher;
78 try {
79 app = env.APPS.get(script);
80 } catch {
81 return missing();
82 }
83 try {
84 return await app.fetch(request);
85 } catch (error) {
86 if (/worker not found|script not found|does not exist/i.test(String(error))) return missing();
87 return page(
88 502,
89 "The app failed",
90 `<p>The app at <code>${escape(shown)}</code> threw an error before it could answer.</p>`,
91 shown,
92 );
93 }
94}
95
Deployments: a preview for every pull request, production on g1t.page96export default {
97 async fetch(request: Request, env: Env): Promise<Response> {
Agents and memory, checks and conflicts, profiles, slug renames, custom domains98 const host = new URL(request.url).hostname;
99 const where = route(host);
100 switch (where.kind) {
101 case "home":
102 return page(
103 200,
104 "g1t.page",
105 `<p>Apps deployed by <a href="https://g1t.sh">g1t</a>: every pull request's preview, and each repository's production.</p>
106 <p><a href="https://docs.g1t.sh/guides/deployments/">How deployments work</a></p>`,
107 );
108 case "fallback":
109 return page(
110 200,
111 "Custom domains on g1t",
112 `<p>Point a custom domain here, with a CNAME to <code>${FALLBACK}</code>, to serve a project's production on it. Add the domain first, under the project's Settings, Deployments, on g1t.</p>
113 <p><a href="https://docs.g1t.sh/guides/deployments/#custom-domains">How custom domains work</a></p>`,
114 );
115 case "invalid":
116 return page(404, "Nothing here", "<p>This is not the address of an app on g1t.page.</p>");
117 case "custom":
118 return custom(request, env, where.hostname);
119 case "app": {
120 const label = where.label;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas121 // An address the app had before its project moved (its workspace or
122 // repository was renamed or transferred) redirects to where it is
123 // now, whatever the app it named answers, paused or not.
124 const moved = await appRedirect(request, env, label);
125 if (moved) return moved;
Agents and memory, checks and conflicts, profiles, slug renames, custom domains126 let response = await dispatch(env, request, label, `${label}.${DOMAIN}`, () => missing(label));
127 // Previews are for the people reviewing a change, not search engines.
128 if (isPreview(label)) {
129 response = new Response(response.body, response);
130 response.headers.set("x-robots-tag", "noindex");
131 }
132 return response;
133 }
Deployments: a preview for every pull request, production on g1t.page134 }
135 },
136} satisfies ExportedHandler<Env>;
137
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas138/**
139 * The redirect for an app's old address, if the deployments service left
140 * one (in `DOMAINS`, under the old hostname, as `{ script, redirect }`),
141 * else null. A lookup that fails serves the app as it is rather than an
142 * error.
143 */
144async function appRedirect(request: Request, env: Env, label: string): Promise<Response | null> {
145 const hostname = `${label}.${DOMAIN}`;
146 let entry: DomainEntry | null;
147 try {
148 entry = await lookup(env, hostname);
149 } catch (error) {
150 console.error("could not read redirect", label, error);
151 return null;
152 }
153 if (!entry?.redirect || entry.redirect === hostname) return null;
154 return new Response(null, {
155 status: 301,
156 headers: {
157 location: redirectTo(request.url, entry.redirect),
158 "x-robots-tag": "noindex",
159 "cache-control": "public, max-age=3600",
160 },
161 });
162}
163
Agents and memory, checks and conflicts, profiles, slug renames, custom domains164/** A custom domain: its app, or a 308 to the hostname it is paired with. */
165async function custom(request: Request, env: Env, hostname: string): Promise<Response> {
166 let entry: DomainEntry | null;
167 try {
168 entry = await lookup(env, hostname);
169 } catch (error) {
170 console.error("could not read domain", hostname, error);
171 return page(503, "Try again in a moment", `<p><code>${escape(hostname)}</code> could not be looked up just now.</p>`, hostname);
172 }
173 if (!entry) {
174 return page(
175 404,
176 "This domain is not set up",
177 `<p><code>${escape(hostname)}</code> points at g1t, but no project serves it. Its owner adds it under the project's Settings, Deployments, on <a href="https://g1t.sh">g1t</a>.</p>`,
178 hostname,
179 );
180 }
181 if (entry.redirect && entry.redirect !== hostname) {
182 return new Response(null, {
183 status: 308,
184 headers: { location: redirectTo(request.url, entry.redirect), "cache-control": "public, max-age=300" },
185 });
186 }
187 return dispatch(env, request, entry.script, hostname, () =>
188 page(
189 404,
190 "Nothing deployed here yet",
191 `<p><code>${escape(hostname)}</code> serves a project's production, and it is not up. Deploying the project brings it here.</p>`,
192 hostname,
193 ),
194 );
195}
196
Projects live: fixes from walking them end to end197/** A branch's preview: `<project>-git-<branch>-<workspace>`. */
198function isPreview(label: string): boolean {
199 return label.includes("-git-");
200}
201
Deployments: a preview for every pull request, production on g1t.page202function missing(label: string): Response {
Projects live: fixes from walking them end to end203 const preview = isPreview(label);
Deployments: a preview for every pull request, production on g1t.page204 return page(
205 404,
206 preview ? "This preview is not up" : "Nothing deployed here",
207 preview
Projects live: fixes from walking them end to end208 ? `<p>A preview comes down when its pull request is closed or merged, or after its project's idle days without a visit. Pushing to the branch, or redeploying it from the project's Deployments page, brings it back.</p>`
209 : `<p>No app is deployed at <code>${escape(label)}.${DOMAIN}</code>. Deployments are turned on per project, from its Settings on g1t.</p>`,
Deployments: a preview for every pull request, production on g1t.page210 );
211}