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,142 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.

A clone answered from the pack cache is served before the free operation cap: it is not an operation1//! 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.
A clone answered from the pack cache is served before the free operation cap: it is not an operation5
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]
A clone answered from the pack cache is served before the free operation cap: it is not an operation64fn 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();
A clone answered from the pack cache is served before the free operation cap: it is not an operation78 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 {
A clone answered from the pack cache is served before the free operation cap: it is not an operation91 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()
A clone answered from the pack cache is served before the free operation cap: it is not an operation98 };
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 {
A clone answered from the pack cache is served before the free operation cap: it is not an operation125 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 {
A clone answered from the pack cache is served before the free operation cap: it is not an operation139 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");
A clone answered from the pack cache is served before the free operation cap: it is not an operation148 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 );
A clone answered from the pack cache is served before the free operation cap: it is not an operation158 }
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 };
A clone answered from the pack cache is served before the free operation cap: it is not an operation172 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 );
A clone answered from the pack cache is served before the free operation cap: it is not an operation192 // 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-",
Release tags' runs no longer save to the cache what only their own tag could read, and the CLI's release builds in a pinned image from a cache kept on main. scripts/sccache.sh sets SCCACHE_GHA_RW_MODE=READ_ONLY when GITHUB_REF_TYPE is tag, for the server and the steps after it: against a stand-in for the cache service, sccache 0.18.0 on a branch's run made a CreateCacheEntry call and on a tag's run made none, and looked its compile up in both. The CLI's release ran in ghcr.io/rust-cross/cargo-zigbuild:latest; it now runs in that image pinned by digest (sha256:d8313491ec57…, Rust 1.93.0, zig 0.16.0), reads the crates entry CI saves from the job's ~/.cargo/registry instead of an empty Docker volume, hands what it wrote as root back to the job's user, and restores target/zig from cli-release-<os>-<image digest>-<deps>, which the Caches workflow builds whole when the key is new (ci-cache.mjs keys gives the digest as zigbuild). Built in that image on an 8-core machine, the five platforms took 162 s from nothing and 103 s from the cache, with only the g1t crate compiled again for each; the entry is 1.3 GB, 463 MB compressed. Tests hold the tag rule in sccache.sh, the CLI's entry to the image's digest and to being built whole, and the builder's flags; CONTRIBUTING.md and the Actions guide say both.347 "cli-release-",
Merge branch 'worktree-agent-a63425cb2d4935c5f'348 ] {
349 assert!(seen.contains_key(family), "{family}: {:?}", seen.keys());
350 }
351}
352
353#[test]
354fn main_saves_every_cache_that_pull_requests_and_releases_restore() {
355 // A run restores from its own ref, its pull request's base and main;
356 // a release tag's run never reads another tag's entries. So every
357 // entry any step restores is saved by a workflow that runs on pushes
358 // to main, and the release workflows only restore.
359 let on_main = ["ci.yml", "caches.yml", "deploy.yml"];
360 for file in on_main {
361 let push = read(file).trigger("push").cloned().unwrap();
362 assert!(push.branches.allows("main"), "{file}");
363 }
364 let releases = [
365 "cli-release.yml",
366 "desktop-release.yml",
367 "mobile-build.yml",
368 "mobile-release.yml",
369 "runner-release.yml",
370 ];
371 let steps = cache_steps();
372 let saved_on_main: std::collections::BTreeSet<String> = steps
373 .iter()
374 .filter(|(file, _, step)| on_main.contains(&file.as_str()) && saves(step))
375 .map(|(_, _, step)| family(step.raw["with"]["key"].as_str().unwrap_or_default()))
376 .collect();
377 for (file, job, step) in &steps {
378 let family = family(step.raw["with"]["key"].as_str().unwrap_or_default());
379 // The conformance run restores what its own first job saved.
380 if family == "conformance-" {
381 continue;
382 }
383 assert!(
384 saved_on_main.contains(&family),
385 "{file} {}: nothing on main saves {family}",
386 job.id
387 );
388 if releases.contains(&file.as_str()) {
389 assert!(!saves(step), "{file} {}: a release saves {family}, which only its own tag would read", job.id);
390 }
391 }
392}
393
394#[test]
395fn rust_caches_are_keyed_by_the_toolchain_and_the_dependencies() {
396 // Not by Cargo.lock as a whole: 25 of its 31 changes in a week were a
397 // release's version bump, and each saved every Rust cache again.
398 for (file, job, step) in cache_steps() {
399 let key = step.raw["with"]["key"].as_str().unwrap_or_default();
400 assert!(
401 !key.contains("Cargo.lock") && !key.contains("base.json"),
402 "{file} {}: {key}",
403 job.id
404 );
405 let target = paths(&step).iter().any(|p| p.contains("target"));
406 if target {
Release tags' runs no longer save to the cache what only their own tag could read, and the CLI's release builds in a pinned image from a cache kept on main. scripts/sccache.sh sets SCCACHE_GHA_RW_MODE=READ_ONLY when GITHUB_REF_TYPE is tag, for the server and the steps after it: against a stand-in for the cache service, sccache 0.18.0 on a branch's run made a CreateCacheEntry call and on a tag's run made none, and looked its compile up in both. The CLI's release ran in ghcr.io/rust-cross/cargo-zigbuild:latest; it now runs in that image pinned by digest (sha256:d8313491ec57…, Rust 1.93.0, zig 0.16.0), reads the crates entry CI saves from the job's ~/.cargo/registry instead of an empty Docker volume, hands what it wrote as root back to the job's user, and restores target/zig from cli-release-<os>-<image digest>-<deps>, which the Caches workflow builds whole when the key is new (ci-cache.mjs keys gives the digest as zigbuild). Built in that image on an 8-core machine, the five platforms took 162 s from nothing and 103 s from the cache, with only the g1t crate compiled again for each; the entry is 1.3 GB, 463 MB compressed. Tests hold the tag rule in sccache.sh, the CLI's entry to the image's digest and to being built whole, and the builder's flags; CONTRIBUTING.md and the Actions guide say both.407 // The toolchain that built it: this machine's rustc, or for
408 // the CLI's release, the cargo-zigbuild image it builds in.
409 let toolchain = if paths(&step).iter().any(|p| p.starts_with("target/zig")) {
410 "steps.keys.outputs.zigbuild"
411 } else {
412 "steps.keys.outputs.rust"
413 };
414 assert!(key.contains(toolchain), "{file} {}: {key}", job.id);
Merge branch 'worktree-agent-a63425cb2d4935c5f'415 // A restore key never reaches another toolchain's entries.
416 for prefix in step.raw["with"]["restore-keys"]
417 .as_str()
418 .unwrap_or_default()
419 .lines()
420 .filter(|l| !l.trim().is_empty())
421 {
Release tags' runs no longer save to the cache what only their own tag could read, and the CLI's release builds in a pinned image from a cache kept on main. scripts/sccache.sh sets SCCACHE_GHA_RW_MODE=READ_ONLY when GITHUB_REF_TYPE is tag, for the server and the steps after it: against a stand-in for the cache service, sccache 0.18.0 on a branch's run made a CreateCacheEntry call and on a tag's run made none, and looked its compile up in both. The CLI's release ran in ghcr.io/rust-cross/cargo-zigbuild:latest; it now runs in that image pinned by digest (sha256:d8313491ec57…, Rust 1.93.0, zig 0.16.0), reads the crates entry CI saves from the job's ~/.cargo/registry instead of an empty Docker volume, hands what it wrote as root back to the job's user, and restores target/zig from cli-release-<os>-<image digest>-<deps>, which the Caches workflow builds whole when the key is new (ci-cache.mjs keys gives the digest as zigbuild). Built in that image on an 8-core machine, the five platforms took 162 s from nothing and 103 s from the cache, with only the g1t crate compiled again for each; the entry is 1.3 GB, 463 MB compressed. Tests hold the tag rule in sccache.sh, the CLI's entry to the image's digest and to being built whole, and the builder's flags; CONTRIBUTING.md and the Actions guide say both.422 assert!(prefix.contains(toolchain), "{file} {}: {prefix}", job.id);
Merge branch 'worktree-agent-a63425cb2d4935c5f'423 }
424 }
425 if key.contains("steps.keys.outputs.") {
426 let at = job.steps.iter().position(|s| s == &step).unwrap();
427 let keys = job
428 .steps
429 .iter()
430 .position(|s| s.id.as_deref() == Some("keys"))
431 .unwrap_or_else(|| panic!("{file} {}: no keys step", job.id));
432 assert!(keys < at, "{file} {}", job.id);
433 assert_eq!(
434 job.steps[keys].run.as_deref(),
435 Some("node scripts/deploy/ci-cache.mjs keys")
436 );
437 }
438 }
439}
440
441#[test]
442fn a_cargo_target_restored_from_an_older_entry_is_pruned_before_it_is_saved() {
443 // Otherwise the old versions of what changed ride along forever: CI's
444 // test build grew from 671 MB to 1,687 MB in a day.
445 for (file, id, cache) in [
446 ("ci.yml", "rust", "test-build"),
447 ("ci.yml", "desktop", "desktop-build"),
448 ("caches.yml", "runner", "runner-build"),
449 ] {
450 let workflow = read(file);
451 let job = workflow.jobs.iter().find(|j| j.id == id).unwrap();
452 let at = job
453 .steps
454 .iter()
455 .position(|s| s.id.as_deref() == Some(cache))
456 .unwrap();
457 assert!(saves(&job.steps[at]), "{file} {id}");
458 let prune = job
459 .steps
460 .iter()
461 .position(|s| {
462 s.run
463 .as_deref()
464 .is_some_and(|r| r.starts_with("node scripts/deploy/ci-cache.mjs prune "))
465 })
466 .unwrap_or_else(|| panic!("{file} {id}: no prune"));
467 assert!(at < prune, "{file} {id}");
468 assert_eq!(
469 job.steps[prune].condition.as_deref().map(str::trim),
470 Some(format!("${{{{ steps.{cache}.outputs.cache-hit != 'true' }}}}").as_str()),
471 "{file} {id}"
472 );
473 }
Release tags' runs no longer save to the cache what only their own tag could read, and the CLI's release builds in a pinned image from a cache kept on main. scripts/sccache.sh sets SCCACHE_GHA_RW_MODE=READ_ONLY when GITHUB_REF_TYPE is tag, for the server and the steps after it: against a stand-in for the cache service, sccache 0.18.0 on a branch's run made a CreateCacheEntry call and on a tag's run made none, and looked its compile up in both. The CLI's release ran in ghcr.io/rust-cross/cargo-zigbuild:latest; it now runs in that image pinned by digest (sha256:d8313491ec57…, Rust 1.93.0, zig 0.16.0), reads the crates entry CI saves from the job's ~/.cargo/registry instead of an empty Docker volume, hands what it wrote as root back to the job's user, and restores target/zig from cli-release-<os>-<image digest>-<deps>, which the Caches workflow builds whole when the key is new (ci-cache.mjs keys gives the digest as zigbuild). Built in that image on an 8-core machine, the five platforms took 162 s from nothing and 103 s from the cache, with only the g1t crate compiled again for each; the entry is 1.3 GB, 463 MB compressed. Tests hold the tag rule in sccache.sh, the CLI's entry to the image's digest and to being built whole, and the builder's flags; CONTRIBUTING.md and the Actions guide say both.474 // The CLI's build is made in the builder image, whose Cargo the host
475 // cannot ask what it built: so it is never started from an older entry,
476 // and has nothing old to prune.
477 let caches = read("caches.yml");
478 let cli = caches.jobs.iter().find(|j| j.id == "cli").unwrap();
479 let build = cli
480 .steps
481 .iter()
482 .find(|s| s.raw["with"]["key"].as_str().is_some_and(|k| k.starts_with("cli-release-")))
483 .unwrap();
484 assert!(saves(build));
485 assert!(build.raw["with"].get("restore-keys").is_none());
Merge branch 'worktree-agent-a63425cb2d4935c5f'486 let ci = read("ci.yml");
487 let rust = ci.jobs.iter().find(|j| j.id == "rust").unwrap();
488 assert_eq!(
489 rust.raw["env"]["CARGO_PROFILE_DEV_DEBUG"],
490 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.491 );
Merge branch 'worktree-agent-af070177ded5baf00'492}
493
A clone answered from the pack cache is served before the free operation cap: it is not an operation494#[test]
Merge branch 'worktree-agent-a63425cb2d4935c5f'495fn the_caches_workflow_builds_only_what_is_missing() {
496 let caches = read("caches.yml");
497 let push = caches.trigger("push").unwrap();
498 assert!(push.branches.allows("main"));
499 assert!(!push.branches.allows("feature"));
500 // Never cut off halfway: a build half done saves nothing.
501 assert_eq!(
502 caches.concurrency.as_ref().unwrap().cancel_in_progress,
503 json!(false)
504 );
505 let look = caches.jobs.iter().find(|j| j.id == "look").unwrap();
506 // Looked up, not downloaded.
507 for step in &look.steps {
508 if step.uses.as_deref().is_some_and(|u| u.starts_with("actions/cache")) {
509 assert_eq!(step.uses.as_deref(), Some("actions/cache/restore@v4"));
510 assert_eq!(step.raw["with"]["lookup-only"], json!(true), "{:?}", step.id);
511 }
512 }
513 for job in &caches.jobs {
514 if job.id != "look" {
515 assert_eq!(job.needs, ["look"], "{}", job.id);
516 assert!(
517 job.condition
518 .as_deref()
519 .is_some_and(|c| c.contains("needs.look.outputs.")),
520 "{}",
521 job.id
522 );
523 }
524 }
525}
526
527#[test]
A clone answered from the pack cache is served before the free operation cap: it is not an operation528fn deploy_stages_follow_one_another() {
529 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.530 let all = plan_outputs(
531 true,
532 &[("rust", "events,repos", true), ("ts", "projects", false)],
533 &[("rust", "api", true)],
534 &[("web", "web", false)],
535 );
A clone answered from the pack cache is served before the free operation cap: it is not an operation536 let push = json!({});
537
538 // Everything succeeds: each stage runs after the last.
539 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.540 assert!(starts(
541 &deploy,
542 "core",
543 &[("migrate", "success")],
544 &all,
545 push.clone(),
546 false
547 ));
548 assert!(starts(
549 &deploy,
550 "edge",
551 &[("migrate", "success"), ("core", "success")],
552 &all,
553 push.clone(),
554 false
555 ));
556 assert!(starts(
557 &deploy,
558 "front",
559 &[
560 ("migrate", "success"),
561 ("core", "success"),
562 ("edge", "success")
563 ],
564 &all,
565 push.clone(),
566 false
567 ));
A clone answered from the pack cache is served before the free operation cap: it is not an operation568
569 // 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.570 let none = plan_outputs(
571 false,
572 &[("ts", "projects", false)],
573 &[],
574 &[("web", "web", false)],
575 );
A clone answered from the pack cache is served before the free operation cap: it is not an operation576 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.577 assert!(starts(
578 &deploy,
579 "core",
580 &[("migrate", "skipped")],
581 &none,
582 push.clone(),
583 false
584 ));
A clone answered from the pack cache is served before the free operation cap: it is not an operation585 // 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.586 assert!(!starts(
587 &deploy,
588 "edge",
589 &[("migrate", "skipped"), ("core", "success")],
590 &none,
591 push.clone(),
592 false
593 ));
594 assert!(starts(
595 &deploy,
596 "front",
597 &[
598 ("migrate", "skipped"),
599 ("core", "success"),
600 ("edge", "skipped")
601 ],
602 &none,
603 push.clone(),
604 false
605 ));
A clone answered from the pack cache is served before the free operation cap: it is not an operation606
607 // 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.608 assert!(!starts(
609 &deploy,
610 "core",
611 &[("migrate", "failure")],
612 &all,
613 push.clone(),
614 false
615 ));
616 assert!(!starts(
617 &deploy,
618 "edge",
619 &[("migrate", "success"), ("core", "failure")],
620 &all,
621 push.clone(),
622 false
623 ));
624 assert!(!starts(
625 &deploy,
626 "front",
627 &[
628 ("migrate", "success"),
629 ("core", "success"),
630 ("edge", "failure")
631 ],
632 &all,
633 push.clone(),
634 false
635 ));
636 assert!(!starts(
637 &deploy,
638 "front",
639 &[
640 ("migrate", "success"),
641 ("core", "failure"),
642 ("edge", "skipped")
643 ],
644 &all,
645 push.clone(),
646 false
647 ));
A clone answered from the pack cache is served before the free operation cap: it is not an operation648 // 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.649 assert!(!starts(
650 &deploy,
651 "edge",
652 &[("migrate", "success"), ("core", "success")],
653 &all,
654 push.clone(),
655 true
656 ));
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check657 // Check and plan run side by side: plan waits for nothing.
658 let plan = deploy.jobs.iter().find(|j| j.id == "plan").unwrap();
659 assert!(plan.needs.is_empty(), "{:?}", plan.needs);
660 for id in ["migrate", "core", "edge", "front", "smoke"] {
661 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.662 assert!(
663 job.needs.iter().any(|n| n == "check") && job.needs.iter().any(|n| n == "plan"),
664 "{id}: {:?}",
665 job.needs
666 );
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check667 }
668 // A failed check, though plan succeeded: nothing migrates or deploys,
669 // 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.670 assert!(!starts(
671 &deploy,
672 "migrate",
673 &[("check", "failure"), ("plan", "success")],
674 &all,
675 push.clone(),
676 false
677 ));
678 assert!(!starts(
679 &deploy,
680 "core",
681 &[
682 ("check", "failure"),
683 ("plan", "success"),
684 ("migrate", "skipped")
685 ],
686 &all,
687 push.clone(),
688 false
689 ));
690 assert!(!starts(
691 &deploy,
692 "front",
693 &[
694 ("check", "failure"),
695 ("plan", "success"),
696 ("migrate", "skipped"),
697 ("core", "skipped"),
698 ("edge", "skipped")
699 ],
700 &all,
701 push.clone(),
702 false
703 ));
704 let skipped = [
705 ("migrate", "skipped"),
706 ("core", "skipped"),
707 ("edge", "skipped"),
708 ];
709 assert!(!starts_after(
710 &deploy,
711 "front",
712 &skipped,
713 &all,
714 push.clone(),
715 false,
716 true
717 ));
718 assert!(!starts(
719 &deploy,
720 "smoke",
721 &[
722 ("check", "failure"),
723 ("core", "skipped"),
724 ("edge", "skipped"),
725 ("front", "skipped")
726 ],
727 &all,
728 push.clone(),
729 false
730 ));
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check731 // 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.732 assert!(!starts(
733 &deploy,
734 "migrate",
735 &[("plan", "failure")],
736 &all,
737 push.clone(),
738 false
739 ));
740 assert!(!starts(
741 &deploy,
742 "core",
743 &[("plan", "failure"), ("migrate", "skipped")],
744 &all,
745 push.clone(),
746 false
747 ));
A clone answered from the pack cache is served before the free operation cap: it is not an operation748
Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh749 // 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.750 assert!(starts(
751 &deploy,
752 "smoke",
753 &[
754 ("core", "success"),
755 ("edge", "success"),
756 ("front", "success")
757 ],
758 &all,
759 push.clone(),
760 false
761 ));
762 assert!(starts(
763 &deploy,
764 "smoke",
765 &[
766 ("core", "success"),
767 ("edge", "skipped"),
768 ("front", "success")
769 ],
770 &none,
771 push.clone(),
772 false
773 ));
774 assert!(!starts(
775 &deploy,
776 "smoke",
777 &[
778 ("core", "success"),
779 ("edge", "failure"),
780 ("front", "skipped")
781 ],
782 &all,
783 push.clone(),
784 false
785 ));
Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh786 // Nothing deployed: nothing to smoke-test.
787 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.788 assert!(!starts(
789 &deploy,
790 "smoke",
791 &[
792 ("core", "skipped"),
793 ("edge", "skipped"),
794 ("front", "skipped")
795 ],
796 &nothing,
797 push.clone(),
798 false
799 ));
Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh800
A clone answered from the pack cache is served before the free operation cap: it is not an operation801 // A dry run plans and stops.
802 let dry = json!({ "dry_run": true, "units": "", "all": false });
803 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.804 assert!(!starts(
805 &deploy,
806 "core",
807 &[("migrate", "skipped")],
808 &all,
809 dry.clone(),
810 false
811 ));
812 assert!(!starts(
813 &deploy,
814 "smoke",
815 &[
816 ("core", "skipped"),
817 ("edge", "skipped"),
818 ("front", "skipped")
819 ],
820 &all,
821 dry,
822 false
823 ));
A clone answered from the pack cache is served before the free operation cap: it is not an operation824}
825
826#[test]
827fn deploy_stage_matrices_come_from_the_plan() {
828 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.829 let outputs = plan_outputs(
830 false,
831 &[
832 ("rust-1", "events,work", true),
833 ("rust-2", "repos", true),
834 ("ts", "projects,og", false),
835 ],
836 &[],
837 &[],
838 );
A clone answered from the pack cache is served before the free operation cap: it is not an operation839 let core = deploy.jobs.iter().find(|j| j.id == "core").unwrap();
840 assert!(!core.fail_fast);
841 assert_eq!(core.max_parallel, Some(4));
842 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.843 contexts.insert(
844 "needs".into(),
845 json!({ "plan": { "result": "success", "outputs": outputs } }),
846 );
847 let scope = Scope {
848 contexts: &contexts,
849 status: Status::Success,
850 hash_files: None,
851 };
A clone answered from the pack cache is served before the free operation cap: it is not an operation852 let value = expr::interpolate_value(core.matrix.as_ref().unwrap(), &scope).unwrap();
853 let jobs = matrix::expand(&value).unwrap();
854 let groups: Vec<(&str, &str, bool)> = jobs
855 .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.856 .map(|c| {
857 (
858 c["group"].as_str().unwrap(),
859 c["units"].as_str().unwrap(),
860 c["rust"].as_bool().unwrap(),
861 )
862 })
A clone answered from the pack cache is served before the free operation cap: it is not an operation863 .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.864 assert_eq!(
865 groups,
866 [
867 ("rust-1", "events,work", true),
868 ("rust-2", "repos", true),
869 ("ts", "projects,og", false)
870 ]
871 );
A clone answered from the pack cache is served before the free operation cap: it is not an operation872}
873
874#[test]
875fn deploy_builds_rust_on_the_larger_machine_with_its_target_cached() {
876 let deploy = read("deploy.yml");
877 for stage in ["core", "edge", "front"] {
878 let job = deploy.jobs.iter().find(|j| j.id == stage).unwrap();
Merge branch 'main' into actions-toolkit-oidc-artifacts879 let on = |rust: bool, image: bool| {
A clone answered from the pack cache is served before the free operation cap: it is not an operation880 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.881 contexts.insert(
882 "matrix".into(),
883 json!({ "group": "g", "units": "u", "rust": rust, "image": image }),
884 );
885 let scope = Scope {
886 contexts: &contexts,
887 status: Status::Success,
888 hash_files: None,
889 };
A clone answered from the pack cache is served before the free operation cap: it is not an operation890 expr::interpolate_value(&job.runs_on, &scope).unwrap()
891 };
Merge branch 'main' into actions-toolkit-oidc-artifacts892 assert_eq!(on(true, false), json!("g1t-4core"), "{stage}");
893 // The runner's image is built with the job's own Docker Engine.
894 assert_eq!(on(false, true), json!("g1t-4core"), "{stage}");
895 assert_eq!(on(false, false), json!("ubuntu-latest"), "{stage}");
A clone answered from the pack cache is served before the free operation cap: it is not an operation896 }
897 let source = std::fs::read_to_string(workflows_dir().join("deploy.yml")).unwrap();
898 assert!(source.contains("target/wasm32-unknown-unknown/release"));
899 assert!(source.contains("!target/**/incremental"));
Merge branch 'main' into actions-toolkit-oidc-artifacts900 assert!(source.contains("target/x86_64-unknown-linux-musl/release"));
A clone answered from the pack cache is served before the free operation cap: it is not an operation901}
902
Merge remote-tracking branch 'origin/main' into workspace-chat903/// Whether a step runs, for a job going `status`, with `matrix`.
904fn step_runs(step: &workflow::Step, matrix: Value, status: Status) -> bool {
905 let mut contexts = Map::new();
906 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.907 let scope = Scope {
908 contexts: &contexts,
909 status,
910 hash_files: None,
911 };
Merge remote-tracking branch 'origin/main' into workspace-chat912 expr::condition(step.condition.as_deref().unwrap_or_default(), &scope).unwrap()
913}
914
915#[test]
916fn rust_builds_go_through_sccache_before_cargo_and_report_after() {
917 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.918 let at = job
919 .steps
920 .iter()
921 .position(|s| s.run.as_deref() == Some(run))
922 .unwrap_or_else(|| panic!("{}: no step runs {run}", job.id));
Merge remote-tracking branch 'origin/main' into workspace-chat923 (at, job.steps[at].clone())
924 };
925 let deploy = read("deploy.yml");
926 for stage in ["core", "edge", "front"] {
927 let job = deploy.jobs.iter().find(|j| j.id == stage).unwrap();
928 let (install_at, install) = step(job, "bash scripts/sccache.sh install");
929 let (stats_at, stats) = step(job, "bash scripts/sccache.sh stats");
930 // Before anything runs Cargo (worker-build's install, the build),
931 // 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.932 let install_step = job
933 .steps
934 .iter()
935 .position(|s| s.name.as_deref() == Some("Install"))
936 .unwrap();
Merge remote-tracking branch 'origin/main' into workspace-chat937 assert!(install_at < install_step, "{stage}");
938 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.939 for (rust, image, uses) in [
940 (true, false, true),
941 (false, true, true),
942 (false, false, false),
943 ] {
Merge remote-tracking branch 'origin/main' into workspace-chat944 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.945 assert_eq!(
946 step_runs(&install, matrix.clone(), Status::Success),
947 uses,
948 "{stage} rust={rust} image={image}"
949 );
950 assert_eq!(
951 step_runs(&stats, matrix.clone(), Status::Success),
952 uses,
953 "{stage}"
954 );
Merge remote-tracking branch 'origin/main' into workspace-chat955 // A failed build still says what was cached.
956 assert_eq!(step_runs(&stats, matrix, Status::Failure), uses, "{stage}");
957 }
958 }
959 let ci = read("ci.yml");
960 let rust = ci.jobs.iter().find(|j| j.id == "rust").unwrap();
961 let (install_at, _) = step(rust, "bash scripts/sccache.sh install");
962 let (tests_at, _) = step(rust, "cargo test --workspace --locked --quiet");
963 let (stats_at, stats) = step(rust, "bash scripts/sccache.sh stats");
964 assert!(install_at < tests_at && tests_at < stats_at);
965 assert!(step_runs(&stats, json!({}), Status::Failure));
966 // main's runs keep the caches pull requests restore from: only Rust.
967 assert!(ci.trigger("push").unwrap().branches.allows("main"));
968 assert!(ci.trigger("pull_request").is_some());
969 for (id, on_push) in [("rust", true), ("typescript", false), ("build", false)] {
970 let job = ci.jobs.iter().find(|j| j.id == id).unwrap();
971 for (event, expected) in [("push", on_push), ("pull_request", true)] {
972 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.973 contexts.insert(
974 "github".into(),
975 json!({ "event_name": event, "ref": "refs/heads/main" }),
976 );
977 let scope = Scope {
978 contexts: &contexts,
979 status: Status::Success,
980 hash_files: None,
981 };
982 assert_eq!(
983 expr::condition(job.condition.as_deref().unwrap_or_default(), &scope).unwrap(),
984 expected,
985 "{id} on {event}"
986 );
Merge remote-tracking branch 'origin/main' into workspace-chat987 }
988 }
989 // The download is pinned by version and checksum.
990 let script = std::fs::read_to_string(workflows_dir().join("../../scripts/sccache.sh")).unwrap();
991 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.992 assert!(script.lines().any(|line| {
993 line.strip_prefix("SHA256=")
994 .is_some_and(|sum| sum.len() == 64)
995 }));
Merge remote-tracking branch 'origin/main' into workspace-chat996 assert!(script.contains("sha256sum -c"));
997 assert!(script.contains("RUSTC_WRAPPER="));
998 assert!(script.contains("GITHUB_STEP_SUMMARY"));
Release tags' runs no longer save to the cache what only their own tag could read, and the CLI's release builds in a pinned image from a cache kept on main. scripts/sccache.sh sets SCCACHE_GHA_RW_MODE=READ_ONLY when GITHUB_REF_TYPE is tag, for the server and the steps after it: against a stand-in for the cache service, sccache 0.18.0 on a branch's run made a CreateCacheEntry call and on a tag's run made none, and looked its compile up in both. The CLI's release ran in ghcr.io/rust-cross/cargo-zigbuild:latest; it now runs in that image pinned by digest (sha256:d8313491ec57…, Rust 1.93.0, zig 0.16.0), reads the crates entry CI saves from the job's ~/.cargo/registry instead of an empty Docker volume, hands what it wrote as root back to the job's user, and restores target/zig from cli-release-<os>-<image digest>-<deps>, which the Caches workflow builds whole when the key is new (ci-cache.mjs keys gives the digest as zigbuild). Built in that image on an 8-core machine, the five platforms took 162 s from nothing and 103 s from the cache, with only the g1t crate compiled again for each; the entry is 1.3 GB, 463 MB compressed. Tests hold the tag rule in sccache.sh, the CLI's entry to the image's digest and to being built whole, and the builder's flags; CONTRIBUTING.md and the Actions guide say both.999 // A tag's run reads main's entries and saves none, which only its own
1000 // tag could read; the mode reaches the steps after it too.
1001 assert!(script.contains("if [ \"${GITHUB_REF_TYPE:-}\" = tag ]; then\n export SCCACHE_GHA_RW_MODE=READ_ONLY"));
1002 assert!(script.contains("echo \"SCCACHE_GHA_RW_MODE=$SCCACHE_GHA_RW_MODE\""));
1003 let tagged = read("runner-release.yml");
1004 assert!(tagged.trigger("push").unwrap().tags.allows("runner-v0.4.0"));
Merge remote-tracking branch 'origin/main' into workspace-chat1005}
1006
A clone answered from the pack cache is served before the free operation cap: it is not an operation1007#[test]
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91008fn g1ts_own_actions_are_published_when_they_change() {
1009 let release = read("actions-release.yml");
1010 let push = release.trigger("push").unwrap();
1011 assert!(push.branches.allows("main"));
1012 assert!(!push.branches.allows("feature"));
1013 assert!(release.trigger("workflow_dispatch").is_some());
1014 let source = std::fs::read_to_string(workflows_dir().join("actions-release.yml")).unwrap();
1015 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.1016 assert!(source.contains("secrets.ACTIONS_PUBLISH_TOKEN"));
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91017 // Every action is a composite one with its README, and the copies of
1018 // the script that finds g1t-runner are the same file.
1019 let actions = workflows_dir().join("../../actions");
1020 let mut finders = Vec::new();
1021 for entry in std::fs::read_dir(&actions).unwrap() {
1022 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.1023 let manifest = std::fs::read_to_string(dir.join("action.yml"))
1024 .unwrap_or_else(|_| panic!("{} has no action.yml", dir.display()));
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91025 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.1026 assert_eq!(
1027 manifest["runs"]["using"].as_str(),
1028 Some("composite"),
1029 "{}",
1030 dir.display()
1031 );
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91032 assert!(dir.join("README.md").exists(), "{}", dir.display());
1033 finders.push(std::fs::read_to_string(dir.join("runner.sh")).unwrap());
1034 }
1035 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.1036 assert!(
1037 finders.windows(2).all(|pair| pair[0] == pair[1]),
1038 "actions/*/runner.sh differ"
1039 );
1040 let deploy: serde_yaml::Value =
1041 serde_yaml::from_str(&std::fs::read_to_string(actions.join("deploy/action.yml")).unwrap())
1042 .unwrap();
1043 assert_eq!(
1044 deploy["inputs"]["token"]["default"].as_str(),
1045 Some("${{ github.token }}")
1046 );
Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r91047 for output in ["url", "deployment-id"] {
1048 assert!(deploy["outputs"][output]["value"].is_string(), "{output}");
1049 }
1050}
1051
1052#[test]
A clone answered from the pack cache is served before the free operation cap: it is not an operation1053fn the_runner_base_rebuilds_weekly_on_a_machine_with_docker() {
1054 let base = read("runner-base.yml");
1055 assert!(base.trigger("schedule").is_some());
1056 assert!(base.trigger("workflow_dispatch").is_some());
1057 assert!(base.trigger("push").unwrap().branches.allows("main"));
1058 let job = base.jobs.iter().find(|j| j.id == "build").unwrap();
1059 assert_eq!(job.runs_on, json!(["self-hosted", "docker"]));
1060}
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'1061
1062#[test]
1063fn 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.1064 let root = workflows_dir()
1065 .parent()
1066 .unwrap()
1067 .parent()
1068 .unwrap()
1069 .to_path_buf();
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'1070 let ours = std::fs::read_to_string(root.join(".g1t/workflows/conformance.yml")).unwrap();
1071 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.1072 assert_eq!(
1073 ours, theirs,
1074 "copy .g1t/workflows/conformance.yml over .github/workflows/conformance.yml"
1075 );
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'1076 // It runs only by hand, and reads nothing it could change.
1077 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.1078 assert_eq!(
1079 workflow
1080 .triggers
1081 .iter()
1082 .map(|t| t.event.as_str())
1083 .collect::<Vec<_>>(),
1084 ["workflow_dispatch"]
1085 );
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9'1086 assert!(!ours.contains("secrets."));
1087}
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.1088
1089/// No workflow of g1t's asks for a secret no one can set. Secret names
1090/// starting with G1T_ or GITHUB_ are g1t's own and refused when saved, so a
1091/// workflow reading one gets nothing: the Actions release asked for
1092/// G1T_ACTIONS_PUBLISH_TOKEN, which could never exist. G1T_TOKEN, the job's
1093/// own token, is the one g1t gives every job.
1094#[test]
1095fn workflows_read_only_secrets_that_can_be_set() {
1096 for entry in std::fs::read_dir(workflows_dir()).unwrap() {
1097 let path = entry.unwrap().path();
1098 if path.extension().is_none_or(|ext| ext != "yml") {
1099 continue;
1100 }
1101 let source = std::fs::read_to_string(&path).unwrap();
1102 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.1103 let name: String = source[at + "secrets.".len()..]
1104 .chars()
1105 .take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
1106 .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.1107 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.1108 assert!(
1109 !reserved || name == "G1T_TOKEN" || name == "GITHUB_TOKEN",
1110 "{} reads secrets.{name}, a name g1t keeps for its own",
1111 path.display()
1112 );
1113 }
1114 }
1115}
1116
1117#[test]
1118fn every_install_runs_again_when_a_connection_drops() {
1119 // npm does not retry a body that breaks off midway; npm-ci.mjs does.
1120 for entry in std::fs::read_dir(workflows_dir()).unwrap() {
1121 let path = entry.unwrap().path();
1122 if path.extension().and_then(|e| e.to_str()) != Some("yml") {
1123 continue;
1124 }
1125 let name = path.file_name().unwrap().to_string_lossy().to_string();
1126 for job in read(&name).jobs {
1127 for step in &job.steps {
1128 let Some(run) = step.run.as_deref() else {
1129 continue;
1130 };
1131 for line in run.lines() {
1132 assert!(
1133 !line.trim().starts_with("npm ci"),
1134 "{name} {}: `{}` should be `node scripts/deploy/npm-ci.mjs …`",
1135 job.id,
1136 line.trim()
1137 );
1138 }
1139 }
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.1140 }
1141 }
1142}

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