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.
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 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. |
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 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); | |
| 27 | let job = workflow.jobs.iter().find(|job| job.id == id).unwrap().clone(); | |
| 28 | job.permissions(&workflow, TokenDefault::Restricted) | |
| 29 | }; | |
| 30 | // CI and Deploy only read: nothing they do writes with the token. | |
| Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date. | 31 | // The actions release pushes with its own secret, never the job's token. |
| 32 | for (name, id) in [("ci.yml", "rust"), ("deploy.yml", "core"), ("runner-release.yml", "binaries"), ("actions-release.yml", "publish")] { | |
| Merge branch 'worktree-agent-a3abfcce648e87dca' | 33 | let permissions = job(name, id); |
| 34 | assert!(permissions.listed().iter().all(|(_, access)| *access <= Access::Read), "{name} {id}"); | |
| 35 | } | |
| 36 | // The runner base pushes a branch and opens a pull request. | |
| 37 | let base = job("runner-base.yml", "build"); | |
| 38 | assert_eq!(base.get("contents"), Access::Write); | |
| 39 | assert_eq!(base.get("pull-requests"), Access::Write); | |
| 40 | assert_eq!(base.get("packages"), Access::None); | |
| 41 | // The runner's image goes to g1t's registry. | |
| 42 | let image = job("runner-release.yml", "image"); | |
| 43 | assert_eq!(image.get("packages"), Access::Write); | |
| 44 | assert_eq!(image.get("contents"), Access::Read); | |
| 45 | } | |
| 46 | ||
| 47 | #[test] | |
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 48 | fn every_workflow_reads_and_runs_on_g1t() { |
| 49 | let mut count = 0; | |
| 50 | for entry in std::fs::read_dir(workflows_dir()).unwrap() { | |
| 51 | let path = entry.unwrap().path(); | |
| 52 | if path.extension().and_then(|e| e.to_str()) != Some("yml") { | |
| 53 | continue; | |
| 54 | } | |
| 55 | let name = path.file_name().unwrap().to_string_lossy().to_string(); | |
| 56 | let workflow = read(&name); | |
| 57 | let unsupported: Vec<_> = workflow.notes.iter().filter(|n| n.severity != Severity::Info).collect(); | |
| 58 | assert!(unsupported.is_empty(), "{name}: {unsupported:?}"); | |
| 59 | count += 1; | |
| 60 | } | |
| 61 | assert!(count >= 1); | |
| 62 | } | |
| 63 | ||
| 64 | /// The plan job's outputs for a deploy of `stages` (each with its jobs). | |
| 65 | fn plan_outputs(migrate: bool, core: &[(&str, &str, bool)], edge: &[(&str, &str, bool)], front: &[(&str, &str, bool)]) -> Value { | |
| 66 | let matrix = |jobs: &[(&str, &str, bool)]| { | |
| 67 | let include: Vec<Value> = if jobs.is_empty() { | |
| 68 | vec![json!({ "group": "none", "units": "" })] | |
| 69 | } else { | |
| 70 | jobs.iter().map(|(group, units, rust)| json!({ "group": group, "units": units, "rust": rust })).collect() | |
| 71 | }; | |
| 72 | json!({ "include": include }).to_string() | |
| 73 | }; | |
| 74 | json!({ | |
| 75 | "migrate": migrate.to_string(), | |
| 76 | "migrate_units": if migrate { "events" } else { "" }, | |
| 77 | "has_core": (!core.is_empty()).to_string(), | |
| 78 | "core": matrix(core), | |
| 79 | "has_edge": (!edge.is_empty()).to_string(), | |
| 80 | "edge": matrix(edge), | |
| 81 | "has_front": (!front.is_empty()).to_string(), | |
| 82 | "front": matrix(front), | |
| 83 | }) | |
| 84 | } | |
| 85 | ||
| 86 | /// Whether `job` starts, given its needs' results, as the actions service | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 87 | /// decides it (services/actions/src/plan.rs `decide`, with |
| 88 | /// `expr::job_status`): a need that was skipped is not a failure, and a | |
| 89 | /// failure anywhere before the job is. | |
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 90 | fn starts(workflow: &Workflow, job: &str, needs: &[(&str, &str)], outputs: &Value, inputs: Value, cancelled: bool) -> bool { |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 91 | starts_after(workflow, job, needs, outputs, inputs, cancelled, false) |
| 92 | } | |
| 93 | ||
| 94 | /// `starts`, where `failed_before` says a job further back failed (one the | |
| 95 | /// needs were skipped for). | |
| 96 | fn starts_after(workflow: &Workflow, job: &str, needs: &[(&str, &str)], outputs: &Value, inputs: Value, cancelled: bool, failed_before: bool) -> bool { | |
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 97 | let job = workflow.jobs.iter().find(|j| j.id == job).unwrap(); |
| 98 | let mut needs_ctx = Map::new(); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 99 | let mut results = Vec::new(); |
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 100 | for need in &job.needs { |
| 101 | let result = needs.iter().find(|(name, _)| name == need).map(|(_, r)| *r).unwrap_or("success"); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 102 | results.push(result); |
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 103 | let outputs = if need == "plan" { outputs.clone() } else { json!({}) }; |
| 104 | needs_ctx.insert(need.clone(), json!({ "result": result, "outputs": outputs })); | |
| 105 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 106 | let status = expr::job_status(results, failed_before, cancelled); |
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 107 | let mut contexts = Map::new(); |
| 108 | contexts.insert("needs".into(), Value::Object(needs_ctx)); | |
| 109 | contexts.insert("inputs".into(), inputs); | |
| 110 | contexts.insert("github".into(), json!({ "event_name": "push", "ref": "refs/heads/main" })); | |
| 111 | let scope = Scope { contexts: &contexts, status, hash_files: None }; | |
| 112 | expr::condition(job.condition.as_deref().unwrap_or_default(), &scope).unwrap() | |
| 113 | } | |
| 114 | ||
| 115 | #[test] | |
| 116 | fn deploy_runs_on_main_and_by_hand_one_at_a_time() { | |
| 117 | let deploy = read("deploy.yml"); | |
| 118 | let push = deploy.trigger("push").unwrap(); | |
| 119 | assert!(push.branches.allows("main")); | |
| 120 | assert!(!push.branches.allows("feature")); | |
| 121 | let dispatch = deploy.trigger("workflow_dispatch").unwrap(); | |
| 122 | for input in ["units", "all", "dry_run"] { | |
| 123 | assert!(dispatch.inputs.contains_key(input), "{input}"); | |
| 124 | } | |
| 125 | let concurrency = deploy.concurrency.as_ref().unwrap(); | |
| 126 | assert_eq!(concurrency.group, "deploy-production"); | |
| 127 | assert_eq!(concurrency.cancel_in_progress, json!(false)); | |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 128 | assert_eq!(deploy.job_order(), ["check", "plan", "migrate", "core", "edge", "front", "smoke"]); |
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 129 | // The stages share their steps (a YAML alias), and read the token only |
| 130 | // where they deploy. | |
| 131 | let steps = |id: &str| deploy.jobs.iter().find(|j| j.id == id).unwrap().steps.len(); | |
| 132 | assert_eq!(steps("core"), steps("edge")); | |
| 133 | assert_eq!(steps("core"), steps("front")); | |
| 134 | let source = std::fs::read_to_string(workflows_dir().join("deploy.yml")).unwrap(); | |
| 135 | assert!(!source.contains("cancel-in-progress: true")); | |
| 136 | } | |
| 137 | ||
| 138 | #[test] | |
| 139 | fn deploy_stages_follow_one_another() { | |
| 140 | let deploy = read("deploy.yml"); | |
| 141 | let all = plan_outputs(true, &[("rust", "events,repos", true), ("ts", "projects", false)], &[("rust", "api", true)], &[("web", "web", false)]); | |
| 142 | let push = json!({}); | |
| 143 | ||
| 144 | // Everything succeeds: each stage runs after the last. | |
| 145 | assert!(starts(&deploy, "migrate", &[], &all, push.clone(), false)); | |
| 146 | assert!(starts(&deploy, "core", &[("migrate", "success")], &all, push.clone(), false)); | |
| 147 | assert!(starts(&deploy, "edge", &[("migrate", "success"), ("core", "success")], &all, push.clone(), false)); | |
| 148 | assert!(starts(&deploy, "front", &[("migrate", "success"), ("core", "success"), ("edge", "success")], &all, push.clone(), false)); | |
| 149 | ||
| 150 | // No migrations: the migrate job is skipped, and core still runs. | |
| 151 | let none = plan_outputs(false, &[("ts", "projects", false)], &[], &[("web", "web", false)]); | |
| 152 | assert!(!starts(&deploy, "migrate", &[], &none, push.clone(), false)); | |
| 153 | assert!(starts(&deploy, "core", &[("migrate", "skipped")], &none, push.clone(), false)); | |
| 154 | // An empty stage is skipped, and the next one still runs. | |
| 155 | assert!(!starts(&deploy, "edge", &[("migrate", "skipped"), ("core", "success")], &none, push.clone(), false)); | |
| 156 | assert!(starts(&deploy, "front", &[("migrate", "skipped"), ("core", "success"), ("edge", "skipped")], &none, push.clone(), false)); | |
| 157 | ||
| 158 | // A failure stops every later stage. | |
| 159 | assert!(!starts(&deploy, "core", &[("migrate", "failure")], &all, push.clone(), false)); | |
| 160 | assert!(!starts(&deploy, "edge", &[("migrate", "success"), ("core", "failure")], &all, push.clone(), false)); | |
| 161 | assert!(!starts(&deploy, "front", &[("migrate", "success"), ("core", "success"), ("edge", "failure")], &all, push.clone(), false)); | |
| 162 | assert!(!starts(&deploy, "front", &[("migrate", "success"), ("core", "failure"), ("edge", "skipped")], &all, push.clone(), false)); | |
| 163 | // A cancelled run starts nothing more. | |
| 164 | assert!(!starts(&deploy, "edge", &[("migrate", "success"), ("core", "success")], &all, push.clone(), true)); | |
| 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 | 165 | // Check and plan run side by side: plan waits for nothing. |
| 166 | let plan = deploy.jobs.iter().find(|j| j.id == "plan").unwrap(); | |
| 167 | assert!(plan.needs.is_empty(), "{:?}", plan.needs); | |
| 168 | for id in ["migrate", "core", "edge", "front", "smoke"] { | |
| 169 | let job = deploy.jobs.iter().find(|j| j.id == id).unwrap(); | |
| 170 | assert!(job.needs.iter().any(|n| n == "check") && job.needs.iter().any(|n| n == "plan"), "{id}: {:?}", job.needs); | |
| 171 | } | |
| 172 | // A failed check, though plan succeeded: nothing migrates or deploys, | |
| 173 | // and failure() still sees the check's failure through skipped jobs. | |
| 174 | assert!(!starts(&deploy, "migrate", &[("check", "failure"), ("plan", "success")], &all, push.clone(), false)); | |
| 175 | assert!(!starts(&deploy, "core", &[("check", "failure"), ("plan", "success"), ("migrate", "skipped")], &all, push.clone(), false)); | |
| 176 | assert!(!starts(&deploy, "front", &[("check", "failure"), ("plan", "success"), ("migrate", "skipped"), ("core", "skipped"), ("edge", "skipped")], &all, push.clone(), false)); | |
| 177 | let skipped = [("migrate", "skipped"), ("core", "skipped"), ("edge", "skipped")]; | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 178 | assert!(!starts_after(&deploy, "front", &skipped, &all, push.clone(), false, true)); |
| 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 | 179 | assert!(!starts(&deploy, "smoke", &[("check", "failure"), ("core", "skipped"), ("edge", "skipped"), ("front", "skipped")], &all, push.clone(), false)); |
| 180 | // A failed plan stops everything too. | |
| 181 | assert!(!starts(&deploy, "migrate", &[("plan", "failure")], &all, push.clone(), false)); | |
| 182 | assert!(!starts(&deploy, "core", &[("plan", "failure"), ("migrate", "skipped")], &all, push.clone(), false)); | |
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 183 | |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 184 | // Smoke follows the last stage that ran, and not a failed one. |
| 185 | assert!(starts(&deploy, "smoke", &[("core", "success"), ("edge", "success"), ("front", "success")], &all, push.clone(), false)); | |
| 186 | assert!(starts(&deploy, "smoke", &[("core", "success"), ("edge", "skipped"), ("front", "success")], &none, push.clone(), false)); | |
| 187 | assert!(!starts(&deploy, "smoke", &[("core", "success"), ("edge", "failure"), ("front", "skipped")], &all, push.clone(), false)); | |
| 188 | // Nothing deployed: nothing to smoke-test. | |
| 189 | let nothing = plan_outputs(false, &[], &[], &[]); | |
| 190 | assert!(!starts(&deploy, "smoke", &[("core", "skipped"), ("edge", "skipped"), ("front", "skipped")], ¬hing, push.clone(), false)); | |
| 191 | ||
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 192 | // A dry run plans and stops. |
| 193 | let dry = json!({ "dry_run": true, "units": "", "all": false }); | |
| 194 | assert!(!starts(&deploy, "migrate", &[], &all, dry.clone(), false)); | |
| Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh | 195 | assert!(!starts(&deploy, "core", &[("migrate", "skipped")], &all, dry.clone(), false)); |
| 196 | assert!(!starts(&deploy, "smoke", &[("core", "skipped"), ("edge", "skipped"), ("front", "skipped")], &all, dry, false)); | |
| Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow | 197 | } |
| 198 | ||
| 199 | #[test] | |
| 200 | fn deploy_stage_matrices_come_from_the_plan() { | |
| 201 | let deploy = read("deploy.yml"); | |
| 202 | let outputs = plan_outputs(false, &[("rust-1", "events,work", true), ("rust-2", "repos", true), ("ts", "projects,og", false)], &[], &[]); | |
| 203 | let core = deploy.jobs.iter().find(|j| j.id == "core").unwrap(); | |
| 204 | assert!(!core.fail_fast); | |
| 205 | assert_eq!(core.max_parallel, Some(4)); | |
| 206 | let mut contexts = Map::new(); | |
| 207 | contexts.insert("needs".into(), json!({ "plan": { "result": "success", "outputs": outputs } })); | |
| 208 | let scope = Scope { contexts: &contexts, status: Status::Success, hash_files: None }; | |
| 209 | let value = expr::interpolate_value(core.matrix.as_ref().unwrap(), &scope).unwrap(); | |
| 210 | let jobs = matrix::expand(&value).unwrap(); | |
| 211 | let groups: Vec<(&str, &str, bool)> = jobs | |
| 212 | .iter() | |
| 213 | .map(|c| (c["group"].as_str().unwrap(), c["units"].as_str().unwrap(), c["rust"].as_bool().unwrap())) | |
| 214 | .collect(); | |
| 215 | assert_eq!(groups, [("rust-1", "events,work", true), ("rust-2", "repos", true), ("ts", "projects,og", false)]); | |
| 216 | } | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 217 | |
| 218 | #[test] | |
| 219 | fn deploy_builds_rust_on_the_larger_machine_with_its_target_cached() { | |
| 220 | let deploy = read("deploy.yml"); | |
| 221 | for stage in ["core", "edge", "front"] { | |
| 222 | let job = deploy.jobs.iter().find(|j| j.id == stage).unwrap(); | |
| Merge branch 'main' into actions-toolkit-oidc-artifacts | 223 | let on = |rust: bool, image: bool| { |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 224 | let mut contexts = Map::new(); |
| Merge branch 'main' into actions-toolkit-oidc-artifacts | 225 | contexts.insert("matrix".into(), json!({ "group": "g", "units": "u", "rust": rust, "image": image })); |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 226 | let scope = Scope { contexts: &contexts, status: Status::Success, hash_files: None }; |
| 227 | expr::interpolate_value(&job.runs_on, &scope).unwrap() | |
| 228 | }; | |
| Merge branch 'main' into actions-toolkit-oidc-artifacts | 229 | assert_eq!(on(true, false), json!("g1t-4core"), "{stage}"); |
| 230 | // The runner's image is built with the job's own Docker Engine. | |
| 231 | assert_eq!(on(false, true), json!("g1t-4core"), "{stage}"); | |
| 232 | assert_eq!(on(false, false), json!("ubuntu-latest"), "{stage}"); | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 233 | } |
| 234 | let source = std::fs::read_to_string(workflows_dir().join("deploy.yml")).unwrap(); | |
| 235 | assert!(source.contains("target/wasm32-unknown-unknown/release")); | |
| 236 | assert!(source.contains("!target/**/incremental")); | |
| Merge branch 'main' into actions-toolkit-oidc-artifacts | 237 | assert!(source.contains("target/x86_64-unknown-linux-musl/release")); |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 238 | } |
| 239 | ||
| Merge remote-tracking branch 'origin/main' into workspace-chat | 240 | /// Whether a step runs, for a job going `status`, with `matrix`. |
| 241 | fn step_runs(step: &workflow::Step, matrix: Value, status: Status) -> bool { | |
| 242 | let mut contexts = Map::new(); | |
| 243 | contexts.insert("matrix".into(), matrix); | |
| 244 | let scope = Scope { contexts: &contexts, status, hash_files: None }; | |
| 245 | expr::condition(step.condition.as_deref().unwrap_or_default(), &scope).unwrap() | |
| 246 | } | |
| 247 | ||
| 248 | #[test] | |
| 249 | fn rust_builds_go_through_sccache_before_cargo_and_report_after() { | |
| 250 | let step = |job: &workflow::Job, run: &str| -> (usize, workflow::Step) { | |
| 251 | let at = job.steps.iter().position(|s| s.run.as_deref() == Some(run)).unwrap_or_else(|| panic!("{}: no step runs {run}", job.id)); | |
| 252 | (at, job.steps[at].clone()) | |
| 253 | }; | |
| 254 | let deploy = read("deploy.yml"); | |
| 255 | for stage in ["core", "edge", "front"] { | |
| 256 | let job = deploy.jobs.iter().find(|j| j.id == stage).unwrap(); | |
| 257 | let (install_at, install) = step(job, "bash scripts/sccache.sh install"); | |
| 258 | let (stats_at, stats) = step(job, "bash scripts/sccache.sh stats"); | |
| 259 | // Before anything runs Cargo (worker-build's install, the build), | |
| 260 | // and the statistics last. | |
| 261 | let install_step = job.steps.iter().position(|s| s.name.as_deref() == Some("Install")).unwrap(); | |
| 262 | assert!(install_at < install_step, "{stage}"); | |
| 263 | assert_eq!(stats_at, job.steps.len() - 1, "{stage}"); | |
| 264 | for (rust, image, uses) in [(true, false, true), (false, true, true), (false, false, false)] { | |
| 265 | let matrix = json!({ "group": "g", "units": "u", "rust": rust, "image": image }); | |
| 266 | assert_eq!(step_runs(&install, matrix.clone(), Status::Success), uses, "{stage} rust={rust} image={image}"); | |
| 267 | assert_eq!(step_runs(&stats, matrix.clone(), Status::Success), uses, "{stage}"); | |
| 268 | // A failed build still says what was cached. | |
| 269 | assert_eq!(step_runs(&stats, matrix, Status::Failure), uses, "{stage}"); | |
| 270 | } | |
| 271 | } | |
| 272 | let ci = read("ci.yml"); | |
| 273 | let rust = ci.jobs.iter().find(|j| j.id == "rust").unwrap(); | |
| 274 | let (install_at, _) = step(rust, "bash scripts/sccache.sh install"); | |
| 275 | let (tests_at, _) = step(rust, "cargo test --workspace --locked --quiet"); | |
| 276 | let (stats_at, stats) = step(rust, "bash scripts/sccache.sh stats"); | |
| 277 | assert!(install_at < tests_at && tests_at < stats_at); | |
| 278 | assert!(step_runs(&stats, json!({}), Status::Failure)); | |
| 279 | // main's runs keep the caches pull requests restore from: only Rust. | |
| 280 | assert!(ci.trigger("push").unwrap().branches.allows("main")); | |
| 281 | assert!(ci.trigger("pull_request").is_some()); | |
| 282 | for (id, on_push) in [("rust", true), ("typescript", false), ("build", false)] { | |
| 283 | let job = ci.jobs.iter().find(|j| j.id == id).unwrap(); | |
| 284 | for (event, expected) in [("push", on_push), ("pull_request", true)] { | |
| 285 | let mut contexts = Map::new(); | |
| 286 | contexts.insert("github".into(), json!({ "event_name": event, "ref": "refs/heads/main" })); | |
| 287 | let scope = Scope { contexts: &contexts, status: Status::Success, hash_files: None }; | |
| 288 | assert_eq!(expr::condition(job.condition.as_deref().unwrap_or_default(), &scope).unwrap(), expected, "{id} on {event}"); | |
| 289 | } | |
| 290 | } | |
| 291 | // The download is pinned by version and checksum. | |
| 292 | let script = std::fs::read_to_string(workflows_dir().join("../../scripts/sccache.sh")).unwrap(); | |
| 293 | assert!(script.contains("VERSION=0.18.0")); | |
| 294 | assert!(script.lines().any(|line| line.strip_prefix("SHA256=").is_some_and(|sum| sum.len() == 64))); | |
| 295 | assert!(script.contains("sha256sum -c")); | |
| 296 | assert!(script.contains("RUSTC_WRAPPER=")); | |
| 297 | assert!(script.contains("GITHUB_STEP_SUMMARY")); | |
| 298 | } | |
| 299 | ||
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 300 | #[test] |
| Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date. | 301 | fn g1ts_own_actions_are_published_when_they_change() { |
| 302 | let release = read("actions-release.yml"); | |
| 303 | let push = release.trigger("push").unwrap(); | |
| 304 | assert!(push.branches.allows("main")); | |
| 305 | assert!(!push.branches.allows("feature")); | |
| 306 | assert!(release.trigger("workflow_dispatch").is_some()); | |
| 307 | let source = std::fs::read_to_string(workflows_dir().join("actions-release.yml")).unwrap(); | |
| 308 | assert!(source.contains("actions/**")); | |
| 309 | assert!(source.contains("secrets.G1T_ACTIONS_PUBLISH_TOKEN")); | |
| 310 | // Every action is a composite one with its README, and the copies of | |
| 311 | // the script that finds g1t-runner are the same file. | |
| 312 | let actions = workflows_dir().join("../../actions"); | |
| 313 | let mut finders = Vec::new(); | |
| 314 | for entry in std::fs::read_dir(&actions).unwrap() { | |
| 315 | let dir = entry.unwrap().path(); | |
| 316 | let manifest = std::fs::read_to_string(dir.join("action.yml")).unwrap_or_else(|_| panic!("{} has no action.yml", dir.display())); | |
| 317 | let manifest: serde_yaml::Value = serde_yaml::from_str(&manifest).unwrap(); | |
| 318 | assert_eq!(manifest["runs"]["using"].as_str(), Some("composite"), "{}", dir.display()); | |
| 319 | assert!(dir.join("README.md").exists(), "{}", dir.display()); | |
| 320 | finders.push(std::fs::read_to_string(dir.join("runner.sh")).unwrap()); | |
| 321 | } | |
| 322 | assert!(finders.len() >= 2); | |
| 323 | assert!(finders.windows(2).all(|pair| pair[0] == pair[1]), "actions/*/runner.sh differ"); | |
| 324 | let deploy: serde_yaml::Value = serde_yaml::from_str(&std::fs::read_to_string(actions.join("deploy/action.yml")).unwrap()).unwrap(); | |
| 325 | assert_eq!(deploy["inputs"]["token"]["default"].as_str(), Some("${{ github.token }}")); | |
| 326 | for output in ["url", "deployment-id"] { | |
| 327 | assert!(deploy["outputs"][output]["value"].is_string(), "{output}"); | |
| 328 | } | |
| 329 | } | |
| 330 | ||
| 331 | #[test] | |
| Fast pages, required checks on the branch, self-hosted runners, honest incidents | 332 | fn the_runner_base_rebuilds_weekly_on_a_machine_with_docker() { |
| 333 | let base = read("runner-base.yml"); | |
| 334 | assert!(base.trigger("schedule").is_some()); | |
| 335 | assert!(base.trigger("workflow_dispatch").is_some()); | |
| 336 | assert!(base.trigger("push").unwrap().branches.allows("main")); | |
| 337 | let job = base.jobs.iter().find(|j| j.id == "build").unwrap(); | |
| 338 | assert_eq!(job.runs_on, json!(["self-hosted", "docker"])); | |
| 339 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.