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 operation | 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. |
| A clone answered from the pack cache is served before the free operation cap: it is not an operation | 5 | |
| 6 | use std::path::PathBuf; | |
| 7 | ||
| 8 | use g1t_actions::expr::{self, Scope, Status}; | |
| 9 | use g1t_actions::matrix; | |
| 10 | use g1t_actions::workflow::{self, Severity, Workflow}; | |
| 11 | use serde_json::{Map, Value, json}; | |
| 12 | ||
| 13 | fn workflows_dir() -> PathBuf { | |
| 14 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.g1t/workflows") | |
| 15 | } | |
| 16 | ||
| 17 | fn 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' | 23 | fn 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-7nm8r9 | 36 | // 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 operation | 64 | fn 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 operation | 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. | 85 | fn 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 operation | 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() | |
| A clone answered from the pack cache is served before the free operation cap: it is not an operation | 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. | 117 | fn 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 operation | 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. | 130 | fn 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 operation | 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"); | |
| A clone answered from the pack cache is served before the free operation cap: it is not an operation | 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 | ); | |
| A clone answered from the pack cache is served before the free operation cap: it is not an operation | 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 | }; | |
| A clone answered from the pack cache is served before the free operation cap: it is not an operation | 172 | expr::condition(job.condition.as_deref().unwrap_or_default(), &scope).unwrap() |
| 173 | } | |
| 174 | ||
| 175 | #[test] | |
| 176 | fn 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 operation | 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. | |
| 203 | fn 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] | |
| 214 | fn 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). | |
| 266 | fn 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-`). | |
| 292 | fn family(key: &str) -> String { | |
| 293 | key.split("${{").next().unwrap_or_default().to_owned() | |
| 294 | } | |
| 295 | ||
| 296 | fn 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 | ||
| 302 | fn 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] | |
| 312 | fn 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] | |
| 354 | fn 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] | |
| 395 | fn 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] | |
| 442 | fn 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 operation | 494 | #[test] |
| Merge branch 'worktree-agent-a63425cb2d4935c5f' | 495 | fn 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 operation | 528 | fn 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 operation | 536 | 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 operation | 568 | |
| 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 operation | 576 | 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 operation | 585 | // 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 operation | 606 | |
| 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 operation | 648 | // 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 check | 657 | // 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 check | 667 | } |
| 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 check | 731 | // 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 operation | 748 | |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 749 | // 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.sh | 786 | // 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 | ¬hing, | |
| 797 | push.clone(), | |
| 798 | false | |
| 799 | )); | |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 800 | |
| A clone answered from the pack cache is served before the free operation cap: it is not an operation | 801 | // 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 operation | 824 | } |
| 825 | ||
| 826 | #[test] | |
| 827 | fn 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 operation | 839 | 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 operation | 852 | 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 operation | 863 | .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 operation | 872 | } |
| 873 | ||
| 874 | #[test] | |
| 875 | fn 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-artifacts | 879 | let on = |rust: bool, image: bool| { |
| A clone answered from the pack cache is served before the free operation cap: it is not an operation | 880 | 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 operation | 890 | expr::interpolate_value(&job.runs_on, &scope).unwrap() |
| 891 | }; | |
| Merge branch 'main' into actions-toolkit-oidc-artifacts | 892 | 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 operation | 896 | } |
| 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-artifacts | 900 | 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 operation | 901 | } |
| 902 | ||
| Merge remote-tracking branch 'origin/main' into workspace-chat | 903 | /// Whether a step runs, for a job going `status`, with `matrix`. |
| 904 | fn 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-chat | 912 | expr::condition(step.condition.as_deref().unwrap_or_default(), &scope).unwrap() |
| 913 | } | |
| 914 | ||
| 915 | #[test] | |
| 916 | fn 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-chat | 923 | (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-chat | 937 | 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-chat | 944 | 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-chat | 955 | // 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-chat | 987 | } |
| 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-chat | 996 | 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-chat | 1005 | } |
| 1006 | ||
| A clone answered from the pack cache is served before the free operation cap: it is not an operation | 1007 | #[test] |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1008 | fn 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-7nm8r9 | 1017 | // 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-7nm8r9 | 1025 | 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-7nm8r9 | 1032 | 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-7nm8r9 | 1047 | 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 operation | 1053 | fn 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] | |
| 1063 | fn 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] | |
| 1095 | fn 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] | |
| 1118 | fn 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.