Projects live: fixes from walking them end to end
Verified on g1t.sh and g1t.page: the workspace page of project cards; a project's overview; production of automation-lab at its new address (automation-lab-syntaqx.g1t.page) with its config and its secret's Production row; a branch's preview (automation-lab-git-fix-login-syntaqx.g1t.page) with the Preview row; a preview live 42 s after its pull request opened and down 5 s after it closed. - Taking an app down now replaces it with a "not up" notice before deleting it. A deleted app kept answering for over 15 minutes where Cloudflare still had it warm; a new version reaches the edge at once. - Secrets and variables kept under a repository move to its primary project on every read, however the project is named. Deployments name the project, so the move never ran and the first build on the new address got none of its rows. - The dispatcher marks a preview noindex by its new name (-git-), and its pages speak of projects. - "New project" everywhere the site still said "New repository".
| 646 | 646 | const words: Record<string, string> = { | |
| 647 | 647 | settings: "Account", | |
| 648 | 648 | explore: "Explore", | |
| 649 | − | new: "New repository", | |
| 649 | + | new: "New project", | |
| 650 | 650 | search: "Search", | |
| 651 | 651 | workspaces: "New workspace", | |
| 652 | 652 | }; |
| 169 | 169 | <> | |
| 170 | 170 | <Link | |
| 171 | 171 | to="/new" | |
| 172 | − | aria-label="New repository" | |
| 172 | + | aria-label="New project" | |
| 173 | 173 | className="rounded-md border border-line p-1.5 text-muted transition-colors hover:border-line-strong hover:text-fg" | |
| 174 | 174 | > | |
| 175 | 175 | <Plus size={16} /> | |
| 214 | 214 | <DropdownMenuItem asChild> | |
| 215 | 215 | <Link to="/new"> | |
| 216 | 216 | <Plus /> | |
| 217 | − | New repository | |
| 217 | + | New project | |
| 218 | 218 | </Link> | |
| 219 | 219 | </DropdownMenuItem> | |
| 220 | 220 | <DropdownMenuSeparator /> |
| 80 | 80 | {role && ( | |
| 81 | 81 | <ButtonLink to={`/new?workspace=${workspace.slug}`}> | |
| 82 | 82 | <Plus size={15} /> | |
| 83 | − | New repository | |
| 83 | + | New project | |
| 84 | 84 | </ButtonLink> | |
| 85 | 85 | )} | |
| 86 | 86 | </div> |
| 482 | 482 | /// and its running app get. | |
| 483 | 483 | pub async fn resolve_settings(&self, a: ResolveSettingsArgs) -> Result<ResolvedSettings> { | |
| 484 | 484 | let environment = a.environment.as_deref(); | |
| 485 | − | let (project_id, slug) = match (a.project_id, a.project_slug) { | |
| 486 | − | (Some(id), Some(slug)) => (id, slug), | |
| 487 | − | _ => match self.project_of(&a.repo_id).await? { | |
| 488 | − | Some(project) => (project.id, project.slug), | |
| 489 | − | None => return Ok(ResolvedSettings::default()), | |
| 490 | − | }, | |
| 485 | + | // Rows kept under the repository from before projects move to its | |
| 486 | + | // primary project first, however the project is named here. | |
| 487 | + | let primary = self.project_of(&a.repo_id).await?; | |
| 488 | + | let (project_id, slug) = match (a.project_id, a.project_slug, primary) { | |
| 489 | + | (Some(id), Some(slug), _) => (id, slug), | |
| 490 | + | (_, _, Some(project)) => (project.id, project.slug), | |
| 491 | + | _ => return Ok(ResolvedSettings::default()), | |
| 491 | 492 | }; | |
| 492 | 493 | Ok(ResolvedSettings { | |
| 493 | 494 | secrets: self |
| 33 | 33 | /** Serves the site's files, for an app that brings no code of its own. */ | |
| 34 | 34 | const ASSETS_ONLY = `export default { fetch(request, env) { return env.ASSETS.fetch(request); } };\n`; | |
| 35 | 35 | ||
| 36 | − | const HTML_HANDLING = ["auto-trailing-slash", "force-trailing-slash", "drop-trailing-slash", "none"]; | |
| 36 | + | /** What an app answers between being taken down and being gone. */ | |
| 37 | + | const TAKEN_DOWN = `export default { | |
| 38 | + | fetch() { | |
| 39 | + | return new Response("<!doctype html><meta charset=utf-8><meta name=robots content=noindex><title>Not up</title><body style='font:16px system-ui;background:#121214;color:#ececf1;display:grid;place-items:center;min-height:100vh;margin:0'><main><h1>This app is not up</h1><p style='color:#9a9aa6'>It was taken down. Deploying it again brings it back.</p></main>", { status: 404, headers: { "content-type": "text/html; charset=utf-8", "x-robots-tag": "noindex", "cache-control": "no-store" } }); | |
| 40 | + | }, | |
| 41 | + | }; | |
| 42 | + | `; | |
| 43 | + | ||
| 44 | + | const HTML_HANDLING =["auto-trailing-slash", "force-trailing-slash", "drop-trailing-slash", "none"]; | |
| 37 | 45 | const NOT_FOUND_HANDLING = ["single-page-application", "404-page", "none"]; | |
| 38 | 46 | ||
| 39 | 47 | export class Cloudflare { | |
| 148 | 156 | ); | |
| 149 | 157 | } | |
| 150 | 158 | ||
| 151 | − | /** Takes an app down. Already gone is fine. */ | |
| 159 | + | /** | |
| 160 | + | * Takes an app down. Already gone is fine. | |
| 161 | + | * | |
| 162 | + | * A deleted app can keep answering for a while where Cloudflare still has | |
| 163 | + | * it warm. So it is first replaced by a notice that it is down, which | |
| 164 | + | * reaches the edge as fast as any deploy, and deleted after. | |
| 165 | + | */ | |
| 152 | 166 | async deleteScript(script: string): Promise<void> { | |
| 167 | + | const form = new FormData(); | |
| 168 | + | form.append( | |
| 169 | + | "metadata", | |
| 170 | + | JSON.stringify({ main_module: "index.js", compatibility_date: "2026-09-26", bindings: [], tags: ["taken-down"] }), | |
| 171 | + | ); | |
| 172 | + | form.append("index.js", new File([TAKEN_DOWN], "index.js", { type: "application/javascript+module" })); | |
| 173 | + | await this.call("PUT", this.scriptPath(script), form).catch(() => undefined); | |
| 153 | 174 | try { | |
| 154 | 175 | await this.call("DELETE", `${this.scriptPath(script)}?force=true`); | |
| 155 | 176 | } catch (error) { |
| 2 | 2 | * The dispatcher for g1t.page: every app deployed by g1t is served here. | |
| 3 | 3 | * | |
| 4 | 4 | * The hostname's first label is the app's script name in the Workers for | |
| 5 | − | * Platforms namespace (`pr-12--web--acme.g1t.page` is pull request 12's | |
| 6 | − | * preview of acme/web), so a request needs no lookup: the app is fetched | |
| 7 | − | * by name and runs only for as long as it answers. An app no one visits | |
| 5 | + | * Platforms namespace (`web-git-fix-login-acme.g1t.page` is the fix-login | |
| 6 | + | * branch's preview of acme's web project), so a request needs no lookup: | |
| 7 | + | * the app is fetched by name and runs only for as long as it answers. An app no one visits | |
| 8 | 8 | * runs nothing and costs nothing. | |
| 9 | 9 | * | |
| 10 | 10 | * Kept apart from g1t.sh, so apps share no cookies or origin with the site | |
| 81 | 81 | ); | |
| 82 | 82 | } | |
| 83 | 83 | // Previews are for the people reviewing a change, not search engines. | |
| 84 | − | if (label.startsWith("pr-")) { | |
| 84 | + | if (isPreview(label)) { | |
| 85 | 85 | response = new Response(response.body, response); | |
| 86 | 86 | response.headers.set("x-robots-tag", "noindex"); | |
| 87 | 87 | } | |
| 89 | 89 | }, | |
| 90 | 90 | } satisfies ExportedHandler<Env>; | |
| 91 | 91 | ||
| 92 | + | /** A branch's preview: `<project>-git-<branch>-<workspace>`. */ | |
| 93 | + | function isPreview(label: string): boolean { | |
| 94 | + | return label.includes("-git-"); | |
| 95 | + | } | |
| 96 | + | ||
| 92 | 97 | function missing(label: string): Response { | |
| 93 | − | const preview = label.startsWith("pr-"); | |
| 98 | + | const preview = isPreview(label); | |
| 94 | 99 | return page( | |
| 95 | 100 | 404, | |
| 96 | 101 | preview ? "This preview is not up" : "Nothing deployed here", | |
| 97 | 102 | preview | |
| 98 | − | ? `<p>A preview comes down when its pull request is closed or merged, or after its repository's idle days without a visit. Pushing to the pull request, or redeploying it from the repository's Deployments page, brings it back.</p>` | |
| 99 | − | : `<p>No app is deployed at <code>${escape(label)}.${DOMAIN}</code>. Deployments are turned on per repository, from its Deployments page on g1t.</p>`, | |
| 103 | + | ? `<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>` | |
| 104 | + | : `<p>No app is deployed at <code>${escape(label)}.${DOMAIN}</code>. Deployments are turned on per project, from its Settings on g1t.</p>`, | |
| 100 | 105 | ); | |
| 101 | 106 | } |