Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge branch 'worktree-agent-a6a121745e81f639f' | 1 | //! g1t's own workflows (`.g1t/workflows/*.yml`), read by the same parser |
| 2 | //! and expressions the actions service runs them with: each reads, nothing | |
| 3 | //! in it is unsupported, and the deploy workflow's jobs start, wait and | |
| The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were. | 4 | //! stop as docs.g1t.sh/guides/deploy-to-cloudflare/ says. |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 5 | |
| 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] | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 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(); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 78 | assert!(unsupported.is_empty(), "{name}: {unsupported:?}"); |
| 79 | count += 1; | |
| 80 | } | |
| 81 | assert!(count >= 1); | |
| 82 | } | |
| 83 | ||
| 84 | /// The plan job's outputs for a deploy of `stages` (each with its jobs). | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 85 | fn plan_outputs( |
| 86 | migrate: bool, | |
| 87 | core: &[(&str, &str, bool)], | |
| 88 | edge: &[(&str, &str, bool)], | |
| 89 | front: &[(&str, &str, bool)], | |
| 90 | ) -> Value { | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 91 | let matrix = |jobs: &[(&str, &str, bool)]| { |
| 92 | let include: Vec<Value> = if jobs.is_empty() { | |
| 93 | vec![json!({ "group": "none", "units": "" })] | |
| 94 | } else { | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 95 | jobs.iter() |
| 96 | .map(|(group, units, rust)| json!({ "group": group, "units": units, "rust": rust })) | |
| 97 | .collect() | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 98 | }; |
| 99 | json!({ "include": include }).to_string() | |
| 100 | }; | |
| 101 | json!({ | |
| 102 | "migrate": migrate.to_string(), | |
| 103 | "migrate_units": if migrate { "events" } else { "" }, | |
| 104 | "has_core": (!core.is_empty()).to_string(), | |
| 105 | "core": matrix(core), | |
| 106 | "has_edge": (!edge.is_empty()).to_string(), | |
| 107 | "edge": matrix(edge), | |
| 108 | "has_front": (!front.is_empty()).to_string(), | |
| 109 | "front": matrix(front), | |
| 110 | }) | |
| 111 | } | |
| 112 | ||
| 113 | /// Whether `job` starts, given its needs' results, as the actions service | |
| 114 | /// decides it (services/actions/src/plan.rs `decide`, with | |
| 115 | /// `expr::job_status`): a need that was skipped is not a failure, and a | |
| 116 | /// failure anywhere before the job is. | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 117 | fn starts( |
| 118 | workflow: &Workflow, | |
| 119 | job: &str, | |
| 120 | needs: &[(&str, &str)], | |
| 121 | outputs: &Value, | |
| 122 | inputs: Value, | |
| 123 | cancelled: bool, | |
| 124 | ) -> bool { | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 125 | starts_after(workflow, job, needs, outputs, inputs, cancelled, false) |
| 126 | } | |
| 127 | ||
| 128 | /// `starts`, where `failed_before` says a job further back failed (one the | |
| 129 | /// needs were skipped for). | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 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 { | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 139 | let job = workflow.jobs.iter().find(|j| j.id == job).unwrap(); |
| 140 | let mut needs_ctx = Map::new(); | |
| 141 | let mut results = Vec::new(); | |
| 142 | for need in &job.needs { | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 143 | let result = needs |
| 144 | .iter() | |
| 145 | .find(|(name, _)| name == need) | |
| 146 | .map(|(_, r)| *r) | |
| 147 | .unwrap_or("success"); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 148 | results.push(result); |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 149 | let outputs = if need == "plan" { |
| 150 | outputs.clone() | |
| 151 | } else { | |
| 152 | json!({}) | |
| 153 | }; | |
| 154 | needs_ctx.insert( | |
| 155 | need.clone(), | |
| 156 | json!({ "result": result, "outputs": outputs }), | |
| 157 | ); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 158 | } |
| 159 | let status = expr::job_status(results, failed_before, cancelled); | |
| 160 | let mut contexts = Map::new(); | |
| 161 | contexts.insert("needs".into(), Value::Object(needs_ctx)); | |
| 162 | contexts.insert("inputs".into(), inputs); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 163 | contexts.insert( |
| 164 | "github".into(), | |
| 165 | json!({ "event_name": "push", "ref": "refs/heads/main" }), | |
| 166 | ); | |
| 167 | let scope = Scope { | |
| 168 | contexts: &contexts, | |
| 169 | status, | |
| 170 | hash_files: None, | |
| 171 | }; | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 172 | expr::condition(job.condition.as_deref().unwrap_or_default(), &scope).unwrap() |
| 173 | } | |
| 174 | ||
| 175 | #[test] | |
| 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 | ); |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 192 | // The stages share their steps (a YAML alias), and read the token only |
| 193 | // where they deploy. | |
| 194 | let steps = |id: &str| deploy.jobs.iter().find(|j| j.id == id).unwrap().steps.len(); | |
| 195 | assert_eq!(steps("core"), steps("edge")); | |
| 196 | assert_eq!(steps("core"), steps("front")); | |
| 197 | let source = std::fs::read_to_string(workflows_dir().join("deploy.yml")).unwrap(); | |
| 198 | assert!(!source.contains("cancel-in-progress: true")); | |
| 199 | } | |
| 200 | ||
| Merge branch 'worktree-agent-af070177ded5baf00' | 201 | /// Whether a step's `run` installs the whole lockfile: `npm ci` with no |
| 202 | /// workspace narrowing it, and not the deploy tool's install of some units. | |
| 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-", | |
| 347 | ] { | |
| 348 | assert!(seen.contains_key(family), "{family}: {:?}", seen.keys()); | |
| 349 | } | |
| 350 | } | |
| 351 | ||
| 352 | #[test] | |
| 353 | fn main_saves_every_cache_that_pull_requests_and_releases_restore() { | |
| 354 | // A run restores from its own ref, its pull request's base and main; | |
| 355 | // a release tag's run never reads another tag's entries. So every | |
| 356 | // entry any step restores is saved by a workflow that runs on pushes | |
| 357 | // to main, and the release workflows only restore. | |
| 358 | let on_main = ["ci.yml", "caches.yml", "deploy.yml"]; | |
| 359 | for file in on_main { | |
| 360 | let push = read(file).trigger("push").cloned().unwrap(); | |
| 361 | assert!(push.branches.allows("main"), "{file}"); | |
| 362 | } | |
| 363 | let releases = [ | |
| 364 | "cli-release.yml", | |
| 365 | "desktop-release.yml", | |
| 366 | "mobile-build.yml", | |
| 367 | "mobile-release.yml", | |
| 368 | "runner-release.yml", | |
| 369 | ]; | |
| 370 | let steps = cache_steps(); | |
| 371 | let saved_on_main: std::collections::BTreeSet<String> = steps | |
| 372 | .iter() | |
| 373 | .filter(|(file, _, step)| on_main.contains(&file.as_str()) && saves(step)) | |
| 374 | .map(|(_, _, step)| family(step.raw["with"]["key"].as_str().unwrap_or_default())) | |
| 375 | .collect(); | |
| 376 | for (file, job, step) in &steps { | |
| 377 | let family = family(step.raw["with"]["key"].as_str().unwrap_or_default()); | |
| 378 | // The conformance run restores what its own first job saved. | |
| 379 | if family == "conformance-" { | |
| 380 | continue; | |
| 381 | } | |
| 382 | assert!( | |
| 383 | saved_on_main.contains(&family), | |
| 384 | "{file} {}: nothing on main saves {family}", | |
| 385 | job.id | |
| 386 | ); | |
| 387 | if releases.contains(&file.as_str()) { | |
| 388 | assert!(!saves(step), "{file} {}: a release saves {family}, which only its own tag would read", job.id); | |
| 389 | } | |
| 390 | } | |
| 391 | } | |
| 392 | ||
| 393 | #[test] | |
| 394 | fn rust_caches_are_keyed_by_the_toolchain_and_the_dependencies() { | |
| 395 | // Not by Cargo.lock as a whole: 25 of its 31 changes in a week were a | |
| 396 | // release's version bump, and each saved every Rust cache again. | |
| 397 | for (file, job, step) in cache_steps() { | |
| 398 | let key = step.raw["with"]["key"].as_str().unwrap_or_default(); | |
| 399 | assert!( | |
| 400 | !key.contains("Cargo.lock") && !key.contains("base.json"), | |
| 401 | "{file} {}: {key}", | |
| 402 | job.id | |
| 403 | ); | |
| 404 | let target = paths(&step).iter().any(|p| p.contains("target")); | |
| 405 | if target { | |
| 406 | assert!(key.contains("steps.keys.outputs.rust"), "{file} {}: {key}", job.id); | |
| 407 | // A restore key never reaches another toolchain's entries. | |
| 408 | for prefix in step.raw["with"]["restore-keys"] | |
| 409 | .as_str() | |
| 410 | .unwrap_or_default() | |
| 411 | .lines() | |
| 412 | .filter(|l| !l.trim().is_empty()) | |
| 413 | { | |
| 414 | assert!(prefix.contains("steps.keys.outputs.rust"), "{file} {}: {prefix}", job.id); | |
| 415 | } | |
| 416 | } | |
| 417 | if key.contains("steps.keys.outputs.") { | |
| 418 | let at = job.steps.iter().position(|s| s == &step).unwrap(); | |
| 419 | let keys = job | |
| 420 | .steps | |
| 421 | .iter() | |
| 422 | .position(|s| s.id.as_deref() == Some("keys")) | |
| 423 | .unwrap_or_else(|| panic!("{file} {}: no keys step", job.id)); | |
| 424 | assert!(keys < at, "{file} {}", job.id); | |
| 425 | assert_eq!( | |
| 426 | job.steps[keys].run.as_deref(), | |
| 427 | Some("node scripts/deploy/ci-cache.mjs keys") | |
| 428 | ); | |
| 429 | } | |
| 430 | } | |
| 431 | } | |
| 432 | ||
| 433 | #[test] | |
| 434 | fn a_cargo_target_restored_from_an_older_entry_is_pruned_before_it_is_saved() { | |
| 435 | // Otherwise the old versions of what changed ride along forever: CI's | |
| 436 | // test build grew from 671 MB to 1,687 MB in a day. | |
| 437 | for (file, id, cache) in [ | |
| 438 | ("ci.yml", "rust", "test-build"), | |
| 439 | ("ci.yml", "desktop", "desktop-build"), | |
| 440 | ("caches.yml", "runner", "runner-build"), | |
| 441 | ] { | |
| 442 | let workflow = read(file); | |
| 443 | let job = workflow.jobs.iter().find(|j| j.id == id).unwrap(); | |
| 444 | let at = job | |
| 445 | .steps | |
| 446 | .iter() | |
| 447 | .position(|s| s.id.as_deref() == Some(cache)) | |
| 448 | .unwrap(); | |
| 449 | assert!(saves(&job.steps[at]), "{file} {id}"); | |
| 450 | let prune = job | |
| 451 | .steps | |
| 452 | .iter() | |
| 453 | .position(|s| { | |
| 454 | s.run | |
| 455 | .as_deref() | |
| 456 | .is_some_and(|r| r.starts_with("node scripts/deploy/ci-cache.mjs prune ")) | |
| 457 | }) | |
| 458 | .unwrap_or_else(|| panic!("{file} {id}: no prune")); | |
| 459 | assert!(at < prune, "{file} {id}"); | |
| 460 | assert_eq!( | |
| 461 | job.steps[prune].condition.as_deref().map(str::trim), | |
| 462 | Some(format!("${{{{ steps.{cache}.outputs.cache-hit != 'true' }}}}").as_str()), | |
| 463 | "{file} {id}" | |
| 464 | ); | |
| 465 | } | |
| 466 | let ci = read("ci.yml"); | |
| 467 | let rust = ci.jobs.iter().find(|j| j.id == "rust").unwrap(); | |
| 468 | assert_eq!( | |
| 469 | rust.raw["env"]["CARGO_PROFILE_DEV_DEBUG"], | |
| 470 | json!("line-tables-only") | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 471 | ); |
| Merge branch 'worktree-agent-af070177ded5baf00' | 472 | } |
| 473 | ||
| Merge branch 'worktree-agent-a6a121745e81f639f' | 474 | #[test] |
| Merge branch 'worktree-agent-a63425cb2d4935c5f' | 475 | fn the_caches_workflow_builds_only_what_is_missing() { |
| 476 | let caches = read("caches.yml"); | |
| 477 | let push = caches.trigger("push").unwrap(); | |
| 478 | assert!(push.branches.allows("main")); | |
| 479 | assert!(!push.branches.allows("feature")); | |
| 480 | // Never cut off halfway: a build half done saves nothing. | |
| 481 | assert_eq!( | |
| 482 | caches.concurrency.as_ref().unwrap().cancel_in_progress, | |
| 483 | json!(false) | |
| 484 | ); | |
| 485 | let look = caches.jobs.iter().find(|j| j.id == "look").unwrap(); | |
| 486 | // Looked up, not downloaded. | |
| 487 | for step in &look.steps { | |
| 488 | if step.uses.as_deref().is_some_and(|u| u.starts_with("actions/cache")) { | |
| 489 | assert_eq!(step.uses.as_deref(), Some("actions/cache/restore@v4")); | |
| 490 | assert_eq!(step.raw["with"]["lookup-only"], json!(true), "{:?}", step.id); | |
| 491 | } | |
| 492 | } | |
| 493 | for job in &caches.jobs { | |
| 494 | if job.id != "look" { | |
| 495 | assert_eq!(job.needs, ["look"], "{}", job.id); | |
| 496 | assert!( | |
| 497 | job.condition | |
| 498 | .as_deref() | |
| 499 | .is_some_and(|c| c.contains("needs.look.outputs.")), | |
| 500 | "{}", | |
| 501 | job.id | |
| 502 | ); | |
| 503 | } | |
| 504 | } | |
| 505 | } | |
| 506 | ||
| 507 | #[test] | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 508 | fn deploy_stages_follow_one_another() { |
| 509 | let deploy = read("deploy.yml"); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 510 | let all = plan_outputs( |
| 511 | true, | |
| 512 | &[("rust", "events,repos", true), ("ts", "projects", false)], | |
| 513 | &[("rust", "api", true)], | |
| 514 | &[("web", "web", false)], | |
| 515 | ); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 516 | let push = json!({}); |
| 517 | ||
| 518 | // Everything succeeds: each stage runs after the last. | |
| 519 | assert!(starts(&deploy, "migrate", &[], &all, push.clone(), false)); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 520 | assert!(starts( |
| 521 | &deploy, | |
| 522 | "core", | |
| 523 | &[("migrate", "success")], | |
| 524 | &all, | |
| 525 | push.clone(), | |
| 526 | false | |
| 527 | )); | |
| 528 | assert!(starts( | |
| 529 | &deploy, | |
| 530 | "edge", | |
| 531 | &[("migrate", "success"), ("core", "success")], | |
| 532 | &all, | |
| 533 | push.clone(), | |
| 534 | false | |
| 535 | )); | |
| 536 | assert!(starts( | |
| 537 | &deploy, | |
| 538 | "front", | |
| 539 | &[ | |
| 540 | ("migrate", "success"), | |
| 541 | ("core", "success"), | |
| 542 | ("edge", "success") | |
| 543 | ], | |
| 544 | &all, | |
| 545 | push.clone(), | |
| 546 | false | |
| 547 | )); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 548 | |
| 549 | // No migrations: the migrate job is skipped, and core still runs. | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 550 | let none = plan_outputs( |
| 551 | false, | |
| 552 | &[("ts", "projects", false)], | |
| 553 | &[], | |
| 554 | &[("web", "web", false)], | |
| 555 | ); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 556 | assert!(!starts(&deploy, "migrate", &[], &none, push.clone(), false)); |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 557 | assert!(starts( |
| 558 | &deploy, | |
| 559 | "core", | |
| 560 | &[("migrate", "skipped")], | |
| 561 | &none, | |
| 562 | push.clone(), | |
| 563 | false | |
| 564 | )); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 565 | // An empty stage is skipped, and the next one still runs. |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 566 | assert!(!starts( |
| 567 | &deploy, | |
| 568 | "edge", | |
| 569 | &[("migrate", "skipped"), ("core", "success")], | |
| 570 | &none, | |
| 571 | push.clone(), | |
| 572 | false | |
| 573 | )); | |
| 574 | assert!(starts( | |
| 575 | &deploy, | |
| 576 | "front", | |
| 577 | &[ | |
| 578 | ("migrate", "skipped"), | |
| 579 | ("core", "success"), | |
| 580 | ("edge", "skipped") | |
| 581 | ], | |
| 582 | &none, | |
| 583 | push.clone(), | |
| 584 | false | |
| 585 | )); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 586 | |
| 587 | // A failure stops every later stage. | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 588 | assert!(!starts( |
| 589 | &deploy, | |
| 590 | "core", | |
| 591 | &[("migrate", "failure")], | |
| 592 | &all, | |
| 593 | push.clone(), | |
| 594 | false | |
| 595 | )); | |
| 596 | assert!(!starts( | |
| 597 | &deploy, | |
| 598 | "edge", | |
| 599 | &[("migrate", "success"), ("core", "failure")], | |
| 600 | &all, | |
| 601 | push.clone(), | |
| 602 | false | |
| 603 | )); | |
| 604 | assert!(!starts( | |
| 605 | &deploy, | |
| 606 | "front", | |
| 607 | &[ | |
| 608 | ("migrate", "success"), | |
| 609 | ("core", "success"), | |
| 610 | ("edge", "failure") | |
| 611 | ], | |
| 612 | &all, | |
| 613 | push.clone(), | |
| 614 | false | |
| 615 | )); | |
| 616 | assert!(!starts( | |
| 617 | &deploy, | |
| 618 | "front", | |
| 619 | &[ | |
| 620 | ("migrate", "success"), | |
| 621 | ("core", "failure"), | |
| 622 | ("edge", "skipped") | |
| 623 | ], | |
| 624 | &all, | |
| 625 | push.clone(), | |
| 626 | false | |
| 627 | )); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 628 | // A cancelled run starts nothing more. |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 629 | assert!(!starts( |
| 630 | &deploy, | |
| 631 | "edge", | |
| 632 | &[("migrate", "success"), ("core", "success")], | |
| 633 | &all, | |
| 634 | push.clone(), | |
| 635 | true | |
| 636 | )); | |
| Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check | 637 | // Check and plan run side by side: plan waits for nothing. |
| 638 | let plan = deploy.jobs.iter().find(|j| j.id == "plan").unwrap(); | |
| 639 | assert!(plan.needs.is_empty(), "{:?}", plan.needs); | |
| 640 | for id in ["migrate", "core", "edge", "front", "smoke"] { | |
| 641 | let job = deploy.jobs.iter().find(|j| j.id == id).unwrap(); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 642 | assert!( |
| 643 | job.needs.iter().any(|n| n == "check") && job.needs.iter().any(|n| n == "plan"), | |
| 644 | "{id}: {:?}", | |
| 645 | job.needs | |
| 646 | ); | |
| Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check | 647 | } |
| 648 | // A failed check, though plan succeeded: nothing migrates or deploys, | |
| 649 | // and failure() still sees the check's failure through skipped jobs. | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 650 | assert!(!starts( |
| 651 | &deploy, | |
| 652 | "migrate", | |
| 653 | &[("check", "failure"), ("plan", "success")], | |
| 654 | &all, | |
| 655 | push.clone(), | |
| 656 | false | |
| 657 | )); | |
| 658 | assert!(!starts( | |
| 659 | &deploy, | |
| 660 | "core", | |
| 661 | &[ | |
| 662 | ("check", "failure"), | |
| 663 | ("plan", "success"), | |
| 664 | ("migrate", "skipped") | |
| 665 | ], | |
| 666 | &all, | |
| 667 | push.clone(), | |
| 668 | false | |
| 669 | )); | |
| 670 | assert!(!starts( | |
| 671 | &deploy, | |
| 672 | "front", | |
| 673 | &[ | |
| 674 | ("check", "failure"), | |
| 675 | ("plan", "success"), | |
| 676 | ("migrate", "skipped"), | |
| 677 | ("core", "skipped"), | |
| 678 | ("edge", "skipped") | |
| 679 | ], | |
| 680 | &all, | |
| 681 | push.clone(), | |
| 682 | false | |
| 683 | )); | |
| 684 | let skipped = [ | |
| 685 | ("migrate", "skipped"), | |
| 686 | ("core", "skipped"), | |
| 687 | ("edge", "skipped"), | |
| 688 | ]; | |
| 689 | assert!(!starts_after( | |
| 690 | &deploy, | |
| 691 | "front", | |
| 692 | &skipped, | |
| 693 | &all, | |
| 694 | push.clone(), | |
| 695 | false, | |
| 696 | true | |
| 697 | )); | |
| 698 | assert!(!starts( | |
| 699 | &deploy, | |
| 700 | "smoke", | |
| 701 | &[ | |
| 702 | ("check", "failure"), | |
| 703 | ("core", "skipped"), | |
| 704 | ("edge", "skipped"), | |
| 705 | ("front", "skipped") | |
| 706 | ], | |
| 707 | &all, | |
| 708 | push.clone(), | |
| 709 | false | |
| 710 | )); | |
| Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check | 711 | // A failed plan stops everything too. |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 712 | assert!(!starts( |
| 713 | &deploy, | |
| 714 | "migrate", | |
| 715 | &[("plan", "failure")], | |
| 716 | &all, | |
| 717 | push.clone(), | |
| 718 | false | |
| 719 | )); | |
| 720 | assert!(!starts( | |
| 721 | &deploy, | |
| 722 | "core", | |
| 723 | &[("plan", "failure"), ("migrate", "skipped")], | |
| 724 | &all, | |
| 725 | push.clone(), | |
| 726 | false | |
| 727 | )); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 728 | |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 729 | // Smoke follows the last stage that ran, and not a failed one. |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 730 | assert!(starts( |
| 731 | &deploy, | |
| 732 | "smoke", | |
| 733 | &[ | |
| 734 | ("core", "success"), | |
| 735 | ("edge", "success"), | |
| 736 | ("front", "success") | |
| 737 | ], | |
| 738 | &all, | |
| 739 | push.clone(), | |
| 740 | false | |
| 741 | )); | |
| 742 | assert!(starts( | |
| 743 | &deploy, | |
| 744 | "smoke", | |
| 745 | &[ | |
| 746 | ("core", "success"), | |
| 747 | ("edge", "skipped"), | |
| 748 | ("front", "success") | |
| 749 | ], | |
| 750 | &none, | |
| 751 | push.clone(), | |
| 752 | false | |
| 753 | )); | |
| 754 | assert!(!starts( | |
| 755 | &deploy, | |
| 756 | "smoke", | |
| 757 | &[ | |
| 758 | ("core", "success"), | |
| 759 | ("edge", "failure"), | |
| 760 | ("front", "skipped") | |
| 761 | ], | |
| 762 | &all, | |
| 763 | push.clone(), | |
| 764 | false | |
| 765 | )); | |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 766 | // Nothing deployed: nothing to smoke-test. |
| 767 | let nothing = plan_outputs(false, &[], &[], &[]); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 768 | assert!(!starts( |
| 769 | &deploy, | |
| 770 | "smoke", | |
| 771 | &[ | |
| 772 | ("core", "skipped"), | |
| 773 | ("edge", "skipped"), | |
| 774 | ("front", "skipped") | |
| 775 | ], | |
| 776 | ¬hing, | |
| 777 | push.clone(), | |
| 778 | false | |
| 779 | )); | |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 780 | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 781 | // A dry run plans and stops. |
| 782 | let dry = json!({ "dry_run": true, "units": "", "all": false }); | |
| 783 | assert!(!starts(&deploy, "migrate", &[], &all, dry.clone(), false)); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 784 | assert!(!starts( |
| 785 | &deploy, | |
| 786 | "core", | |
| 787 | &[("migrate", "skipped")], | |
| 788 | &all, | |
| 789 | dry.clone(), | |
| 790 | false | |
| 791 | )); | |
| 792 | assert!(!starts( | |
| 793 | &deploy, | |
| 794 | "smoke", | |
| 795 | &[ | |
| 796 | ("core", "skipped"), | |
| 797 | ("edge", "skipped"), | |
| 798 | ("front", "skipped") | |
| 799 | ], | |
| 800 | &all, | |
| 801 | dry, | |
| 802 | false | |
| 803 | )); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 804 | } |
| 805 | ||
| 806 | #[test] | |
| 807 | fn deploy_stage_matrices_come_from_the_plan() { | |
| 808 | let deploy = read("deploy.yml"); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 809 | let outputs = plan_outputs( |
| 810 | false, | |
| 811 | &[ | |
| 812 | ("rust-1", "events,work", true), | |
| 813 | ("rust-2", "repos", true), | |
| 814 | ("ts", "projects,og", false), | |
| 815 | ], | |
| 816 | &[], | |
| 817 | &[], | |
| 818 | ); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 819 | let core = deploy.jobs.iter().find(|j| j.id == "core").unwrap(); |
| 820 | assert!(!core.fail_fast); | |
| 821 | assert_eq!(core.max_parallel, Some(4)); | |
| 822 | let mut contexts = Map::new(); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 823 | contexts.insert( |
| 824 | "needs".into(), | |
| 825 | json!({ "plan": { "result": "success", "outputs": outputs } }), | |
| 826 | ); | |
| 827 | let scope = Scope { | |
| 828 | contexts: &contexts, | |
| 829 | status: Status::Success, | |
| 830 | hash_files: None, | |
| 831 | }; | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 832 | let value = expr::interpolate_value(core.matrix.as_ref().unwrap(), &scope).unwrap(); |
| 833 | let jobs = matrix::expand(&value).unwrap(); | |
| 834 | let groups: Vec<(&str, &str, bool)> = jobs | |
| 835 | .iter() | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 836 | .map(|c| { |
| 837 | ( | |
| 838 | c["group"].as_str().unwrap(), | |
| 839 | c["units"].as_str().unwrap(), | |
| 840 | c["rust"].as_bool().unwrap(), | |
| 841 | ) | |
| 842 | }) | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 843 | .collect(); |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 844 | assert_eq!( |
| 845 | groups, | |
| 846 | [ | |
| 847 | ("rust-1", "events,work", true), | |
| 848 | ("rust-2", "repos", true), | |
| 849 | ("ts", "projects,og", false) | |
| 850 | ] | |
| 851 | ); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 852 | } |
| 853 | ||
| 854 | #[test] | |
| 855 | fn deploy_builds_rust_on_the_larger_machine_with_its_target_cached() { | |
| 856 | let deploy = read("deploy.yml"); | |
| 857 | for stage in ["core", "edge", "front"] { | |
| 858 | let job = deploy.jobs.iter().find(|j| j.id == stage).unwrap(); | |
| Merge branch 'main' into actions-toolkit-oidc-artifacts | 859 | let on = |rust: bool, image: bool| { |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 860 | let mut contexts = Map::new(); |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 861 | contexts.insert( |
| 862 | "matrix".into(), | |
| 863 | json!({ "group": "g", "units": "u", "rust": rust, "image": image }), | |
| 864 | ); | |
| 865 | let scope = Scope { | |
| 866 | contexts: &contexts, | |
| 867 | status: Status::Success, | |
| 868 | hash_files: None, | |
| 869 | }; | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 870 | expr::interpolate_value(&job.runs_on, &scope).unwrap() |
| 871 | }; | |
| Merge branch 'main' into actions-toolkit-oidc-artifacts | 872 | assert_eq!(on(true, false), json!("g1t-4core"), "{stage}"); |
| 873 | // The runner's image is built with the job's own Docker Engine. | |
| 874 | assert_eq!(on(false, true), json!("g1t-4core"), "{stage}"); | |
| 875 | assert_eq!(on(false, false), json!("ubuntu-latest"), "{stage}"); | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 876 | } |
| 877 | let source = std::fs::read_to_string(workflows_dir().join("deploy.yml")).unwrap(); | |
| 878 | assert!(source.contains("target/wasm32-unknown-unknown/release")); | |
| 879 | assert!(source.contains("!target/**/incremental")); | |
| Merge branch 'main' into actions-toolkit-oidc-artifacts | 880 | assert!(source.contains("target/x86_64-unknown-linux-musl/release")); |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 881 | } |
| 882 | ||
| Merge remote-tracking branch 'origin/main' into workspace-chat | 883 | /// Whether a step runs, for a job going `status`, with `matrix`. |
| 884 | fn step_runs(step: &workflow::Step, matrix: Value, status: Status) -> bool { | |
| 885 | let mut contexts = Map::new(); | |
| 886 | contexts.insert("matrix".into(), matrix); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 887 | let scope = Scope { |
| 888 | contexts: &contexts, | |
| 889 | status, | |
| 890 | hash_files: None, | |
| 891 | }; | |
| Merge remote-tracking branch 'origin/main' into workspace-chat | 892 | expr::condition(step.condition.as_deref().unwrap_or_default(), &scope).unwrap() |
| 893 | } | |
| 894 | ||
| 895 | #[test] | |
| 896 | fn rust_builds_go_through_sccache_before_cargo_and_report_after() { | |
| 897 | let step = |job: &workflow::Job, run: &str| -> (usize, workflow::Step) { | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 898 | let at = job |
| 899 | .steps | |
| 900 | .iter() | |
| 901 | .position(|s| s.run.as_deref() == Some(run)) | |
| 902 | .unwrap_or_else(|| panic!("{}: no step runs {run}", job.id)); | |
| Merge remote-tracking branch 'origin/main' into workspace-chat | 903 | (at, job.steps[at].clone()) |
| 904 | }; | |
| 905 | let deploy = read("deploy.yml"); | |
| 906 | for stage in ["core", "edge", "front"] { | |
| 907 | let job = deploy.jobs.iter().find(|j| j.id == stage).unwrap(); | |
| 908 | let (install_at, install) = step(job, "bash scripts/sccache.sh install"); | |
| 909 | let (stats_at, stats) = step(job, "bash scripts/sccache.sh stats"); | |
| 910 | // Before anything runs Cargo (worker-build's install, the build), | |
| 911 | // and the statistics last. | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 912 | let install_step = job |
| 913 | .steps | |
| 914 | .iter() | |
| 915 | .position(|s| s.name.as_deref() == Some("Install")) | |
| 916 | .unwrap(); | |
| Merge remote-tracking branch 'origin/main' into workspace-chat | 917 | assert!(install_at < install_step, "{stage}"); |
| 918 | assert_eq!(stats_at, job.steps.len() - 1, "{stage}"); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 919 | for (rust, image, uses) in [ |
| 920 | (true, false, true), | |
| 921 | (false, true, true), | |
| 922 | (false, false, false), | |
| 923 | ] { | |
| Merge remote-tracking branch 'origin/main' into workspace-chat | 924 | let matrix = json!({ "group": "g", "units": "u", "rust": rust, "image": image }); |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 925 | assert_eq!( |
| 926 | step_runs(&install, matrix.clone(), Status::Success), | |
| 927 | uses, | |
| 928 | "{stage} rust={rust} image={image}" | |
| 929 | ); | |
| 930 | assert_eq!( | |
| 931 | step_runs(&stats, matrix.clone(), Status::Success), | |
| 932 | uses, | |
| 933 | "{stage}" | |
| 934 | ); | |
| Merge remote-tracking branch 'origin/main' into workspace-chat | 935 | // A failed build still says what was cached. |
| 936 | assert_eq!(step_runs(&stats, matrix, Status::Failure), uses, "{stage}"); | |
| 937 | } | |
| 938 | } | |
| 939 | let ci = read("ci.yml"); | |
| 940 | let rust = ci.jobs.iter().find(|j| j.id == "rust").unwrap(); | |
| 941 | let (install_at, _) = step(rust, "bash scripts/sccache.sh install"); | |
| 942 | let (tests_at, _) = step(rust, "cargo test --workspace --locked --quiet"); | |
| 943 | let (stats_at, stats) = step(rust, "bash scripts/sccache.sh stats"); | |
| 944 | assert!(install_at < tests_at && tests_at < stats_at); | |
| 945 | assert!(step_runs(&stats, json!({}), Status::Failure)); | |
| 946 | // main's runs keep the caches pull requests restore from: only Rust. | |
| 947 | assert!(ci.trigger("push").unwrap().branches.allows("main")); | |
| 948 | assert!(ci.trigger("pull_request").is_some()); | |
| 949 | for (id, on_push) in [("rust", true), ("typescript", false), ("build", false)] { | |
| 950 | let job = ci.jobs.iter().find(|j| j.id == id).unwrap(); | |
| 951 | for (event, expected) in [("push", on_push), ("pull_request", true)] { | |
| 952 | let mut contexts = Map::new(); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 953 | contexts.insert( |
| 954 | "github".into(), | |
| 955 | json!({ "event_name": event, "ref": "refs/heads/main" }), | |
| 956 | ); | |
| 957 | let scope = Scope { | |
| 958 | contexts: &contexts, | |
| 959 | status: Status::Success, | |
| 960 | hash_files: None, | |
| 961 | }; | |
| 962 | assert_eq!( | |
| 963 | expr::condition(job.condition.as_deref().unwrap_or_default(), &scope).unwrap(), | |
| 964 | expected, | |
| 965 | "{id} on {event}" | |
| 966 | ); | |
| Merge remote-tracking branch 'origin/main' into workspace-chat | 967 | } |
| 968 | } | |
| 969 | // The download is pinned by version and checksum. | |
| 970 | let script = std::fs::read_to_string(workflows_dir().join("../../scripts/sccache.sh")).unwrap(); | |
| 971 | assert!(script.contains("VERSION=0.18.0")); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 972 | assert!(script.lines().any(|line| { |
| 973 | line.strip_prefix("SHA256=") | |
| 974 | .is_some_and(|sum| sum.len() == 64) | |
| 975 | })); | |
| Merge remote-tracking branch 'origin/main' into workspace-chat | 976 | assert!(script.contains("sha256sum -c")); |
| 977 | assert!(script.contains("RUSTC_WRAPPER=")); | |
| 978 | assert!(script.contains("GITHUB_STEP_SUMMARY")); | |
| 979 | } | |
| 980 | ||
| Merge branch 'worktree-agent-a6a121745e81f639f' | 981 | #[test] |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 982 | fn g1ts_own_actions_are_published_when_they_change() { |
| 983 | let release = read("actions-release.yml"); | |
| 984 | let push = release.trigger("push").unwrap(); | |
| 985 | assert!(push.branches.allows("main")); | |
| 986 | assert!(!push.branches.allows("feature")); | |
| 987 | assert!(release.trigger("workflow_dispatch").is_some()); | |
| 988 | let source = std::fs::read_to_string(workflows_dir().join("actions-release.yml")).unwrap(); | |
| 989 | assert!(source.contains("actions/**")); | |
| The Actions release reads its token from ACTIONS_PUBLISH_TOKEN. It asked for G1T_ACTIONS_PUBLISH_TOKEN, a name g1t keeps for its own and refuses when a secret is saved, so the secret it needed could never be set. A test now fails any of g1t's workflows that reads a secret named G1T_ or GITHUB_ other than the job's own token. | 990 | assert!(source.contains("secrets.ACTIONS_PUBLISH_TOKEN")); |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 991 | // Every action is a composite one with its README, and the copies of |
| 992 | // the script that finds g1t-runner are the same file. | |
| 993 | let actions = workflows_dir().join("../../actions"); | |
| 994 | let mut finders = Vec::new(); | |
| 995 | for entry in std::fs::read_dir(&actions).unwrap() { | |
| 996 | let dir = entry.unwrap().path(); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 997 | let manifest = std::fs::read_to_string(dir.join("action.yml")) |
| 998 | .unwrap_or_else(|_| panic!("{} has no action.yml", dir.display())); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 999 | let manifest: serde_yaml::Value = serde_yaml::from_str(&manifest).unwrap(); |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 1000 | assert_eq!( |
| 1001 | manifest["runs"]["using"].as_str(), | |
| 1002 | Some("composite"), | |
| 1003 | "{}", | |
| 1004 | dir.display() | |
| 1005 | ); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1006 | assert!(dir.join("README.md").exists(), "{}", dir.display()); |
| 1007 | finders.push(std::fs::read_to_string(dir.join("runner.sh")).unwrap()); | |
| 1008 | } | |
| 1009 | assert!(finders.len() >= 2); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 1010 | assert!( |
| 1011 | finders.windows(2).all(|pair| pair[0] == pair[1]), | |
| 1012 | "actions/*/runner.sh differ" | |
| 1013 | ); | |
| 1014 | let deploy: serde_yaml::Value = | |
| 1015 | serde_yaml::from_str(&std::fs::read_to_string(actions.join("deploy/action.yml")).unwrap()) | |
| 1016 | .unwrap(); | |
| 1017 | assert_eq!( | |
| 1018 | deploy["inputs"]["token"]["default"].as_str(), | |
| 1019 | Some("${{ github.token }}") | |
| 1020 | ); | |
| Merge branch 'worktree-agent-a4d35ee70c50683d0' into claude/cool-newton-7nm8r9 | 1021 | for output in ["url", "deployment-id"] { |
| 1022 | assert!(deploy["outputs"][output]["value"].is_string(), "{output}"); | |
| 1023 | } | |
| 1024 | } | |
| 1025 | ||
| 1026 | #[test] | |
| Merge branch 'worktree-agent-a6a121745e81f639f' | 1027 | fn the_runner_base_rebuilds_weekly_on_a_machine_with_docker() { |
| 1028 | let base = read("runner-base.yml"); | |
| 1029 | assert!(base.trigger("schedule").is_some()); | |
| 1030 | assert!(base.trigger("workflow_dispatch").is_some()); | |
| 1031 | assert!(base.trigger("push").unwrap().branches.allows("main")); | |
| 1032 | let job = base.jobs.iter().find(|j| j.id == "build").unwrap(); | |
| 1033 | assert_eq!(job.runs_on, json!(["self-hosted", "docker"])); | |
| 1034 | } | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' | 1035 | |
| 1036 | #[test] | |
| 1037 | 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. | 1038 | let root = workflows_dir() |
| 1039 | .parent() | |
| 1040 | .unwrap() | |
| 1041 | .parent() | |
| 1042 | .unwrap() | |
| 1043 | .to_path_buf(); | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' | 1044 | let ours = std::fs::read_to_string(root.join(".g1t/workflows/conformance.yml")).unwrap(); |
| 1045 | let theirs = std::fs::read_to_string(root.join(".github/workflows/conformance.yml")).unwrap(); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 1046 | assert_eq!( |
| 1047 | ours, theirs, | |
| 1048 | "copy .g1t/workflows/conformance.yml over .github/workflows/conformance.yml" | |
| 1049 | ); | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' | 1050 | // It runs only by hand, and reads nothing it could change. |
| 1051 | let workflow = read("conformance.yml"); | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 1052 | assert_eq!( |
| 1053 | workflow | |
| 1054 | .triggers | |
| 1055 | .iter() | |
| 1056 | .map(|t| t.event.as_str()) | |
| 1057 | .collect::<Vec<_>>(), | |
| 1058 | ["workflow_dispatch"] | |
| 1059 | ); | |
| Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' | 1060 | assert!(!ours.contains("secrets.")); |
| 1061 | } | |
| The Actions release reads its token from ACTIONS_PUBLISH_TOKEN. It asked for G1T_ACTIONS_PUBLISH_TOKEN, a name g1t keeps for its own and refuses when a secret is saved, so the secret it needed could never be set. A test now fails any of g1t's workflows that reads a secret named G1T_ or GITHUB_ other than the job's own token. | 1062 | |
| 1063 | /// No workflow of g1t's asks for a secret no one can set. Secret names | |
| 1064 | /// starting with G1T_ or GITHUB_ are g1t's own and refused when saved, so a | |
| 1065 | /// workflow reading one gets nothing: the Actions release asked for | |
| 1066 | /// G1T_ACTIONS_PUBLISH_TOKEN, which could never exist. G1T_TOKEN, the job's | |
| 1067 | /// own token, is the one g1t gives every job. | |
| 1068 | #[test] | |
| 1069 | fn workflows_read_only_secrets_that_can_be_set() { | |
| 1070 | for entry in std::fs::read_dir(workflows_dir()).unwrap() { | |
| 1071 | let path = entry.unwrap().path(); | |
| 1072 | if path.extension().is_none_or(|ext| ext != "yml") { | |
| 1073 | continue; | |
| 1074 | } | |
| 1075 | let source = std::fs::read_to_string(&path).unwrap(); | |
| 1076 | for (at, _) in source.match_indices("secrets.") { | |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 1077 | let name: String = source[at + "secrets.".len()..] |
| 1078 | .chars() | |
| 1079 | .take_while(|c| c.is_ascii_alphanumeric() || *c == '_') | |
| 1080 | .collect(); | |
| The Actions release reads its token from ACTIONS_PUBLISH_TOKEN. It asked for G1T_ACTIONS_PUBLISH_TOKEN, a name g1t keeps for its own and refuses when a secret is saved, so the secret it needed could never be set. A test now fails any of g1t's workflows that reads a secret named G1T_ or GITHUB_ other than the job's own token. | 1081 | let reserved = name.starts_with("G1T_") || name.starts_with("GITHUB_"); |
| Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it. | 1082 | assert!( |
| 1083 | !reserved || name == "G1T_TOKEN" || name == "GITHUB_TOKEN", | |
| 1084 | "{} reads secrets.{name}, a name g1t keeps for its own", | |
| 1085 | path.display() | |
| 1086 | ); | |
| 1087 | } | |
| 1088 | } | |
| 1089 | } | |
| 1090 | ||
| 1091 | #[test] | |
| 1092 | fn every_install_runs_again_when_a_connection_drops() { | |
| 1093 | // npm does not retry a body that breaks off midway; npm-ci.mjs does. | |
| 1094 | for entry in std::fs::read_dir(workflows_dir()).unwrap() { | |
| 1095 | let path = entry.unwrap().path(); | |
| 1096 | if path.extension().and_then(|e| e.to_str()) != Some("yml") { | |
| 1097 | continue; | |
| 1098 | } | |
| 1099 | let name = path.file_name().unwrap().to_string_lossy().to_string(); | |
| 1100 | for job in read(&name).jobs { | |
| 1101 | for step in &job.steps { | |
| 1102 | let Some(run) = step.run.as_deref() else { | |
| 1103 | continue; | |
| 1104 | }; | |
| 1105 | for line in run.lines() { | |
| 1106 | assert!( | |
| 1107 | !line.trim().starts_with("npm ci"), | |
| 1108 | "{name} {}: `{}` should be `node scripts/deploy/npm-ci.mjs …`", | |
| 1109 | job.id, | |
| 1110 | line.trim() | |
| 1111 | ); | |
| 1112 | } | |
| 1113 | } | |
| The Actions release reads its token from ACTIONS_PUBLISH_TOKEN. It asked for G1T_ACTIONS_PUBLISH_TOKEN, a name g1t keeps for its own and refuses when a secret is saved, so the secret it needed could never be set. A test now fails any of g1t's workflows that reads a secret named G1T_ or GITHUB_ other than the job's own token. | 1114 | } |
| 1115 | } | |
| 1116 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.