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.
| What g1t can't do yet, and an open letter to Cloudflare | 1 | //! Builds one commit of a repository and hands the result to Cloudflare, as |
| 2 | //! a preview of a pull request or as the repository's production. | |
| 3 | //! | |
| 4 | //! The sandbox never holds a Cloudflare credential that could touch anything | |
| 5 | //! else. The deployments service opens an upload for exactly the files | |
| 6 | //! this build produced and gives back a key that can only upload those; | |
| 7 | //! the sandbox uploads them with it, and sends the Worker's code to the | |
| 8 | //! service, which puts the app in place. | |
| 9 | //! | |
| 10 | //! What gets built: | |
| 11 | //! | |
| 12 | //! - A Workers project (a `wrangler.jsonc`, `wrangler.json` or | |
| 13 | //! `wrangler.toml`): bundled by `wrangler deploy --dry-run`, with its | |
| 14 | //! static assets, compatibility settings and `vars`. Other bindings (D1, | |
| 15 | //! KV, R2, Durable Objects…) are not provisioned yet; the deployment says | |
| 16 | //! which were left out. | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 17 | //! - A framework g1t knows (deploy/frameworks.rs: Vite, Astro, Next.js, |
| 18 | //! SvelteKit, Nuxt, Hugo, Jekyll and the rest), found by the package's | |
| 19 | //! dependencies and config files: built with its own command and served | |
| 20 | //! from the folder it writes. A Cloudflare adapter's Worker (Astro's | |
| 21 | //! `dist/_worker.js`, SvelteKit's `.svelte-kit/cloudflare/_worker.js`) is | |
| 22 | //! bundled as a Workers project's is, without a Wrangler config of its | |
| 23 | //! own. A framework set up to need a server fails before anything is | |
| 24 | //! installed, saying what to change. | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 25 | //! - A container (deploy/container.rs): a project with a `Dockerfile` (or |
| 26 | //! `Containerfile`, or the path it chose) and no framework that builds a | |
| 27 | //! static site or a Worker, a framework set up to need a server, or a | |
| 28 | //! Node.js, Python, Go, Ruby or PHP server, for which g1t writes a | |
| 29 | //! Dockerfile and shows it in the log. The image is built with Docker | |
| 30 | //! and pushed to Cloudflare's registry with a push-only login the | |
| 31 | //! deployments service gives this build alone, for minutes (the | |
| 32 | //! `registry` step); the service runs it. | |
| What g1t can't do yet, and an open letter to Cloudflare | 33 | //! - Anything else: a static site. Its `build` script runs, and the first |
| 34 | //! of `dist`, `build`, `out`, `public`, `_site` or `.output/public` that | |
| 35 | //! exists is served, or the repository itself if it has an `index.html`. | |
| 36 | //! | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 37 | //! The project's own **Build as** (`BUILD_AS`: auto, static, workers or |
| 38 | //! docker), **Dockerfile** (`DOCKERFILE`), **Build command** and **Output | |
| 39 | //! directory** win over what is detected. | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 40 | //! |
| What g1t can't do yet, and an open letter to Cloudflare | 41 | //! Configuration comes from the environment: |
| 42 | //! | |
| 43 | //! - `G1T_API`, `DEPLOY_ID`, `DEPLOY_TOKEN`: where and how to report. | |
| 44 | //! - `GIT_REMOTE`, `GIT_COMMIT`, `G1T_USER`, `G1T_TOKEN`: what to check out. | |
| 45 | //! - `ROOT_DIR`: where in the repository the project lives; empty for all | |
| 46 | //! of it. Everything below is relative to it. | |
| 47 | //! - `BUILD_COMMAND`, `OUTPUT_DIR`: the project's own choices, if any. | |
| 48 | //! - `BUILD_ENV`, `BUILD_SECRETS`: JSON objects of the repository's | |
| 49 | //! variables and secrets for deploy builds. Both are set for the build; | |
| 50 | //! secrets' values are redacted from its log. | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 51 | //! |
| 52 | //! From a workflow step, `g1t-runner deploy` runs the same build in the | |
| 53 | //! job's own checkout: without `GIT_REMOTE` nothing is cloned, and the | |
| 54 | //! project is the current directory (`ROOT_DIR` below it). The build's id | |
| 55 | //! and token come from `POST /repos/{owner}/{repo}/deployments/builds`, | |
| 56 | //! which the `g1t/deploy` action calls (actions/deploy). Its output goes | |
| 57 | //! to the step's log as well as the deployment's. `g1t-runner detect` | |
| 58 | //! says what the build would find, as `key=value` lines for | |
| 59 | //! `$GITHUB_OUTPUT`, without building anything. | |
| What g1t can't do yet, and an open letter to Cloudflare | 60 | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 61 | use std::collections::{BTreeMap, BTreeSet}; |
| What g1t can't do yet, and an open letter to Cloudflare | 62 | use std::path::{Path, PathBuf}; |
| 63 | use std::time::Instant; | |
| 64 | ||
| 65 | use anyhow::{Context, Result, bail}; | |
| 66 | use base64::Engine; | |
| 67 | use base64::engine::general_purpose::STANDARD; | |
| 68 | use serde::{Deserialize, Serialize}; | |
| 69 | use serde_json::{Value, json}; | |
| 70 | use sha2::{Digest, Sha256}; | |
| 71 | ||
| 72 | use crate::checks::{redact, run_command}; | |
| 73 | use crate::{WORKDIR, auth_option, env, git}; | |
| 74 | ||
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 75 | mod container; |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 76 | mod frameworks; |
| 77 | ||
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 78 | use container::BuildAs; |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 79 | use frameworks::{Adapter, Framework}; |
| 80 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 81 | /// Where `wrangler deploy --dry-run` writes the bundle in a sandbox. |
| What g1t can't do yet, and an open letter to Cloudflare | 82 | const BUNDLE_DIR: &str = "/work/g1t-bundle"; |
| 83 | /// Cloudflare's limits on a Worker's static assets. | |
| 84 | const MAX_FILES: usize = 20_000; | |
| 85 | const MAX_FILE_BYTES: u64 = 25 * 1024 * 1024; | |
| 86 | /// How much of the build's output is kept for the deployment's log. | |
| 87 | const MAX_LOG_CHARS: usize = 20_000; | |
| 88 | /// Directories a static build usually writes to, in the order they are tried. | |
| 89 | const OUTPUT_DIRS: [&str; 6] = ["dist", "build", "out", "public", "_site", ".output/public"]; | |
| 90 | /// Bindings a Workers project may declare that are not provisioned yet. | |
| 91 | const UNSUPPORTED_BINDINGS: [&str; 10] = [ | |
| 92 | "kv_namespaces", | |
| 93 | "d1_databases", | |
| 94 | "r2_buckets", | |
| 95 | "durable_objects", | |
| 96 | "services", | |
| 97 | "queues", | |
| 98 | "vectorize", | |
| 99 | "hyperdrive", | |
| 100 | "ai", | |
| 101 | "workflows", | |
| 102 | ]; | |
| 103 | ||
| 104 | struct Reporter { | |
| 105 | base: String, | |
| 106 | token: String, | |
| 107 | } | |
| 108 | ||
| 109 | impl Reporter { | |
| 110 | fn send(&self, step: &str, mut body: Value) -> Result<Value> { | |
| 111 | body["token"] = self.token.clone().into(); | |
| 112 | let response = ureq::post(&format!("{}/{step}", self.base)) | |
| 113 | .send_json(body) | |
| 114 | .with_context(|| format!("could not report `{step}` to g1t"))?; | |
| 115 | Ok(response.into_json().unwrap_or(Value::Null)) | |
| 116 | } | |
| 117 | } | |
| 118 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 119 | /// The build's log, kept to its end; also printed, in a workflow step. |
| What g1t can't do yet, and an open letter to Cloudflare | 120 | #[derive(Default)] |
| 121 | struct Log { | |
| 122 | text: String, | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 123 | /// Each line to standard output as well, redacted. |
| 124 | echo: Option<Vec<String>>, | |
| What g1t can't do yet, and an open letter to Cloudflare | 125 | } |
| 126 | ||
| 127 | impl Log { | |
| 128 | fn line(&mut self, line: &str) { | |
| 129 | self.text.push_str(line); | |
| 130 | self.text.push('\n'); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 131 | if let Some(secrets) = &self.echo { |
| 132 | println!("{}", redact(line, secrets)); | |
| 133 | } | |
| What g1t can't do yet, and an open letter to Cloudflare | 134 | } |
| 135 | ||
| 136 | fn tail(&self) -> String { | |
| 137 | let length = self.text.chars().count(); | |
| 138 | if length <= MAX_LOG_CHARS { | |
| 139 | return self.text.clone(); | |
| 140 | } | |
| 141 | let kept: String = self.text.chars().skip(length - MAX_LOG_CHARS).collect(); | |
| 142 | format!("… (earlier output not shown)\n{kept}") | |
| 143 | } | |
| 144 | } | |
| 145 | ||
| 146 | /// Runs a command in the checkout, logging it; fails if it fails. | |
| 147 | fn step(log: &mut Log, command: &str, secrets: &[String]) -> Result<()> { | |
| 148 | log.line(&format!("$ {command}")); | |
| 149 | let result = run_command(command, &project_dir(), secrets); | |
| 150 | if !result.output_text().is_empty() { | |
| 151 | log.line(result.output_text()); | |
| 152 | } | |
| 153 | if !result.passed { | |
| 154 | bail!("`{command}` failed"); | |
| 155 | } | |
| 156 | Ok(()) | |
| 157 | } | |
| 158 | ||
| 159 | /// A Workers project's settings, from whichever config file it has. | |
| 160 | #[derive(Debug, Default, Deserialize)] | |
| 161 | struct WranglerConfig { | |
| 162 | main: Option<String>, | |
| 163 | compatibility_date: Option<String>, | |
| 164 | #[serde(default)] | |
| 165 | compatibility_flags: Vec<String>, | |
| 166 | assets: Option<AssetsConfig>, | |
| 167 | #[serde(default)] | |
| 168 | vars: BTreeMap<String, Value>, | |
| 169 | #[serde(flatten)] | |
| 170 | rest: BTreeMap<String, Value>, | |
| 171 | } | |
| 172 | ||
| 173 | #[derive(Debug, Default, Deserialize)] | |
| 174 | struct AssetsConfig { | |
| 175 | directory: Option<String>, | |
| 176 | binding: Option<String>, | |
| 177 | html_handling: Option<String>, | |
| 178 | not_found_handling: Option<String>, | |
| 179 | } | |
| 180 | ||
| 181 | /// JSON with comments and trailing commas, as `wrangler.jsonc` allows. | |
| 182 | fn strip_jsonc(text: &str) -> String { | |
| 183 | let mut out = String::with_capacity(text.len()); | |
| 184 | let mut chars = text.chars().peekable(); | |
| 185 | let mut in_string = false; | |
| 186 | while let Some(c) = chars.next() { | |
| 187 | if in_string { | |
| 188 | out.push(c); | |
| 189 | if c == '\\' { | |
| 190 | if let Some(next) = chars.next() { | |
| 191 | out.push(next); | |
| 192 | } | |
| 193 | } else if c == '"' { | |
| 194 | in_string = false; | |
| 195 | } | |
| 196 | continue; | |
| 197 | } | |
| 198 | match (c, chars.peek()) { | |
| 199 | ('"', _) => { | |
| 200 | in_string = true; | |
| 201 | out.push(c); | |
| 202 | } | |
| 203 | ('/', Some('/')) => { | |
| 204 | for c in chars.by_ref() { | |
| 205 | if c == '\n' { | |
| 206 | out.push('\n'); | |
| 207 | break; | |
| 208 | } | |
| 209 | } | |
| 210 | } | |
| 211 | ('/', Some('*')) => { | |
| 212 | chars.next(); | |
| 213 | let mut last = ' '; | |
| 214 | for c in chars.by_ref() { | |
| 215 | if last == '*' && c == '/' { | |
| 216 | break; | |
| 217 | } | |
| 218 | last = c; | |
| 219 | } | |
| 220 | } | |
| 221 | _ => out.push(c), | |
| 222 | } | |
| 223 | } | |
| 224 | // Trailing commas before a closing bracket. | |
| 225 | let mut cleaned = String::with_capacity(out.len()); | |
| 226 | let chars: Vec<char> = out.chars().collect(); | |
| 227 | let mut in_string = false; | |
| 228 | let mut i = 0; | |
| 229 | while i < chars.len() { | |
| 230 | let c = chars[i]; | |
| 231 | if c == '"' && (i == 0 || chars[i - 1] != '\\') { | |
| 232 | in_string = !in_string; | |
| 233 | } | |
| 234 | if c == ',' && !in_string { | |
| 235 | let next = chars[i + 1..].iter().find(|c| !c.is_whitespace()); | |
| 236 | if matches!(next, Some('}') | Some(']')) { | |
| 237 | i += 1; | |
| 238 | continue; | |
| 239 | } | |
| 240 | } | |
| 241 | cleaned.push(c); | |
| 242 | i += 1; | |
| 243 | } | |
| 244 | cleaned | |
| 245 | } | |
| 246 | ||
| 247 | fn read_wrangler(dir: &Path) -> Result<Option<WranglerConfig>> { | |
| 248 | for name in ["wrangler.jsonc", "wrangler.json"] { | |
| 249 | let path = dir.join(name); | |
| 250 | if path.exists() { | |
| 251 | let text = std::fs::read_to_string(&path)?; | |
| 252 | return Ok(Some( | |
| 253 | serde_json::from_str(&strip_jsonc(&text)).with_context(|| format!("could not read {name}"))?, | |
| 254 | )); | |
| 255 | } | |
| 256 | } | |
| 257 | let path = dir.join("wrangler.toml"); | |
| 258 | if path.exists() { | |
| 259 | let text = std::fs::read_to_string(&path)?; | |
| 260 | return Ok(Some(toml::from_str(&text).context("could not read wrangler.toml")?)); | |
| 261 | } | |
| 262 | Ok(None) | |
| 263 | } | |
| 264 | ||
| 265 | /// How to install the project's dependencies, judged by its lockfile. | |
| 266 | fn install_command(dir: &Path) -> Option<&'static str> { | |
| 267 | if !dir.join("package.json").exists() { | |
| 268 | return None; | |
| 269 | } | |
| 270 | Some(if dir.join("pnpm-lock.yaml").exists() { | |
| 271 | "corepack enable && pnpm install --frozen-lockfile" | |
| 272 | } else if dir.join("yarn.lock").exists() { | |
| 273 | "corepack enable && yarn install" | |
| 274 | } else if dir.join("bun.lockb").exists() || dir.join("bun.lock").exists() { | |
| 275 | "npx --yes bun install" | |
| 276 | } else if dir.join("package-lock.json").exists() { | |
| 277 | "npm ci" | |
| 278 | } else { | |
| 279 | "npm install" | |
| 280 | }) | |
| 281 | } | |
| 282 | ||
| 283 | fn has_build_script(dir: &Path) -> bool { | |
| 284 | std::fs::read_to_string(dir.join("package.json")) | |
| 285 | .ok() | |
| 286 | .and_then(|text| serde_json::from_str::<Value>(&text).ok()) | |
| 287 | .is_some_and(|package| package["scripts"]["build"].is_string()) | |
| 288 | } | |
| 289 | ||
| 290 | /// One file of the site, as Cloudflare's asset upload names it. | |
| 291 | struct Asset { | |
| 292 | path: String, | |
| 293 | hash: String, | |
| 294 | size: u64, | |
| 295 | file: PathBuf, | |
| 296 | } | |
| 297 | ||
| 298 | fn content_type(path: &str) -> &'static str { | |
| 299 | let extension = path.rsplit('.').next().unwrap_or("").to_ascii_lowercase(); | |
| 300 | match extension.as_str() { | |
| 301 | "html" | "htm" => "text/html", | |
| 302 | "css" => "text/css", | |
| 303 | "js" | "mjs" => "application/javascript", | |
| 304 | "json" | "map" => "application/json", | |
| 305 | "svg" => "image/svg+xml", | |
| 306 | "png" => "image/png", | |
| 307 | "jpg" | "jpeg" => "image/jpeg", | |
| 308 | "gif" => "image/gif", | |
| 309 | "webp" => "image/webp", | |
| 310 | "avif" => "image/avif", | |
| 311 | "ico" => "image/x-icon", | |
| 312 | "woff" => "font/woff", | |
| 313 | "woff2" => "font/woff2", | |
| 314 | "ttf" => "font/ttf", | |
| 315 | "txt" => "text/plain", | |
| 316 | "xml" => "application/xml", | |
| 317 | "wasm" => "application/wasm", | |
| 318 | "pdf" => "application/pdf", | |
| 319 | "mp4" => "video/mp4", | |
| 320 | "webm" => "video/webm", | |
| 321 | _ => "application/octet-stream", | |
| 322 | } | |
| 323 | } | |
| 324 | ||
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 325 | /// What is never served from the top of a site: a Pages-style Worker |
| 326 | /// (`_worker.js`, its server code, which a Cloudflare adapter writes beside | |
| 327 | /// the site's files), `_routes.json`, and `.assetsignore`, with each name | |
| 328 | /// that `.assetsignore` lists, as wrangler leaves them out. | |
| 329 | fn ignored_at_root(root: &Path) -> BTreeSet<String> { | |
| 330 | let mut ignored: BTreeSet<String> = ["_worker.js", "_routes.json", ".assetsignore"].map(str::to_owned).into(); | |
| 331 | if let Ok(text) = std::fs::read_to_string(root.join(".assetsignore")) { | |
| 332 | ignored.extend( | |
| 333 | text.lines() | |
| 334 | .map(|line| line.trim().trim_matches('/')) | |
| 335 | .filter(|line| !line.is_empty() && !line.starts_with('#') && !line.contains(['*', '/', '!'])) | |
| 336 | .map(str::to_owned), | |
| 337 | ); | |
| 338 | } | |
| 339 | ignored | |
| 340 | } | |
| 341 | ||
| 342 | /// Every file of the site at `root`, but for what never belongs in one. | |
| 343 | fn collect_site(root: &Path, skip_project: bool) -> Result<Vec<Asset>> { | |
| 344 | let mut assets = Vec::new(); | |
| 345 | collect(root, root, skip_project, &ignored_at_root(root), &mut assets)?; | |
| 346 | Ok(assets) | |
| 347 | } | |
| 348 | ||
| 349 | fn collect(root: &Path, dir: &Path, skip_project: bool, ignored: &BTreeSet<String>, out: &mut Vec<Asset>) -> Result<()> { | |
| What g1t can't do yet, and an open letter to Cloudflare | 350 | for entry in std::fs::read_dir(dir)? { |
| 351 | let entry = entry?; | |
| 352 | let name = entry.file_name().to_string_lossy().into_owned(); | |
| 353 | let path = entry.path(); | |
| 354 | let kind = entry.file_type()?; | |
| 355 | if name == ".git" || (skip_project && (name == "node_modules" || name.starts_with(".g1t"))) { | |
| 356 | continue; | |
| 357 | } | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 358 | if dir == root && ignored.contains(&name) { |
| 359 | continue; | |
| 360 | } | |
| What g1t can't do yet, and an open letter to Cloudflare | 361 | if kind.is_dir() { |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 362 | collect(root, &path, skip_project, ignored, out)?; |
| What g1t can't do yet, and an open letter to Cloudflare | 363 | continue; |
| 364 | } | |
| 365 | if !kind.is_file() || name == "_headers" || name == "_redirects" { | |
| 366 | continue; | |
| 367 | } | |
| 368 | let size = entry.metadata()?.len(); | |
| 369 | let relative = path | |
| 370 | .strip_prefix(root)? | |
| 371 | .to_string_lossy() | |
| 372 | .replace('\\', "/"); | |
| 373 | if size > MAX_FILE_BYTES { | |
| 374 | bail!("{relative} is larger than Cloudflare's 25 MiB limit for one file"); | |
| 375 | } | |
| 376 | let bytes = std::fs::read(&path)?; | |
| 377 | let digest = hex::encode(Sha256::digest(&bytes)); | |
| 378 | out.push(Asset { | |
| 379 | path: format!("/{relative}"), | |
| 380 | hash: digest[..32].to_owned(), | |
| 381 | size, | |
| 382 | file: path, | |
| 383 | }); | |
| 384 | if out.len() > MAX_FILES { | |
| 385 | bail!("the site has more than {MAX_FILES} files, Cloudflare's limit"); | |
| 386 | } | |
| 387 | } | |
| 388 | Ok(()) | |
| 389 | } | |
| 390 | ||
| 391 | /// The upload, as the API sends it: `snake_case`, or `uploadUrl` from | |
| 392 | /// before its bodies were. | |
| 393 | #[derive(Deserialize)] | |
| 394 | struct UploadSession { | |
| 395 | jwt: String, | |
| 396 | #[serde(default)] | |
| 397 | buckets: Vec<Vec<String>>, | |
| 398 | #[serde(alias = "uploadUrl")] | |
| 399 | upload_url: String, | |
| 400 | } | |
| 401 | ||
| 402 | /// Sends one bucket of files with the upload's key. The last bucket's | |
| 403 | /// answer carries the key that completes the upload. | |
| 404 | fn upload_bucket(session: &UploadSession, bucket: &[String], by_hash: &BTreeMap<&str, &Asset>) -> Result<Option<String>> { | |
| 405 | let boundary = format!("g1t-{}", hex::encode(Sha256::digest(bucket.join(",").as_bytes()))[..24].to_owned()); | |
| 406 | let mut body: Vec<u8> = Vec::new(); | |
| 407 | for hash in bucket { | |
| 408 | let asset = by_hash | |
| 409 | .get(hash.as_str()) | |
| 410 | .with_context(|| format!("Cloudflare asked for a file this build does not have ({hash})"))?; | |
| 411 | let bytes = std::fs::read(&asset.file)?; | |
| 412 | body.extend_from_slice( | |
| 413 | format!( | |
| 414 | "--{boundary}\r\nContent-Disposition: form-data; name=\"{hash}\"; filename=\"{hash}\"\r\nContent-Type: {}\r\n\r\n", | |
| 415 | content_type(&asset.path) | |
| 416 | ) | |
| 417 | .as_bytes(), | |
| 418 | ); | |
| 419 | body.extend_from_slice(STANDARD.encode(bytes).as_bytes()); | |
| 420 | body.extend_from_slice(b"\r\n"); | |
| 421 | } | |
| 422 | body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes()); | |
| 423 | let response = ureq::post(&session.upload_url) | |
| 424 | .set("authorization", &format!("Bearer {}", session.jwt)) | |
| 425 | .set("content-type", &format!("multipart/form-data; boundary={boundary}")) | |
| 426 | .send_bytes(&body); | |
| 427 | let response = match response { | |
| 428 | Ok(response) => response, | |
| 429 | Err(ureq::Error::Status(code, response)) => { | |
| 430 | bail!("Cloudflare refused the upload ({code}): {}", response.into_string().unwrap_or_default()) | |
| 431 | } | |
| 432 | Err(error) => bail!("could not upload to Cloudflare: {error}"), | |
| 433 | }; | |
| 434 | let answer: Value = response.into_json().unwrap_or(Value::Null); | |
| 435 | Ok(answer["result"]["jwt"].as_str().map(str::to_owned)) | |
| 436 | } | |
| 437 | ||
| 438 | #[derive(Serialize)] | |
| 439 | #[serde(rename_all = "camelCase")] | |
| 440 | struct Module { | |
| 441 | name: String, | |
| 442 | content_base64: String, | |
| 443 | content_type: String, | |
| 444 | } | |
| 445 | ||
| 446 | /// The bundle `wrangler deploy --dry-run` wrote, main module first. | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 447 | fn bundle_modules(main: &str, bundle: &Path) -> Result<(String, Vec<Module>)> { |
| What g1t can't do yet, and an open letter to Cloudflare | 448 | let stem = Path::new(main) |
| 449 | .file_stem() | |
| 450 | .map(|stem| stem.to_string_lossy().into_owned()) | |
| 451 | .unwrap_or_else(|| "index".to_owned()); | |
| 452 | let mut modules = Vec::new(); | |
| 453 | let mut files = Vec::new(); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 454 | collect_files(bundle, &mut files)?; |
| What g1t can't do yet, and an open letter to Cloudflare | 455 | for file in files { |
| 456 | let name = file | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 457 | .strip_prefix(bundle)? |
| What g1t can't do yet, and an open letter to Cloudflare | 458 | .to_string_lossy() |
| 459 | .replace('\\', "/"); | |
| 460 | let kind = match name.rsplit('.').next().unwrap_or("") { | |
| 461 | "js" | "mjs" => "application/javascript+module", | |
| 462 | "wasm" => "application/wasm", | |
| 463 | "map" | "md" => continue, | |
| 464 | _ => "text/plain", | |
| 465 | }; | |
| 466 | modules.push(Module { | |
| 467 | content_base64: STANDARD.encode(std::fs::read(&file)?), | |
| 468 | content_type: kind.to_owned(), | |
| 469 | name, | |
| 470 | }); | |
| 471 | } | |
| 472 | let main_name = modules | |
| 473 | .iter() | |
| 474 | .map(|module| module.name.clone()) | |
| 475 | .find(|name| *name == format!("{stem}.js") || *name == format!("{stem}.mjs")) | |
| 476 | .or_else(|| { | |
| 477 | modules | |
| 478 | .iter() | |
| 479 | .find(|module| module.content_type == "application/javascript+module") | |
| 480 | .map(|module| module.name.clone()) | |
| 481 | }) | |
| 482 | .context("wrangler wrote no JavaScript module")?; | |
| 483 | Ok((main_name, modules)) | |
| 484 | } | |
| 485 | ||
| 486 | fn collect_files(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> { | |
| 487 | for entry in std::fs::read_dir(dir)? { | |
| 488 | let entry = entry?; | |
| 489 | if entry.file_type()?.is_dir() { | |
| 490 | collect_files(&entry.path(), out)?; | |
| 491 | } else { | |
| 492 | out.push(entry.path()); | |
| 493 | } | |
| 494 | } | |
| 495 | Ok(()) | |
| 496 | } | |
| 497 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 498 | /// What a Workers config asks for that g1t.page does not provision, said |
| 499 | /// as the deployment's warnings: bindings it runs without, and scheduled | |
| 500 | /// (`triggers.crons`) runs that never fire. | |
| 501 | fn config_warnings(config: &WranglerConfig) -> Vec<String> { | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 502 | // An empty list, as a config a framework's build writes has for each |
| 503 | // kind of binding (`"d1_databases": []`), declares nothing. | |
| 504 | fn declares(value: &Value) -> bool { | |
| 505 | match value { | |
| 506 | Value::Null => false, | |
| 507 | Value::Array(items) => !items.is_empty(), | |
| 508 | Value::Object(fields) => fields.values().any(declares), | |
| 509 | _ => true, | |
| 510 | } | |
| 511 | } | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 512 | let mut warnings = Vec::new(); |
| 513 | for binding in UNSUPPORTED_BINDINGS { | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 514 | if config.rest.get(binding).is_some_and(declares) { |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 515 | warnings.push(format!("`{binding}` is not provisioned on g1t.page yet, so the app runs without it.")); |
| 516 | } | |
| 517 | } | |
| 518 | let crons = config | |
| 519 | .rest | |
| 520 | .get("triggers") | |
| 521 | .and_then(|triggers| triggers.get("crons")) | |
| 522 | .and_then(Value::as_array) | |
| 523 | .map_or(0, |crons| crons.iter().filter(|cron| cron.as_str().is_some_and(|c| !c.trim().is_empty())).count()); | |
| 524 | if crons > 0 { | |
| 525 | let schedules = if crons == 1 { "schedule" } else { "schedules" }; | |
| 526 | warnings.push(format!( | |
| 527 | "`triggers.crons` ({crons} {schedules}) is not set up on g1t.page yet, so the Worker's `scheduled` handler never runs." | |
| 528 | )); | |
| 529 | } | |
| 530 | warnings | |
| 531 | } | |
| 532 | ||
| What g1t can't do yet, and an open letter to Cloudflare | 533 | /// What was built: the Worker's code and settings, and where its site is. |
| 534 | struct Built { | |
| 535 | worker: Value, | |
| 536 | assets_dir: Option<PathBuf>, | |
| 537 | warnings: Vec<String>, | |
| 538 | /// What kind of project it was found to be, for its settings page: | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 539 | /// `workers`, `static` (a build's output), `html` (the files as they |
| 540 | /// are) or `container`. | |
| What g1t can't do yet, and an open letter to Cloudflare | 541 | detected: &'static str, |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 542 | /// The framework it was found to be built with, if any (`astro`…), or |
| 543 | /// for a container what it was built from (`dockerfile`, `flask`…). | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 544 | framework: Option<&'static str>, |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 545 | /// For a container: the image pushed, its digest, port and size. |
| 546 | container: Option<Value>, | |
| What g1t can't do yet, and an open letter to Cloudflare | 547 | } |
| 548 | ||
| 549 | /// What a project without a Workers config is: a site it built, or its own | |
| 550 | /// files served as they are. | |
| 551 | fn static_kind(project: &Path, assets_dir: &Path, built: bool) -> &'static str { | |
| 552 | if !built && assets_dir == project { "html" } else { "static" } | |
| 553 | } | |
| 554 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 555 | /// The command that bundles a Workers project, without deploying it, |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 556 | /// into `bundle` when given, by the config at `config` when given. |
| 557 | fn wrangler_build(bundle: Option<&Path>, config: Option<&Path>) -> String { | |
| 558 | let mut command = "npx --yes wrangler@4 deploy --dry-run".to_owned(); | |
| 559 | if let Some(config) = config { | |
| 560 | command.push_str(&format!(" --config {}", config.display())); | |
| 561 | } | |
| 562 | if let Some(bundle) = bundle { | |
| 563 | command.push_str(&format!(" --outdir {}", bundle.display())); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 564 | } |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 565 | command |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 566 | } |
| 567 | ||
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 568 | /// The config a framework's build wrote for wrangler, which wrangler reads |
| 569 | /// in place of the project's own (`.wrangler/deploy/config.json`, as | |
| 570 | /// `@cloudflare/vite-plugin` writes it), with the folder its paths are | |
| 571 | /// relative to. | |
| 572 | fn redirected_config(dir: &Path) -> Result<Option<(WranglerConfig, PathBuf)>> { | |
| 573 | let pointer = dir.join(".wrangler/deploy/config.json"); | |
| 574 | let Ok(text) = std::fs::read_to_string(&pointer) else { return Ok(None) }; | |
| 575 | let Some(path) = serde_json::from_str::<Value>(&text).ok().and_then(|v| v["configPath"].as_str().map(str::to_owned)) else { | |
| 576 | return Ok(None); | |
| 577 | }; | |
| 578 | let path = dir.join(".wrangler/deploy").join(path); | |
| 579 | let path = path.canonicalize().unwrap_or(path); | |
| 580 | let text = std::fs::read_to_string(&path).with_context(|| format!("could not read {}", path.display()))?; | |
| 581 | let config = serde_json::from_str(&strip_jsonc(&text)).with_context(|| format!("could not read {}", path.display()))?; | |
| 582 | Ok(Some((config, path.parent().map_or_else(|| dir.to_path_buf(), Path::to_path_buf)))) | |
| 583 | } | |
| 584 | ||
| 585 | /// The entry of the Worker a framework's adapter wrote, once it has built: | |
| 586 | /// the file, or a directory's `index.js`. | |
| 587 | fn adapter_entry(dir: &Path, adapter: &Adapter) -> Option<PathBuf> { | |
| 588 | let entry = dir.join(adapter.entry); | |
| 589 | if entry.is_file() { | |
| 590 | return Some(entry); | |
| 591 | } | |
| 592 | ["index.js", "index.mjs"].iter().map(|name| entry.join(name)).find(|index| index.is_file()) | |
| 593 | } | |
| 594 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 595 | /// The first of the usual output directories a static build wrote (or the |
| 596 | /// project's own `index.html`), relative to the project: `.` for itself. | |
| 597 | fn find_output(dir: &Path) -> Option<String> { | |
| 598 | OUTPUT_DIRS | |
| 599 | .iter() | |
| 600 | .find(|name| { | |
| 601 | let path = dir.join(name); | |
| 602 | path.join("index.html").exists() || (path.is_dir() && **name != "public") | |
| 603 | }) | |
| 604 | .map(|name| (*name).to_owned()) | |
| 605 | .or_else(|| dir.join("index.html").exists().then(|| ".".to_owned())) | |
| 606 | } | |
| 607 | ||
| 608 | /// What a build of the project at `dir` does, found before anything runs: | |
| 609 | /// what it is, how its dependencies are installed, what builds it, and | |
| 610 | /// where what it serves is. For a site with a build of its own, the output | |
| 611 | /// directory may only exist once it has run; then `output_dir` is `None` | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 612 | /// unless one was chosen or the framework says where it writes. |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 613 | #[derive(Debug, PartialEq, Eq)] |
| 614 | pub(crate) struct Detected { | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 615 | /// `workers`, `static`, `html`, `container`, or `none` when there is |
| 616 | /// nothing to serve. | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 617 | pub kind: &'static str, |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 618 | /// The framework it is built with, if g1t knows it. |
| 619 | pub framework: Option<Framework>, | |
| 620 | pub install: Option<String>, | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 621 | pub build: Option<String>, |
| 622 | pub output_dir: Option<String>, | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 623 | /// Why it cannot be served as it is set up, and what to change: the |
| 624 | /// kind is then `none`. | |
| 625 | pub problem: Option<String>, | |
| 626 | /// What runs before wrangler bundles a Workers project; for anything | |
| 627 | /// else, `build`. | |
| 628 | pre_build: Option<String>, | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 629 | /// For a container: its Dockerfile, or the one g1t writes. |
| 630 | pub container: Option<ContainerBuild>, | |
| 631 | /// What it would be served as without its Dockerfile, when it is a | |
| 632 | /// container only for having one: built so while g1t.page runs no | |
| 633 | /// containers (`not_enabled` from the registry step). | |
| 634 | fallback: Option<Box<Detected>>, | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 635 | } |
| 636 | ||
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 637 | /// How a container is built. |
| 638 | #[derive(Debug, Clone, PartialEq, Eq)] | |
| 639 | pub(crate) struct ContainerBuild { | |
| 640 | /// The project's own Dockerfile, relative to the project; none when | |
| 641 | /// g1t writes one. | |
| 642 | pub dockerfile: Option<String>, | |
| 643 | /// The Dockerfile g1t writes, for a server it recognises. | |
| 644 | pub generated: Option<container::Server>, | |
| 645 | /// What it listens on: its Dockerfile's `EXPOSE`, else 8080. `PORT` | |
| 646 | /// is set to it. | |
| 647 | pub port: u16, | |
| 648 | } | |
| 649 | ||
| 650 | impl Detected { | |
| 651 | fn nothing(framework: Option<Framework>, install: Option<String>, problem: String) -> Detected { | |
| 652 | Detected { | |
| 653 | kind: "none", | |
| 654 | framework, | |
| 655 | install, | |
| 656 | build: None, | |
| 657 | output_dir: None, | |
| 658 | problem: Some(problem), | |
| 659 | pre_build: None, | |
| 660 | container: None, | |
| 661 | fallback: None, | |
| 662 | } | |
| 663 | } | |
| 664 | ||
| 665 | /// The `framework` `g1t-runner detect` prints: the framework, else for | |
| 666 | /// a container the server g1t wrote a Dockerfile for (`flask`…) or | |
| 667 | /// `dockerfile`. | |
| 668 | pub(crate) fn framework_id(&self) -> Option<&'static str> { | |
| 669 | self.framework.as_ref().map(|f| f.id).or_else(|| { | |
| 670 | self.container.as_ref().map(|c| c.generated.as_ref().map_or("dockerfile", |server| server.id)) | |
| 671 | }) | |
| 672 | } | |
| 673 | } | |
| 674 | ||
| 675 | /// Why nothing could be found to serve, and what to add. | |
| 676 | const NOTHING_TO_SERVE: &str = "found nothing to serve: no Workers config, no Dockerfile, no index.html, and none of dist, build, out, \ | |
| 677 | public, _site or .output/public. Add a `Dockerfile` to deploy it as a container, or set **Build command** and \ | |
| 678 | **Output directory** for a static site."; | |
| 679 | ||
| 680 | /// The project as a container: its own Dockerfile, else one g1t writes | |
| 681 | /// for `framework` or for a server it recognises. None when it has neither. | |
| 682 | fn as_container(dir: &Path, framework: Option<Framework>, own: Option<String>) -> Option<Detected> { | |
| 683 | let build = match own { | |
| 684 | Some(path) => { | |
| 685 | let text = std::fs::read_to_string(dir.join(&path)).unwrap_or_default(); | |
| 686 | ContainerBuild { port: container::exposed_port(&text).unwrap_or(container::DEFAULT_PORT), dockerfile: Some(path), generated: None } | |
| 687 | } | |
| 688 | None => { | |
| 689 | let server = match &framework { | |
| 690 | // One set up for Workers (OpenNext's adapter) is not run as a server. | |
| 691 | Some(framework) if framework.workers => None, | |
| 692 | Some(framework) => container::for_framework(dir, framework.id), | |
| 693 | None => container::server(dir), | |
| 694 | }?; | |
| 695 | ContainerBuild { dockerfile: None, generated: Some(server), port: container::DEFAULT_PORT } | |
| 696 | } | |
| 697 | }; | |
| 698 | let file = build.dockerfile.clone().unwrap_or_else(|| "Dockerfile (written by g1t)".to_owned()); | |
| 699 | Some(Detected { | |
| 700 | kind: "container", | |
| 701 | framework, | |
| 702 | // The image installs what it needs: nothing runs before its build. | |
| 703 | install: None, | |
| 704 | build: Some(format!("docker build -f {file} .")), | |
| 705 | output_dir: None, | |
| 706 | problem: None, | |
| 707 | pre_build: None, | |
| 708 | container: Some(build), | |
| 709 | fallback: None, | |
| 710 | }) | |
| 711 | } | |
| 712 | ||
| 713 | #[cfg(test)] | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 714 | pub(crate) fn detect(dir: &Path, custom_build: Option<&str>, chosen_output: Option<&str>) -> Result<Detected> { |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 715 | detect_as(dir, custom_build, chosen_output, BuildAs::Auto, None) |
| 716 | } | |
| 717 | ||
| 718 | /// What the project is, by its **Build as** setting (`build_as`) and its | |
| 719 | /// Dockerfile's path (`dockerfile`, when it chose one): | |
| 720 | /// | |
| 721 | /// - **Docker**: a container, from its Dockerfile or one g1t writes. | |
| 722 | /// - **Static site**: built and served as files, whatever else it has. | |
| 723 | /// - **Workers**: its Wrangler config or a framework adapter's Worker. | |
| 724 | /// - **Auto**: a Wrangler config is a Workers project; a framework that | |
| 725 | /// builds a static site or a Worker is that, which costs nothing while | |
| 726 | /// no one visits; a framework set up to need a server is a container | |
| 727 | /// when it has a Dockerfile, or when g1t can write one; a project with | |
| 728 | /// no framework g1t knows is a container when it has a Dockerfile or is | |
| 729 | /// a server g1t recognises (a Node.js, Python, Go, Ruby or PHP app), | |
| 730 | /// else a static site, else nothing, saying to add a Dockerfile. | |
| 731 | pub(crate) fn detect_as( | |
| 732 | dir: &Path, | |
| 733 | custom_build: Option<&str>, | |
| 734 | chosen_output: Option<&str>, | |
| 735 | build_as: BuildAs, | |
| 736 | dockerfile: Option<&str>, | |
| 737 | ) -> Result<Detected> { | |
| 738 | let own = container::own_dockerfile(dir, dockerfile)?; | |
| 739 | match build_as { | |
| 740 | BuildAs::Docker => { | |
| 741 | let framework = frameworks::detect(dir, read_wrangler(dir)?.is_some()); | |
| 742 | Ok(as_container(dir, framework.clone(), own).unwrap_or_else(|| { | |
| 743 | Detected::nothing( | |
| 744 | framework, | |
| 745 | None, | |
| 746 | "**Build as** is Docker, but this project has no Dockerfile and g1t doesn't recognise a server it can write \ | |
| 747 | one for. Add a `Dockerfile` (or set its path under Settings, Deployments)." | |
| 748 | .to_owned(), | |
| 749 | ) | |
| 750 | })) | |
| 751 | } | |
| 752 | BuildAs::Static => detect_site(dir, custom_build, chosen_output, true), | |
| 753 | BuildAs::Workers => { | |
| 754 | let site = detect_site(dir, custom_build, chosen_output, false)?; | |
| 755 | if site.kind == "workers" { | |
| 756 | return Ok(site); | |
| 757 | } | |
| 758 | Ok(Detected::nothing( | |
| 759 | site.framework, | |
| 760 | site.install, | |
| 761 | "**Build as** is Workers, but this project has no Wrangler config (`wrangler.jsonc`, `wrangler.json` or \ | |
| 762 | `wrangler.toml`) and no framework adapter that writes a Worker." | |
| 763 | .to_owned(), | |
| 764 | )) | |
| 765 | } | |
| 766 | BuildAs::Auto => { | |
| 767 | let site = detect_site(dir, custom_build, chosen_output, false)?; | |
| 768 | if site.kind == "workers" || (site.framework.is_some() && site.problem.is_none()) { | |
| 769 | return Ok(site); | |
| 770 | } | |
| 771 | if let Some(problem) = site.problem.clone() { | |
| 772 | // A framework set up to need a server. | |
| 773 | let found = as_container(dir, site.framework.clone(), own); | |
| 774 | return Ok(found.unwrap_or_else(|| { | |
| 775 | Detected::nothing(site.framework, site.install, format!("{problem} Or add a `Dockerfile` to run it as a container.")) | |
| 776 | })); | |
| 777 | } | |
| 778 | let has_own = own.is_some(); | |
| 779 | if let Some(mut found) = as_container(dir, None, own) { | |
| 780 | // A Dockerfile beside a site that could be served as it is. | |
| 781 | if has_own && (site.kind == "static" || site.kind == "html") { | |
| 782 | found.fallback = Some(Box::new(site)); | |
| 783 | } | |
| 784 | return Ok(found); | |
| 785 | } | |
| 786 | if site.kind == "none" { | |
| 787 | return Ok(Detected::nothing(site.framework, site.install, NOTHING_TO_SERVE.to_owned())); | |
| 788 | } | |
| 789 | Ok(site) | |
| 790 | } | |
| 791 | } | |
| 792 | } | |
| 793 | ||
| 794 | /// What the project is as a static site or Workers project: `as_static` | |
| 795 | /// ignores its Wrangler config and any framework adapter, and builds it | |
| 796 | /// even when its framework is set up to need a server. | |
| 797 | fn detect_site(dir: &Path, custom_build: Option<&str>, chosen_output: Option<&str>, as_static: bool) -> Result<Detected> { | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 798 | let custom_build = custom_build.map(str::trim).filter(|c| !c.is_empty()); |
| 799 | let chosen_output = chosen_output.map(|d| d.trim().trim_matches('/')).filter(|d| !d.is_empty()); | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 800 | let config = if as_static { None } else { read_wrangler(dir)? }; |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 801 | let framework = frameworks::detect(dir, config.is_some()); |
| 802 | let installs: Vec<&str> = [install_command(dir), framework.as_ref().and_then(|f| f.install)].into_iter().flatten().collect(); | |
| 803 | let install = (!installs.is_empty()).then(|| installs.join(" && ")); | |
| 804 | let framework_build = framework.as_ref().and_then(|f| f.build.clone()); | |
| 805 | if let Some(config) = config { | |
| 806 | // A framework built for Workers builds before wrangler bundles what | |
| 807 | // it wrote; any other project's config builds it, as it always has. | |
| 808 | let pre_build = custom_build | |
| 809 | .map(str::to_owned) | |
| 810 | .or_else(|| framework.as_ref().filter(|f| f.workers).and(framework_build)); | |
| 811 | let mut steps: Vec<String> = pre_build.clone().into_iter().collect(); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 812 | if config.main.is_some() { |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 813 | steps.push(wrangler_build(None, None)); |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 814 | } |
| 815 | return Ok(Detected { | |
| 816 | kind: "workers", | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 817 | framework, |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 818 | install, |
| 819 | build: (!steps.is_empty()).then(|| steps.join(" && ")), | |
| 820 | output_dir: config.assets.and_then(|assets| assets.directory), | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 821 | problem: None, |
| 822 | pre_build, | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 823 | container: None, |
| 824 | fallback: None, | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 825 | }); |
| 826 | } | |
| 827 | let build = custom_build | |
| 828 | .map(str::to_owned) | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 829 | .or(framework_build) |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 830 | .or_else(|| has_build_script(dir).then(|| "npm run build".to_owned())); |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 831 | // An adapter's Worker, unless the project chose a folder to serve. |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 832 | if !as_static && chosen_output.is_none() && framework.as_ref().is_some_and(|f| f.adapter.is_some()) { |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 833 | // Where its files are is known once it has built: the adapter's |
| 834 | // own folder, or where the config it writes says. | |
| 835 | return Ok(Detected { | |
| 836 | kind: "workers", | |
| 837 | output_dir: None, | |
| 838 | framework, | |
| 839 | install, | |
| 840 | pre_build: build.clone(), | |
| 841 | build, | |
| 842 | problem: None, | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 843 | container: None, |
| 844 | fallback: None, | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 845 | }); |
| 846 | } | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 847 | // A command or a folder of the project's own says it knows better, as |
| 848 | // does **Build as** set to Static site. | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 849 | let problem = framework |
| 850 | .as_ref() | |
| 851 | .and_then(|f| f.problem.clone()) | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 852 | .filter(|_| !as_static && custom_build.is_none() && chosen_output.is_none()); |
| 853 | if let Some(problem) = problem { | |
| 854 | return Ok(Detected::nothing(framework, install, problem)); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 855 | } |
| 856 | let output_dir = chosen_output | |
| 857 | .map(str::to_owned) | |
| 858 | .or_else(|| framework.as_ref().and_then(|f| f.output.clone())) | |
| 859 | .or_else(|| find_output(dir)); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 860 | // `.` is the project itself: its own files, served as they are. |
| 861 | let kind = match (&build, output_dir.as_deref()) { | |
| 862 | (None, Some(".")) => "html", | |
| 863 | (None, None) => "none", | |
| 864 | _ => "static", | |
| 865 | }; | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 866 | Ok(Detected { kind, framework, install, pre_build: build.clone(), build, output_dir, problem: None, container: None, fallback: None }) |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 867 | } |
| 868 | ||
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 869 | /// The compatibility date a Worker gets when its config names none. |
| 870 | const COMPATIBILITY_DATE: &str = "2026-09-26"; | |
| 871 | ||
| 872 | /// A Workers project bundled by wrangler: `config` with its paths relative | |
| 873 | /// to `base`, read by wrangler from `config_file` when given (else the | |
| 874 | /// project's own, or the one its build wrote). | |
| 875 | fn build_worker( | |
| 876 | log: &mut Log, | |
| 877 | secrets: &[String], | |
| 878 | config: WranglerConfig, | |
| 879 | base: &Path, | |
| 880 | config_file: Option<&Path>, | |
| 881 | ) -> Result<(Value, Option<PathBuf>, Vec<String>)> { | |
| 882 | let bundle = bundle_dir(); | |
| 883 | let warnings = config_warnings(&config); | |
| 884 | let mut worker = json!({ | |
| 885 | "compatibilityDate": config.compatibility_date.clone().unwrap_or_else(|| COMPATIBILITY_DATE.to_owned()), | |
| 886 | "compatibilityFlags": config.compatibility_flags, | |
| 887 | "vars": config.vars, | |
| 888 | }); | |
| 889 | if let Some(main) = &config.main { | |
| 890 | // `--dry-run` runs the config's own build and bundles it, without | |
| 891 | // deploying anywhere. | |
| 892 | let _ = std::fs::remove_dir_all(&bundle); | |
| 893 | step(log, &wrangler_build(Some(&bundle), config_file), secrets)?; | |
| 894 | let (main_module, modules) = bundle_modules(main, &bundle)?; | |
| 895 | worker["mainModule"] = main_module.into(); | |
| 896 | worker["modules"] = serde_json::to_value(modules)?; | |
| 897 | } | |
| 898 | let assets = config.assets.unwrap_or_default(); | |
| 899 | let assets_dir = assets.directory.as_ref().map(|directory| base.join(directory)); | |
| 900 | worker["assetsBinding"] = assets.binding.into(); | |
| 901 | worker["htmlHandling"] = assets.html_handling.into(); | |
| 902 | worker["notFoundHandling"] = assets.not_found_handling.into(); | |
| 903 | Ok((worker, assets_dir, warnings)) | |
| 904 | } | |
| 905 | ||
| 906 | /// Where the config for an adapter's Worker is written: beside the | |
| 907 | /// bundle, never in the checkout. | |
| 908 | fn adapter_config_path() -> PathBuf { | |
| 909 | let bundle = bundle_dir(); | |
| 910 | bundle.with_file_name(format!("{}.wrangler.json", bundle.file_name().unwrap_or_default().to_string_lossy())) | |
| 911 | } | |
| 912 | ||
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 913 | /// What a container build says when g1t.page runs no containers. |
| 914 | const CONTAINERS_OFF: &str = "This project deploys as a container, and Docker deployments are not switched on for g1t.page yet. \ | |
| 915 | Set **Build as** to Static site or Workers under Settings, Deployments if it can be built as one."; | |
| 916 | ||
| 917 | /// Where a container's registry login is kept for the build: its own | |
| 918 | /// folder, never the job's `~/.docker`. | |
| 919 | fn docker_config_dir() -> PathBuf { | |
| 920 | bundle_dir().with_file_name(format!("{}.docker", bundle_dir().file_name().unwrap_or_default().to_string_lossy())) | |
| 921 | } | |
| 922 | ||
| 923 | /// Where the Dockerfile g1t writes for a project without one goes: beside | |
| 924 | /// the bundle, never in the checkout. | |
| 925 | fn generated_dockerfile_path() -> PathBuf { | |
| 926 | bundle_dir().with_file_name(format!("{}.Dockerfile", bundle_dir().file_name().unwrap_or_default().to_string_lossy())) | |
| 927 | } | |
| 928 | ||
| 929 | /// What the deployments service answers the `registry` step with: where | |
| 930 | /// to push the app's image, with a login good for pushing only and only | |
| 931 | /// for minutes, and the largest image the app's instance can run. | |
| 932 | #[derive(Debug, Deserialize)] | |
| 933 | struct RegistryGrant { | |
| 934 | registry: String, | |
| 935 | repository: String, | |
| 936 | tag: String, | |
| 937 | username: String, | |
| 938 | password: String, | |
| 939 | #[serde(alias = "maxImageBytes")] | |
| 940 | max_image_bytes: u64, | |
| 941 | #[serde(default, alias = "instanceType")] | |
| 942 | instance_type: Option<String>, | |
| 943 | } | |
| 944 | ||
| 945 | /// `name@sha256:…` for `repository` among an image's repository digests, | |
| 946 | /// as `docker image inspect` lists them, one a line. | |
| 947 | fn pushed_digest(repo_digests: &str, registry: &str, repository: &str) -> Option<String> { | |
| 948 | let prefix = format!("{registry}/{repository}@sha256:"); | |
| 949 | repo_digests | |
| 950 | .lines() | |
| 951 | .map(str::trim) | |
| 952 | .find(|line| line.starts_with(&prefix) && line.len() == prefix.len() + 64) | |
| 953 | .map(str::to_owned) | |
| 954 | } | |
| 955 | ||
| 956 | /// Bytes, as a person reads them: `1.4 GB`. | |
| 957 | fn size_text(bytes: u64) -> String { | |
| 958 | format!("{:.1} GB", bytes as f64 / 1_000_000_000.0) | |
| 959 | } | |
| 960 | ||
| 961 | /// Runs `docker` with `args`, signed in through `config`; its output, or | |
| 962 | /// why it failed, redacted. | |
| 963 | fn docker(args: &[&str], config: &Path, input: Option<&str>, secrets: &[String]) -> Result<String> { | |
| 964 | use std::io::Write; | |
| 965 | use std::process::{Command, Stdio}; | |
| 966 | let mut command = Command::new("docker"); | |
| 967 | command.args(args).env("DOCKER_CONFIG", config).stdout(Stdio::piped()).stderr(Stdio::piped()); | |
| 968 | command.stdin(if input.is_some() { Stdio::piped() } else { Stdio::null() }); | |
| 969 | let mut child = command.spawn().context("could not run docker")?; | |
| 970 | if let (Some(input), Some(mut stdin)) = (input, child.stdin.take()) { | |
| 971 | stdin.write_all(input.as_bytes())?; | |
| 972 | } | |
| 973 | let output = child.wait_with_output()?; | |
| 974 | let text = redact(&format!("{}{}", String::from_utf8_lossy(&output.stdout), String::from_utf8_lossy(&output.stderr)), secrets); | |
| 975 | if !output.status.success() { | |
| 976 | bail!("`docker {}` failed: {}", args.first().copied().unwrap_or_default(), text.trim()); | |
| 977 | } | |
| 978 | Ok(text) | |
| 979 | } | |
| 980 | ||
| 981 | /// Builds the project's container image with Docker and pushes it to | |
| 982 | /// Cloudflare's registry, at the place and with the short-lived push-only | |
| 983 | /// login the deployments service gives for this build (`registry`). The | |
| 984 | /// sandbox never holds g1t's own Cloudflare token. None when g1t.page runs | |
| 985 | /// no containers (the service answers `not_enabled`). | |
| 986 | fn container_build(reporter: &Reporter, log: &mut Log, secrets: &[String], dir: &Path, plan: &Detected) -> Result<Option<Value>> { | |
| 987 | let build = plan.container.as_ref().context("not a container")?; | |
| 988 | // Asked first: a build that could not be pushed is not run. | |
| 989 | let answer = reporter.send("registry", json!({}))?; | |
| 990 | if answer["ok"] == false { | |
| 991 | if answer["error"]["code"] == "not_enabled" { | |
| 992 | return Ok(None); | |
| 993 | } | |
| 994 | bail!("{}", answer["error"]["message"].as_str().unwrap_or("g1t refused the image's upload")); | |
| 995 | } | |
| 996 | let grant: RegistryGrant = | |
| 997 | serde_json::from_value(answer["value"].clone()).context("g1t's answer to the image's upload was not understood")?; | |
| 998 | let mut secrets = secrets.to_vec(); | |
| 999 | secrets.push(grant.password.clone()); | |
| 1000 | let dockerfile = match (&build.dockerfile, &build.generated) { | |
| 1001 | (Some(own), _) => { | |
| 1002 | log.line(&format!("Building a container from {own}, listening on port {}.", build.port)); | |
| 1003 | dir.join(own) | |
| 1004 | } | |
| 1005 | (None, Some(server)) => { | |
| 1006 | // Shown whole, so it can be committed and changed. | |
| 1007 | log.line(&format!("This {} project has no Dockerfile, so g1t wrote this one (commit it as `Dockerfile` to change it):", server.name)); | |
| 1008 | for line in server.dockerfile.lines() { | |
| 1009 | log.line(&format!(" {line}")); | |
| 1010 | } | |
| 1011 | let path = generated_dockerfile_path(); | |
| 1012 | std::fs::write(&path, &server.dockerfile)?; | |
| 1013 | // BuildKit reads `<Dockerfile>.dockerignore` beside it. | |
| 1014 | std::fs::write(path.with_file_name(format!("{}.dockerignore", path.file_name().unwrap_or_default().to_string_lossy())), container::DOCKERIGNORE)?; | |
| 1015 | path | |
| 1016 | } | |
| 1017 | (None, None) => bail!("nothing to build the container from"), | |
| 1018 | }; | |
| 1019 | if std::env::var("BUILD_COMMAND").is_ok_and(|c| !c.trim().is_empty()) { | |
| 1020 | log.line("Build command is not used for a container: its Dockerfile builds it."); | |
| 1021 | } | |
| 1022 | // A sandbox's own Docker Engine, started for the build; in a workflow | |
| 1023 | // step, the job's. | |
| 1024 | if clones() { | |
| 1025 | if std::env::var("G1T_DOCKER").as_deref() != Ok("on") { | |
| 1026 | bail!("Docker is turned off for g1t's sandboxes here, so a container cannot be built."); | |
| 1027 | } | |
| 1028 | crate::docker::engine::enable(crate::docker::engine::Options::default()) | |
| 1029 | .map_err(|problem| anyhow::anyhow!("Docker is not available in this sandbox: {problem}"))?; | |
| 1030 | let started = crate::docker::engine::ensure_started(); | |
| 1031 | for note in crate::docker::take_notes() { | |
| 1032 | log.line(¬e); | |
| 1033 | } | |
| 1034 | started.map_err(|problem| anyhow::anyhow!("Docker could not start: {problem}"))?; | |
| 1035 | } | |
| 1036 | let config = docker_config_dir(); | |
| 1037 | std::fs::create_dir_all(&config)?; | |
| 1038 | let image = format!("{}/{}:{}", grant.registry, grant.repository, grant.tag); | |
| 1039 | let quote = |path: &Path| format!("'{}'", path.display().to_string().replace('\'', r"'\''")); | |
| 1040 | let built = step( | |
| 1041 | log, | |
| 1042 | &format!("docker build --platform linux/amd64 --pull -f {} -t {image} .", quote(&dockerfile)), | |
| 1043 | &secrets, | |
| 1044 | ); | |
| 1045 | let pushed = built.and_then(|()| { | |
| 1046 | let size: u64 = docker(&["image", "inspect", "--format", "{{.Size}}", &image], &config, None, &secrets)?.trim().parse().unwrap_or(0); | |
| 1047 | log.line(&format!("The image is {}.", size_text(size))); | |
| 1048 | if size > grant.max_image_bytes { | |
| 1049 | bail!( | |
| 1050 | "The image is {}, more than the {} an app of its size ({}) can hold. Make it smaller (a slim base image, a multi-stage build), or choose a larger instance under Settings, Deployments.", | |
| 1051 | size_text(size), | |
| 1052 | size_text(grant.max_image_bytes), | |
| 1053 | grant.instance_type.as_deref().unwrap_or("its instance type"), | |
| 1054 | ); | |
| 1055 | } | |
| 1056 | docker(&["login", &grant.registry, "--username", &grant.username, "--password-stdin"], &config, Some(&grant.password), &secrets)?; | |
| 1057 | log.line(&format!("$ docker push {image}")); | |
| 1058 | let output = docker(&["push", &image], &config, None, &secrets)?; | |
| 1059 | log.line(output.trim_end()); | |
| 1060 | let digests = docker(&["image", "inspect", "--format", "{{join .RepoDigests \"\\n\"}}", &image], &config, None, &secrets)?; | |
| 1061 | let reference = pushed_digest(&digests, &grant.registry, &grant.repository).context("the pushed image's digest was not found")?; | |
| 1062 | Ok(json!({ | |
| 1063 | "image": reference, | |
| 1064 | "port": build.port, | |
| 1065 | "sizeBytes": size, | |
| 1066 | "dockerfile": build.dockerfile, | |
| 1067 | "generated": build.generated.as_ref().map(|server| server.id), | |
| 1068 | })) | |
| 1069 | }); | |
| 1070 | let _ = std::fs::remove_dir_all(&config); | |
| 1071 | let _ = std::fs::remove_file(generated_dockerfile_path()); | |
| 1072 | pushed.map(Some) | |
| 1073 | } | |
| 1074 | ||
| 1075 | fn build(reporter: &Reporter, log: &mut Log, secrets: &[String]) -> Result<Built> { | |
| What g1t can't do yet, and an open letter to Cloudflare | 1076 | let project = project_dir(); |
| 1077 | let dir = project.as_path(); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1078 | let custom_build = std::env::var("BUILD_COMMAND").ok(); |
| 1079 | let chosen = std::env::var("OUTPUT_DIR").ok().filter(|d| !d.trim().is_empty()); | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1080 | let build_as = BuildAs::parse(&std::env::var("BUILD_AS").unwrap_or_default()); |
| 1081 | let dockerfile = std::env::var("DOCKERFILE").ok(); | |
| 1082 | let mut plan = detect_as(dir, custom_build.as_deref(), chosen.as_deref(), build_as, dockerfile.as_deref())?; | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1083 | if let Some(framework) = &plan.framework { |
| 1084 | log.line(&format!("Detected {}: framework={}.", framework.name, framework.id)); | |
| 1085 | } | |
| 1086 | // Said before anything is installed: it would fail after, less clearly. | |
| 1087 | if let Some(problem) = &plan.problem { | |
| 1088 | bail!("{problem}"); | |
| 1089 | } | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1090 | if plan.container.is_some() { |
| 1091 | match container_build(reporter, log, secrets, dir, &plan)? { | |
| 1092 | Some(image) => { | |
| 1093 | return Ok(Built { | |
| 1094 | worker: json!({}), | |
| 1095 | assets_dir: None, | |
| 1096 | warnings: Vec::new(), | |
| 1097 | detected: "container", | |
| 1098 | framework: plan.framework_id(), | |
| 1099 | container: Some(image), | |
| 1100 | }); | |
| 1101 | } | |
| 1102 | // g1t.page runs no containers yet: the site it is without its | |
| 1103 | // Dockerfile, as it was built before. | |
| 1104 | None => match plan.fallback.take() { | |
| 1105 | Some(site) => { | |
| 1106 | log.line(&format!("Building it as a {} site instead, as it is without its Dockerfile.", if site.kind == "html" { "plain HTML" } else { "static" })); | |
| 1107 | plan = *site; | |
| 1108 | } | |
| 1109 | None => bail!("{CONTAINERS_OFF}"), | |
| 1110 | }, | |
| 1111 | } | |
| 1112 | } | |
| 1113 | let framework_id = plan.framework.as_ref().map(|f| f.id); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1114 | if let Some(install) = &plan.install { |
| What g1t can't do yet, and an open letter to Cloudflare | 1115 | step(log, install, secrets)?; |
| 1116 | } | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1117 | if let Some(command) = &plan.pre_build { |
| 1118 | step(log, command, secrets)?; | |
| 1119 | } | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1120 | // A static site (by **Build as**, or found to be one) never reads a |
| 1121 | // Workers config. | |
| 1122 | let workers = plan.kind == "workers"; | |
| 1123 | let own_config = if workers { read_wrangler(dir)? } else { None }; | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1124 | // A framework built for Workers may write the config wrangler reads |
| 1125 | // (`@cloudflare/vite-plugin` does, and Astro's adapter with it), with | |
| 1126 | // or without one of the project's own. | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1127 | let written = if own_config.is_some() || (workers && plan.framework.as_ref().is_some_and(|f| f.workers)) { |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1128 | redirected_config(dir)? |
| 1129 | } else { | |
| 1130 | None | |
| 1131 | }; | |
| 1132 | if let Some((config, base)) = written { | |
| 1133 | log.line(&format!("Using the Workers config the build wrote, in {}.", base.display())); | |
| 1134 | let (worker, assets_dir, warnings) = build_worker(log, secrets, config, &base, None)?; | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1135 | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id, container: None }); |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1136 | } |
| 1137 | if let Some(config) = own_config { | |
| 1138 | let (worker, assets_dir, warnings) = build_worker(log, secrets, config, dir, None)?; | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1139 | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id, container: None }); |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1140 | } |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1141 | let adapter = plan.framework.as_ref().and_then(|f| f.adapter.as_ref()).filter(|_| workers && chosen.is_none()); |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1142 | if let Some(adapter) = adapter |
| 1143 | && let Some(entry) = adapter_entry(dir, adapter) | |
| 1144 | { | |
| 1145 | // The adapter's Worker, bundled as a Workers project's is. Its | |
| 1146 | // static files are uploaded by g1t, so wrangler is given none. | |
| 1147 | let file = adapter_config_path(); | |
| 1148 | let flags = vec!["nodejs_compat".to_owned()]; | |
| 1149 | std::fs::write( | |
| 1150 | &file, | |
| 1151 | serde_json::to_string_pretty(&json!({ | |
| 1152 | "name": "g1t-app", | |
| 1153 | "main": entry, | |
| 1154 | "compatibility_date": COMPATIBILITY_DATE, | |
| 1155 | "compatibility_flags": flags, | |
| 1156 | }))?, | |
| 1157 | )?; | |
| 1158 | log.line(&format!("Bundling the Worker the adapter wrote, {}.", adapter.entry)); | |
| 1159 | let config = WranglerConfig { | |
| 1160 | main: Some(entry.to_string_lossy().into_owned()), | |
| 1161 | compatibility_flags: flags, | |
| 1162 | assets: Some(AssetsConfig { | |
| 1163 | directory: Some(adapter.assets.to_owned()), | |
| 1164 | binding: Some("ASSETS".to_owned()), | |
| 1165 | ..AssetsConfig::default() | |
| 1166 | }), | |
| 1167 | ..WranglerConfig::default() | |
| 1168 | }; | |
| 1169 | let built = build_worker(log, secrets, config, dir, Some(&file)); | |
| 1170 | let _ = std::fs::remove_file(&file); | |
| 1171 | let (worker, assets_dir, warnings) = built?; | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1172 | return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id, container: None }); |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1173 | } |
| 1174 | if let (Some(adapter), Some(framework)) = (adapter, &plan.framework) | |
| 1175 | && framework.output.is_none() | |
| 1176 | { | |
| 1177 | bail!("{}'s build did not write its Worker, `{}`", framework.name, adapter.entry); | |
| 1178 | } | |
| 1179 | let assets_dir = match &chosen { | |
| 1180 | Some(chosen) => { | |
| 1181 | let path = dir.join(chosen.trim().trim_matches('/')); | |
| 1182 | if !path.is_dir() { | |
| 1183 | bail!("the output directory `{chosen}` does not exist after the build"); | |
| What g1t can't do yet, and an open letter to Cloudflare | 1184 | } |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1185 | path |
| What g1t can't do yet, and an open letter to Cloudflare | 1186 | } |
| 1187 | None => { | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1188 | let expected = plan.framework.as_ref().and_then(|f| f.output.as_deref()); |
| 1189 | match expected.filter(|output| dir.join(output).is_dir()) { | |
| 1190 | Some(output) => dir.join(output), | |
| 1191 | None => { | |
| 1192 | let found = find_output(dir).with_context(|| match (&plan.framework, expected) { | |
| 1193 | (Some(framework), Some(expected)) => format!( | |
| 1194 | "found nothing to serve: {} writes its site to `{expected}`, which the build did not write. \ | |
| 1195 | Set **Output directory** if it writes somewhere else.", | |
| 1196 | framework.name | |
| 1197 | ), | |
| 1198 | (Some(framework), None) if framework.problem.is_some() => { | |
| 1199 | format!("found nothing to serve. {}", framework.problem.as_deref().unwrap_or_default()) | |
| 1200 | } | |
| 1201 | _ => "found nothing to serve: no Workers config, no index.html, and none of dist, build, out, public, _site or .output/public".to_owned(), | |
| 1202 | })?; | |
| 1203 | if let Some(expected) = expected { | |
| 1204 | log.line(&format!("`{expected}` was not written; serving `{found}`.")); | |
| What g1t can't do yet, and an open letter to Cloudflare | 1205 | } |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1206 | if found == "." { dir.to_path_buf() } else { dir.join(found) } |
| What g1t can't do yet, and an open letter to Cloudflare | 1207 | } |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1208 | } |
| What g1t can't do yet, and an open letter to Cloudflare | 1209 | } |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1210 | }; |
| 1211 | let spa = !assets_dir.join("404.html").exists(); | |
| 1212 | Ok(Built { | |
| 1213 | worker: json!({ | |
| 1214 | "compatibilityDate": COMPATIBILITY_DATE, | |
| 1215 | "compatibilityFlags": [], | |
| 1216 | "vars": {}, | |
| 1217 | "notFoundHandling": if spa { "single-page-application" } else { "404-page" }, | |
| 1218 | }), | |
| 1219 | detected: static_kind(dir, &assets_dir, plan.build.is_some()), | |
| 1220 | assets_dir: Some(assets_dir), | |
| 1221 | warnings: Vec::new(), | |
| 1222 | framework: framework_id, | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1223 | container: None, |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1224 | }) |
| What g1t can't do yet, and an open letter to Cloudflare | 1225 | } |
| 1226 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1227 | /// Whether the build checks out its own commit (a sandbox, given |
| 1228 | /// `GIT_REMOTE`) rather than building the checkout it runs in. | |
| 1229 | fn clones() -> bool { | |
| 1230 | std::env::var("GIT_REMOTE").is_ok_and(|remote| !remote.trim().is_empty()) | |
| 1231 | } | |
| 1232 | ||
| 1233 | /// The checkout the build runs in: the sandbox's clone, or, from a | |
| 1234 | /// workflow step, the current directory. | |
| 1235 | fn checkout_dir() -> PathBuf { | |
| 1236 | if clones() { | |
| What g1t can't do yet, and an open letter to Cloudflare | 1237 | PathBuf::from(WORKDIR) |
| 1238 | } else { | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1239 | std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")) |
| 1240 | } | |
| 1241 | } | |
| 1242 | ||
| 1243 | /// Where `wrangler deploy --dry-run` writes the bundle: in a workflow | |
| 1244 | /// step, a directory of the job's temporary space, never the checkout. | |
| 1245 | fn bundle_dir() -> PathBuf { | |
| 1246 | if clones() { | |
| 1247 | return PathBuf::from(BUNDLE_DIR); | |
| 1248 | } | |
| 1249 | let base = std::env::var_os("RUNNER_TEMP").map(PathBuf::from).unwrap_or_else(std::env::temp_dir); | |
| 1250 | base.join(format!("g1t-bundle-{}", std::process::id())) | |
| 1251 | } | |
| 1252 | ||
| 1253 | /// Where the project lives in the checkout `base`: its root directory, | |
| 1254 | /// never above the checkout. | |
| 1255 | fn project_dir_in(base: &Path, root: &str) -> PathBuf { | |
| 1256 | let root = root.trim().trim_matches('/'); | |
| 1257 | if root.is_empty() || root == "." || root.split('/').any(|part| part == "..") { | |
| 1258 | base.to_path_buf() | |
| 1259 | } else { | |
| 1260 | base.join(root) | |
| What g1t can't do yet, and an open letter to Cloudflare | 1261 | } |
| 1262 | } | |
| 1263 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1264 | /// Where the project lives: `ROOT_DIR` in the checkout. |
| 1265 | fn project_dir() -> PathBuf { | |
| 1266 | project_dir_in(&checkout_dir(), &std::env::var("ROOT_DIR").unwrap_or_default()) | |
| 1267 | } | |
| 1268 | ||
| What g1t can't do yet, and an open letter to Cloudflare | 1269 | fn check_out(secrets: &[String]) -> Result<()> { |
| 1270 | let remote = env("GIT_REMOTE")?; | |
| 1271 | let commit = env("GIT_COMMIT")?; | |
| 1272 | let auth = auth_option(&env("G1T_USER")?, &env("G1T_TOKEN")?); | |
| 1273 | std::fs::create_dir_all("/work")?; | |
| Previews build from any branch; Agent in place of Ask AI; pin from the sidebar | 1274 | let workdir = Path::new(WORKDIR); |
| 1275 | // The clone is shallow and of the default branch; a preview's commit is | |
| 1276 | // often on another one, so it is fetched before checking out. | |
| 1277 | let cloned = crate::clone::clone(Path::new("/work"), &auth, &[], &remote, WORKDIR) | |
| 1278 | .and_then(|_| git(workdir, &["rev-parse", "--abbrev-ref", "HEAD"])) | |
| 1279 | .and_then(|branch| crate::clone::ensure(workdir, &auth, "origin", &branch, &commit)) | |
| 1280 | .and_then(|_| git(workdir, &["-c", "advice.detachedHead=false", "checkout", "--quiet", &commit])); | |
| What g1t can't do yet, and an open letter to Cloudflare | 1281 | if let Err(error) = cloned { |
| 1282 | bail!("{}", redact(&format!("{error:#}"), secrets)); | |
| 1283 | } | |
| 1284 | Ok(()) | |
| 1285 | } | |
| 1286 | ||
| 1287 | fn deploy(reporter: &Reporter, log: &mut Log, secrets: &[String]) -> Result<Value> { | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1288 | if clones() { |
| 1289 | check_out(secrets).context("the commit could not be checked out")?; | |
| 1290 | } else { | |
| 1291 | let dir = project_dir(); | |
| 1292 | if !dir.is_dir() { | |
| 1293 | bail!("the project's directory {} does not exist in this checkout", dir.display()); | |
| 1294 | } | |
| 1295 | log.line(&format!("Building {} as it is checked out.", dir.display())); | |
| 1296 | } | |
| What g1t can't do yet, and an open letter to Cloudflare | 1297 | // The repository's variables and secrets for deploy builds. |
| 1298 | for source in ["BUILD_ENV", "BUILD_SECRETS"] { | |
| 1299 | if let Ok(vars) = std::env::var(source) | |
| 1300 | && let Ok(Value::Object(vars)) = serde_json::from_str::<Value>(&vars) | |
| 1301 | { | |
| 1302 | for (name, value) in vars { | |
| 1303 | if let Some(value) = value.as_str() { | |
| 1304 | // SAFETY: single-threaded; set before any command runs. | |
| 1305 | unsafe { std::env::set_var(name, value) }; | |
| 1306 | } | |
| 1307 | } | |
| 1308 | } | |
| 1309 | } | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1310 | let built = build(reporter, log, secrets)?; |
| What g1t can't do yet, and an open letter to Cloudflare | 1311 | let mut finish = json!({ |
| 1312 | "worker": built.worker, | |
| 1313 | "warnings": built.warnings, | |
| 1314 | "detected": built.detected, | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1315 | // What it is built with, shown beside `detected` on its settings. |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1316 | "framework": built.framework, |
| What g1t can't do yet, and an open letter to Cloudflare | 1317 | }); |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1318 | if let Some(image) = built.container { |
| 1319 | finish["container"] = image; | |
| 1320 | } | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1321 | for warning in &built.warnings { |
| 1322 | log.line(&format!("Warning: {warning}")); | |
| 1323 | } | |
| What g1t can't do yet, and an open letter to Cloudflare | 1324 | if let Some(dir) = &built.assets_dir { |
| 1325 | let skip_project = *dir == project_dir(); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1326 | let assets = collect_site(dir, skip_project)?; |
| What g1t can't do yet, and an open letter to Cloudflare | 1327 | if assets.is_empty() { |
| 1328 | bail!("the site to serve is empty"); | |
| 1329 | } | |
| 1330 | log.line(&format!("Uploading {} files.", assets.len())); | |
| 1331 | for special in ["_headers", "_redirects"] { | |
| 1332 | if let Ok(text) = std::fs::read_to_string(dir.join(special)) { | |
| 1333 | finish["worker"][special] = text.into(); | |
| 1334 | } | |
| 1335 | } | |
| 1336 | let manifest: BTreeMap<&str, Value> = assets | |
| 1337 | .iter() | |
| 1338 | .map(|asset| (asset.path.as_str(), json!({ "hash": asset.hash, "size": asset.size }))) | |
| 1339 | .collect(); | |
| 1340 | let answer = reporter.send("session", json!({ "manifest": manifest }))?; | |
| 1341 | if answer["ok"] == false { | |
| 1342 | bail!("{}", answer["error"]["message"].as_str().unwrap_or("g1t refused the upload")); | |
| 1343 | } | |
| 1344 | let session: UploadSession = | |
| 1345 | serde_json::from_value(answer["value"].clone()).context("g1t's answer to the upload was not understood")?; | |
| 1346 | let by_hash: BTreeMap<&str, &Asset> = assets.iter().map(|asset| (asset.hash.as_str(), asset)).collect(); | |
| 1347 | let mut completion = session.jwt.clone(); | |
| 1348 | for bucket in &session.buckets { | |
| 1349 | if let Some(jwt) = upload_bucket(&session, bucket, &by_hash)? { | |
| 1350 | completion = jwt; | |
| 1351 | } | |
| 1352 | } | |
| 1353 | finish["completionJwt"] = completion.into(); | |
| 1354 | } | |
| 1355 | Ok(finish) | |
| 1356 | } | |
| 1357 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1358 | /// A sandbox's build (`MODE=deploy`). |
| What g1t can't do yet, and an open letter to Cloudflare | 1359 | pub fn main() -> i32 { |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1360 | run(false) |
| 1361 | } | |
| 1362 | ||
| 1363 | /// `g1t-runner deploy`, from a workflow step: the build in the job's own | |
| 1364 | /// checkout, its log in the step's as well. Exits 1 when the build fails, | |
| 1365 | /// after reporting it, so the step fails too. | |
| 1366 | pub fn cli(args: &[String]) -> i32 { | |
| 1367 | if args.iter().any(|arg| arg == "--help" || arg == "-h") { | |
| 1368 | println!("{CLI_HELP}"); | |
| 1369 | return 0; | |
| 1370 | } | |
| 1371 | run(true) | |
| 1372 | } | |
| 1373 | ||
| 1374 | const CLI_HELP: &str = "g1t-runner deploy: builds the project in this checkout and puts it on g1t.page. | |
| 1375 | ||
| 1376 | Reads DEPLOY_ID and DEPLOY_TOKEN (from POST /repos/{owner}/{repo}/deployments/builds), | |
| 1377 | G1T_API (https://api.g1t.sh), and optionally ROOT_DIR, BUILD_COMMAND, OUTPUT_DIR, | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1378 | BUILD_AS (auto, static, workers or docker), DOCKERFILE, and BUILD_ENV and |
| 1379 | BUILD_SECRETS (JSON objects). The g1t/deploy action sets them all. A container | |
| 1380 | is built with the docker on PATH. | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1381 | |
| 1382 | g1t-runner detect prints what the build would find, as key=value lines."; | |
| 1383 | ||
| 1384 | fn run(step_log: bool) -> i32 { | |
| What g1t can't do yet, and an open letter to Cloudflare | 1385 | let reporter = match (env("G1T_API"), env("DEPLOY_ID"), env("DEPLOY_TOKEN")) { |
| 1386 | (Ok(api), Ok(id), Ok(token)) => Reporter { | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1387 | base: format!("{}/deployments/jobs/{id}", api.trim_end_matches('/')), |
| What g1t can't do yet, and an open letter to Cloudflare | 1388 | token, |
| 1389 | }, | |
| 1390 | _ => { | |
| 1391 | eprintln!("g1t-runner: G1T_API, DEPLOY_ID and DEPLOY_TOKEN must be set"); | |
| 1392 | return 2; | |
| 1393 | } | |
| 1394 | }; | |
| 1395 | let mut secrets: Vec<String> = ["G1T_TOKEN", "DEPLOY_TOKEN"] | |
| 1396 | .iter() | |
| 1397 | .filter_map(|name| std::env::var(name).ok()) | |
| 1398 | .filter(|secret| !secret.is_empty()) | |
| 1399 | .collect(); | |
| 1400 | // The repository's build secrets never appear in the log. | |
| 1401 | if let Ok(Value::Object(build)) = serde_json::from_str::<Value>(&std::env::var("BUILD_SECRETS").unwrap_or_default()) { | |
| 1402 | secrets.extend(build.values().filter_map(Value::as_str).filter(|v| v.len() >= 4).map(str::to_owned)); | |
| 1403 | } | |
| 1404 | if let Err(error) = reporter.send("started", json!({})) { | |
| 1405 | eprintln!("g1t-runner: {error:#}"); | |
| 1406 | return 1; | |
| 1407 | } | |
| 1408 | let started = Instant::now(); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1409 | let mut log = Log { echo: step_log.then(|| secrets.clone()), ..Log::default() }; |
| What g1t can't do yet, and an open letter to Cloudflare | 1410 | let outcome = deploy(&reporter, &mut log, &secrets); |
| 1411 | let seconds = started.elapsed().as_secs(); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1412 | if step_log { |
| 1413 | let _ = std::fs::remove_dir_all(bundle_dir()); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1414 | let _ = std::fs::remove_file(adapter_config_path()); |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1415 | let _ = std::fs::remove_dir_all(docker_config_dir()); |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1416 | } |
| 1417 | let (sent, failed) = match outcome { | |
| What g1t can't do yet, and an open letter to Cloudflare | 1418 | Ok(mut finish) => { |
| 1419 | finish["log"] = redact(&log.tail(), &secrets).into(); | |
| 1420 | finish["buildSeconds"] = seconds.into(); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1421 | (reporter.send("finish", finish), false) |
| What g1t can't do yet, and an open letter to Cloudflare | 1422 | } |
| 1423 | Err(error) => { | |
| 1424 | let message = redact(&format!("{error:#}"), &secrets); | |
| 1425 | log.line(&format!("The build failed: {message}")); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1426 | let sent = reporter.send( |
| What g1t can't do yet, and an open letter to Cloudflare | 1427 | "fail", |
| 1428 | json!({ "message": message, "log": redact(&log.tail(), &secrets), "buildSeconds": seconds }), | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1429 | ); |
| 1430 | (sent, true) | |
| What g1t can't do yet, and an open letter to Cloudflare | 1431 | } |
| 1432 | }; | |
| 1433 | match sent { | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1434 | // A sandbox's build has said how it ended; a step fails with its build. |
| 1435 | Ok(answer) if step_log && (failed || answer["ok"] == false || answer["value"] == false) => { | |
| 1436 | if !failed { | |
| 1437 | eprintln!("g1t-runner: g1t did not take the build; its deployment says why"); | |
| 1438 | } | |
| 1439 | 1 | |
| 1440 | } | |
| What g1t can't do yet, and an open letter to Cloudflare | 1441 | Ok(_) => 0, |
| 1442 | Err(error) => { | |
| 1443 | eprintln!("g1t-runner: {error:#}"); | |
| 1444 | 1 | |
| 1445 | } | |
| 1446 | } | |
| 1447 | } | |
| 1448 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1449 | /// `g1t-runner detect`: what a build of the project would find, as |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1450 | /// `key=value` lines for `$GITHUB_OUTPUT`: `kind`, `install`, `build`, |
| 1451 | /// `output-dir`, and `framework` when one is found. Reads `ROOT_DIR`, | |
| 1452 | /// `BUILD_COMMAND` and `OUTPUT_DIR` as the build does; `--root-dir DIR` is | |
| 1453 | /// the same as `ROOT_DIR`. A framework set up to need a server is `none`, | |
| 1454 | /// with what to change on standard error. | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1455 | pub fn detect_cli(args: &[String]) -> i32 { |
| 1456 | let mut root = std::env::var("ROOT_DIR").unwrap_or_default(); | |
| 1457 | let mut rest = args.iter(); | |
| 1458 | while let Some(arg) = rest.next() { | |
| 1459 | match arg.as_str() { | |
| 1460 | "--root-dir" => root = rest.next().cloned().unwrap_or_default(), | |
| 1461 | "--help" | "-h" => { | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1462 | println!( |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1463 | "g1t-runner detect [--root-dir DIR]: prints kind, install, build, output-dir and framework (when one is found), and for a container dockerfile and port, as key=value lines. Reads ROOT_DIR, BUILD_COMMAND, OUTPUT_DIR, BUILD_AS (auto, static, workers or docker) and DOCKERFILE as a build does." |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1464 | ); |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1465 | return 0; |
| 1466 | } | |
| 1467 | other => { | |
| 1468 | eprintln!("g1t-runner detect: unknown argument {other}"); | |
| 1469 | return 2; | |
| 1470 | } | |
| 1471 | } | |
| 1472 | } | |
| 1473 | let dir = project_dir_in(&checkout_dir(), &root); | |
| 1474 | let custom = std::env::var("BUILD_COMMAND").ok(); | |
| 1475 | let output = std::env::var("OUTPUT_DIR").ok(); | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1476 | let build_as = BuildAs::parse(&std::env::var("BUILD_AS").unwrap_or_default()); |
| 1477 | let dockerfile = std::env::var("DOCKERFILE").ok(); | |
| 1478 | match detect_as(&dir, custom.as_deref(), output.as_deref(), build_as, dockerfile.as_deref()) { | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1479 | Ok(found) => { |
| 1480 | print!("{}", output_lines(&found)); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1481 | if let Some(problem) = &found.problem { |
| 1482 | eprintln!("g1t-runner detect: {problem}"); | |
| 1483 | } | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1484 | 0 |
| 1485 | } | |
| 1486 | Err(error) => { | |
| 1487 | eprintln!("g1t-runner detect: {error:#}"); | |
| 1488 | 1 | |
| 1489 | } | |
| 1490 | } | |
| 1491 | } | |
| 1492 | ||
| 1493 | /// `Detected` as `$GITHUB_OUTPUT` lines; a value on one line, empty when | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1494 | /// there is none. `framework` is left out when none was found, which a |
| 1495 | /// step's outputs read as empty all the same. | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1496 | fn output_lines(found: &Detected) -> String { |
| 1497 | let one_line = |value: Option<&str>| value.unwrap_or_default().replace(['\r', '\n'], " "); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1498 | let mut lines = format!( |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1499 | "kind={}\ninstall={}\nbuild={}\noutput-dir={}\n", |
| 1500 | found.kind, | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1501 | one_line(found.install.as_deref()), |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1502 | one_line(found.build.as_deref()), |
| 1503 | one_line(found.output_dir.as_deref()), | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1504 | ); |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1505 | if let Some(framework) = found.framework_id() { |
| 1506 | lines.push_str(&format!("framework={framework}\n")); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1507 | } |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1508 | // A container's Dockerfile (empty when g1t writes one) and its port. |
| 1509 | if let Some(container) = &found.container { | |
| 1510 | lines.push_str(&format!("dockerfile={}\nport={}\n", one_line(container.dockerfile.as_deref()), container.port)); | |
| 1511 | } | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1512 | lines |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1513 | } |
| 1514 | ||
| What g1t can't do yet, and an open letter to Cloudflare | 1515 | #[cfg(test)] |
| 1516 | mod tests { | |
| 1517 | use super::*; | |
| 1518 | ||
| 1519 | #[test] | |
| 1520 | fn the_upload_session_is_read_in_either_spelling() { | |
| 1521 | for answer in [ | |
| 1522 | json!({ "jwt": "j", "buckets": [["a"]], "upload_url": "https://u" }), | |
| 1523 | json!({ "jwt": "j", "uploadUrl": "https://u" }), | |
| 1524 | ] { | |
| 1525 | let session: UploadSession = serde_json::from_value(answer).unwrap(); | |
| 1526 | assert_eq!(session.upload_url, "https://u"); | |
| 1527 | } | |
| 1528 | } | |
| 1529 | ||
| 1530 | #[test] | |
| 1531 | fn jsonc_comments_and_trailing_commas_are_dropped() { | |
| 1532 | let text = r#"{ | |
| 1533 | // a comment | |
| 1534 | "main": "src/index.ts", /* another */ | |
| 1535 | "vars": { "URL": "https://x.dev//not-a-comment", }, | |
| 1536 | }"#; | |
| 1537 | let config: WranglerConfig = serde_json::from_str(&strip_jsonc(text)).unwrap(); | |
| 1538 | assert_eq!(config.main.as_deref(), Some("src/index.ts")); | |
| 1539 | assert_eq!(config.vars["URL"], "https://x.dev//not-a-comment"); | |
| 1540 | } | |
| 1541 | ||
| 1542 | #[test] | |
| 1543 | fn unsupported_bindings_are_noticed() { | |
| 1544 | let config: WranglerConfig = | |
| 1545 | serde_json::from_str(r#"{ "main": "a.js", "d1_databases": [{ "binding": "DB" }] }"#).unwrap(); | |
| 1546 | assert!(config.rest.contains_key("d1_databases")); | |
| 1547 | } | |
| 1548 | ||
| 1549 | #[test] | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 1550 | fn crons_g1t_does_not_run_are_warned_about() { |
| 1551 | let config: WranglerConfig = | |
| 1552 | serde_json::from_str(r#"{ "main": "a.js", "triggers": { "crons": ["*/5 * * * *", "0 3 * * *"] }, "kv_namespaces": [{ "binding": "KV" }] }"#) | |
| 1553 | .unwrap(); | |
| 1554 | let warnings = config_warnings(&config); | |
| 1555 | assert_eq!(warnings.len(), 2); | |
| 1556 | assert!(warnings.iter().any(|w| w.contains("kv_namespaces"))); | |
| 1557 | assert!(warnings.iter().any(|w| w.contains("triggers.crons") && w.contains("2 schedules"))); | |
| 1558 | let none: WranglerConfig = serde_json::from_str(r#"{ "main": "a.js", "triggers": { "crons": [] } }"#).unwrap(); | |
| 1559 | assert!(config_warnings(&none).is_empty()); | |
| 1560 | let toml: WranglerConfig = toml::from_str("main = \"a.js\" | |
| 1561 | [triggers] | |
| 1562 | crons = [\"0 * * * *\"] | |
| 1563 | ").unwrap(); | |
| 1564 | assert!(config_warnings(&toml)[0].contains("1 schedule)")); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1565 | // A config a build wrote lists every kind of binding, mostly empty. |
| 1566 | let written: WranglerConfig = serde_json::from_str( | |
| 1567 | r#"{ "main": "entry.mjs", "d1_databases": [], "durable_objects": { "bindings": [] }, "triggers": {}, "kv_namespaces": [{ "binding": "SESSION" }] }"#, | |
| 1568 | ) | |
| 1569 | .unwrap(); | |
| 1570 | assert_eq!(config_warnings(&written).len(), 1); | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 1571 | } |
| 1572 | ||
| 1573 | #[test] | |
| What g1t can't do yet, and an open letter to Cloudflare | 1574 | fn a_site_is_plain_html_only_when_its_own_files_are_served_unbuilt() { |
| 1575 | let project = Path::new("/w/site"); | |
| 1576 | assert_eq!(static_kind(project, project, false), "html"); | |
| 1577 | assert_eq!(static_kind(project, project, true), "static"); | |
| 1578 | assert_eq!(static_kind(project, &project.join("dist"), false), "static"); | |
| 1579 | } | |
| 1580 | ||
| 1581 | #[test] | |
| 1582 | fn files_are_typed_by_extension() { | |
| 1583 | assert_eq!(content_type("/index.HTML"), "text/html"); | |
| 1584 | assert_eq!(content_type("/a/b.woff2"), "font/woff2"); | |
| 1585 | assert_eq!(content_type("/LICENSE"), "application/octet-stream"); | |
| 1586 | } | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1587 | |
| 1588 | /// A fresh directory with `files` (path, contents) in it. | |
| 1589 | fn project(name: &str, files: &[(&str, &str)]) -> PathBuf { | |
| 1590 | let dir = std::env::temp_dir().join(format!("g1t-detect-{name}-{}", std::process::id())); | |
| 1591 | let _ = std::fs::remove_dir_all(&dir); | |
| 1592 | for (path, text) in files { | |
| 1593 | let path = dir.join(path); | |
| 1594 | std::fs::create_dir_all(path.parent().unwrap()).unwrap(); | |
| 1595 | std::fs::write(path, text).unwrap(); | |
| 1596 | } | |
| 1597 | std::fs::create_dir_all(&dir).unwrap(); | |
| 1598 | dir | |
| 1599 | } | |
| 1600 | ||
| 1601 | #[test] | |
| 1602 | fn detect_finds_a_workers_project_and_how_it_builds() { | |
| 1603 | let dir = project( | |
| 1604 | "workers", | |
| 1605 | &[ | |
| 1606 | ("wrangler.jsonc", r#"{ "main": "src/index.ts", "assets": { "directory": "./public" }, }"#), | |
| 1607 | ("package.json", "{}"), | |
| 1608 | ("pnpm-lock.yaml", ""), | |
| 1609 | ], | |
| 1610 | ); | |
| 1611 | let found = detect(&dir, None, None).unwrap(); | |
| 1612 | assert_eq!(found.kind, "workers"); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1613 | assert_eq!(found.install.as_deref(), Some("corepack enable && pnpm install --frozen-lockfile")); |
| 1614 | assert_eq!(found.framework, None); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1615 | assert_eq!(found.build.as_deref(), Some("npx --yes wrangler@4 deploy --dry-run")); |
| 1616 | assert_eq!(found.output_dir.as_deref(), Some("./public")); | |
| 1617 | // The project's own build command runs first. | |
| 1618 | let custom = detect(&dir, Some("npm run codegen"), None).unwrap(); | |
| 1619 | assert_eq!(custom.build.as_deref(), Some("npm run codegen && npx --yes wrangler@4 deploy --dry-run")); | |
| 1620 | } | |
| 1621 | ||
| 1622 | #[test] | |
| 1623 | fn detect_finds_a_static_build_and_a_plain_site() { | |
| 1624 | let built = project("static", &[("package.json", r#"{ "scripts": { "build": "vite build" } }"#), ("package-lock.json", "{}")]); | |
| 1625 | let found = detect(&built, None, None).unwrap(); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1626 | assert_eq!(found.kind, "static"); |
| 1627 | assert_eq!(found.install.as_deref(), Some("npm ci")); | |
| 1628 | assert_eq!(found.build.as_deref(), Some("npm run build")); | |
| 1629 | assert_eq!(found.output_dir, None, "its output is found once it has built"); | |
| 1630 | assert_eq!(found.framework, None, "a build script alone names no framework"); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1631 | std::fs::create_dir_all(built.join("dist")).unwrap(); |
| 1632 | std::fs::write(built.join("dist/index.html"), "<h1>hi</h1>").unwrap(); | |
| 1633 | assert_eq!(detect(&built, None, None).unwrap().output_dir.as_deref(), Some("dist")); | |
| 1634 | assert_eq!(detect(&built, None, Some("/site/")).unwrap().output_dir.as_deref(), Some("site")); | |
| 1635 | ||
| 1636 | let html = project("html", &[("index.html", "<h1>hi</h1>")]); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1637 | let found = detect(&html, None, None).unwrap(); |
| 1638 | assert_eq!((found.kind, found.install, found.build, found.output_dir.as_deref()), ("html", None, None, Some("."))); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1639 | // A `public` folder alone, without an index.html, is not a site. |
| 1640 | let empty = project("empty", &[("public/robots.txt", ""), ("README.md", "")]); | |
| 1641 | assert_eq!(detect(&empty, None, None).unwrap().kind, "none"); | |
| 1642 | } | |
| 1643 | ||
| 1644 | #[test] | |
| 1645 | fn detect_prints_lines_for_github_output() { | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1646 | let mut found = Detected { |
| 1647 | kind: "static", | |
| 1648 | framework: None, | |
| 1649 | install: Some("npm ci".into()), | |
| 1650 | build: Some("a\nb".into()), | |
| 1651 | output_dir: None, | |
| 1652 | problem: None, | |
| 1653 | pre_build: None, | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1654 | container: None, |
| 1655 | fallback: None, | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1656 | }; |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1657 | assert_eq!(output_lines(&found), "kind=static\ninstall=npm ci\nbuild=a b\noutput-dir=\n"); |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1658 | let vite = project("lines", &[("package.json", r#"{ "devDependencies": { "vite": "7" } }"#)]); |
| 1659 | found.framework = frameworks::detect(&vite, false); | |
| 1660 | assert!(output_lines(&found).ends_with("output-dir=\nframework=vite\n")); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1661 | } |
| 1662 | ||
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1663 | /// One project of the table below: its files, the project's own |
| 1664 | /// settings, and what detect should say. | |
| 1665 | struct Case { | |
| 1666 | name: &'static str, | |
| 1667 | files: &'static [(&'static str, &'static str)], | |
| 1668 | custom_build: Option<&'static str>, | |
| 1669 | chosen_output: Option<&'static str>, | |
| 1670 | kind: &'static str, | |
| 1671 | framework: Option<&'static str>, | |
| 1672 | build: Option<&'static str>, | |
| 1673 | output_dir: Option<&'static str>, | |
| 1674 | /// A phrase the problem says, if there is one. | |
| 1675 | problem: Option<&'static str>, | |
| 1676 | } | |
| 1677 | ||
| 1678 | const CASE: Case = Case { | |
| 1679 | name: "", | |
| 1680 | files: &[], | |
| 1681 | custom_build: None, | |
| 1682 | chosen_output: None, | |
| 1683 | kind: "static", | |
| 1684 | framework: None, | |
| 1685 | build: Some("npm run build"), | |
| 1686 | output_dir: None, | |
| 1687 | problem: None, | |
| 1688 | }; | |
| 1689 | ||
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1690 | #[test] |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1691 | fn detect_knows_each_framework() { |
| 1692 | let cases = [ | |
| 1693 | Case { | |
| 1694 | name: "vite", | |
| 1695 | files: &[("package.json", r#"{ "devDependencies": { "vite": "7" } }"#)], | |
| 1696 | framework: Some("vite"), | |
| 1697 | build: Some("npx vite build"), | |
| 1698 | output_dir: Some("dist"), | |
| 1699 | ..CASE | |
| 1700 | }, | |
| 1701 | Case { | |
| 1702 | name: "vite-react", | |
| 1703 | files: &[ | |
| 1704 | ("package.json", r#"{ "scripts": { "build": "tsc && vite build" }, "dependencies": { "react": "19" }, "devDependencies": { "vite": "7", "@vitejs/plugin-react": "5" } }"#), | |
| 1705 | ("vite.config.ts", "export default defineConfig({ plugins: [react()], build: { outDir: 'site' } })"), | |
| 1706 | ], | |
| 1707 | framework: Some("vite-react"), | |
| 1708 | output_dir: Some("site"), | |
| 1709 | ..CASE | |
| 1710 | }, | |
| 1711 | Case { | |
| 1712 | name: "vite-vue", | |
| 1713 | files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "vite": "7", "@vitejs/plugin-vue": "6" } }"#)], | |
| 1714 | framework: Some("vite-vue"), | |
| 1715 | output_dir: Some("dist"), | |
| 1716 | ..CASE | |
| 1717 | }, | |
| 1718 | Case { | |
| 1719 | name: "vite-svelte", | |
| 1720 | files: &[("package.json", r#"{ "devDependencies": { "vite": "7", "@sveltejs/vite-plugin-svelte": "6" } }"#)], | |
| 1721 | framework: Some("vite-svelte"), | |
| 1722 | build: Some("npx vite build"), | |
| 1723 | output_dir: Some("dist"), | |
| 1724 | ..CASE | |
| 1725 | }, | |
| 1726 | Case { | |
| 1727 | name: "vite-solid", | |
| 1728 | files: &[("package.json", r#"{ "devDependencies": { "vite": "7", "vite-plugin-solid": "2" } }"#)], | |
| 1729 | framework: Some("vite-solid"), | |
| 1730 | build: Some("npx vite build"), | |
| 1731 | output_dir: Some("dist"), | |
| 1732 | ..CASE | |
| 1733 | }, | |
| 1734 | Case { | |
| 1735 | name: "vite-cloudflare", | |
| 1736 | files: &[ | |
| 1737 | ("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "vite": "7", "@cloudflare/vite-plugin": "1" } }"#), | |
| 1738 | ("wrangler.jsonc", r#"{ "main": "worker/index.ts" }"#), | |
| 1739 | ], | |
| 1740 | kind: "workers", | |
| 1741 | framework: Some("vite"), | |
| 1742 | build: Some("npm run build && npx --yes wrangler@4 deploy --dry-run"), | |
| 1743 | ..CASE | |
| 1744 | }, | |
| 1745 | Case { | |
| 1746 | name: "astro", | |
| 1747 | files: &[("package.json", r#"{ "scripts": { "build": "astro build" }, "dependencies": { "astro": "5" } }"#), ("astro.config.mjs", "export default defineConfig({ site: 'https://x.dev' })")], | |
| 1748 | framework: Some("astro"), | |
| 1749 | output_dir: Some("dist"), | |
| 1750 | ..CASE | |
| 1751 | }, | |
| 1752 | Case { | |
| 1753 | name: "astro-server", | |
| 1754 | files: &[("package.json", r#"{ "dependencies": { "astro": "5", "@astrojs/node": "9" } }"#), ("astro.config.mjs", "export default { output: \"server\" }")], | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1755 | kind: "container", |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1756 | framework: Some("astro"), |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1757 | build: Some(GENERATED), |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1758 | ..CASE |
| 1759 | }, | |
| 1760 | Case { | |
| 1761 | name: "astro-cloudflare", | |
| 1762 | files: &[("package.json", r#"{ "scripts": { "build": "astro build" }, "dependencies": { "astro": "5", "@astrojs/cloudflare": "12" } }"#)], | |
| 1763 | kind: "workers", | |
| 1764 | framework: Some("astro"), | |
| 1765 | ..CASE | |
| 1766 | }, | |
| 1767 | Case { | |
| 1768 | name: "astro-cloudflare-folder", | |
| 1769 | files: &[("package.json", r#"{ "scripts": { "build": "astro build" }, "dependencies": { "astro": "5", "@astrojs/cloudflare": "12" } }"#)], | |
| 1770 | chosen_output: Some("dist"), | |
| 1771 | framework: Some("astro"), | |
| 1772 | output_dir: Some("dist"), | |
| 1773 | ..CASE | |
| 1774 | }, | |
| 1775 | Case { | |
| 1776 | name: "next-export", | |
| 1777 | files: &[("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16" } }"#), ("next.config.ts", "const config: NextConfig = {\n output: 'export',\n};")], | |
| 1778 | framework: Some("nextjs"), | |
| 1779 | output_dir: Some("out"), | |
| 1780 | ..CASE | |
| 1781 | }, | |
| 1782 | Case { | |
| 1783 | name: "next-server", | |
| 1784 | files: &[("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16" } }"#), ("next.config.mjs", "export default {}")], | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1785 | kind: "container", |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1786 | framework: Some("nextjs"), |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1787 | build: Some(GENERATED), |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1788 | ..CASE |
| 1789 | }, | |
| 1790 | Case { | |
| 1791 | name: "next-server-own-folder", | |
| 1792 | files: &[("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16" } }"#)], | |
| 1793 | chosen_output: Some("out"), | |
| 1794 | framework: Some("nextjs"), | |
| 1795 | output_dir: Some("out"), | |
| 1796 | ..CASE | |
| 1797 | }, | |
| 1798 | Case { | |
| 1799 | name: "opennext", | |
| 1800 | files: &[ | |
| 1801 | ("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16", "@opennextjs/cloudflare": "1" } }"#), | |
| 1802 | ("wrangler.jsonc", r#"{ "main": ".open-next/worker.js", "assets": { "directory": ".open-next/assets", "binding": "ASSETS" } }"#), | |
| 1803 | ], | |
| 1804 | kind: "workers", | |
| 1805 | framework: Some("nextjs"), | |
| 1806 | build: Some("npx opennextjs-cloudflare build && npx --yes wrangler@4 deploy --dry-run"), | |
| 1807 | output_dir: Some(".open-next/assets"), | |
| 1808 | ..CASE | |
| 1809 | }, | |
| 1810 | Case { | |
| 1811 | name: "opennext-no-config", | |
| 1812 | files: &[("package.json", r#"{ "dependencies": { "next": "16", "@opennextjs/cloudflare": "1" } }"#)], | |
| 1813 | kind: "none", | |
| 1814 | framework: Some("nextjs"), | |
| 1815 | build: None, | |
| 1816 | problem: Some("`.open-next/worker.js`"), | |
| 1817 | ..CASE | |
| 1818 | }, | |
| 1819 | Case { | |
| 1820 | name: "sveltekit-static", | |
| 1821 | files: &[ | |
| 1822 | ("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "2", "@sveltejs/adapter-static": "3", "vite": "7" } }"#), | |
| 1823 | ("svelte.config.js", "import adapter from '@sveltejs/adapter-static';\nexport default { kit: { adapter: adapter() } };"), | |
| 1824 | ], | |
| 1825 | framework: Some("sveltekit"), | |
| 1826 | output_dir: Some("build"), | |
| 1827 | ..CASE | |
| 1828 | }, | |
| 1829 | Case { | |
| 1830 | name: "sveltekit-3-static", | |
| 1831 | files: &[ | |
| 1832 | ("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "3", "@sveltejs/adapter-static": "4", "@sveltejs/adapter-cloudflare": "8" } }"#), | |
| 1833 | ("vite.config.js", "import adapter from '@sveltejs/adapter-static';\nexport default defineConfig({ plugins: [sveltekit({ adapter: adapter({ pages: 'www' }) })] });"), | |
| 1834 | ], | |
| 1835 | framework: Some("sveltekit"), | |
| 1836 | output_dir: Some("www"), | |
| 1837 | ..CASE | |
| 1838 | }, | |
| 1839 | Case { | |
| 1840 | name: "sveltekit-cloudflare", | |
| 1841 | files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "2", "@sveltejs/adapter-cloudflare": "7" } }"#)], | |
| 1842 | kind: "workers", | |
| 1843 | framework: Some("sveltekit"), | |
| 1844 | ..CASE | |
| 1845 | }, | |
| 1846 | Case { | |
| 1847 | name: "sveltekit-auto", | |
| 1848 | files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "2", "@sveltejs/adapter-auto": "6" } }"#)], | |
| 1849 | kind: "none", | |
| 1850 | framework: Some("sveltekit"), | |
| 1851 | build: None, | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1852 | problem: Some("Or add a `Dockerfile`"), |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1853 | ..CASE |
| 1854 | }, | |
| 1855 | Case { | |
| 1856 | name: "nuxt", | |
| 1857 | files: &[("package.json", r#"{ "scripts": { "build": "nuxt build", "generate": "nuxt generate" }, "dependencies": { "nuxt": "4" } }"#)], | |
| 1858 | framework: Some("nuxt"), | |
| 1859 | build: Some("npm run generate"), | |
| 1860 | output_dir: Some(".output/public"), | |
| 1861 | ..CASE | |
| 1862 | }, | |
| 1863 | Case { | |
| 1864 | name: "nuxt-no-generate", | |
| 1865 | files: &[("package.json", r#"{ "scripts": { "build": "nuxt build" }, "dependencies": { "nuxt": "4" } }"#)], | |
| 1866 | framework: Some("nuxt"), | |
| 1867 | build: Some("npx nuxi generate"), | |
| 1868 | output_dir: Some(".output/public"), | |
| 1869 | ..CASE | |
| 1870 | }, | |
| 1871 | Case { | |
| 1872 | name: "react-router-spa", | |
| 1873 | files: &[ | |
| 1874 | ("package.json", r#"{ "scripts": { "build": "react-router build" }, "devDependencies": { "@react-router/dev": "7", "vite": "7" } }"#), | |
| 1875 | ("react-router.config.ts", "export default {\n // Server-side render by default\n ssr: false,\n} satisfies Config;"), | |
| 1876 | ], | |
| 1877 | framework: Some("react-router"), | |
| 1878 | output_dir: Some("build/client"), | |
| 1879 | ..CASE | |
| 1880 | }, | |
| 1881 | Case { | |
| 1882 | name: "react-router-ssr", | |
| 1883 | files: &[("package.json", r#"{ "devDependencies": { "@react-router/dev": "7" } }"#), ("react-router.config.ts", "export default { ssr: true }")], | |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1884 | kind: "container", |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1885 | framework: Some("react-router"), |
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 1886 | build: Some(GENERATED), |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 1887 | ..CASE |
| 1888 | }, | |
| 1889 | Case { | |
| 1890 | name: "remix-spa", | |
| 1891 | files: &[ | |
| 1892 | ("package.json", r#"{ "scripts": { "build": "remix vite:build" }, "devDependencies": { "@remix-run/dev": "2", "vite": "5" } }"#), | |
| 1893 | ("vite.config.ts", "export default defineConfig({ plugins: [remix({ ssr: false })] })"), | |
| 1894 | ], | |
| 1895 | framework: Some("remix"), | |
| 1896 | output_dir: Some("build/client"), | |
| 1897 | ..CASE | |
| 1898 | }, | |
| 1899 | Case { | |
| 1900 | name: "gatsby", | |
| 1901 | files: &[("package.json", r#"{ "scripts": { "build": "gatsby build" }, "dependencies": { "gatsby": "5" } }"#)], | |
| 1902 | framework: Some("gatsby"), | |
| 1903 | output_dir: Some("public"), | |
| 1904 | ..CASE | |
| 1905 | }, | |
| 1906 | Case { | |
| 1907 | name: "docusaurus", | |
| 1908 | files: &[("package.json", r#"{ "scripts": { "build": "docusaurus build" }, "dependencies": { "@docusaurus/core": "3" } }"#)], | |
| 1909 | framework: Some("docusaurus"), | |
| 1910 | output_dir: Some("build"), | |
| 1911 | ..CASE | |
| 1912 | }, | |
| 1913 | Case { | |
| 1914 | name: "eleventy", | |
| 1915 | files: &[("package.json", r#"{ "devDependencies": { "@11ty/eleventy": "3" } }"#)], | |
| 1916 | framework: Some("eleventy"), | |
| 1917 | build: Some("npx @11ty/eleventy"), | |
| 1918 | output_dir: Some("_site"), | |
| 1919 | ..CASE | |
| 1920 | }, | |
| 1921 | Case { | |
| 1922 | name: "angular", | |
| 1923 | files: &[ | |
| 1924 | ("package.json", r#"{ "scripts": { "build": "ng build" }, "dependencies": { "@angular/core": "20" } }"#), | |
| 1925 | ("angular.json", r#"{ "projects": { "shop": { "projectType": "application", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/shop" } } } } } }"#), | |
| 1926 | ], | |
| 1927 | framework: Some("angular"), | |
| 1928 | output_dir: Some("dist/shop/browser"), | |
| 1929 | ..CASE | |
| 1930 | }, | |
| 1931 | Case { | |
| 1932 | name: "angular-default", | |
| 1933 | files: &[ | |
| 1934 | ("package.json", r#"{ "dependencies": { "@angular/core": "20" } }"#), | |
| 1935 | ("angular.json", r#"{ "projects": { "app": { "projectType": "application", "architect": { "build": { "builder": "@angular/build:application", "options": {} } } } } }"#), | |
| 1936 | ], | |
| 1937 | framework: Some("angular"), | |
| 1938 | build: Some("npx ng build"), | |
| 1939 | output_dir: Some("dist/app/browser"), | |
| 1940 | ..CASE | |
| 1941 | }, | |
| 1942 | Case { | |
| 1943 | name: "hugo", | |
| 1944 | files: &[("hugo.toml", "baseURL = 'https://example.org/'\ntitle = 'Site'\n")], | |
| 1945 | framework: Some("hugo"), | |
| 1946 | build: Some(frameworks_hugo()), | |
| 1947 | output_dir: Some("public"), | |
| 1948 | ..CASE | |
| 1949 | }, | |
| 1950 | Case { | |
| 1951 | name: "hugo-config-toml", | |
| 1952 | files: &[("config.toml", "baseURL = \"https://example.org/\"\npublishDir = \"site\"\n")], | |
| 1953 | framework: Some("hugo"), | |
| 1954 | build: Some(frameworks_hugo()), | |
| 1955 | output_dir: Some("site"), | |
| 1956 | ..CASE | |
| 1957 | }, | |
| 1958 | Case { | |
| 1959 | name: "not-hugo", | |
| 1960 | files: &[("config.toml", "base_url = \"https://example.org\"\n"), ("index.html", "")], | |
| 1961 | kind: "html", | |
| 1962 | build: None, | |
| 1963 | output_dir: Some("."), | |
| 1964 | ..CASE | |
| 1965 | }, | |
| 1966 | Case { | |
| 1967 | name: "jekyll", | |
| 1968 | files: &[("_config.yml", "title: Blog\n"), ("Gemfile", "gem 'jekyll'\n")], | |
| 1969 | framework: Some("jekyll"), | |
| 1970 | build: Some("bundle exec jekyll build"), | |
| 1971 | output_dir: Some("_site"), | |
| 1972 | ..CASE | |
| 1973 | }, | |
| 1974 | Case { | |
| 1975 | name: "mdbook", | |
| 1976 | files: &[("book.toml", "[book]\ntitle = \"Guide\"\n")], | |
| 1977 | framework: Some("mdbook"), | |
| 1978 | build: Some(frameworks_mdbook()), | |
| 1979 | output_dir: Some("book"), | |
| 1980 | ..CASE | |
| 1981 | }, | |
| 1982 | Case { | |
| 1983 | name: "mdbook-build-dir", | |
| 1984 | files: &[("book.toml", "[book]\ntitle = \"Guide\"\n[build]\nbuild-dir = \"site\"\n")], | |
| 1985 | framework: Some("mdbook"), | |
| 1986 | build: Some(frameworks_mdbook()), | |
| 1987 | output_dir: Some("site"), | |
| 1988 | ..CASE | |
| 1989 | }, | |
| 1990 | // The project's own settings win. | |
| 1991 | Case { | |
| 1992 | name: "own-settings", | |
| 1993 | files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "vite": "7" } }"#)], | |
| 1994 | custom_build: Some("make site"), | |
| 1995 | chosen_output: Some("www"), | |
| 1996 | framework: Some("vite"), | |
| 1997 | build: Some("make site"), | |
| 1998 | output_dir: Some("www"), | |
| 1999 | ..CASE | |
| 2000 | }, | |
| 2001 | ]; | |
| 2002 | for case in cases { | |
| 2003 | let dir = project(&format!("fw-{}", case.name), case.files); | |
| 2004 | let found = detect(&dir, case.custom_build, case.chosen_output).unwrap(); | |
| 2005 | let name = case.name; | |
| 2006 | assert_eq!(found.kind, case.kind, "{name}: kind"); | |
| 2007 | assert_eq!(found.framework.as_ref().map(|f| f.id), case.framework, "{name}: framework"); | |
| 2008 | assert_eq!(found.build.as_deref(), case.build, "{name}: build"); | |
| 2009 | assert_eq!(found.output_dir.as_deref(), case.output_dir, "{name}: output-dir"); | |
| 2010 | match case.problem { | |
| 2011 | Some(phrase) => assert!(found.problem.as_deref().is_some_and(|p| p.contains(phrase)), "{name}: {:?}", found.problem), | |
| 2012 | None => assert_eq!(found.problem, None, "{name}: problem"), | |
| 2013 | } | |
| 2014 | let _ = std::fs::remove_dir_all(&dir); | |
| 2015 | } | |
| 2016 | } | |
| 2017 | ||
| Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9 | 2018 | #[test] |
| 2019 | fn build_as_and_dockerfiles_decide_in_order() { | |
| 2020 | let detect_with = |dir: &Path, build_as: BuildAs, dockerfile: Option<&str>| detect_as(dir, None, None, build_as, dockerfile).unwrap(); | |
| 2021 | // A framework that builds a static site stays one beside a | |
| 2022 | // Dockerfile: it costs nothing while no one visits. | |
| 2023 | let vite = project("p-vite", &[("package.json", r#"{ "devDependencies": { "vite": "7" } }"#), ("Dockerfile", "FROM nginx\nEXPOSE 80\n")]); | |
| 2024 | assert_eq!(detect_with(&vite, BuildAs::Auto, None).kind, "static"); | |
| 2025 | // Unless the project says Docker. | |
| 2026 | let found = detect_with(&vite, BuildAs::Docker, None); | |
| 2027 | assert_eq!((found.kind, found.framework_id()), ("container", Some("vite"))); | |
| 2028 | let container = found.container.unwrap(); | |
| 2029 | assert_eq!((container.dockerfile.as_deref(), container.port), (Some("Dockerfile"), 80)); | |
| 2030 | // A Wrangler config wins in Auto, and not when it says Static site. | |
| 2031 | let workers = project("p-workers", &[("wrangler.jsonc", r#"{ "assets": { "directory": "public" } }"#), ("index.html", "")]); | |
| 2032 | assert_eq!(detect_with(&workers, BuildAs::Auto, None).kind, "workers"); | |
| 2033 | assert_eq!(detect_with(&workers, BuildAs::Static, None).kind, "html"); | |
| 2034 | // Workers, without a config or adapter, says what is missing. | |
| 2035 | let html = project("p-html", &[("index.html", "<h1>hi</h1>")]); | |
| 2036 | let found = detect_with(&html, BuildAs::Workers, None); | |
| 2037 | assert_eq!(found.kind, "none"); | |
| 2038 | assert!(found.problem.unwrap().contains("no Wrangler config")); | |
| 2039 | // No framework and a Dockerfile: a container, built as the site it | |
| 2040 | // is without one while g1t.page runs no containers. | |
| 2041 | let both = project("p-both", &[("index.html", ""), ("Containerfile", "FROM nginx")]); | |
| 2042 | let found = detect_with(&both, BuildAs::Auto, None); | |
| 2043 | assert_eq!((found.kind, found.framework_id()), ("container", Some("dockerfile"))); | |
| 2044 | assert_eq!(found.container.as_ref().unwrap().port, 8080); | |
| 2045 | assert_eq!(found.fallback.as_ref().map(|site| site.kind), Some("html")); | |
| 2046 | // A Dockerfile at a path of the project's choosing. | |
| 2047 | let chosen = project("p-chosen", &[("ops/web.Dockerfile", "FROM node\nEXPOSE 3000")]); | |
| 2048 | assert_eq!(detect_with(&chosen, BuildAs::Auto, None).kind, "none"); | |
| 2049 | let found = detect_with(&chosen, BuildAs::Auto, Some("ops/web.Dockerfile")); | |
| 2050 | assert_eq!((found.kind, found.container.unwrap().port), ("container", 3000)); | |
| 2051 | assert!(detect_as(&chosen, None, None, BuildAs::Auto, Some("nope")).is_err()); | |
| 2052 | // A server g1t recognises gets a Dockerfile written for it. | |
| 2053 | let express = project("p-express", &[("package.json", r#"{ "scripts": { "start": "node index.js" }, "dependencies": { "express": "5" } }"#)]); | |
| 2054 | let found = detect_with(&express, BuildAs::Auto, None); | |
| 2055 | assert_eq!((found.kind, found.framework_id(), found.install.as_deref()), ("container", Some("node"), None)); | |
| 2056 | assert!(found.fallback.is_none()); | |
| 2057 | assert!(output_lines(&found).ends_with("framework=node\ndockerfile=\nport=8080\n")); | |
| 2058 | // Nothing at all says to add a Dockerfile. | |
| 2059 | let nothing = project("p-nothing", &[("README.md", "")]); | |
| 2060 | let found = detect_with(¬hing, BuildAs::Auto, None); | |
| 2061 | assert_eq!(found.kind, "none"); | |
| 2062 | assert!(found.problem.as_deref().unwrap().starts_with("found nothing to serve")); | |
| 2063 | assert!(found.problem.unwrap().contains("Add a `Dockerfile`")); | |
| 2064 | let found = detect_with(¬hing, BuildAs::Docker, None); | |
| 2065 | assert!(found.problem.unwrap().contains("**Build as** is Docker")); | |
| 2066 | // A framework set up for a server, with a Dockerfile of its own. | |
| 2067 | let next = project("p-next", &[("package.json", r#"{ "dependencies": { "next": "16" } }"#), ("Dockerfile", "FROM node")]); | |
| 2068 | let found = detect_with(&next, BuildAs::Auto, None); | |
| 2069 | assert_eq!((found.kind, found.framework_id()), ("container", Some("nextjs"))); | |
| 2070 | assert_eq!(found.container.unwrap().dockerfile.as_deref(), Some("Dockerfile")); | |
| 2071 | // OpenNext's adapter means Workers: it is not run as a server. | |
| 2072 | let opennext = project("p-opennext", &[("package.json", r#"{ "dependencies": { "next": "16", "@opennextjs/cloudflare": "1" } }"#)]); | |
| 2073 | assert_eq!(detect_with(&opennext, BuildAs::Auto, None).kind, "none"); | |
| 2074 | for dir in [vite, workers, html, both, chosen, express, nothing, next, opennext] { | |
| 2075 | let _ = std::fs::remove_dir_all(dir); | |
| 2076 | } | |
| 2077 | } | |
| 2078 | ||
| 2079 | #[test] | |
| 2080 | fn the_pushed_digest_is_the_one_in_the_apps_repository() { | |
| 2081 | let digest = "a".repeat(64); | |
| 2082 | let lines = format!("docker.io/x@sha256:{digest}\nregistry.cloudflare.com/acc/other@sha256:{digest}\nregistry.cloudflare.com/acc/g1t-web@sha256:{digest}\n"); | |
| 2083 | assert_eq!( | |
| 2084 | pushed_digest(&lines, "registry.cloudflare.com", "acc/g1t-web"), | |
| 2085 | Some(format!("registry.cloudflare.com/acc/g1t-web@sha256:{digest}")) | |
| 2086 | ); | |
| 2087 | assert_eq!(pushed_digest("registry.cloudflare.com/acc/g1t-web@sha256:short", "registry.cloudflare.com", "acc/g1t-web"), None); | |
| 2088 | let grant: RegistryGrant = serde_json::from_value(json!({ | |
| 2089 | "registry": "registry.cloudflare.com", "repository": "acc/g1t-web", "tag": "dpl_1", | |
| 2090 | "username": "u", "password": "p", "max_image_bytes": 2_000_000_000u64, "instance_type": "lite" | |
| 2091 | })) | |
| 2092 | .unwrap(); | |
| 2093 | assert_eq!((grant.max_image_bytes, grant.instance_type.as_deref()), (2_000_000_000, Some("lite"))); | |
| 2094 | assert_eq!(size_text(1_450_000_000), "1.4 GB"); | |
| 2095 | } | |
| 2096 | ||
| 2097 | /// What a container g1t writes the Dockerfile for is built with. | |
| 2098 | const GENERATED: &str = "docker build -f Dockerfile (written by g1t) ."; | |
| 2099 | ||
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 2100 | fn frameworks_hugo() -> &'static str { |
| 2101 | "if command -v hugo >/dev/null 2>&1; then hugo --minify; else npx --yes hugo-extended@0.166.0 --minify; fi" | |
| 2102 | } | |
| 2103 | ||
| 2104 | fn frameworks_mdbook() -> &'static str { | |
| 2105 | "if ! command -v mdbook >/dev/null 2>&1; then cargo install mdbook --version 0.5.4 --locked --quiet; fi && mdbook build" | |
| 2106 | } | |
| 2107 | ||
| 2108 | #[test] | |
| 2109 | fn jekyll_installs_its_gems_after_the_packages() { | |
| 2110 | let dir = project("jekyll-install", &[("_config.yml", ""), ("Gemfile", ""), ("package.json", "{}")]); | |
| 2111 | let found = detect(&dir, None, None).unwrap(); | |
| 2112 | assert_eq!(found.install.as_deref(), Some("npm install && bundle config set --local path vendor/bundle && bundle install")); | |
| 2113 | } | |
| 2114 | ||
| 2115 | #[test] | |
| 2116 | fn an_adapters_worker_is_found_once_built_and_never_served() { | |
| 2117 | let dir = project( | |
| 2118 | "adapter", | |
| 2119 | &[ | |
| 2120 | ("dist/_worker.js/index.js", "export default {}"), | |
| 2121 | ("dist/_routes.json", "{}"), | |
| 2122 | ("dist/.assetsignore", "_worker.js\nsecret.txt\n"), | |
| 2123 | ("dist/secret.txt", "no"), | |
| 2124 | ("dist/index.html", "<h1>hi</h1>"), | |
| 2125 | ("dist/about/_worker.js", "a page's own file"), | |
| 2126 | ("kit/.svelte-kit/cloudflare/_worker.js", "export default {}"), | |
| 2127 | ], | |
| 2128 | ); | |
| 2129 | let astro = Adapter { entry: "dist/_worker.js", assets: "dist" }; | |
| 2130 | assert_eq!(adapter_entry(&dir, &astro), Some(dir.join("dist/_worker.js/index.js"))); | |
| 2131 | let kit = Adapter { entry: ".svelte-kit/cloudflare/_worker.js", assets: ".svelte-kit/cloudflare" }; | |
| 2132 | assert_eq!(adapter_entry(&dir.join("kit"), &kit), Some(dir.join("kit/.svelte-kit/cloudflare/_worker.js"))); | |
| 2133 | assert_eq!(adapter_entry(&dir.join("missing"), &kit), None); | |
| 2134 | let mut served: Vec<String> = collect_site(&dir.join("dist"), false).unwrap().into_iter().map(|a| a.path).collect(); | |
| 2135 | served.sort(); | |
| 2136 | assert_eq!(served, ["/about/_worker.js", "/index.html"]); | |
| 2137 | } | |
| 2138 | ||
| 2139 | #[test] | |
| 2140 | fn a_config_the_build_wrote_is_read_in_place_of_the_projects() { | |
| 2141 | let dir = project( | |
| 2142 | "redirected", | |
| 2143 | &[ | |
| 2144 | ("wrangler.jsonc", r#"{ "main": "worker/index.ts" }"#), | |
| 2145 | (".wrangler/deploy/config.json", r#"{ "configPath": "../../dist/app/wrangler.json" }"#), | |
| 2146 | ("dist/app/wrangler.json", r#"{ "main": "index.js", "assets": { "directory": "../client" } }"#), | |
| 2147 | ], | |
| 2148 | ); | |
| 2149 | let (config, base) = redirected_config(&dir).unwrap().unwrap(); | |
| 2150 | assert_eq!(config.main.as_deref(), Some("index.js")); | |
| 2151 | assert_eq!(base, dir.join("dist/app").canonicalize().unwrap()); | |
| 2152 | assert!(base.join(config.assets.unwrap().directory.unwrap()).ends_with("dist/app/../client")); | |
| 2153 | assert!(redirected_config(&project("not-redirected", &[])).unwrap().is_none()); | |
| 2154 | } | |
| 2155 | ||
| 2156 | #[test] | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 2157 | fn the_root_directory_stays_inside_the_checkout() { |
| 2158 | let base = Path::new("/w/repo"); | |
| 2159 | assert_eq!(project_dir_in(base, ""), base); | |
| 2160 | assert_eq!(project_dir_in(base, "."), base); | |
| 2161 | assert_eq!(project_dir_in(base, "/apps/site/"), base.join("apps/site")); | |
| 2162 | assert_eq!(project_dir_in(base, "../etc"), base); | |
| 2163 | assert_eq!(project_dir_in(base, "apps/../../x"), base); | |
| 2164 | } | |
| 2165 | ||
| 2166 | #[test] | |
| 2167 | fn a_step_prints_the_log_redacted() { | |
| 2168 | let mut log = Log { echo: Some(vec!["hunter22".to_owned()]), ..Log::default() }; | |
| 2169 | log.line("token hunter22 used"); | |
| 2170 | assert!(log.text.contains("hunter22"), "the deployment's log is redacted when it is sent"); | |
| g1t.page builds know the framework a project is built with, from its package's dependencies and config files: Vite (and React, Vue, Svelte, Solid or Preact on it), Astro, Next.js with `output: 'export'`, SvelteKit with the static adapter, Nuxt by `nuxi generate`, React Router and Remix in SPA mode, Gatsby, Docusaurus, Eleventy, Angular, Create React App, Hugo, Jekyll and mdBook are each built with their own command and served from the folder they write, read from their config where it moves it. A Cloudflare adapter's Worker deploys as a Workers project with no Wrangler config of the project's own: Astro's, which now writes its config for wrangler in `.wrangler/deploy/config.json` (as `@cloudflare/vite-plugin` does, and that is read too), SvelteKit's `.svelte-kit/cloudflare/_worker.js`, and OpenNext's with a `wrangler.jsonc`; a site's `_worker.js`, `_routes.json` and what `.assetsignore` lists are never uploaded, and a written config's empty binding lists are no longer warned about. A framework set up to need a server (Next.js without a static export, Astro's server output, SvelteKit's auto or node adapter, a server-rendered React Router or Remix app) fails before anything is installed, saying what to change, in place of "found nothing to serve". The project's Build command and Output directory still win. The build's log names the framework, the finish report carries it as `framework` (which the deployments service ignores for now), and `g1t-runner detect` and the `g1t/detect` action output it. What deploys has a table of every framework, and the detect action's README lists them. | 2171 | assert_eq!(wrangler_build(Some(Path::new("/tmp/b")), None), "npx --yes wrangler@4 deploy --dry-run --outdir /tmp/b"); |
| 2172 | assert_eq!( | |
| 2173 | wrangler_build(Some(Path::new("/tmp/b")), Some(Path::new("/tmp/w.json"))), | |
| 2174 | "npx --yes wrangler@4 deploy --dry-run --config /tmp/w.json --outdir /tmp/b" | |
| 2175 | ); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 2176 | } |
| What g1t can't do yet, and an open letter to Cloudflare | 2177 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.