Skip to content
1,116 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.

Merge branch 'worktree-agent-a6a121745e81f639f'1//! g1t's own workflows (`.g1t/workflows/*.yml`), read by the same parser
2//! and expressions the actions service runs them with: each reads, nothing
3//! in it is unsupported, and the deploy workflow's jobs start, wait and
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.4//! stop as docs.g1t.sh/guides/deploy-to-cloudflare/ says.
Merge branch 'worktree-agent-a6a121745e81f639f'5
6use std::path::PathBuf;
7
8use g1t_actions::expr::{self, Scope, Status};
9use g1t_actions::matrix;
10use g1t_actions::workflow::{self, Severity, Workflow};
11use serde_json::{Map, Value, json};
12
13fn workflows_dir() -> PathBuf {
14 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.g1t/workflows")
15}
16
17fn read(name: &str) -> Workflow {
18 let source = std::fs::read_to_string(workflows_dir().join(name)).unwrap();
19 workflow::parse(&source).unwrap_or_else(|problem| panic!("{name}: {problem}"))
20}
21
22#[test]
Merge branch 'worktree-agent-a3abfcce648e87dca'23fn every_workflow_asks_for_only_what_its_token_does() {
24 use g1t_actions::permissions::{Access, TokenDefault};
25 let job = |name: &str, id: &str| {
26 let workflow = read(name);
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.27 let job = workflow
28 .jobs
29 .iter()
30 .find(|job| job.id == id)
31 .unwrap()
32 .clone();
Merge branch 'worktree-agent-a3abfcce648e87dca'33 job.permissions(&workflow, TokenDefault::Restricted)
34 };
35 // CI and Deploy only read: nothing they do writes with the token.
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r936 // The actions release pushes with its own secret, never the job's token.
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.37 for (name, id) in [
38 ("ci.yml", "rust"),
39 ("deploy.yml", "core"),
40 ("runner-release.yml", "binaries"),
41 ("actions-release.yml", "publish"),
42 ] {
Merge branch 'worktree-agent-a3abfcce648e87dca'43 let permissions = job(name, id);
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.44 assert!(
45 permissions
46 .listed()
47 .iter()
48 .all(|(_, access)| *access <= Access::Read),
49 "{name} {id}"
50 );
Merge branch 'worktree-agent-a3abfcce648e87dca'51 }
52 // The runner base pushes a branch and opens a pull request.
53 let base = job("runner-base.yml", "build");
54 assert_eq!(base.get("contents"), Access::Write);
55 assert_eq!(base.get("pull-requests"), Access::Write);
56 assert_eq!(base.get("packages"), Access::None);
57 // The runner's image goes to g1t's registry.
58 let image = job("runner-release.yml", "image");
59 assert_eq!(image.get("packages"), Access::Write);
60 assert_eq!(image.get("contents"), Access::Read);
61}
62
63#[test]
Merge branch 'worktree-agent-a6a121745e81f639f'64fn every_workflow_reads_and_runs_on_g1t() {
65 let mut count = 0;
66 for entry in std::fs::read_dir(workflows_dir()).unwrap() {
67 let path = entry.unwrap().path();
68 if path.extension().and_then(|e| e.to_str()) != Some("yml") {
69 continue;
70 }
71 let name = path.file_name().unwrap().to_string_lossy().to_string();
72 let workflow = read(&name);
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.73 let unsupported: Vec<_> = workflow
74 .notes
75 .iter()
76 .filter(|n| n.severity != Severity::Info)
77 .collect();
Merge branch 'worktree-agent-a6a121745e81f639f'78 assert!(unsupported.is_empty(), "{name}: {unsupported:?}");
79 count += 1;
80 }
81 assert!(count >= 1);
82}
83
84/// The plan job's outputs for a deploy of `stages` (each with its jobs).
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.85fn plan_outputs(
86 migrate: bool,
87 core: &[(&str, &str, bool)],
88 edge: &[(&str, &str, bool)],
89 front: &[(&str, &str, bool)],
90) -> Value {
Merge branch 'worktree-agent-a6a121745e81f639f'91 let matrix = |jobs: &[(&str, &str, bool)]| {
92 let include: Vec<Value> = if jobs.is_empty() {
93 vec![json!({ "group": "none", "units": "" })]
94 } else {
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.95 jobs.iter()
96 .map(|(group, units, rust)| json!({ "group": group, "units": units, "rust": rust }))
97 .collect()
Merge branch 'worktree-agent-a6a121745e81f639f'98 };
99 json!({ "include": include }).to_string()
100 };
101 json!({
102 "migrate": migrate.to_string(),
103 "migrate_units": if migrate { "events" } else { "" },
104 "has_core": (!core.is_empty()).to_string(),
105 "core": matrix(core),
106 "has_edge": (!edge.is_empty()).to_string(),
107 "edge": matrix(edge),
108 "has_front": (!front.is_empty()).to_string(),
109 "front": matrix(front),
110 })
111}
112
113/// Whether `job` starts, given its needs' results, as the actions service
114/// decides it (services/actions/src/plan.rs `decide`, with
115/// `expr::job_status`): a need that was skipped is not a failure, and a
116/// failure anywhere before the job is.
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.117fn starts(
118 workflow: &Workflow,
119 job: &str,
120 needs: &[(&str, &str)],
121 outputs: &Value,
122 inputs: Value,
123 cancelled: bool,
124) -> bool {
Merge branch 'worktree-agent-a6a121745e81f639f'125 starts_after(workflow, job, needs, outputs, inputs, cancelled, false)
126}
127
128/// `starts`, where `failed_before` says a job further back failed (one the
129/// needs were skipped for).
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.130fn starts_after(
131 workflow: &Workflow,
132 job: &str,
133 needs: &[(&str, &str)],
134 outputs: &Value,
135 inputs: Value,
136 cancelled: bool,
137 failed_before: bool,
138) -> bool {
Merge branch 'worktree-agent-a6a121745e81f639f'139 let job = workflow.jobs.iter().find(|j| j.id == job).unwrap();
140 let mut needs_ctx = Map::new();
141 let mut results = Vec::new();
142 for need in &job.needs {
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.143 let result = needs
144 .iter()
145 .find(|(name, _)| name == need)
146 .map(|(_, r)| *r)
147 .unwrap_or("success");
Merge branch 'worktree-agent-a6a121745e81f639f'148 results.push(result);
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.149 let outputs = if need == "plan" {
150 outputs.clone()
151 } else {
152 json!({})
153 };
154 needs_ctx.insert(
155 need.clone(),
156 json!({ "result": result, "outputs": outputs }),
157 );
Merge branch 'worktree-agent-a6a121745e81f639f'158 }
159 let status = expr::job_status(results, failed_before, cancelled);
160 let mut contexts = Map::new();
161 contexts.insert("needs".into(), Value::Object(needs_ctx));
162 contexts.insert("inputs".into(), inputs);
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.163 contexts.insert(
164 "github".into(),
165 json!({ "event_name": "push", "ref": "refs/heads/main" }),
166 );
167 let scope = Scope {
168 contexts: &contexts,
169 status,
170 hash_files: None,
171 };
Merge branch 'worktree-agent-a6a121745e81f639f'172 expr::condition(job.condition.as_deref().unwrap_or_default(), &scope).unwrap()
173}
174
175#[test]
176fn deploy_runs_on_main_and_by_hand_one_at_a_time() {
177 let deploy = read("deploy.yml");
178 let push = deploy.trigger("push").unwrap();
179 assert!(push.branches.allows("main"));
180 assert!(!push.branches.allows("feature"));
181 let dispatch = deploy.trigger("workflow_dispatch").unwrap();
182 for input in ["units", "all", "dry_run"] {
183 assert!(dispatch.inputs.contains_key(input), "{input}");
184 }
185 let concurrency = deploy.concurrency.as_ref().unwrap();
186 assert_eq!(concurrency.group, "deploy-production");
187 assert_eq!(concurrency.cancel_in_progress, json!(false));
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.188 assert_eq!(
189 deploy.job_order(),
Merge branch 'worktree-agent-a63425cb2d4935c5f'190 ["check", "plan", "migrate", "core", "edge", "front", "smoke"]
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.191 );
Merge branch 'worktree-agent-a6a121745e81f639f'192 // The stages share their steps (a YAML alias), and read the token only
193 // where they deploy.
194 let steps = |id: &str| deploy.jobs.iter().find(|j| j.id == id).unwrap().steps.len();
195 assert_eq!(steps("core"), steps("edge"));
196 assert_eq!(steps("core"), steps("front"));
197 let source = std::fs::read_to_string(workflows_dir().join("deploy.yml")).unwrap();
198 assert!(!source.contains("cancel-in-progress: true"));
199}
200
Merge branch 'worktree-agent-af070177ded5baf00'201/// Whether a step's `run` installs the whole lockfile: `npm ci` with no
202/// workspace narrowing it, and not the deploy tool's install of some units.
203fn installs_everything(run: &str) -> bool {
204 run.split('\n').any(|line| {
205 let line = line.trim();
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.206 // Installs go through npm-ci.mjs, which runs `npm ci` again when a connection drops.
207 (line.starts_with("npm ci") || line.starts_with("node scripts/deploy/npm-ci.mjs"))
208 && !line.contains(" -w ")
209 && !line.contains("--workspace")
Merge branch 'worktree-agent-af070177ded5baf00'210 })
211}
212
213#[test]
214fn npm_downloads_are_cached_whole_and_only_whole_installs_save_them() {
215 // Every job's npm cache is the whole lockfile's, under one key. A job
216 // that installs part of it only restores: had it saved, its part would
217 // stand in for the whole under that key (the first job to finish used
218 // to save Wrangler alone, and every later install downloaded the rest).
219 let mut saving = Vec::new();
220 for entry in std::fs::read_dir(workflows_dir()).unwrap() {
221 let path = entry.unwrap().path();
222 if path.extension().and_then(|e| e.to_str()) != Some("yml") {
223 continue;
224 }
225 let name = path.file_name().unwrap().to_string_lossy().to_string();
226 for job in read(&name).jobs {
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.227 let whole = job
228 .steps
229 .iter()
230 .any(|s| s.run.as_deref().is_some_and(installs_everything));
Merge branch 'worktree-agent-af070177ded5baf00'231 for step in &job.steps {
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.232 let Some(uses) = step.uses.as_deref() else {
233 continue;
234 };
235 if !uses.starts_with("actions/cache") || step.raw["with"]["path"] != json!("~/.npm")
236 {
Merge branch 'worktree-agent-af070177ded5baf00'237 continue;
238 }
239 let key = step.raw["with"]["key"].as_str().unwrap_or_default();
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.240 assert_eq!(
241 key, "npm-full-${{ runner.os }}-${{ hashFiles('package-lock.json') }}",
242 "{name} {}",
243 job.id
244 );
Merge branch 'worktree-agent-af070177ded5baf00'245 if let Some(prefix) = step.raw["with"]["restore-keys"].as_str() {
246 assert_eq!(prefix, "npm-full-${{ runner.os }}-", "{name} {}", job.id);
247 }
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.248 let saves =
249 uses.starts_with("actions/cache@") || uses.starts_with("actions/cache/save@");
250 assert!(
251 !saves || whole,
252 "{name} {}: saves npm's cache but installs only part of the lockfile",
253 job.id
254 );
Merge branch 'worktree-agent-af070177ded5baf00'255 if saves {
256 saving.push(format!("{name} {}", job.id));
257 }
258 }
259 }
260 }
Merge branch 'worktree-agent-a63425cb2d4935c5f'261 // main's whole cache is kept by the Caches workflow.
262 assert!(saving.contains(&"caches.yml npm".to_owned()), "{saving:?}");
263}
264
265/// Every `actions/cache` step of every workflow: (file, job, step).
266fn cache_steps() -> Vec<(String, workflow::Job, workflow::Step)> {
267 let mut out = Vec::new();
268 let mut names: Vec<String> = std::fs::read_dir(workflows_dir())
269 .unwrap()
270 .map(|e| e.unwrap().file_name().to_string_lossy().to_string())
271 .filter(|n| n.ends_with(".yml"))
272 .collect();
273 names.sort();
274 for name in names {
275 for job in read(&name).jobs {
276 for step in &job.steps {
277 if step
278 .uses
279 .as_deref()
280 .is_some_and(|u| u.starts_with("actions/cache"))
281 {
282 out.push((name.clone(), job.clone(), step.clone()));
283 }
284 }
285 }
286 }
287 out
288}
289
290/// What a cache step's key starts with before its first expression: the
291/// entry it is, whatever its inputs (`cargo-test-`, `apt-tauri-windows-`).
292fn family(key: &str) -> String {
293 key.split("${{").next().unwrap_or_default().to_owned()
294}
295
296fn saves(step: &workflow::Step) -> bool {
297 let uses = step.uses.as_deref().unwrap_or_default();
298 (uses.starts_with("actions/cache@") || uses.starts_with("actions/cache/save@"))
299 && step.raw["with"]["lookup-only"] != json!(true)
300}
301
302fn paths(step: &workflow::Step) -> Vec<String> {
303 let text = step.raw["with"]["path"].as_str().unwrap_or_default();
304 text.lines()
305 .map(str::trim)
306 .filter(|l| !l.is_empty())
307 .map(str::to_owned)
308 .collect()
309}
310
311#[test]
312fn each_cache_has_one_set_of_paths_wherever_it_is_used() {
313 // An entry's version is the hash of its paths: a step that names the
314 // same key with other paths finds nothing, silently. So every step of
315 // an entry, in every workflow, names the same paths.
316 let mut seen: std::collections::BTreeMap<String, (Vec<String>, String)> = Default::default();
317 for (file, job, step) in cache_steps() {
318 let key = step.raw["with"]["key"].as_str().unwrap_or_default();
319 let family = family(key);
320 assert!(!family.is_empty(), "{file} {}: a key starts with its name", job.id);
321 let here = format!("{file} {}", job.id);
322 match seen.get(&family) {
323 Some((first, at)) => assert_eq!(
324 &paths(&step),
325 first,
326 "{here} names other paths for {family} than {at}"
327 ),
328 None => {
329 seen.insert(family, (paths(&step), here));
330 }
331 }
332 }
333 for family in [
334 "npm-full-",
335 "cargo-crates-",
336 "cargo-test-",
337 "cargo-workers-",
338 "runner-musl-",
339 "runner-release-",
340 "desktop-check-",
341 "desktop-release-linux-",
342 "desktop-release-windows-",
343 "apt-tauri-",
344 "apt-tauri-windows-",
345 "xwin-",
346 "zig-0.13.0-zigbuild-",
347 ] {
348 assert!(seen.contains_key(family), "{family}: {:?}", seen.keys());
349 }
350}
351
352#[test]
353fn main_saves_every_cache_that_pull_requests_and_releases_restore() {
354 // A run restores from its own ref, its pull request's base and main;
355 // a release tag's run never reads another tag's entries. So every
356 // entry any step restores is saved by a workflow that runs on pushes
357 // to main, and the release workflows only restore.
358 let on_main = ["ci.yml", "caches.yml", "deploy.yml"];
359 for file in on_main {
360 let push = read(file).trigger("push").cloned().unwrap();
361 assert!(push.branches.allows("main"), "{file}");
362 }
363 let releases = [
364 "cli-release.yml",
365 "desktop-release.yml",
366 "mobile-build.yml",
367 "mobile-release.yml",
368 "runner-release.yml",
369 ];
370 let steps = cache_steps();
371 let saved_on_main: std::collections::BTreeSet<String> = steps
372 .iter()
373 .filter(|(file, _, step)| on_main.contains(&file.as_str()) && saves(step))
374 .map(|(_, _, step)| family(step.raw["with"]["key"].as_str().unwrap_or_default()))
375 .collect();
376 for (file, job, step) in &steps {
377 let family = family(step.raw["with"]["key"].as_str().unwrap_or_default());
378 // The conformance run restores what its own first job saved.
379 if family == "conformance-" {
380 continue;
381 }
382 assert!(
383 saved_on_main.contains(&family),
384 "{file} {}: nothing on main saves {family}",
385 job.id
386 );
387 if releases.contains(&file.as_str()) {
388 assert!(!saves(step), "{file} {}: a release saves {family}, which only its own tag would read", job.id);
389 }
390 }
391}
392
393#[test]
394fn rust_caches_are_keyed_by_the_toolchain_and_the_dependencies() {
395 // Not by Cargo.lock as a whole: 25 of its 31 changes in a week were a
396 // release's version bump, and each saved every Rust cache again.
397 for (file, job, step) in cache_steps() {
398 let key = step.raw["with"]["key"].as_str().unwrap_or_default();
399 assert!(
400 !key.contains("Cargo.lock") && !key.contains("base.json"),
401 "{file} {}: {key}",
402 job.id
403 );
404 let target = paths(&step).iter().any(|p| p.contains("target"));
405 if target {
406 assert!(key.contains("steps.keys.outputs.rust"), "{file} {}: {key}", job.id);
407 // A restore key never reaches another toolchain's entries.
408 for prefix in step.raw["with"]["restore-keys"]
409 .as_str()
410 .unwrap_or_default()
411 .lines()
412 .filter(|l| !l.trim().is_empty())
413 {
414 assert!(prefix.contains("steps.keys.outputs.rust"), "{file} {}: {prefix}", job.id);
415 }
416 }
417 if key.contains("steps.keys.outputs.") {
418 let at = job.steps.iter().position(|s| s == &step).unwrap();
419 let keys = job
420 .steps
421 .iter()
422 .position(|s| s.id.as_deref() == Some("keys"))
423 .unwrap_or_else(|| panic!("{file} {}: no keys step", job.id));
424 assert!(keys < at, "{file} {}", job.id);
425 assert_eq!(
426 job.steps[keys].run.as_deref(),
427 Some("node scripts/deploy/ci-cache.mjs keys")
428 );
429 }
430 }
431}
432
433#[test]
434fn a_cargo_target_restored_from_an_older_entry_is_pruned_before_it_is_saved() {
435 // Otherwise the old versions of what changed ride along forever: CI's
436 // test build grew from 671 MB to 1,687 MB in a day.
437 for (file, id, cache) in [
438 ("ci.yml", "rust", "test-build"),
439 ("ci.yml", "desktop", "desktop-build"),
440 ("caches.yml", "runner", "runner-build"),
441 ] {
442 let workflow = read(file);
443 let job = workflow.jobs.iter().find(|j| j.id == id).unwrap();
444 let at = job
445 .steps
446 .iter()
447 .position(|s| s.id.as_deref() == Some(cache))
448 .unwrap();
449 assert!(saves(&job.steps[at]), "{file} {id}");
450 let prune = job
451 .steps
452 .iter()
453 .position(|s| {
454 s.run
455 .as_deref()
456 .is_some_and(|r| r.starts_with("node scripts/deploy/ci-cache.mjs prune "))
457 })
458 .unwrap_or_else(|| panic!("{file} {id}: no prune"));
459 assert!(at < prune, "{file} {id}");
460 assert_eq!(
461 job.steps[prune].condition.as_deref().map(str::trim),
462 Some(format!("${{{{ steps.{cache}.outputs.cache-hit != 'true' }}}}").as_str()),
463 "{file} {id}"
464 );
465 }
466 let ci = read("ci.yml");
467 let rust = ci.jobs.iter().find(|j| j.id == "rust").unwrap();
468 assert_eq!(
469 rust.raw["env"]["CARGO_PROFILE_DEV_DEBUG"],
470 json!("line-tables-only")
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.471 );
Merge branch 'worktree-agent-af070177ded5baf00'472}
473
Merge branch 'worktree-agent-a6a121745e81f639f'474#[test]
Merge branch 'worktree-agent-a63425cb2d4935c5f'475fn the_caches_workflow_builds_only_what_is_missing() {
476 let caches = read("caches.yml");
477 let push = caches.trigger("push").unwrap();
478 assert!(push.branches.allows("main"));
479 assert!(!push.branches.allows("feature"));
480 // Never cut off halfway: a build half done saves nothing.
481 assert_eq!(
482 caches.concurrency.as_ref().unwrap().cancel_in_progress,
483 json!(false)
484 );
485 let look = caches.jobs.iter().find(|j| j.id == "look").unwrap();
486 // Looked up, not downloaded.
487 for step in &look.steps {
488 if step.uses.as_deref().is_some_and(|u| u.starts_with("actions/cache")) {
489 assert_eq!(step.uses.as_deref(), Some("actions/cache/restore@v4"));
490 assert_eq!(step.raw["with"]["lookup-only"], json!(true), "{:?}", step.id);
491 }
492 }
493 for job in &caches.jobs {
494 if job.id != "look" {
495 assert_eq!(job.needs, ["look"], "{}", job.id);
496 assert!(
497 job.condition
498 .as_deref()
499 .is_some_and(|c| c.contains("needs.look.outputs.")),
500 "{}",
501 job.id
502 );
503 }
504 }
505}
506
507#[test]
Merge branch 'worktree-agent-a6a121745e81f639f'508fn deploy_stages_follow_one_another() {
509 let deploy = read("deploy.yml");
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.510 let all = plan_outputs(
511 true,
512 &[("rust", "events,repos", true), ("ts", "projects", false)],
513 &[("rust", "api", true)],
514 &[("web", "web", false)],
515 );
Merge branch 'worktree-agent-a6a121745e81f639f'516 let push = json!({});
517
518 // Everything succeeds: each stage runs after the last.
519 assert!(starts(&deploy, "migrate", &[], &all, push.clone(), false));
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.520 assert!(starts(
521 &deploy,
522 "core",
523 &[("migrate", "success")],
524 &all,
525 push.clone(),
526 false
527 ));
528 assert!(starts(
529 &deploy,
530 "edge",
531 &[("migrate", "success"), ("core", "success")],
532 &all,
533 push.clone(),
534 false
535 ));
536 assert!(starts(
537 &deploy,
538 "front",
539 &[
540 ("migrate", "success"),
541 ("core", "success"),
542 ("edge", "success")
543 ],
544 &all,
545 push.clone(),
546 false
547 ));
Merge branch 'worktree-agent-a6a121745e81f639f'548
549 // No migrations: the migrate job is skipped, and core still runs.
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.550 let none = plan_outputs(
551 false,
552 &[("ts", "projects", false)],
553 &[],
554 &[("web", "web", false)],
555 );
Merge branch 'worktree-agent-a6a121745e81f639f'556 assert!(!starts(&deploy, "migrate", &[], &none, push.clone(), false));
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.557 assert!(starts(
558 &deploy,
559 "core",
560 &[("migrate", "skipped")],
561 &none,
562 push.clone(),
563 false
564 ));
Merge branch 'worktree-agent-a6a121745e81f639f'565 // An empty stage is skipped, and the next one still runs.
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.566 assert!(!starts(
567 &deploy,
568 "edge",
569 &[("migrate", "skipped"), ("core", "success")],
570 &none,
571 push.clone(),
572 false
573 ));
574 assert!(starts(
575 &deploy,
576 "front",
577 &[
578 ("migrate", "skipped"),
579 ("core", "success"),
580 ("edge", "skipped")
581 ],
582 &none,
583 push.clone(),
584 false
585 ));
Merge branch 'worktree-agent-a6a121745e81f639f'586
587 // A failure stops every later stage.
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.588 assert!(!starts(
589 &deploy,
590 "core",
591 &[("migrate", "failure")],
592 &all,
593 push.clone(),
594 false
595 ));
596 assert!(!starts(
597 &deploy,
598 "edge",
599 &[("migrate", "success"), ("core", "failure")],
600 &all,
601 push.clone(),
602 false
603 ));
604 assert!(!starts(
605 &deploy,
606 "front",
607 &[
608 ("migrate", "success"),
609 ("core", "success"),
610 ("edge", "failure")
611 ],
612 &all,
613 push.clone(),
614 false
615 ));
616 assert!(!starts(
617 &deploy,
618 "front",
619 &[
620 ("migrate", "success"),
621 ("core", "failure"),
622 ("edge", "skipped")
623 ],
624 &all,
625 push.clone(),
626 false
627 ));
Merge branch 'worktree-agent-a6a121745e81f639f'628 // A cancelled run starts nothing more.
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.629 assert!(!starts(
630 &deploy,
631 "edge",
632 &[("migrate", "success"), ("core", "success")],
633 &all,
634 push.clone(),
635 true
636 ));
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check637 // Check and plan run side by side: plan waits for nothing.
638 let plan = deploy.jobs.iter().find(|j| j.id == "plan").unwrap();
639 assert!(plan.needs.is_empty(), "{:?}", plan.needs);
640 for id in ["migrate", "core", "edge", "front", "smoke"] {
641 let job = deploy.jobs.iter().find(|j| j.id == id).unwrap();
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.642 assert!(
643 job.needs.iter().any(|n| n == "check") && job.needs.iter().any(|n| n == "plan"),
644 "{id}: {:?}",
645 job.needs
646 );
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check647 }
648 // A failed check, though plan succeeded: nothing migrates or deploys,
649 // and failure() still sees the check's failure through skipped jobs.
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.650 assert!(!starts(
651 &deploy,
652 "migrate",
653 &[("check", "failure"), ("plan", "success")],
654 &all,
655 push.clone(),
656 false
657 ));
658 assert!(!starts(
659 &deploy,
660 "core",
661 &[
662 ("check", "failure"),
663 ("plan", "success"),
664 ("migrate", "skipped")
665 ],
666 &all,
667 push.clone(),
668 false
669 ));
670 assert!(!starts(
671 &deploy,
672 "front",
673 &[
674 ("check", "failure"),
675 ("plan", "success"),
676 ("migrate", "skipped"),
677 ("core", "skipped"),
678 ("edge", "skipped")
679 ],
680 &all,
681 push.clone(),
682 false
683 ));
684 let skipped = [
685 ("migrate", "skipped"),
686 ("core", "skipped"),
687 ("edge", "skipped"),
688 ];
689 assert!(!starts_after(
690 &deploy,
691 "front",
692 &skipped,
693 &all,
694 push.clone(),
695 false,
696 true
697 ));
698 assert!(!starts(
699 &deploy,
700 "smoke",
701 &[
702 ("check", "failure"),
703 ("core", "skipped"),
704 ("edge", "skipped"),
705 ("front", "skipped")
706 ],
707 &all,
708 push.clone(),
709 false
710 ));
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check711 // A failed plan stops everything too.
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.712 assert!(!starts(
713 &deploy,
714 "migrate",
715 &[("plan", "failure")],
716 &all,
717 push.clone(),
718 false
719 ));
720 assert!(!starts(
721 &deploy,
722 "core",
723 &[("plan", "failure"), ("migrate", "skipped")],
724 &all,
725 push.clone(),
726 false
727 ));
Merge branch 'worktree-agent-a6a121745e81f639f'728
Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh729 // Smoke follows the last stage that ran, and not a failed one.
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.730 assert!(starts(
731 &deploy,
732 "smoke",
733 &[
734 ("core", "success"),
735 ("edge", "success"),
736 ("front", "success")
737 ],
738 &all,
739 push.clone(),
740 false
741 ));
742 assert!(starts(
743 &deploy,
744 "smoke",
745 &[
746 ("core", "success"),
747 ("edge", "skipped"),
748 ("front", "success")
749 ],
750 &none,
751 push.clone(),
752 false
753 ));
754 assert!(!starts(
755 &deploy,
756 "smoke",
757 &[
758 ("core", "success"),
759 ("edge", "failure"),
760 ("front", "skipped")
761 ],
762 &all,
763 push.clone(),
764 false
765 ));
Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh766 // Nothing deployed: nothing to smoke-test.
767 let nothing = plan_outputs(false, &[], &[], &[]);
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.768 assert!(!starts(
769 &deploy,
770 "smoke",
771 &[
772 ("core", "skipped"),
773 ("edge", "skipped"),
774 ("front", "skipped")
775 ],
776 &nothing,
777 push.clone(),
778 false
779 ));
Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh780
Merge branch 'worktree-agent-a6a121745e81f639f'781 // A dry run plans and stops.
782 let dry = json!({ "dry_run": true, "units": "", "all": false });
783 assert!(!starts(&deploy, "migrate", &[], &all, dry.clone(), false));
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.784 assert!(!starts(
785 &deploy,
786 "core",
787 &[("migrate", "skipped")],
788 &all,
789 dry.clone(),
790 false
791 ));
792 assert!(!starts(
793 &deploy,
794 "smoke",
795 &[
796 ("core", "skipped"),
797 ("edge", "skipped"),
798 ("front", "skipped")
799 ],
800 &all,
801 dry,
802 false
803 ));
Merge branch 'worktree-agent-a6a121745e81f639f'804}
805
806#[test]
807fn deploy_stage_matrices_come_from_the_plan() {
808 let deploy = read("deploy.yml");
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.809 let outputs = plan_outputs(
810 false,
811 &[
812 ("rust-1", "events,work", true),
813 ("rust-2", "repos", true),
814 ("ts", "projects,og", false),
815 ],
816 &[],
817 &[],
818 );
Merge branch 'worktree-agent-a6a121745e81f639f'819 let core = deploy.jobs.iter().find(|j| j.id == "core").unwrap();
820 assert!(!core.fail_fast);
821 assert_eq!(core.max_parallel, Some(4));
822 let mut contexts = Map::new();
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.823 contexts.insert(
824 "needs".into(),
825 json!({ "plan": { "result": "success", "outputs": outputs } }),
826 );
827 let scope = Scope {
828 contexts: &contexts,
829 status: Status::Success,
830 hash_files: None,
831 };
Merge branch 'worktree-agent-a6a121745e81f639f'832 let value = expr::interpolate_value(core.matrix.as_ref().unwrap(), &scope).unwrap();
833 let jobs = matrix::expand(&value).unwrap();
834 let groups: Vec<(&str, &str, bool)> = jobs
835 .iter()
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.836 .map(|c| {
837 (
838 c["group"].as_str().unwrap(),
839 c["units"].as_str().unwrap(),
840 c["rust"].as_bool().unwrap(),
841 )
842 })
Merge branch 'worktree-agent-a6a121745e81f639f'843 .collect();
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.844 assert_eq!(
845 groups,
846 [
847 ("rust-1", "events,work", true),
848 ("rust-2", "repos", true),
849 ("ts", "projects,og", false)
850 ]
851 );
Merge branch 'worktree-agent-a6a121745e81f639f'852}
853
854#[test]
855fn deploy_builds_rust_on_the_larger_machine_with_its_target_cached() {
856 let deploy = read("deploy.yml");
857 for stage in ["core", "edge", "front"] {
858 let job = deploy.jobs.iter().find(|j| j.id == stage).unwrap();
Merge branch 'main' into actions-toolkit-oidc-artifacts859 let on = |rust: bool, image: bool| {
Merge branch 'worktree-agent-a6a121745e81f639f'860 let mut contexts = Map::new();
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.861 contexts.insert(
862 "matrix".into(),
863 json!({ "group": "g", "units": "u", "rust": rust, "image": image }),
864 );
865 let scope = Scope {
866 contexts: &contexts,
867 status: Status::Success,
868 hash_files: None,
869 };
Merge branch 'worktree-agent-a6a121745e81f639f'870 expr::interpolate_value(&job.runs_on, &scope).unwrap()
871 };
Merge branch 'main' into actions-toolkit-oidc-artifacts872 assert_eq!(on(true, false), json!("g1t-4core"), "{stage}");
873 // The runner's image is built with the job's own Docker Engine.
874 assert_eq!(on(false, true), json!("g1t-4core"), "{stage}");
875 assert_eq!(on(false, false), json!("ubuntu-latest"), "{stage}");
Merge branch 'worktree-agent-a6a121745e81f639f'876 }
877 let source = std::fs::read_to_string(workflows_dir().join("deploy.yml")).unwrap();
878 assert!(source.contains("target/wasm32-unknown-unknown/release"));
879 assert!(source.contains("!target/**/incremental"));
Merge branch 'main' into actions-toolkit-oidc-artifacts880 assert!(source.contains("target/x86_64-unknown-linux-musl/release"));
Merge branch 'worktree-agent-a6a121745e81f639f'881}
882
Merge remote-tracking branch 'origin/main' into workspace-chat883/// Whether a step runs, for a job going `status`, with `matrix`.
884fn step_runs(step: &workflow::Step, matrix: Value, status: Status) -> bool {
885 let mut contexts = Map::new();
886 contexts.insert("matrix".into(), matrix);
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.887 let scope = Scope {
888 contexts: &contexts,
889 status,
890 hash_files: None,
891 };
Merge remote-tracking branch 'origin/main' into workspace-chat892 expr::condition(step.condition.as_deref().unwrap_or_default(), &scope).unwrap()
893}
894
895#[test]
896fn rust_builds_go_through_sccache_before_cargo_and_report_after() {
897 let step = |job: &workflow::Job, run: &str| -> (usize, workflow::Step) {
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.898 let at = job
899 .steps
900 .iter()
901 .position(|s| s.run.as_deref() == Some(run))
902 .unwrap_or_else(|| panic!("{}: no step runs {run}", job.id));
Merge remote-tracking branch 'origin/main' into workspace-chat903 (at, job.steps[at].clone())
904 };
905 let deploy = read("deploy.yml");
906 for stage in ["core", "edge", "front"] {
907 let job = deploy.jobs.iter().find(|j| j.id == stage).unwrap();
908 let (install_at, install) = step(job, "bash scripts/sccache.sh install");
909 let (stats_at, stats) = step(job, "bash scripts/sccache.sh stats");
910 // Before anything runs Cargo (worker-build's install, the build),
911 // and the statistics last.
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.912 let install_step = job
913 .steps
914 .iter()
915 .position(|s| s.name.as_deref() == Some("Install"))
916 .unwrap();
Merge remote-tracking branch 'origin/main' into workspace-chat917 assert!(install_at < install_step, "{stage}");
918 assert_eq!(stats_at, job.steps.len() - 1, "{stage}");
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.919 for (rust, image, uses) in [
920 (true, false, true),
921 (false, true, true),
922 (false, false, false),
923 ] {
Merge remote-tracking branch 'origin/main' into workspace-chat924 let matrix = json!({ "group": "g", "units": "u", "rust": rust, "image": image });
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.925 assert_eq!(
926 step_runs(&install, matrix.clone(), Status::Success),
927 uses,
928 "{stage} rust={rust} image={image}"
929 );
930 assert_eq!(
931 step_runs(&stats, matrix.clone(), Status::Success),
932 uses,
933 "{stage}"
934 );
Merge remote-tracking branch 'origin/main' into workspace-chat935 // A failed build still says what was cached.
936 assert_eq!(step_runs(&stats, matrix, Status::Failure), uses, "{stage}");
937 }
938 }
939 let ci = read("ci.yml");
940 let rust = ci.jobs.iter().find(|j| j.id == "rust").unwrap();
941 let (install_at, _) = step(rust, "bash scripts/sccache.sh install");
942 let (tests_at, _) = step(rust, "cargo test --workspace --locked --quiet");
943 let (stats_at, stats) = step(rust, "bash scripts/sccache.sh stats");
944 assert!(install_at < tests_at && tests_at < stats_at);
945 assert!(step_runs(&stats, json!({}), Status::Failure));
946 // main's runs keep the caches pull requests restore from: only Rust.
947 assert!(ci.trigger("push").unwrap().branches.allows("main"));
948 assert!(ci.trigger("pull_request").is_some());
949 for (id, on_push) in [("rust", true), ("typescript", false), ("build", false)] {
950 let job = ci.jobs.iter().find(|j| j.id == id).unwrap();
951 for (event, expected) in [("push", on_push), ("pull_request", true)] {
952 let mut contexts = Map::new();
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.953 contexts.insert(
954 "github".into(),
955 json!({ "event_name": event, "ref": "refs/heads/main" }),
956 );
957 let scope = Scope {
958 contexts: &contexts,
959 status: Status::Success,
960 hash_files: None,
961 };
962 assert_eq!(
963 expr::condition(job.condition.as_deref().unwrap_or_default(), &scope).unwrap(),
964 expected,
965 "{id} on {event}"
966 );
Merge remote-tracking branch 'origin/main' into workspace-chat967 }
968 }
969 // The download is pinned by version and checksum.
970 let script = std::fs::read_to_string(workflows_dir().join("../../scripts/sccache.sh")).unwrap();
971 assert!(script.contains("VERSION=0.18.0"));
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.972 assert!(script.lines().any(|line| {
973 line.strip_prefix("SHA256=")
974 .is_some_and(|sum| sum.len() == 64)
975 }));
Merge remote-tracking branch 'origin/main' into workspace-chat976 assert!(script.contains("sha256sum -c"));
977 assert!(script.contains("RUSTC_WRAPPER="));
978 assert!(script.contains("GITHUB_STEP_SUMMARY"));
979}
980
Merge branch 'worktree-agent-a6a121745e81f639f'981#[test]
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9982fn g1ts_own_actions_are_published_when_they_change() {
983 let release = read("actions-release.yml");
984 let push = release.trigger("push").unwrap();
985 assert!(push.branches.allows("main"));
986 assert!(!push.branches.allows("feature"));
987 assert!(release.trigger("workflow_dispatch").is_some());
988 let source = std::fs::read_to_string(workflows_dir().join("actions-release.yml")).unwrap();
989 assert!(source.contains("actions/**"));
The Actions release reads its token from ACTIONS_PUBLISH_TOKEN. It asked for G1T_ACTIONS_PUBLISH_TOKEN, a name g1t keeps for its own and refuses when a secret is saved, so the secret it needed could never be set. A test now fails any of g1t's workflows that reads a secret named G1T_ or GITHUB_ other than the job's own token.990 assert!(source.contains("secrets.ACTIONS_PUBLISH_TOKEN"));
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9991 // Every action is a composite one with its README, and the copies of
992 // the script that finds g1t-runner are the same file.
993 let actions = workflows_dir().join("../../actions");
994 let mut finders = Vec::new();
995 for entry in std::fs::read_dir(&actions).unwrap() {
996 let dir = entry.unwrap().path();
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.997 let manifest = std::fs::read_to_string(dir.join("action.yml"))
998 .unwrap_or_else(|_| panic!("{} has no action.yml", dir.display()));
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9999 let manifest: serde_yaml::Value = serde_yaml::from_str(&manifest).unwrap();
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.1000 assert_eq!(
1001 manifest["runs"]["using"].as_str(),
1002 Some("composite"),
1003 "{}",
1004 dir.display()
1005 );
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91006 assert!(dir.join("README.md").exists(), "{}", dir.display());
1007 finders.push(std::fs::read_to_string(dir.join("runner.sh")).unwrap());
1008 }
1009 assert!(finders.len() >= 2);
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.1010 assert!(
1011 finders.windows(2).all(|pair| pair[0] == pair[1]),
1012 "actions/*/runner.sh differ"
1013 );
1014 let deploy: serde_yaml::Value =
1015 serde_yaml::from_str(&std::fs::read_to_string(actions.join("deploy/action.yml")).unwrap())
1016 .unwrap();
1017 assert_eq!(
1018 deploy["inputs"]["token"]["default"].as_str(),
1019 Some("${{ github.token }}")
1020 );
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91021 for output in ["url", "deployment-id"] {
1022 assert!(deploy["outputs"][output]["value"].is_string(), "{output}");
1023 }
1024}
1025
1026#[test]
Merge branch 'worktree-agent-a6a121745e81f639f'1027fn the_runner_base_rebuilds_weekly_on_a_machine_with_docker() {
1028 let base = read("runner-base.yml");
1029 assert!(base.trigger("schedule").is_some());
1030 assert!(base.trigger("workflow_dispatch").is_some());
1031 assert!(base.trigger("push").unwrap().branches.allows("main"));
1032 let job = base.jobs.iter().find(|j| j.id == "build").unwrap();
1033 assert_eq!(job.runs_on, json!(["self-hosted", "docker"]));
1034}
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'1035
1036#[test]
1037fn the_conformance_workflow_is_the_same_on_github_and_on_g1t() {
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.1038 let root = workflows_dir()
1039 .parent()
1040 .unwrap()
1041 .parent()
1042 .unwrap()
1043 .to_path_buf();
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'1044 let ours = std::fs::read_to_string(root.join(".g1t/workflows/conformance.yml")).unwrap();
1045 let theirs = std::fs::read_to_string(root.join(".github/workflows/conformance.yml")).unwrap();
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.1046 assert_eq!(
1047 ours, theirs,
1048 "copy .g1t/workflows/conformance.yml over .github/workflows/conformance.yml"
1049 );
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'1050 // It runs only by hand, and reads nothing it could change.
1051 let workflow = read("conformance.yml");
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.1052 assert_eq!(
1053 workflow
1054 .triggers
1055 .iter()
1056 .map(|t| t.event.as_str())
1057 .collect::<Vec<_>>(),
1058 ["workflow_dispatch"]
1059 );
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'1060 assert!(!ours.contains("secrets."));
1061}
The Actions release reads its token from ACTIONS_PUBLISH_TOKEN. It asked for G1T_ACTIONS_PUBLISH_TOKEN, a name g1t keeps for its own and refuses when a secret is saved, so the secret it needed could never be set. A test now fails any of g1t's workflows that reads a secret named G1T_ or GITHUB_ other than the job's own token.1062
1063/// No workflow of g1t's asks for a secret no one can set. Secret names
1064/// starting with G1T_ or GITHUB_ are g1t's own and refused when saved, so a
1065/// workflow reading one gets nothing: the Actions release asked for
1066/// G1T_ACTIONS_PUBLISH_TOKEN, which could never exist. G1T_TOKEN, the job's
1067/// own token, is the one g1t gives every job.
1068#[test]
1069fn workflows_read_only_secrets_that_can_be_set() {
1070 for entry in std::fs::read_dir(workflows_dir()).unwrap() {
1071 let path = entry.unwrap().path();
1072 if path.extension().is_none_or(|ext| ext != "yml") {
1073 continue;
1074 }
1075 let source = std::fs::read_to_string(&path).unwrap();
1076 for (at, _) in source.match_indices("secrets.") {
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.1077 let name: String = source[at + "secrets.".len()..]
1078 .chars()
1079 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
1080 .collect();
The Actions release reads its token from ACTIONS_PUBLISH_TOKEN. It asked for G1T_ACTIONS_PUBLISH_TOKEN, a name g1t keeps for its own and refuses when a secret is saved, so the secret it needed could never be set. A test now fails any of g1t's workflows that reads a secret named G1T_ or GITHUB_ other than the job's own token.1081 let reserved = name.starts_with("G1T_") || name.starts_with("GITHUB_");
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.1082 assert!(
1083 !reserved || name == "G1T_TOKEN" || name == "GITHUB_TOKEN",
1084 "{} reads secrets.{name}, a name g1t keeps for its own",
1085 path.display()
1086 );
1087 }
1088 }
1089}
1090
1091#[test]
1092fn every_install_runs_again_when_a_connection_drops() {
1093 // npm does not retry a body that breaks off midway; npm-ci.mjs does.
1094 for entry in std::fs::read_dir(workflows_dir()).unwrap() {
1095 let path = entry.unwrap().path();
1096 if path.extension().and_then(|e| e.to_str()) != Some("yml") {
1097 continue;
1098 }
1099 let name = path.file_name().unwrap().to_string_lossy().to_string();
1100 for job in read(&name).jobs {
1101 for step in &job.steps {
1102 let Some(run) = step.run.as_deref() else {
1103 continue;
1104 };
1105 for line in run.lines() {
1106 assert!(
1107 !line.trim().starts_with("npm ci"),
1108 "{name} {}: `{}` should be `node scripts/deploy/npm-ci.mjs …`",
1109 job.id,
1110 line.trim()
1111 );
1112 }
1113 }
The Actions release reads its token from ACTIONS_PUBLISH_TOKEN. It asked for G1T_ACTIONS_PUBLISH_TOKEN, a name g1t keeps for its own and refuses when a secret is saved, so the secret it needed could never be set. A test now fails any of g1t's workflows that reads a secret named G1T_ or GITHUB_ other than the job's own token.1114 }
1115 }
1116}

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