Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
1,707 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.

Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1//! 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.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25//! - Anything else: a static site. Its `build` script runs, and the first
26//! of `dist`, `build`, `out`, `public`, `_site` or `.output/public` that
27//! exists is served, or the repository itself if it has an `index.html`.
28//!
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.29//! The project's own **Build command** and **Output directory** win over
30//! what is detected.
31//!
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look32//! Configuration comes from the environment:
33//!
34//! - `G1T_API`, `DEPLOY_ID`, `DEPLOY_TOKEN`: where and how to report.
35//! - `GIT_REMOTE`, `GIT_COMMIT`, `G1T_USER`, `G1T_TOKEN`: what to check out.
36//! - `ROOT_DIR`: where in the repository the project lives; empty for all
37//! of it. Everything below is relative to it.
38//! - `BUILD_COMMAND`, `OUTPUT_DIR`: the project's own choices, if any.
39//! - `BUILD_ENV`, `BUILD_SECRETS`: JSON objects of the repository's
40//! variables and secrets for deploy builds. Both are set for the build;
41//! secrets' values are redacted from its log.
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r942//!
43//! From a workflow step, `g1t-runner deploy` runs the same build in the
44//! job's own checkout: without `GIT_REMOTE` nothing is cloned, and the
45//! project is the current directory (`ROOT_DIR` below it). The build's id
46//! and token come from `POST /repos/{owner}/{repo}/deployments/builds`,
47//! which the `g1t/deploy` action calls (actions/deploy). Its output goes
48//! to the step's log as well as the deployment's. `g1t-runner detect`
49//! says what the build would find, as `key=value` lines for
50//! `$GITHUB_OUTPUT`, without building anything.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look51
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.52use std::collections::{BTreeMap, BTreeSet};
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look53use std::path::{Path, PathBuf};
54use std::time::Instant;
55
56use anyhow::{Context, Result, bail};
57use base64::Engine;
58use base64::engine::general_purpose::STANDARD;
59use serde::{Deserialize, Serialize};
60use serde_json::{Value, json};
61use sha2::{Digest, Sha256};
62
63use crate::checks::{redact, run_command};
64use crate::{WORKDIR, auth_option, env, git};
65
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.66mod frameworks;
67
68use frameworks::{Adapter, Framework};
69
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r970/// Where `wrangler deploy --dry-run` writes the bundle in a sandbox.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look71const BUNDLE_DIR: &str = "/work/g1t-bundle";
72/// Cloudflare's limits on a Worker's static assets.
73const MAX_FILES: usize = 20_000;
74const MAX_FILE_BYTES: u64 = 25 * 1024 * 1024;
75/// How much of the build's output is kept for the deployment's log.
76const MAX_LOG_CHARS: usize = 20_000;
77/// Directories a static build usually writes to, in the order they are tried.
78const OUTPUT_DIRS: [&str; 6] = ["dist", "build", "out", "public", "_site", ".output/public"];
79/// Bindings a Workers project may declare that are not provisioned yet.
80const UNSUPPORTED_BINDINGS: [&str; 10] = [
81 "kv_namespaces",
82 "d1_databases",
83 "r2_buckets",
84 "durable_objects",
85 "services",
86 "queues",
87 "vectorize",
88 "hyperdrive",
89 "ai",
90 "workflows",
91];
92
93struct Reporter {
94 base: String,
95 token: String,
96}
97
98impl Reporter {
99 fn send(&self, step: &str, mut body: Value) -> Result<Value> {
100 body["token"] = self.token.clone().into();
101 let response = ureq::post(&format!("{}/{step}", self.base))
102 .send_json(body)
103 .with_context(|| format!("could not report `{step}` to g1t"))?;
104 Ok(response.into_json().unwrap_or(Value::Null))
105 }
106}
107
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9108/// The build's log, kept to its end; also printed, in a workflow step.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look109#[derive(Default)]
110struct Log {
111 text: String,
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9112 /// Each line to standard output as well, redacted.
113 echo: Option<Vec<String>>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look114}
115
116impl Log {
117 fn line(&mut self, line: &str) {
118 self.text.push_str(line);
119 self.text.push('\n');
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9120 if let Some(secrets) = &self.echo {
121 println!("{}", redact(line, secrets));
122 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look123 }
124
125 fn tail(&self) -> String {
126 let length = self.text.chars().count();
127 if length <= MAX_LOG_CHARS {
128 return self.text.clone();
129 }
130 let kept: String = self.text.chars().skip(length - MAX_LOG_CHARS).collect();
131 format!("… (earlier output not shown)\n{kept}")
132 }
133}
134
135/// Runs a command in the checkout, logging it; fails if it fails.
136fn step(log: &mut Log, command: &str, secrets: &[String]) -> Result<()> {
137 log.line(&format!("$ {command}"));
138 let result = run_command(command, &project_dir(), secrets);
139 if !result.output_text().is_empty() {
140 log.line(result.output_text());
141 }
142 if !result.passed {
143 bail!("`{command}` failed");
144 }
145 Ok(())
146}
147
148/// A Workers project's settings, from whichever config file it has.
149#[derive(Debug, Default, Deserialize)]
150struct WranglerConfig {
151 main: Option<String>,
152 compatibility_date: Option<String>,
153 #[serde(default)]
154 compatibility_flags: Vec<String>,
155 assets: Option<AssetsConfig>,
156 #[serde(default)]
157 vars: BTreeMap<String, Value>,
158 #[serde(flatten)]
159 rest: BTreeMap<String, Value>,
160}
161
162#[derive(Debug, Default, Deserialize)]
163struct AssetsConfig {
164 directory: Option<String>,
165 binding: Option<String>,
166 html_handling: Option<String>,
167 not_found_handling: Option<String>,
168}
169
170/// JSON with comments and trailing commas, as `wrangler.jsonc` allows.
171fn strip_jsonc(text: &str) -> String {
172 let mut out = String::with_capacity(text.len());
173 let mut chars = text.chars().peekable();
174 let mut in_string = false;
175 while let Some(c) = chars.next() {
176 if in_string {
177 out.push(c);
178 if c == '\\' {
179 if let Some(next) = chars.next() {
180 out.push(next);
181 }
182 } else if c == '"' {
183 in_string = false;
184 }
185 continue;
186 }
187 match (c, chars.peek()) {
188 ('"', _) => {
189 in_string = true;
190 out.push(c);
191 }
192 ('/', Some('/')) => {
193 for c in chars.by_ref() {
194 if c == '\n' {
195 out.push('\n');
196 break;
197 }
198 }
199 }
200 ('/', Some('*')) => {
201 chars.next();
202 let mut last = ' ';
203 for c in chars.by_ref() {
204 if last == '*' && c == '/' {
205 break;
206 }
207 last = c;
208 }
209 }
210 _ => out.push(c),
211 }
212 }
213 // Trailing commas before a closing bracket.
214 let mut cleaned = String::with_capacity(out.len());
215 let chars: Vec<char> = out.chars().collect();
216 let mut in_string = false;
217 let mut i = 0;
218 while i < chars.len() {
219 let c = chars[i];
220 if c == '"' && (i == 0 || chars[i - 1] != '\\') {
221 in_string = !in_string;
222 }
223 if c == ',' && !in_string {
224 let next = chars[i + 1..].iter().find(|c| !c.is_whitespace());
225 if matches!(next, Some('}') | Some(']')) {
226 i += 1;
227 continue;
228 }
229 }
230 cleaned.push(c);
231 i += 1;
232 }
233 cleaned
234}
235
236fn read_wrangler(dir: &Path) -> Result<Option<WranglerConfig>> {
237 for name in ["wrangler.jsonc", "wrangler.json"] {
238 let path = dir.join(name);
239 if path.exists() {
240 let text = std::fs::read_to_string(&path)?;
241 return Ok(Some(
242 serde_json::from_str(&strip_jsonc(&text)).with_context(|| format!("could not read {name}"))?,
243 ));
244 }
245 }
246 let path = dir.join("wrangler.toml");
247 if path.exists() {
248 let text = std::fs::read_to_string(&path)?;
249 return Ok(Some(toml::from_str(&text).context("could not read wrangler.toml")?));
250 }
251 Ok(None)
252}
253
254/// How to install the project's dependencies, judged by its lockfile.
255fn install_command(dir: &Path) -> Option<&'static str> {
256 if !dir.join("package.json").exists() {
257 return None;
258 }
259 Some(if dir.join("pnpm-lock.yaml").exists() {
260 "corepack enable && pnpm install --frozen-lockfile"
261 } else if dir.join("yarn.lock").exists() {
262 "corepack enable && yarn install"
263 } else if dir.join("bun.lockb").exists() || dir.join("bun.lock").exists() {
264 "npx --yes bun install"
265 } else if dir.join("package-lock.json").exists() {
266 "npm ci"
267 } else {
268 "npm install"
269 })
270}
271
272fn has_build_script(dir: &Path) -> bool {
273 std::fs::read_to_string(dir.join("package.json"))
274 .ok()
275 .and_then(|text| serde_json::from_str::<Value>(&text).ok())
276 .is_some_and(|package| package["scripts"]["build"].is_string())
277}
278
279/// One file of the site, as Cloudflare's asset upload names it.
280struct Asset {
281 path: String,
282 hash: String,
283 size: u64,
284 file: PathBuf,
285}
286
287fn content_type(path: &str) -> &'static str {
288 let extension = path.rsplit('.').next().unwrap_or("").to_ascii_lowercase();
289 match extension.as_str() {
290 "html" | "htm" => "text/html",
291 "css" => "text/css",
292 "js" | "mjs" => "application/javascript",
293 "json" | "map" => "application/json",
294 "svg" => "image/svg+xml",
295 "png" => "image/png",
296 "jpg" | "jpeg" => "image/jpeg",
297 "gif" => "image/gif",
298 "webp" => "image/webp",
299 "avif" => "image/avif",
300 "ico" => "image/x-icon",
301 "woff" => "font/woff",
302 "woff2" => "font/woff2",
303 "ttf" => "font/ttf",
304 "txt" => "text/plain",
305 "xml" => "application/xml",
306 "wasm" => "application/wasm",
307 "pdf" => "application/pdf",
308 "mp4" => "video/mp4",
309 "webm" => "video/webm",
310 _ => "application/octet-stream",
311 }
312}
313
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.314/// What is never served from the top of a site: a Pages-style Worker
315/// (`_worker.js`, its server code, which a Cloudflare adapter writes beside
316/// the site's files), `_routes.json`, and `.assetsignore`, with each name
317/// that `.assetsignore` lists, as wrangler leaves them out.
318fn ignored_at_root(root: &Path) -> BTreeSet<String> {
319 let mut ignored: BTreeSet<String> = ["_worker.js", "_routes.json", ".assetsignore"].map(str::to_owned).into();
320 if let Ok(text) = std::fs::read_to_string(root.join(".assetsignore")) {
321 ignored.extend(
322 text.lines()
323 .map(|line| line.trim().trim_matches('/'))
324 .filter(|line| !line.is_empty() && !line.starts_with('#') && !line.contains(['*', '/', '!']))
325 .map(str::to_owned),
326 );
327 }
328 ignored
329}
330
331/// Every file of the site at `root`, but for what never belongs in one.
332fn collect_site(root: &Path, skip_project: bool) -> Result<Vec<Asset>> {
333 let mut assets = Vec::new();
334 collect(root, root, skip_project, &ignored_at_root(root), &mut assets)?;
335 Ok(assets)
336}
337
338fn collect(root: &Path, dir: &Path, skip_project: bool, ignored: &BTreeSet<String>, out: &mut Vec<Asset>) -> Result<()> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look339 for entry in std::fs::read_dir(dir)? {
340 let entry = entry?;
341 let name = entry.file_name().to_string_lossy().into_owned();
342 let path = entry.path();
343 let kind = entry.file_type()?;
344 if name == ".git" || (skip_project && (name == "node_modules" || name.starts_with(".g1t"))) {
345 continue;
346 }
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.347 if dir == root && ignored.contains(&name) {
348 continue;
349 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look350 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.351 collect(root, &path, skip_project, ignored, out)?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look352 continue;
353 }
354 if !kind.is_file() || name == "_headers" || name == "_redirects" {
355 continue;
356 }
357 let size = entry.metadata()?.len();
358 let relative = path
359 .strip_prefix(root)?
360 .to_string_lossy()
361 .replace('\\', "/");
362 if size > MAX_FILE_BYTES {
363 bail!("{relative} is larger than Cloudflare's 25 MiB limit for one file");
364 }
365 let bytes = std::fs::read(&path)?;
366 let digest = hex::encode(Sha256::digest(&bytes));
367 out.push(Asset {
368 path: format!("/{relative}"),
369 hash: digest[..32].to_owned(),
370 size,
371 file: path,
372 });
373 if out.len() > MAX_FILES {
374 bail!("the site has more than {MAX_FILES} files, Cloudflare's limit");
375 }
376 }
377 Ok(())
378}
379
380/// The upload, as the API sends it: `snake_case`, or `uploadUrl` from
381/// before its bodies were.
382#[derive(Deserialize)]
383struct UploadSession {
384 jwt: String,
385 #[serde(default)]
386 buckets: Vec<Vec<String>>,
387 #[serde(alias = "uploadUrl")]
388 upload_url: String,
389}
390
391/// Sends one bucket of files with the upload's key. The last bucket's
392/// answer carries the key that completes the upload.
393fn upload_bucket(session: &UploadSession, bucket: &[String], by_hash: &BTreeMap<&str, &Asset>) -> Result<Option<String>> {
394 let boundary = format!("g1t-{}", hex::encode(Sha256::digest(bucket.join(",").as_bytes()))[..24].to_owned());
395 let mut body: Vec<u8> = Vec::new();
396 for hash in bucket {
397 let asset = by_hash
398 .get(hash.as_str())
399 .with_context(|| format!("Cloudflare asked for a file this build does not have ({hash})"))?;
400 let bytes = std::fs::read(&asset.file)?;
401 body.extend_from_slice(
402 format!(
403 "--{boundary}\r\nContent-Disposition: form-data; name=\"{hash}\"; filename=\"{hash}\"\r\nContent-Type: {}\r\n\r\n",
404 content_type(&asset.path)
405 )
406 .as_bytes(),
407 );
408 body.extend_from_slice(STANDARD.encode(bytes).as_bytes());
409 body.extend_from_slice(b"\r\n");
410 }
411 body.extend_from_slice(format!("--{boundary}--\r\n").as_bytes());
412 let response = ureq::post(&session.upload_url)
413 .set("authorization", &format!("Bearer {}", session.jwt))
414 .set("content-type", &format!("multipart/form-data; boundary={boundary}"))
415 .send_bytes(&body);
416 let response = match response {
417 Ok(response) => response,
418 Err(ureq::Error::Status(code, response)) => {
419 bail!("Cloudflare refused the upload ({code}): {}", response.into_string().unwrap_or_default())
420 }
421 Err(error) => bail!("could not upload to Cloudflare: {error}"),
422 };
423 let answer: Value = response.into_json().unwrap_or(Value::Null);
424 Ok(answer["result"]["jwt"].as_str().map(str::to_owned))
425}
426
427#[derive(Serialize)]
428#[serde(rename_all = "camelCase")]
429struct Module {
430 name: String,
431 content_base64: String,
432 content_type: String,
433}
434
435/// The bundle `wrangler deploy --dry-run` wrote, main module first.
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9436fn bundle_modules(main: &str, bundle: &Path) -> Result<(String, Vec<Module>)> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look437 let stem = Path::new(main)
438 .file_stem()
439 .map(|stem| stem.to_string_lossy().into_owned())
440 .unwrap_or_else(|| "index".to_owned());
441 let mut modules = Vec::new();
442 let mut files = Vec::new();
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9443 collect_files(bundle, &mut files)?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look444 for file in files {
445 let name = file
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9446 .strip_prefix(bundle)?
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look447 .to_string_lossy()
448 .replace('\\', "/");
449 let kind = match name.rsplit('.').next().unwrap_or("") {
450 "js" | "mjs" => "application/javascript+module",
451 "wasm" => "application/wasm",
452 "map" | "md" => continue,
453 _ => "text/plain",
454 };
455 modules.push(Module {
456 content_base64: STANDARD.encode(std::fs::read(&file)?),
457 content_type: kind.to_owned(),
458 name,
459 });
460 }
461 let main_name = modules
462 .iter()
463 .map(|module| module.name.clone())
464 .find(|name| *name == format!("{stem}.js") || *name == format!("{stem}.mjs"))
465 .or_else(|| {
466 modules
467 .iter()
468 .find(|module| module.content_type == "application/javascript+module")
469 .map(|module| module.name.clone())
470 })
471 .context("wrangler wrote no JavaScript module")?;
472 Ok((main_name, modules))
473}
474
475fn collect_files(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
476 for entry in std::fs::read_dir(dir)? {
477 let entry = entry?;
478 if entry.file_type()?.is_dir() {
479 collect_files(&entry.path(), out)?;
480 } else {
481 out.push(entry.path());
482 }
483 }
484 Ok(())
485}
486
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily487/// What a Workers config asks for that g1t.page does not provision, said
488/// as the deployment's warnings: bindings it runs without, and scheduled
489/// (`triggers.crons`) runs that never fire.
490fn 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.491 // An empty list, as a config a framework's build writes has for each
492 // kind of binding (`"d1_databases": []`), declares nothing.
493 fn declares(value: &Value) -> bool {
494 match value {
495 Value::Null => false,
496 Value::Array(items) => !items.is_empty(),
497 Value::Object(fields) => fields.values().any(declares),
498 _ => true,
499 }
500 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily501 let mut warnings = Vec::new();
502 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.503 if config.rest.get(binding).is_some_and(declares) {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily504 warnings.push(format!("`{binding}` is not provisioned on g1t.page yet, so the app runs without it."));
505 }
506 }
507 let crons = config
508 .rest
509 .get("triggers")
510 .and_then(|triggers| triggers.get("crons"))
511 .and_then(Value::as_array)
512 .map_or(0, |crons| crons.iter().filter(|cron| cron.as_str().is_some_and(|c| !c.trim().is_empty())).count());
513 if crons > 0 {
514 let schedules = if crons == 1 { "schedule" } else { "schedules" };
515 warnings.push(format!(
516 "`triggers.crons` ({crons} {schedules}) is not set up on g1t.page yet, so the Worker's `scheduled` handler never runs."
517 ));
518 }
519 warnings
520}
521
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look522/// What was built: the Worker's code and settings, and where its site is.
523struct Built {
524 worker: Value,
525 assets_dir: Option<PathBuf>,
526 warnings: Vec<String>,
527 /// What kind of project it was found to be, for its settings page:
528 /// `workers`, `static` (a build's output) or `html` (the files as they are).
529 detected: &'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.530 /// The framework it was found to be built with, if any (`astro`…).
531 framework: Option<&'static str>,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look532}
533
534/// What a project without a Workers config is: a site it built, or its own
535/// files served as they are.
536fn static_kind(project: &Path, assets_dir: &Path, built: bool) -> &'static str {
537 if !built && assets_dir == project { "html" } else { "static" }
538}
539
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9540/// 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.541/// into `bundle` when given, by the config at `config` when given.
542fn wrangler_build(bundle: Option<&Path>, config: Option<&Path>) -> String {
543 let mut command = "npx --yes wrangler@4 deploy --dry-run".to_owned();
544 if let Some(config) = config {
545 command.push_str(&format!(" --config {}", config.display()));
546 }
547 if let Some(bundle) = bundle {
548 command.push_str(&format!(" --outdir {}", bundle.display()));
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9549 }
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.550 command
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9551}
552
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.553/// The config a framework's build wrote for wrangler, which wrangler reads
554/// in place of the project's own (`.wrangler/deploy/config.json`, as
555/// `@cloudflare/vite-plugin` writes it), with the folder its paths are
556/// relative to.
557fn redirected_config(dir: &Path) -> Result<Option<(WranglerConfig, PathBuf)>> {
558 let pointer = dir.join(".wrangler/deploy/config.json");
559 let Ok(text) = std::fs::read_to_string(&pointer) else { return Ok(None) };
560 let Some(path) = serde_json::from_str::<Value>(&text).ok().and_then(|v| v["configPath"].as_str().map(str::to_owned)) else {
561 return Ok(None);
562 };
563 let path = dir.join(".wrangler/deploy").join(path);
564 let path = path.canonicalize().unwrap_or(path);
565 let text = std::fs::read_to_string(&path).with_context(|| format!("could not read {}", path.display()))?;
566 let config = serde_json::from_str(&strip_jsonc(&text)).with_context(|| format!("could not read {}", path.display()))?;
567 Ok(Some((config, path.parent().map_or_else(|| dir.to_path_buf(), Path::to_path_buf))))
568}
569
570/// The entry of the Worker a framework's adapter wrote, once it has built:
571/// the file, or a directory's `index.js`.
572fn adapter_entry(dir: &Path, adapter: &Adapter) -> Option<PathBuf> {
573 let entry = dir.join(adapter.entry);
574 if entry.is_file() {
575 return Some(entry);
576 }
577 ["index.js", "index.mjs"].iter().map(|name| entry.join(name)).find(|index| index.is_file())
578}
579
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9580/// The first of the usual output directories a static build wrote (or the
581/// project's own `index.html`), relative to the project: `.` for itself.
582fn find_output(dir: &Path) -> Option<String> {
583 OUTPUT_DIRS
584 .iter()
585 .find(|name| {
586 let path = dir.join(name);
587 path.join("index.html").exists() || (path.is_dir() && **name != "public")
588 })
589 .map(|name| (*name).to_owned())
590 .or_else(|| dir.join("index.html").exists().then(|| ".".to_owned()))
591}
592
593/// What a build of the project at `dir` does, found before anything runs:
594/// what it is, how its dependencies are installed, what builds it, and
595/// where what it serves is. For a site with a build of its own, the output
596/// 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.597/// unless one was chosen or the framework says where it writes.
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9598#[derive(Debug, PartialEq, Eq)]
599pub(crate) struct Detected {
600 /// `workers`, `static`, `html`, or `none` when there is nothing to serve.
601 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.602 /// The framework it is built with, if g1t knows it.
603 pub framework: Option<Framework>,
604 pub install: Option<String>,
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9605 pub build: Option<String>,
606 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.607 /// Why it cannot be served as it is set up, and what to change: the
608 /// kind is then `none`.
609 pub problem: Option<String>,
610 /// What runs before wrangler bundles a Workers project; for anything
611 /// else, `build`.
612 pre_build: Option<String>,
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9613}
614
615pub(crate) fn detect(dir: &Path, custom_build: Option<&str>, chosen_output: Option<&str>) -> Result<Detected> {
616 let custom_build = custom_build.map(str::trim).filter(|c| !c.is_empty());
617 let chosen_output = chosen_output.map(|d| d.trim().trim_matches('/')).filter(|d| !d.is_empty());
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 let config = read_wrangler(dir)?;
619 let framework = frameworks::detect(dir, config.is_some());
620 let installs: Vec<&str> = [install_command(dir), framework.as_ref().and_then(|f| f.install)].into_iter().flatten().collect();
621 let install = (!installs.is_empty()).then(|| installs.join(" && "));
622 let framework_build = framework.as_ref().and_then(|f| f.build.clone());
623 if let Some(config) = config {
624 // A framework built for Workers builds before wrangler bundles what
625 // it wrote; any other project's config builds it, as it always has.
626 let pre_build = custom_build
627 .map(str::to_owned)
628 .or_else(|| framework.as_ref().filter(|f| f.workers).and(framework_build));
629 let mut steps: Vec<String> = pre_build.clone().into_iter().collect();
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9630 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.631 steps.push(wrangler_build(None, None));
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9632 }
633 return Ok(Detected {
634 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.635 framework,
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9636 install,
637 build: (!steps.is_empty()).then(|| steps.join(" && ")),
638 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.639 problem: None,
640 pre_build,
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9641 });
642 }
643 let build = custom_build
644 .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.645 .or(framework_build)
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9646 .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.647 // An adapter's Worker, unless the project chose a folder to serve.
648 if chosen_output.is_none() && framework.as_ref().is_some_and(|f| f.adapter.is_some()) {
649 // Where its files are is known once it has built: the adapter's
650 // own folder, or where the config it writes says.
651 return Ok(Detected {
652 kind: "workers",
653 output_dir: None,
654 framework,
655 install,
656 pre_build: build.clone(),
657 build,
658 problem: None,
659 });
660 }
661 // A command or a folder of the project's own says it knows better.
662 let problem = framework
663 .as_ref()
664 .and_then(|f| f.problem.clone())
665 .filter(|_| custom_build.is_none() && chosen_output.is_none());
666 if problem.is_some() {
667 return Ok(Detected { kind: "none", framework, install, build: None, output_dir: None, problem, pre_build: None });
668 }
669 let output_dir = chosen_output
670 .map(str::to_owned)
671 .or_else(|| framework.as_ref().and_then(|f| f.output.clone()))
672 .or_else(|| find_output(dir));
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9673 // `.` is the project itself: its own files, served as they are.
674 let kind = match (&build, output_dir.as_deref()) {
675 (None, Some(".")) => "html",
676 (None, None) => "none",
677 _ => "static",
678 };
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.679 Ok(Detected { kind, framework, install, pre_build: build.clone(), build, output_dir, problem: None })
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9680}
681
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.682/// The compatibility date a Worker gets when its config names none.
683const COMPATIBILITY_DATE: &str = "2026-09-26";
684
685/// A Workers project bundled by wrangler: `config` with its paths relative
686/// to `base`, read by wrangler from `config_file` when given (else the
687/// project's own, or the one its build wrote).
688fn build_worker(
689 log: &mut Log,
690 secrets: &[String],
691 config: WranglerConfig,
692 base: &Path,
693 config_file: Option<&Path>,
694) -> Result<(Value, Option<PathBuf>, Vec<String>)> {
695 let bundle = bundle_dir();
696 let warnings = config_warnings(&config);
697 let mut worker = json!({
698 "compatibilityDate": config.compatibility_date.clone().unwrap_or_else(|| COMPATIBILITY_DATE.to_owned()),
699 "compatibilityFlags": config.compatibility_flags,
700 "vars": config.vars,
701 });
702 if let Some(main) = &config.main {
703 // `--dry-run` runs the config's own build and bundles it, without
704 // deploying anywhere.
705 let _ = std::fs::remove_dir_all(&bundle);
706 step(log, &wrangler_build(Some(&bundle), config_file), secrets)?;
707 let (main_module, modules) = bundle_modules(main, &bundle)?;
708 worker["mainModule"] = main_module.into();
709 worker["modules"] = serde_json::to_value(modules)?;
710 }
711 let assets = config.assets.unwrap_or_default();
712 let assets_dir = assets.directory.as_ref().map(|directory| base.join(directory));
713 worker["assetsBinding"] = assets.binding.into();
714 worker["htmlHandling"] = assets.html_handling.into();
715 worker["notFoundHandling"] = assets.not_found_handling.into();
716 Ok((worker, assets_dir, warnings))
717}
718
719/// Where the config for an adapter's Worker is written: beside the
720/// bundle, never in the checkout.
721fn adapter_config_path() -> PathBuf {
722 let bundle = bundle_dir();
723 bundle.with_file_name(format!("{}.wrangler.json", bundle.file_name().unwrap_or_default().to_string_lossy()))
724}
725
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look726fn build(log: &mut Log, secrets: &[String]) -> Result<Built> {
727 let project = project_dir();
728 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.729 let custom_build = std::env::var("BUILD_COMMAND").ok();
730 let chosen = std::env::var("OUTPUT_DIR").ok().filter(|d| !d.trim().is_empty());
731 let plan = detect(dir, custom_build.as_deref(), chosen.as_deref())?;
732 let framework_id = plan.framework.as_ref().map(|f| f.id);
733 if let Some(framework) = &plan.framework {
734 log.line(&format!("Detected {}: framework={}.", framework.name, framework.id));
735 }
736 // Said before anything is installed: it would fail after, less clearly.
737 if let Some(problem) = &plan.problem {
738 bail!("{problem}");
739 }
740 if let Some(install) = &plan.install {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look741 step(log, install, secrets)?;
742 }
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.743 if let Some(command) = &plan.pre_build {
744 step(log, command, secrets)?;
745 }
746 let own_config = read_wrangler(dir)?;
747 // A framework built for Workers may write the config wrangler reads
748 // (`@cloudflare/vite-plugin` does, and Astro's adapter with it), with
749 // or without one of the project's own.
750 let written = if own_config.is_some() || plan.framework.as_ref().is_some_and(|f| f.workers) {
751 redirected_config(dir)?
752 } else {
753 None
754 };
755 if let Some((config, base)) = written {
756 log.line(&format!("Using the Workers config the build wrote, in {}.", base.display()));
757 let (worker, assets_dir, warnings) = build_worker(log, secrets, config, &base, None)?;
758 return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id });
759 }
760 if let Some(config) = own_config {
761 let (worker, assets_dir, warnings) = build_worker(log, secrets, config, dir, None)?;
762 return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id });
763 }
764 let adapter = plan.framework.as_ref().and_then(|f| f.adapter.as_ref()).filter(|_| chosen.is_none());
765 if let Some(adapter) = adapter
766 && let Some(entry) = adapter_entry(dir, adapter)
767 {
768 // The adapter's Worker, bundled as a Workers project's is. Its
769 // static files are uploaded by g1t, so wrangler is given none.
770 let file = adapter_config_path();
771 let flags = vec!["nodejs_compat".to_owned()];
772 std::fs::write(
773 &file,
774 serde_json::to_string_pretty(&json!({
775 "name": "g1t-app",
776 "main": entry,
777 "compatibility_date": COMPATIBILITY_DATE,
778 "compatibility_flags": flags,
779 }))?,
780 )?;
781 log.line(&format!("Bundling the Worker the adapter wrote, {}.", adapter.entry));
782 let config = WranglerConfig {
783 main: Some(entry.to_string_lossy().into_owned()),
784 compatibility_flags: flags,
785 assets: Some(AssetsConfig {
786 directory: Some(adapter.assets.to_owned()),
787 binding: Some("ASSETS".to_owned()),
788 ..AssetsConfig::default()
789 }),
790 ..WranglerConfig::default()
791 };
792 let built = build_worker(log, secrets, config, dir, Some(&file));
793 let _ = std::fs::remove_file(&file);
794 let (worker, assets_dir, warnings) = built?;
795 return Ok(Built { worker, assets_dir, warnings, detected: "workers", framework: framework_id });
796 }
797 if let (Some(adapter), Some(framework)) = (adapter, &plan.framework)
798 && framework.output.is_none()
799 {
800 bail!("{}'s build did not write its Worker, `{}`", framework.name, adapter.entry);
801 }
802 let assets_dir = match &chosen {
803 Some(chosen) => {
804 let path = dir.join(chosen.trim().trim_matches('/'));
805 if !path.is_dir() {
806 bail!("the output directory `{chosen}` does not exist after the build");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look807 }
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.808 path
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look809 }
810 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.811 let expected = plan.framework.as_ref().and_then(|f| f.output.as_deref());
812 match expected.filter(|output| dir.join(output).is_dir()) {
813 Some(output) => dir.join(output),
814 None => {
815 let found = find_output(dir).with_context(|| match (&plan.framework, expected) {
816 (Some(framework), Some(expected)) => format!(
817 "found nothing to serve: {} writes its site to `{expected}`, which the build did not write. \
818 Set **Output directory** if it writes somewhere else.",
819 framework.name
820 ),
821 (Some(framework), None) if framework.problem.is_some() => {
822 format!("found nothing to serve. {}", framework.problem.as_deref().unwrap_or_default())
823 }
824 _ => "found nothing to serve: no Workers config, no index.html, and none of dist, build, out, public, _site or .output/public".to_owned(),
825 })?;
826 if let Some(expected) = expected {
827 log.line(&format!("`{expected}` was not written; serving `{found}`."));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look828 }
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 if found == "." { dir.to_path_buf() } else { dir.join(found) }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look830 }
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 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look832 }
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 };
834 let spa = !assets_dir.join("404.html").exists();
835 Ok(Built {
836 worker: json!({
837 "compatibilityDate": COMPATIBILITY_DATE,
838 "compatibilityFlags": [],
839 "vars": {},
840 "notFoundHandling": if spa { "single-page-application" } else { "404-page" },
841 }),
842 detected: static_kind(dir, &assets_dir, plan.build.is_some()),
843 assets_dir: Some(assets_dir),
844 warnings: Vec::new(),
845 framework: framework_id,
846 })
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look847}
848
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9849/// Whether the build checks out its own commit (a sandbox, given
850/// `GIT_REMOTE`) rather than building the checkout it runs in.
851fn clones() -> bool {
852 std::env::var("GIT_REMOTE").is_ok_and(|remote| !remote.trim().is_empty())
853}
854
855/// The checkout the build runs in: the sandbox's clone, or, from a
856/// workflow step, the current directory.
857fn checkout_dir() -> PathBuf {
858 if clones() {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look859 PathBuf::from(WORKDIR)
860 } else {
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9861 std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))
862 }
863}
864
865/// Where `wrangler deploy --dry-run` writes the bundle: in a workflow
866/// step, a directory of the job's temporary space, never the checkout.
867fn bundle_dir() -> PathBuf {
868 if clones() {
869 return PathBuf::from(BUNDLE_DIR);
870 }
871 let base = std::env::var_os("RUNNER_TEMP").map(PathBuf::from).unwrap_or_else(std::env::temp_dir);
872 base.join(format!("g1t-bundle-{}", std::process::id()))
873}
874
875/// Where the project lives in the checkout `base`: its root directory,
876/// never above the checkout.
877fn project_dir_in(base: &Path, root: &str) -> PathBuf {
878 let root = root.trim().trim_matches('/');
879 if root.is_empty() || root == "." || root.split('/').any(|part| part == "..") {
880 base.to_path_buf()
881 } else {
882 base.join(root)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look883 }
884}
885
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9886/// Where the project lives: `ROOT_DIR` in the checkout.
887fn project_dir() -> PathBuf {
888 project_dir_in(&checkout_dir(), &std::env::var("ROOT_DIR").unwrap_or_default())
889}
890
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look891fn check_out(secrets: &[String]) -> Result<()> {
892 let remote = env("GIT_REMOTE")?;
893 let commit = env("GIT_COMMIT")?;
894 let auth = auth_option(&env("G1T_USER")?, &env("G1T_TOKEN")?);
895 std::fs::create_dir_all("/work")?;
Previews build from any branch; Agent in place of Ask AI; pin from the sidebar896 let workdir = Path::new(WORKDIR);
897 // The clone is shallow and of the default branch; a preview's commit is
898 // often on another one, so it is fetched before checking out.
899 let cloned = crate::clone::clone(Path::new("/work"), &auth, &[], &remote, WORKDIR)
900 .and_then(|_| git(workdir, &["rev-parse", "--abbrev-ref", "HEAD"]))
901 .and_then(|branch| crate::clone::ensure(workdir, &auth, "origin", &branch, &commit))
902 .and_then(|_| git(workdir, &["-c", "advice.detachedHead=false", "checkout", "--quiet", &commit]));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look903 if let Err(error) = cloned {
904 bail!("{}", redact(&format!("{error:#}"), secrets));
905 }
906 Ok(())
907}
908
909fn deploy(reporter: &Reporter, log: &mut Log, secrets: &[String]) -> Result<Value> {
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9910 if clones() {
911 check_out(secrets).context("the commit could not be checked out")?;
912 } else {
913 let dir = project_dir();
914 if !dir.is_dir() {
915 bail!("the project's directory {} does not exist in this checkout", dir.display());
916 }
917 log.line(&format!("Building {} as it is checked out.", dir.display()));
918 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look919 // The repository's variables and secrets for deploy builds.
920 for source in ["BUILD_ENV", "BUILD_SECRETS"] {
921 if let Ok(vars) = std::env::var(source)
922 && let Ok(Value::Object(vars)) = serde_json::from_str::<Value>(&vars)
923 {
924 for (name, value) in vars {
925 if let Some(value) = value.as_str() {
926 // SAFETY: single-threaded; set before any command runs.
927 unsafe { std::env::set_var(name, value) };
928 }
929 }
930 }
931 }
932 let built = build(log, secrets)?;
933 let mut finish = json!({
934 "worker": built.worker,
935 "warnings": built.warnings,
936 "detected": built.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.937 // Not stored by the deployments service yet, which ignores fields
938 // it does not know; the build's log names it too.
939 "framework": built.framework,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look940 });
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9941 for warning in &built.warnings {
942 log.line(&format!("Warning: {warning}"));
943 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look944 if let Some(dir) = &built.assets_dir {
945 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.946 let assets = collect_site(dir, skip_project)?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look947 if assets.is_empty() {
948 bail!("the site to serve is empty");
949 }
950 log.line(&format!("Uploading {} files.", assets.len()));
951 for special in ["_headers", "_redirects"] {
952 if let Ok(text) = std::fs::read_to_string(dir.join(special)) {
953 finish["worker"][special] = text.into();
954 }
955 }
956 let manifest: BTreeMap<&str, Value> = assets
957 .iter()
958 .map(|asset| (asset.path.as_str(), json!({ "hash": asset.hash, "size": asset.size })))
959 .collect();
960 let answer = reporter.send("session", json!({ "manifest": manifest }))?;
961 if answer["ok"] == false {
962 bail!("{}", answer["error"]["message"].as_str().unwrap_or("g1t refused the upload"));
963 }
964 let session: UploadSession =
965 serde_json::from_value(answer["value"].clone()).context("g1t's answer to the upload was not understood")?;
966 let by_hash: BTreeMap<&str, &Asset> = assets.iter().map(|asset| (asset.hash.as_str(), asset)).collect();
967 let mut completion = session.jwt.clone();
968 for bucket in &session.buckets {
969 if let Some(jwt) = upload_bucket(&session, bucket, &by_hash)? {
970 completion = jwt;
971 }
972 }
973 finish["completionJwt"] = completion.into();
974 }
975 Ok(finish)
976}
977
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9978/// A sandbox's build (`MODE=deploy`).
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look979pub fn main() -> i32 {
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9980 run(false)
981}
982
983/// `g1t-runner deploy`, from a workflow step: the build in the job's own
984/// checkout, its log in the step's as well. Exits 1 when the build fails,
985/// after reporting it, so the step fails too.
986pub fn cli(args: &[String]) -> i32 {
987 if args.iter().any(|arg| arg == "--help" || arg == "-h") {
988 println!("{CLI_HELP}");
989 return 0;
990 }
991 run(true)
992}
993
994const CLI_HELP: &str = "g1t-runner deploy: builds the project in this checkout and puts it on g1t.page.
995
996Reads DEPLOY_ID and DEPLOY_TOKEN (from POST /repos/{owner}/{repo}/deployments/builds),
997G1T_API (https://api.g1t.sh), and optionally ROOT_DIR, BUILD_COMMAND, OUTPUT_DIR,
998BUILD_ENV and BUILD_SECRETS (JSON objects). The g1t/deploy action sets them all.
999
1000g1t-runner detect prints what the build would find, as key=value lines.";
1001
1002fn run(step_log: bool) -> i32 {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1003 let reporter = match (env("G1T_API"), env("DEPLOY_ID"), env("DEPLOY_TOKEN")) {
1004 (Ok(api), Ok(id), Ok(token)) => Reporter {
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91005 base: format!("{}/deployments/jobs/{id}", api.trim_end_matches('/')),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1006 token,
1007 },
1008 _ => {
1009 eprintln!("g1t-runner: G1T_API, DEPLOY_ID and DEPLOY_TOKEN must be set");
1010 return 2;
1011 }
1012 };
1013 let mut secrets: Vec<String> = ["G1T_TOKEN", "DEPLOY_TOKEN"]
1014 .iter()
1015 .filter_map(|name| std::env::var(name).ok())
1016 .filter(|secret| !secret.is_empty())
1017 .collect();
1018 // The repository's build secrets never appear in the log.
1019 if let Ok(Value::Object(build)) = serde_json::from_str::<Value>(&std::env::var("BUILD_SECRETS").unwrap_or_default()) {
1020 secrets.extend(build.values().filter_map(Value::as_str).filter(|v| v.len() >= 4).map(str::to_owned));
1021 }
1022 if let Err(error) = reporter.send("started", json!({})) {
1023 eprintln!("g1t-runner: {error:#}");
1024 return 1;
1025 }
1026 let started = Instant::now();
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91027 let mut log = Log { echo: step_log.then(|| secrets.clone()), ..Log::default() };
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1028 let outcome = deploy(&reporter, &mut log, &secrets);
1029 let seconds = started.elapsed().as_secs();
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91030 if step_log {
1031 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.1032 let _ = std::fs::remove_file(adapter_config_path());
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91033 }
1034 let (sent, failed) = match outcome {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1035 Ok(mut finish) => {
1036 finish["log"] = redact(&log.tail(), &secrets).into();
1037 finish["buildSeconds"] = seconds.into();
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91038 (reporter.send("finish", finish), false)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1039 }
1040 Err(error) => {
1041 let message = redact(&format!("{error:#}"), &secrets);
1042 log.line(&format!("The build failed: {message}"));
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91043 let sent = reporter.send(
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1044 "fail",
1045 json!({ "message": message, "log": redact(&log.tail(), &secrets), "buildSeconds": seconds }),
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91046 );
1047 (sent, true)
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1048 }
1049 };
1050 match sent {
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91051 // A sandbox's build has said how it ended; a step fails with its build.
1052 Ok(answer) if step_log && (failed || answer["ok"] == false || answer["value"] == false) => {
1053 if !failed {
1054 eprintln!("g1t-runner: g1t did not take the build; its deployment says why");
1055 }
1056 1
1057 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1058 Ok(_) => 0,
1059 Err(error) => {
1060 eprintln!("g1t-runner: {error:#}");
1061 1
1062 }
1063 }
1064}
1065
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91066/// `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.1067/// `key=value` lines for `$GITHUB_OUTPUT`: `kind`, `install`, `build`,
1068/// `output-dir`, and `framework` when one is found. Reads `ROOT_DIR`,
1069/// `BUILD_COMMAND` and `OUTPUT_DIR` as the build does; `--root-dir DIR` is
1070/// the same as `ROOT_DIR`. A framework set up to need a server is `none`,
1071/// with what to change on standard error.
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91072pub fn detect_cli(args: &[String]) -> i32 {
1073 let mut root = std::env::var("ROOT_DIR").unwrap_or_default();
1074 let mut rest = args.iter();
1075 while let Some(arg) = rest.next() {
1076 match arg.as_str() {
1077 "--root-dir" => root = rest.next().cloned().unwrap_or_default(),
1078 "--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.1079 println!(
1080 "g1t-runner detect [--root-dir DIR]: prints kind, install, build, output-dir and framework (when one is found) as key=value lines."
1081 );
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91082 return 0;
1083 }
1084 other => {
1085 eprintln!("g1t-runner detect: unknown argument {other}");
1086 return 2;
1087 }
1088 }
1089 }
1090 let dir = project_dir_in(&checkout_dir(), &root);
1091 let custom = std::env::var("BUILD_COMMAND").ok();
1092 let output = std::env::var("OUTPUT_DIR").ok();
1093 match detect(&dir, custom.as_deref(), output.as_deref()) {
1094 Ok(found) => {
1095 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.1096 if let Some(problem) = &found.problem {
1097 eprintln!("g1t-runner detect: {problem}");
1098 }
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91099 0
1100 }
1101 Err(error) => {
1102 eprintln!("g1t-runner detect: {error:#}");
1103 1
1104 }
1105 }
1106}
1107
1108/// `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.1109/// there is none. `framework` is left out when none was found, which a
1110/// step's outputs read as empty all the same.
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91111fn output_lines(found: &Detected) -> String {
1112 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.1113 let mut lines = format!(
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91114 "kind={}\ninstall={}\nbuild={}\noutput-dir={}\n",
1115 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.1116 one_line(found.install.as_deref()),
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91117 one_line(found.build.as_deref()),
1118 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.1119 );
1120 if let Some(framework) = &found.framework {
1121 lines.push_str(&format!("framework={}\n", framework.id));
1122 }
1123 lines
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91124}
1125
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1126#[cfg(test)]
1127mod tests {
1128 use super::*;
1129
1130 #[test]
1131 fn the_upload_session_is_read_in_either_spelling() {
1132 for answer in [
1133 json!({ "jwt": "j", "buckets": [["a"]], "upload_url": "https://u" }),
1134 json!({ "jwt": "j", "uploadUrl": "https://u" }),
1135 ] {
1136 let session: UploadSession = serde_json::from_value(answer).unwrap();
1137 assert_eq!(session.upload_url, "https://u");
1138 }
1139 }
1140
1141 #[test]
1142 fn jsonc_comments_and_trailing_commas_are_dropped() {
1143 let text = r#"{
1144 // a comment
1145 "main": "src/index.ts", /* another */
1146 "vars": { "URL": "https://x.dev//not-a-comment", },
1147 }"#;
1148 let config: WranglerConfig = serde_json::from_str(&strip_jsonc(text)).unwrap();
1149 assert_eq!(config.main.as_deref(), Some("src/index.ts"));
1150 assert_eq!(config.vars["URL"], "https://x.dev//not-a-comment");
1151 }
1152
1153 #[test]
1154 fn unsupported_bindings_are_noticed() {
1155 let config: WranglerConfig =
1156 serde_json::from_str(r#"{ "main": "a.js", "d1_databases": [{ "binding": "DB" }] }"#).unwrap();
1157 assert!(config.rest.contains_key("d1_databases"));
1158 }
1159
1160 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1161 fn crons_g1t_does_not_run_are_warned_about() {
1162 let config: WranglerConfig =
1163 serde_json::from_str(r#"{ "main": "a.js", "triggers": { "crons": ["*/5 * * * *", "0 3 * * *"] }, "kv_namespaces": [{ "binding": "KV" }] }"#)
1164 .unwrap();
1165 let warnings = config_warnings(&config);
1166 assert_eq!(warnings.len(), 2);
1167 assert!(warnings.iter().any(|w| w.contains("kv_namespaces")));
1168 assert!(warnings.iter().any(|w| w.contains("triggers.crons") && w.contains("2 schedules")));
1169 let none: WranglerConfig = serde_json::from_str(r#"{ "main": "a.js", "triggers": { "crons": [] } }"#).unwrap();
1170 assert!(config_warnings(&none).is_empty());
1171 let toml: WranglerConfig = toml::from_str("main = \"a.js\"
1172[triggers]
1173crons = [\"0 * * * *\"]
1174").unwrap();
1175 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.1176 // A config a build wrote lists every kind of binding, mostly empty.
1177 let written: WranglerConfig = serde_json::from_str(
1178 r#"{ "main": "entry.mjs", "d1_databases": [], "durable_objects": { "bindings": [] }, "triggers": {}, "kv_namespaces": [{ "binding": "SESSION" }] }"#,
1179 )
1180 .unwrap();
1181 assert_eq!(config_warnings(&written).len(), 1);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1182 }
1183
1184 #[test]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1185 fn a_site_is_plain_html_only_when_its_own_files_are_served_unbuilt() {
1186 let project = Path::new("/w/site");
1187 assert_eq!(static_kind(project, project, false), "html");
1188 assert_eq!(static_kind(project, project, true), "static");
1189 assert_eq!(static_kind(project, &project.join("dist"), false), "static");
1190 }
1191
1192 #[test]
1193 fn files_are_typed_by_extension() {
1194 assert_eq!(content_type("/index.HTML"), "text/html");
1195 assert_eq!(content_type("/a/b.woff2"), "font/woff2");
1196 assert_eq!(content_type("/LICENSE"), "application/octet-stream");
1197 }
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91198
1199 /// A fresh directory with `files` (path, contents) in it.
1200 fn project(name: &str, files: &[(&str, &str)]) -> PathBuf {
1201 let dir = std::env::temp_dir().join(format!("g1t-detect-{name}-{}", std::process::id()));
1202 let _ = std::fs::remove_dir_all(&dir);
1203 for (path, text) in files {
1204 let path = dir.join(path);
1205 std::fs::create_dir_all(path.parent().unwrap()).unwrap();
1206 std::fs::write(path, text).unwrap();
1207 }
1208 std::fs::create_dir_all(&dir).unwrap();
1209 dir
1210 }
1211
1212 #[test]
1213 fn detect_finds_a_workers_project_and_how_it_builds() {
1214 let dir = project(
1215 "workers",
1216 &[
1217 ("wrangler.jsonc", r#"{ "main": "src/index.ts", "assets": { "directory": "./public" }, }"#),
1218 ("package.json", "{}"),
1219 ("pnpm-lock.yaml", ""),
1220 ],
1221 );
1222 let found = detect(&dir, None, None).unwrap();
1223 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.1224 assert_eq!(found.install.as_deref(), Some("corepack enable && pnpm install --frozen-lockfile"));
1225 assert_eq!(found.framework, None);
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91226 assert_eq!(found.build.as_deref(), Some("npx --yes wrangler@4 deploy --dry-run"));
1227 assert_eq!(found.output_dir.as_deref(), Some("./public"));
1228 // The project's own build command runs first.
1229 let custom = detect(&dir, Some("npm run codegen"), None).unwrap();
1230 assert_eq!(custom.build.as_deref(), Some("npm run codegen && npx --yes wrangler@4 deploy --dry-run"));
1231 }
1232
1233 #[test]
1234 fn detect_finds_a_static_build_and_a_plain_site() {
1235 let built = project("static", &[("package.json", r#"{ "scripts": { "build": "vite build" } }"#), ("package-lock.json", "{}")]);
1236 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.1237 assert_eq!(found.kind, "static");
1238 assert_eq!(found.install.as_deref(), Some("npm ci"));
1239 assert_eq!(found.build.as_deref(), Some("npm run build"));
1240 assert_eq!(found.output_dir, None, "its output is found once it has built");
1241 assert_eq!(found.framework, None, "a build script alone names no framework");
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91242 std::fs::create_dir_all(built.join("dist")).unwrap();
1243 std::fs::write(built.join("dist/index.html"), "<h1>hi</h1>").unwrap();
1244 assert_eq!(detect(&built, None, None).unwrap().output_dir.as_deref(), Some("dist"));
1245 assert_eq!(detect(&built, None, Some("/site/")).unwrap().output_dir.as_deref(), Some("site"));
1246
1247 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.1248 let found = detect(&html, None, None).unwrap();
1249 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-7nm8r91250 // A `public` folder alone, without an index.html, is not a site.
1251 let empty = project("empty", &[("public/robots.txt", ""), ("README.md", "")]);
1252 assert_eq!(detect(&empty, None, None).unwrap().kind, "none");
1253 }
1254
1255 #[test]
1256 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.1257 let mut found = Detected {
1258 kind: "static",
1259 framework: None,
1260 install: Some("npm ci".into()),
1261 build: Some("a\nb".into()),
1262 output_dir: None,
1263 problem: None,
1264 pre_build: None,
1265 };
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91266 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.1267 let vite = project("lines", &[("package.json", r#"{ "devDependencies": { "vite": "7" } }"#)]);
1268 found.framework = frameworks::detect(&vite, false);
1269 assert!(output_lines(&found).ends_with("output-dir=\nframework=vite\n"));
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91270 }
1271
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.1272 /// One project of the table below: its files, the project's own
1273 /// settings, and what detect should say.
1274 struct Case {
1275 name: &'static str,
1276 files: &'static [(&'static str, &'static str)],
1277 custom_build: Option<&'static str>,
1278 chosen_output: Option<&'static str>,
1279 kind: &'static str,
1280 framework: Option<&'static str>,
1281 build: Option<&'static str>,
1282 output_dir: Option<&'static str>,
1283 /// A phrase the problem says, if there is one.
1284 problem: Option<&'static str>,
1285 }
1286
1287 const CASE: Case = Case {
1288 name: "",
1289 files: &[],
1290 custom_build: None,
1291 chosen_output: None,
1292 kind: "static",
1293 framework: None,
1294 build: Some("npm run build"),
1295 output_dir: None,
1296 problem: None,
1297 };
1298
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91299 #[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.1300 fn detect_knows_each_framework() {
1301 let cases = [
1302 Case {
1303 name: "vite",
1304 files: &[("package.json", r#"{ "devDependencies": { "vite": "7" } }"#)],
1305 framework: Some("vite"),
1306 build: Some("npx vite build"),
1307 output_dir: Some("dist"),
1308 ..CASE
1309 },
1310 Case {
1311 name: "vite-react",
1312 files: &[
1313 ("package.json", r#"{ "scripts": { "build": "tsc && vite build" }, "dependencies": { "react": "19" }, "devDependencies": { "vite": "7", "@vitejs/plugin-react": "5" } }"#),
1314 ("vite.config.ts", "export default defineConfig({ plugins: [react()], build: { outDir: 'site' } })"),
1315 ],
1316 framework: Some("vite-react"),
1317 output_dir: Some("site"),
1318 ..CASE
1319 },
1320 Case {
1321 name: "vite-vue",
1322 files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "vite": "7", "@vitejs/plugin-vue": "6" } }"#)],
1323 framework: Some("vite-vue"),
1324 output_dir: Some("dist"),
1325 ..CASE
1326 },
1327 Case {
1328 name: "vite-svelte",
1329 files: &[("package.json", r#"{ "devDependencies": { "vite": "7", "@sveltejs/vite-plugin-svelte": "6" } }"#)],
1330 framework: Some("vite-svelte"),
1331 build: Some("npx vite build"),
1332 output_dir: Some("dist"),
1333 ..CASE
1334 },
1335 Case {
1336 name: "vite-solid",
1337 files: &[("package.json", r#"{ "devDependencies": { "vite": "7", "vite-plugin-solid": "2" } }"#)],
1338 framework: Some("vite-solid"),
1339 build: Some("npx vite build"),
1340 output_dir: Some("dist"),
1341 ..CASE
1342 },
1343 Case {
1344 name: "vite-cloudflare",
1345 files: &[
1346 ("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "vite": "7", "@cloudflare/vite-plugin": "1" } }"#),
1347 ("wrangler.jsonc", r#"{ "main": "worker/index.ts" }"#),
1348 ],
1349 kind: "workers",
1350 framework: Some("vite"),
1351 build: Some("npm run build && npx --yes wrangler@4 deploy --dry-run"),
1352 ..CASE
1353 },
1354 Case {
1355 name: "astro",
1356 files: &[("package.json", r#"{ "scripts": { "build": "astro build" }, "dependencies": { "astro": "5" } }"#), ("astro.config.mjs", "export default defineConfig({ site: 'https://x.dev' })")],
1357 framework: Some("astro"),
1358 output_dir: Some("dist"),
1359 ..CASE
1360 },
1361 Case {
1362 name: "astro-server",
1363 files: &[("package.json", r#"{ "dependencies": { "astro": "5", "@astrojs/node": "9" } }"#), ("astro.config.mjs", "export default { output: \"server\" }")],
1364 kind: "none",
1365 framework: Some("astro"),
1366 build: None,
1367 problem: Some("`output: 'static'`"),
1368 ..CASE
1369 },
1370 Case {
1371 name: "astro-cloudflare",
1372 files: &[("package.json", r#"{ "scripts": { "build": "astro build" }, "dependencies": { "astro": "5", "@astrojs/cloudflare": "12" } }"#)],
1373 kind: "workers",
1374 framework: Some("astro"),
1375 ..CASE
1376 },
1377 Case {
1378 name: "astro-cloudflare-folder",
1379 files: &[("package.json", r#"{ "scripts": { "build": "astro build" }, "dependencies": { "astro": "5", "@astrojs/cloudflare": "12" } }"#)],
1380 chosen_output: Some("dist"),
1381 framework: Some("astro"),
1382 output_dir: Some("dist"),
1383 ..CASE
1384 },
1385 Case {
1386 name: "next-export",
1387 files: &[("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16" } }"#), ("next.config.ts", "const config: NextConfig = {\n output: 'export',\n};")],
1388 framework: Some("nextjs"),
1389 output_dir: Some("out"),
1390 ..CASE
1391 },
1392 Case {
1393 name: "next-server",
1394 files: &[("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16" } }"#), ("next.config.mjs", "export default {}")],
1395 kind: "none",
1396 framework: Some("nextjs"),
1397 build: None,
1398 problem: Some("`@opennextjs/cloudflare`"),
1399 ..CASE
1400 },
1401 Case {
1402 name: "next-server-own-folder",
1403 files: &[("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16" } }"#)],
1404 chosen_output: Some("out"),
1405 framework: Some("nextjs"),
1406 output_dir: Some("out"),
1407 ..CASE
1408 },
1409 Case {
1410 name: "opennext",
1411 files: &[
1412 ("package.json", r#"{ "scripts": { "build": "next build" }, "dependencies": { "next": "16", "@opennextjs/cloudflare": "1" } }"#),
1413 ("wrangler.jsonc", r#"{ "main": ".open-next/worker.js", "assets": { "directory": ".open-next/assets", "binding": "ASSETS" } }"#),
1414 ],
1415 kind: "workers",
1416 framework: Some("nextjs"),
1417 build: Some("npx opennextjs-cloudflare build && npx --yes wrangler@4 deploy --dry-run"),
1418 output_dir: Some(".open-next/assets"),
1419 ..CASE
1420 },
1421 Case {
1422 name: "opennext-no-config",
1423 files: &[("package.json", r#"{ "dependencies": { "next": "16", "@opennextjs/cloudflare": "1" } }"#)],
1424 kind: "none",
1425 framework: Some("nextjs"),
1426 build: None,
1427 problem: Some("`.open-next/worker.js`"),
1428 ..CASE
1429 },
1430 Case {
1431 name: "sveltekit-static",
1432 files: &[
1433 ("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "2", "@sveltejs/adapter-static": "3", "vite": "7" } }"#),
1434 ("svelte.config.js", "import adapter from '@sveltejs/adapter-static';\nexport default { kit: { adapter: adapter() } };"),
1435 ],
1436 framework: Some("sveltekit"),
1437 output_dir: Some("build"),
1438 ..CASE
1439 },
1440 Case {
1441 name: "sveltekit-3-static",
1442 files: &[
1443 ("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "3", "@sveltejs/adapter-static": "4", "@sveltejs/adapter-cloudflare": "8" } }"#),
1444 ("vite.config.js", "import adapter from '@sveltejs/adapter-static';\nexport default defineConfig({ plugins: [sveltekit({ adapter: adapter({ pages: 'www' }) })] });"),
1445 ],
1446 framework: Some("sveltekit"),
1447 output_dir: Some("www"),
1448 ..CASE
1449 },
1450 Case {
1451 name: "sveltekit-cloudflare",
1452 files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "2", "@sveltejs/adapter-cloudflare": "7" } }"#)],
1453 kind: "workers",
1454 framework: Some("sveltekit"),
1455 ..CASE
1456 },
1457 Case {
1458 name: "sveltekit-auto",
1459 files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "@sveltejs/kit": "2", "@sveltejs/adapter-auto": "6" } }"#)],
1460 kind: "none",
1461 framework: Some("sveltekit"),
1462 build: None,
1463 problem: Some("`@sveltejs/adapter-static`"),
1464 ..CASE
1465 },
1466 Case {
1467 name: "nuxt",
1468 files: &[("package.json", r#"{ "scripts": { "build": "nuxt build", "generate": "nuxt generate" }, "dependencies": { "nuxt": "4" } }"#)],
1469 framework: Some("nuxt"),
1470 build: Some("npm run generate"),
1471 output_dir: Some(".output/public"),
1472 ..CASE
1473 },
1474 Case {
1475 name: "nuxt-no-generate",
1476 files: &[("package.json", r#"{ "scripts": { "build": "nuxt build" }, "dependencies": { "nuxt": "4" } }"#)],
1477 framework: Some("nuxt"),
1478 build: Some("npx nuxi generate"),
1479 output_dir: Some(".output/public"),
1480 ..CASE
1481 },
1482 Case {
1483 name: "react-router-spa",
1484 files: &[
1485 ("package.json", r#"{ "scripts": { "build": "react-router build" }, "devDependencies": { "@react-router/dev": "7", "vite": "7" } }"#),
1486 ("react-router.config.ts", "export default {\n // Server-side render by default\n ssr: false,\n} satisfies Config;"),
1487 ],
1488 framework: Some("react-router"),
1489 output_dir: Some("build/client"),
1490 ..CASE
1491 },
1492 Case {
1493 name: "react-router-ssr",
1494 files: &[("package.json", r#"{ "devDependencies": { "@react-router/dev": "7" } }"#), ("react-router.config.ts", "export default { ssr: true }")],
1495 kind: "none",
1496 framework: Some("react-router"),
1497 build: None,
1498 problem: Some("`ssr: false`"),
1499 ..CASE
1500 },
1501 Case {
1502 name: "remix-spa",
1503 files: &[
1504 ("package.json", r#"{ "scripts": { "build": "remix vite:build" }, "devDependencies": { "@remix-run/dev": "2", "vite": "5" } }"#),
1505 ("vite.config.ts", "export default defineConfig({ plugins: [remix({ ssr: false })] })"),
1506 ],
1507 framework: Some("remix"),
1508 output_dir: Some("build/client"),
1509 ..CASE
1510 },
1511 Case {
1512 name: "gatsby",
1513 files: &[("package.json", r#"{ "scripts": { "build": "gatsby build" }, "dependencies": { "gatsby": "5" } }"#)],
1514 framework: Some("gatsby"),
1515 output_dir: Some("public"),
1516 ..CASE
1517 },
1518 Case {
1519 name: "docusaurus",
1520 files: &[("package.json", r#"{ "scripts": { "build": "docusaurus build" }, "dependencies": { "@docusaurus/core": "3" } }"#)],
1521 framework: Some("docusaurus"),
1522 output_dir: Some("build"),
1523 ..CASE
1524 },
1525 Case {
1526 name: "eleventy",
1527 files: &[("package.json", r#"{ "devDependencies": { "@11ty/eleventy": "3" } }"#)],
1528 framework: Some("eleventy"),
1529 build: Some("npx @11ty/eleventy"),
1530 output_dir: Some("_site"),
1531 ..CASE
1532 },
1533 Case {
1534 name: "angular",
1535 files: &[
1536 ("package.json", r#"{ "scripts": { "build": "ng build" }, "dependencies": { "@angular/core": "20" } }"#),
1537 ("angular.json", r#"{ "projects": { "shop": { "projectType": "application", "architect": { "build": { "builder": "@angular/build:application", "options": { "outputPath": "dist/shop" } } } } } }"#),
1538 ],
1539 framework: Some("angular"),
1540 output_dir: Some("dist/shop/browser"),
1541 ..CASE
1542 },
1543 Case {
1544 name: "angular-default",
1545 files: &[
1546 ("package.json", r#"{ "dependencies": { "@angular/core": "20" } }"#),
1547 ("angular.json", r#"{ "projects": { "app": { "projectType": "application", "architect": { "build": { "builder": "@angular/build:application", "options": {} } } } } }"#),
1548 ],
1549 framework: Some("angular"),
1550 build: Some("npx ng build"),
1551 output_dir: Some("dist/app/browser"),
1552 ..CASE
1553 },
1554 Case {
1555 name: "hugo",
1556 files: &[("hugo.toml", "baseURL = 'https://example.org/'\ntitle = 'Site'\n")],
1557 framework: Some("hugo"),
1558 build: Some(frameworks_hugo()),
1559 output_dir: Some("public"),
1560 ..CASE
1561 },
1562 Case {
1563 name: "hugo-config-toml",
1564 files: &[("config.toml", "baseURL = \"https://example.org/\"\npublishDir = \"site\"\n")],
1565 framework: Some("hugo"),
1566 build: Some(frameworks_hugo()),
1567 output_dir: Some("site"),
1568 ..CASE
1569 },
1570 Case {
1571 name: "not-hugo",
1572 files: &[("config.toml", "base_url = \"https://example.org\"\n"), ("index.html", "")],
1573 kind: "html",
1574 build: None,
1575 output_dir: Some("."),
1576 ..CASE
1577 },
1578 Case {
1579 name: "jekyll",
1580 files: &[("_config.yml", "title: Blog\n"), ("Gemfile", "gem 'jekyll'\n")],
1581 framework: Some("jekyll"),
1582 build: Some("bundle exec jekyll build"),
1583 output_dir: Some("_site"),
1584 ..CASE
1585 },
1586 Case {
1587 name: "mdbook",
1588 files: &[("book.toml", "[book]\ntitle = \"Guide\"\n")],
1589 framework: Some("mdbook"),
1590 build: Some(frameworks_mdbook()),
1591 output_dir: Some("book"),
1592 ..CASE
1593 },
1594 Case {
1595 name: "mdbook-build-dir",
1596 files: &[("book.toml", "[book]\ntitle = \"Guide\"\n[build]\nbuild-dir = \"site\"\n")],
1597 framework: Some("mdbook"),
1598 build: Some(frameworks_mdbook()),
1599 output_dir: Some("site"),
1600 ..CASE
1601 },
1602 // The project's own settings win.
1603 Case {
1604 name: "own-settings",
1605 files: &[("package.json", r#"{ "scripts": { "build": "vite build" }, "devDependencies": { "vite": "7" } }"#)],
1606 custom_build: Some("make site"),
1607 chosen_output: Some("www"),
1608 framework: Some("vite"),
1609 build: Some("make site"),
1610 output_dir: Some("www"),
1611 ..CASE
1612 },
1613 ];
1614 for case in cases {
1615 let dir = project(&format!("fw-{}", case.name), case.files);
1616 let found = detect(&dir, case.custom_build, case.chosen_output).unwrap();
1617 let name = case.name;
1618 assert_eq!(found.kind, case.kind, "{name}: kind");
1619 assert_eq!(found.framework.as_ref().map(|f| f.id), case.framework, "{name}: framework");
1620 assert_eq!(found.build.as_deref(), case.build, "{name}: build");
1621 assert_eq!(found.output_dir.as_deref(), case.output_dir, "{name}: output-dir");
1622 match case.problem {
1623 Some(phrase) => assert!(found.problem.as_deref().is_some_and(|p| p.contains(phrase)), "{name}: {:?}", found.problem),
1624 None => assert_eq!(found.problem, None, "{name}: problem"),
1625 }
1626 let _ = std::fs::remove_dir_all(&dir);
1627 }
1628 }
1629
1630 fn frameworks_hugo() -> &'static str {
1631 "if command -v hugo >/dev/null 2>&1; then hugo --minify; else npx --yes hugo-extended@0.166.0 --minify; fi"
1632 }
1633
1634 fn frameworks_mdbook() -> &'static str {
1635 "if ! command -v mdbook >/dev/null 2>&1; then cargo install mdbook --version 0.5.4 --locked --quiet; fi && mdbook build"
1636 }
1637
1638 #[test]
1639 fn jekyll_installs_its_gems_after_the_packages() {
1640 let dir = project("jekyll-install", &[("_config.yml", ""), ("Gemfile", ""), ("package.json", "{}")]);
1641 let found = detect(&dir, None, None).unwrap();
1642 assert_eq!(found.install.as_deref(), Some("npm install && bundle config set --local path vendor/bundle && bundle install"));
1643 }
1644
1645 #[test]
1646 fn an_adapters_worker_is_found_once_built_and_never_served() {
1647 let dir = project(
1648 "adapter",
1649 &[
1650 ("dist/_worker.js/index.js", "export default {}"),
1651 ("dist/_routes.json", "{}"),
1652 ("dist/.assetsignore", "_worker.js\nsecret.txt\n"),
1653 ("dist/secret.txt", "no"),
1654 ("dist/index.html", "<h1>hi</h1>"),
1655 ("dist/about/_worker.js", "a page's own file"),
1656 ("kit/.svelte-kit/cloudflare/_worker.js", "export default {}"),
1657 ],
1658 );
1659 let astro = Adapter { entry: "dist/_worker.js", assets: "dist" };
1660 assert_eq!(adapter_entry(&dir, &astro), Some(dir.join("dist/_worker.js/index.js")));
1661 let kit = Adapter { entry: ".svelte-kit/cloudflare/_worker.js", assets: ".svelte-kit/cloudflare" };
1662 assert_eq!(adapter_entry(&dir.join("kit"), &kit), Some(dir.join("kit/.svelte-kit/cloudflare/_worker.js")));
1663 assert_eq!(adapter_entry(&dir.join("missing"), &kit), None);
1664 let mut served: Vec<String> = collect_site(&dir.join("dist"), false).unwrap().into_iter().map(|a| a.path).collect();
1665 served.sort();
1666 assert_eq!(served, ["/about/_worker.js", "/index.html"]);
1667 }
1668
1669 #[test]
1670 fn a_config_the_build_wrote_is_read_in_place_of_the_projects() {
1671 let dir = project(
1672 "redirected",
1673 &[
1674 ("wrangler.jsonc", r#"{ "main": "worker/index.ts" }"#),
1675 (".wrangler/deploy/config.json", r#"{ "configPath": "../../dist/app/wrangler.json" }"#),
1676 ("dist/app/wrangler.json", r#"{ "main": "index.js", "assets": { "directory": "../client" } }"#),
1677 ],
1678 );
1679 let (config, base) = redirected_config(&dir).unwrap().unwrap();
1680 assert_eq!(config.main.as_deref(), Some("index.js"));
1681 assert_eq!(base, dir.join("dist/app").canonicalize().unwrap());
1682 assert!(base.join(config.assets.unwrap().directory.unwrap()).ends_with("dist/app/../client"));
1683 assert!(redirected_config(&project("not-redirected", &[])).unwrap().is_none());
1684 }
1685
1686 #[test]
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91687 fn the_root_directory_stays_inside_the_checkout() {
1688 let base = Path::new("/w/repo");
1689 assert_eq!(project_dir_in(base, ""), base);
1690 assert_eq!(project_dir_in(base, "."), base);
1691 assert_eq!(project_dir_in(base, "/apps/site/"), base.join("apps/site"));
1692 assert_eq!(project_dir_in(base, "../etc"), base);
1693 assert_eq!(project_dir_in(base, "apps/../../x"), base);
1694 }
1695
1696 #[test]
1697 fn a_step_prints_the_log_redacted() {
1698 let mut log = Log { echo: Some(vec!["hunter22".to_owned()]), ..Log::default() };
1699 log.line("token hunter22 used");
1700 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.1701 assert_eq!(wrangler_build(Some(Path::new("/tmp/b")), None), "npx --yes wrangler@4 deploy --dry-run --outdir /tmp/b");
1702 assert_eq!(
1703 wrangler_build(Some(Path::new("/tmp/b")), Some(Path::new("/tmp/w.json"))),
1704 "npx --yes wrangler@4 deploy --dry-run --config /tmp/w.json --outdir /tmp/b"
1705 );
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91706 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1707}

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