Skip to content
2,177 linesCodeBlameRaw

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 Cloudflare1//! 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-7nm8r925//! - 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 Cloudflare33//! - 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-7nm8r937//! 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 Cloudflare41//! 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-7nm8r951//!
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 Cloudflare60
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.61use std::collections::{BTreeMap, BTreeSet};
What g1t can't do yet, and an open letter to Cloudflare62use std::path::{Path, PathBuf};
63use std::time::Instant;
64
65use anyhow::{Context, Result, bail};
66use base64::Engine;
67use base64::engine::general_purpose::STANDARD;
68use serde::{Deserialize, Serialize};
69use serde_json::{Value, json};
70use sha2::{Digest, Sha256};
71
72use crate::checks::{redact, run_command};
73use crate::{WORKDIR, auth_option, env, git};
74
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r975mod 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.76mod frameworks;
77
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r978use 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.79use frameworks::{Adapter, Framework};
80
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r981/// Where `wrangler deploy --dry-run` writes the bundle in a sandbox.
What g1t can't do yet, and an open letter to Cloudflare82const BUNDLE_DIR: &str = "/work/g1t-bundle";
83/// Cloudflare's limits on a Worker's static assets.
84const MAX_FILES: usize = 20_000;
85const MAX_FILE_BYTES: u64 = 25 * 1024 * 1024;
86/// How much of the build's output is kept for the deployment's log.
87const MAX_LOG_CHARS: usize = 20_000;
88/// Directories a static build usually writes to, in the order they are tried.
89const OUTPUT_DIRS: [&str; 6] = ["dist", "build", "out", "public", "_site", ".output/public"];
90/// Bindings a Workers project may declare that are not provisioned yet.
91const 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
104struct Reporter {
105 base: String,
106 token: String,
107}
108
109impl 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-7nm8r9119/// 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 Cloudflare120#[derive(Default)]
121struct Log {
122 text: String,
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9123 /// Each line to standard output as well, redacted.
124 echo: Option<Vec<String>>,
What g1t can't do yet, and an open letter to Cloudflare125}
126
127impl 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-7nm8r9131 if let Some(secrets) = &self.echo {
132 println!("{}", redact(line, secrets));
133 }
What g1t can't do yet, and an open letter to Cloudflare134 }
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.
147fn 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)]
161struct 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)]
174struct 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.
182fn 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
247fn 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.
266fn 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
283fn 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.
291struct Asset {
292 path: String,
293 hash: String,
294 size: u64,
295 file: PathBuf,
296}
297
298fn 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.
329fn 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.
343fn 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
349fn 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 Cloudflare350 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 Cloudflare361 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 Cloudflare363 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)]
394struct 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.
404fn 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")]
440struct 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-7nm8r9447fn bundle_modules(main: &str, bundle: &Path) -> Result<(String, Vec<Module>)> {
What g1t can't do yet, and an open letter to Cloudflare448 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-7nm8r9454 collect_files(bundle, &mut files)?;
What g1t can't do yet, and an open letter to Cloudflare455 for file in files {
456 let name = file
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9457 .strip_prefix(bundle)?
What g1t can't do yet, and an open letter to Cloudflare458 .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
486fn 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 daily498/// 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.
501fn 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 daily512 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 daily515 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 Cloudflare533/// What was built: the Worker's code and settings, and where its site is.
534struct 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-7nm8r9539 /// `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 Cloudflare541 detected: &'static str,
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9542 /// 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-7nm8r9545 /// 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 Cloudflare547}
548
549/// What a project without a Workers config is: a site it built, or its own
550/// files served as they are.
551fn 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-7nm8r9555/// 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.
557fn 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-7nm8r9564 }
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-7nm8r9566}
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.
572fn 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`.
587fn 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-7nm8r9595/// 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.
597fn 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-7nm8r9613#[derive(Debug, PartialEq, Eq)]
614pub(crate) struct Detected {
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9615 /// `workers`, `static`, `html`, `container`, or `none` when there is
616 /// nothing to serve.
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9617 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-7nm8r9621 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-7nm8r9629 /// 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-7nm8r9635}
636
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9637/// How a container is built.
638#[derive(Debug, Clone, PartialEq, Eq)]
639pub(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
650impl 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.
676const 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.
682fn 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-7nm8r9714pub(crate) fn detect(dir: &Path, custom_build: Option<&str>, chosen_output: Option<&str>) -> Result<Detected> {
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r9715 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.
731pub(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.
797fn 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-7nm8r9798 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-7nm8r9800 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-7nm8r9812 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-7nm8r9814 }
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-7nm8r9818 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-7nm8r9823 container: None,
824 fallback: None,
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9825 });
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-7nm8r9830 .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-7nm8r9832 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-7nm8r9843 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-7nm8r9847 // 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-7nm8r9852 .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-7nm8r9860 // `.` 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-7nm8r9866 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-7nm8r9867}
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.
870const 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).
875fn 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.
908fn 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-7nm8r9913/// What a container build says when g1t.page runs no containers.
914const 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`.
919fn 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.
925fn 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)]
933struct 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.
947fn 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`.
957fn 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.
963fn 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`).
986fn 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(&note);
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
1075fn build(reporter: &Reporter, log: &mut Log, secrets: &[String]) -> Result<Built> {
What g1t can't do yet, and an open letter to Cloudflare1076 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-7nm8r91080 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-7nm8r91090 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 Cloudflare1115 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-7nm8r91120 // 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-7nm8r91127 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-7nm8r91135 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-7nm8r91139 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-7nm8r91141 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-7nm8r91172 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 Cloudflare1184 }
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 Cloudflare1186 }
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 Cloudflare1205 }
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 Cloudflare1207 }
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 Cloudflare1209 }
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-7nm8r91223 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 Cloudflare1225}
1226
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91227/// Whether the build checks out its own commit (a sandbox, given
1228/// `GIT_REMOTE`) rather than building the checkout it runs in.
1229fn 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.
1235fn checkout_dir() -> PathBuf {
1236 if clones() {
What g1t can't do yet, and an open letter to Cloudflare1237 PathBuf::from(WORKDIR)
1238 } else {
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91239 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.
1245fn 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.
1255fn 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 Cloudflare1261 }
1262}
1263
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91264/// Where the project lives: `ROOT_DIR` in the checkout.
1265fn 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 Cloudflare1269fn 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 sidebar1274 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 Cloudflare1281 if let Err(error) = cloned {
1282 bail!("{}", redact(&format!("{error:#}"), secrets));
1283 }
1284 Ok(())
1285}
1286
1287fn deploy(reporter: &Reporter, log: &mut Log, secrets: &[String]) -> Result<Value> {
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91288 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 Cloudflare1297 // 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-7nm8r91310 let built = build(reporter, log, secrets)?;
What g1t can't do yet, and an open letter to Cloudflare1311 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-7nm8r91315 // 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 Cloudflare1317 });
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r91318 if let Some(image) = built.container {
1319 finish["container"] = image;
1320 }
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91321 for warning in &built.warnings {
1322 log.line(&format!("Warning: {warning}"));
1323 }
What g1t can't do yet, and an open letter to Cloudflare1324 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 Cloudflare1327 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-7nm8r91358/// A sandbox's build (`MODE=deploy`).
What g1t can't do yet, and an open letter to Cloudflare1359pub fn main() -> i32 {
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91360 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.
1366pub 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
1374const CLI_HELP: &str = "g1t-runner deploy: builds the project in this checkout and puts it on g1t.page.
1375
1376Reads DEPLOY_ID and DEPLOY_TOKEN (from POST /repos/{owner}/{repo}/deployments/builds),
1377G1T_API (https://api.g1t.sh), and optionally ROOT_DIR, BUILD_COMMAND, OUTPUT_DIR,
Merge branch 'worktree-agent-a0d1a64e2033a58ed' into claude/cool-newton-7nm8r91378BUILD_AS (auto, static, workers or docker), DOCKERFILE, and BUILD_ENV and
1379BUILD_SECRETS (JSON objects). The g1t/deploy action sets them all. A container
1380is built with the docker on PATH.
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91381
1382g1t-runner detect prints what the build would find, as key=value lines.";
1383
1384fn run(step_log: bool) -> i32 {
What g1t can't do yet, and an open letter to Cloudflare1385 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-7nm8r91387 base: format!("{}/deployments/jobs/{id}", api.trim_end_matches('/')),
What g1t can't do yet, and an open letter to Cloudflare1388 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-7nm8r91409 let mut log = Log { echo: step_log.then(|| secrets.clone()), ..Log::default() };
What g1t can't do yet, and an open letter to Cloudflare1410 let outcome = deploy(&reporter, &mut log, &secrets);
1411 let seconds = started.elapsed().as_secs();
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91412 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-7nm8r91415 let _ = std::fs::remove_dir_all(docker_config_dir());
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91416 }
1417 let (sent, failed) = match outcome {
What g1t can't do yet, and an open letter to Cloudflare1418 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-7nm8r91421 (reporter.send("finish", finish), false)
What g1t can't do yet, and an open letter to Cloudflare1422 }
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-7nm8r91426 let sent = reporter.send(
What g1t can't do yet, and an open letter to Cloudflare1427 "fail",
1428 json!({ "message": message, "log": redact(&log.tail(), &secrets), "buildSeconds": seconds }),
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91429 );
1430 (sent, true)
What g1t can't do yet, and an open letter to Cloudflare1431 }
1432 };
1433 match sent {
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91434 // 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 Cloudflare1441 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-7nm8r91449/// `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-7nm8r91455pub 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-7nm8r91463 "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-7nm8r91465 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-7nm8r91476 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-7nm8r91479 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-7nm8r91484 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-7nm8r91496fn 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-7nm8r91499 "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-7nm8r91502 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-7nm8r91505 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-7nm8r91508 // 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-7nm8r91513}
1514
What g1t can't do yet, and an open letter to Cloudflare1515#[cfg(test)]
1516mod 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 daily1550 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]
1562crons = [\"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 daily1571 }
1572
1573 #[test]
What g1t can't do yet, and an open letter to Cloudflare1574 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-7nm8r91587
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-7nm8r91615 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-7nm8r91631 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-7nm8r91639 // 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-7nm8r91654 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-7nm8r91657 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-7nm8r91661 }
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-7nm8r91690 #[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-7nm8r91755 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-7nm8r91757 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-7nm8r91785 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-7nm8r91787 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-7nm8r91852 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-7nm8r91884 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-7nm8r91886 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-7nm8r92018 #[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(&nothing, 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(&nothing, 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-7nm8r92157 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-7nm8r92176 }
What g1t can't do yet, and an open letter to Cloudflare2177}

This file's history is long; its oldest lines are credited to the oldest commit read.