Skip to content
339 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow1//! 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 workflow5
6use std::path::PathBuf;
7
8use g1t_actions::expr::{self, Scope, Status};
9use g1t_actions::matrix;
10use g1t_actions::workflow::{self, Severity, Workflow};
11use serde_json::{Map, Value, json};
12
13fn workflows_dir() -> PathBuf {
14 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.g1t/workflows")
15}
16
17fn read(name: &str) -> Workflow {
18 let source = std::fs::read_to_string(workflows_dir().join(name)).unwrap();
19 workflow::parse(&source).unwrap_or_else(|problem| panic!("{name}: {problem}"))
20}
21
22#[test]
Merge branch 'worktree-agent-a3abfcce648e87dca'23fn every_workflow_asks_for_only_what_its_token_does() {
24 use g1t_actions::permissions::{Access, TokenDefault};
25 let job = |name: &str, id: &str| {
26 let workflow = read(name);
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 workflow48fn 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).
65fn 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 incidents87/// 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 workflow90fn 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 incidents91 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).
96fn 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 workflow97 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 incidents99 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 workflow100 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 incidents102 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 workflow103 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 incidents106 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 workflow107 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]
116fn 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.sh128 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 workflow129 // 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]
139fn 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 check165 // 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 incidents178 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 check179 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 workflow183
Deploy ends with a smoke test: sign-in, sign-up and the waitlist still work on g1t.sh184 // 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")], &nothing, 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 workflow192 // 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.sh195 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 workflow197}
198
199#[test]
200fn 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 incidents217
218#[test]
219fn 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-artifacts223 let on = |rust: bool, image: bool| {
Fast pages, required checks on the branch, self-hosted runners, honest incidents224 let mut contexts = Map::new();
Merge branch 'main' into actions-toolkit-oidc-artifacts225 contexts.insert("matrix".into(), json!({ "group": "g", "units": "u", "rust": rust, "image": image }));
Fast pages, required checks on the branch, self-hosted runners, honest incidents226 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-artifacts229 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 incidents233 }
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-artifacts237 assert!(source.contains("target/x86_64-unknown-linux-musl/release"));
Fast pages, required checks on the branch, self-hosted runners, honest incidents238}
239
Merge remote-tracking branch 'origin/main' into workspace-chat240/// Whether a step runs, for a job going `status`, with `matrix`.
241fn 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]
249fn 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 incidents300#[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.301fn 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 incidents332fn 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.