flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/crates/actions/tests/repository_workflows.rs

199 lines10,348 bytesCodeBlame
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
4//! stop as docs/DEPLOYING.md says.
5
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]
23fn every_workflow_reads_and_runs_on_g1t() {
24 let mut count = 0;
25 for entry in std::fs::read_dir(workflows_dir()).unwrap() {
26 let path = entry.unwrap().path();
27 if path.extension().and_then(|e| e.to_str()) != Some("yml") {
28 continue;
29 }
30 let name = path.file_name().unwrap().to_string_lossy().to_string();
31 let workflow = read(&name);
32 let unsupported: Vec<_> = workflow.notes.iter().filter(|n| n.severity != Severity::Info).collect();
33 assert!(unsupported.is_empty(), "{name}: {unsupported:?}");
34 count += 1;
35 }
36 assert!(count >= 1);
37}
38
39/// The plan job's outputs for a deploy of `stages` (each with its jobs).
40fn plan_outputs(migrate: bool, core: &[(&str, &str, bool)], edge: &[(&str, &str, bool)], front: &[(&str, &str, bool)]) -> Value {
41 let matrix = |jobs: &[(&str, &str, bool)]| {
42 let include: Vec<Value> = if jobs.is_empty() {
43 vec![json!({ "group": "none", "units": "" })]
44 } else {
45 jobs.iter().map(|(group, units, rust)| json!({ "group": group, "units": units, "rust": rust })).collect()
46 };
47 json!({ "include": include }).to_string()
48 };
49 json!({
50 "migrate": migrate.to_string(),
51 "migrate_units": if migrate { "events" } else { "" },
52 "has_core": (!core.is_empty()).to_string(),
53 "core": matrix(core),
54 "has_edge": (!edge.is_empty()).to_string(),
55 "edge": matrix(edge),
56 "has_front": (!front.is_empty()).to_string(),
57 "front": matrix(front),
58 })
59}
60
61/// Whether `job` starts, given its needs' results, as the actions service
62/// decides it (services/actions/src/plan.rs `decide`, with
63/// `expr::job_status`): a need that was skipped is not a failure, and a
64/// failure anywhere before the job is.
65fn starts(workflow: &Workflow, job: &str, needs: &[(&str, &str)], outputs: &Value, inputs: Value, cancelled: bool) -> bool {
66 starts_after(workflow, job, needs, outputs, inputs, cancelled, false)
67}
68
69/// `starts`, where `failed_before` says a job further back failed (one the
70/// needs were skipped for).
71fn starts_after(workflow: &Workflow, job: &str, needs: &[(&str, &str)], outputs: &Value, inputs: Value, cancelled: bool, failed_before: bool) -> bool {
72 let job = workflow.jobs.iter().find(|j| j.id == job).unwrap();
73 let mut needs_ctx = Map::new();
74 let mut results = Vec::new();
75 for need in &job.needs {
76 let result = needs.iter().find(|(name, _)| name == need).map(|(_, r)| *r).unwrap_or("success");
77 results.push(result);
78 let outputs = if need == "plan" { outputs.clone() } else { json!({}) };
79 needs_ctx.insert(need.clone(), json!({ "result": result, "outputs": outputs }));
80 }
81 let status = expr::job_status(results, failed_before, cancelled);
82 let mut contexts = Map::new();
83 contexts.insert("needs".into(), Value::Object(needs_ctx));
84 contexts.insert("inputs".into(), inputs);
85 contexts.insert("github".into(), json!({ "event_name": "push", "ref": "refs/heads/main" }));
86 let scope = Scope { contexts: &contexts, status, hash_files: None };
87 expr::condition(job.condition.as_deref().unwrap_or_default(), &scope).unwrap()
88}
89
90#[test]
91fn deploy_runs_on_main_and_by_hand_one_at_a_time() {
92 let deploy = read("deploy.yml");
93 let push = deploy.trigger("push").unwrap();
94 assert!(push.branches.allows("main"));
95 assert!(!push.branches.allows("feature"));
96 let dispatch = deploy.trigger("workflow_dispatch").unwrap();
97 for input in ["units", "all", "dry_run"] {
98 assert!(dispatch.inputs.contains_key(input), "{input}");
99 }
100 let concurrency = deploy.concurrency.as_ref().unwrap();
101 assert_eq!(concurrency.group, "deploy-production");
102 assert_eq!(concurrency.cancel_in_progress, json!(false));
103 assert_eq!(deploy.job_order(), ["check", "plan", "migrate", "core", "edge", "front"]);
104 // The stages share their steps (a YAML alias), and read the token only
105 // where they deploy.
106 let steps = |id: &str| deploy.jobs.iter().find(|j| j.id == id).unwrap().steps.len();
107 assert_eq!(steps("core"), steps("edge"));
108 assert_eq!(steps("core"), steps("front"));
109 let source = std::fs::read_to_string(workflows_dir().join("deploy.yml")).unwrap();
110 assert!(!source.contains("cancel-in-progress: true"));
111}
112
113#[test]
114fn deploy_stages_follow_one_another() {
115 let deploy = read("deploy.yml");
116 let all = plan_outputs(true, &[("rust", "events,repos", true), ("ts", "projects", false)], &[("rust", "api", true)], &[("web", "web", false)]);
117 let push = json!({});
118
119 // Everything succeeds: each stage runs after the last.
120 assert!(starts(&deploy, "migrate", &[], &all, push.clone(), false));
121 assert!(starts(&deploy, "core", &[("migrate", "success")], &all, push.clone(), false));
122 assert!(starts(&deploy, "edge", &[("migrate", "success"), ("core", "success")], &all, push.clone(), false));
123 assert!(starts(&deploy, "front", &[("migrate", "success"), ("core", "success"), ("edge", "success")], &all, push.clone(), false));
124
125 // No migrations: the migrate job is skipped, and core still runs.
126 let none = plan_outputs(false, &[("ts", "projects", false)], &[], &[("web", "web", false)]);
127 assert!(!starts(&deploy, "migrate", &[], &none, push.clone(), false));
128 assert!(starts(&deploy, "core", &[("migrate", "skipped")], &none, push.clone(), false));
129 // An empty stage is skipped, and the next one still runs.
130 assert!(!starts(&deploy, "edge", &[("migrate", "skipped"), ("core", "success")], &none, push.clone(), false));
131 assert!(starts(&deploy, "front", &[("migrate", "skipped"), ("core", "success"), ("edge", "skipped")], &none, push.clone(), false));
132
133 // A failure stops every later stage.
134 assert!(!starts(&deploy, "core", &[("migrate", "failure")], &all, push.clone(), false));
135 assert!(!starts(&deploy, "edge", &[("migrate", "success"), ("core", "failure")], &all, push.clone(), false));
136 assert!(!starts(&deploy, "front", &[("migrate", "success"), ("core", "success"), ("edge", "failure")], &all, push.clone(), false));
137 assert!(!starts(&deploy, "front", &[("migrate", "success"), ("core", "failure"), ("edge", "skipped")], &all, push.clone(), false));
138 // A cancelled run starts nothing more.
139 assert!(!starts(&deploy, "edge", &[("migrate", "success"), ("core", "success")], &all, push.clone(), true));
140 // A failed check: plan, and every stage after it, is skipped, and
141 // failure() still sees the check's failure through them.
142 let skipped = [("plan", "skipped"), ("migrate", "skipped"), ("core", "skipped"), ("edge", "skipped")];
143 assert!(!starts_after(&deploy, "plan", &[("check", "failure")], &all, push.clone(), false, false));
144 assert!(!starts_after(&deploy, "core", &skipped, &all, push.clone(), false, true));
145 assert!(!starts_after(&deploy, "front", &skipped, &all, push.clone(), false, true));
146
147 // A dry run plans and stops.
148 let dry = json!({ "dry_run": true, "units": "", "all": false });
149 assert!(!starts(&deploy, "migrate", &[], &all, dry.clone(), false));
150 assert!(!starts(&deploy, "core", &[("migrate", "skipped")], &all, dry, false));
151}
152
153#[test]
154fn deploy_stage_matrices_come_from_the_plan() {
155 let deploy = read("deploy.yml");
156 let outputs = plan_outputs(false, &[("rust-1", "events,work", true), ("rust-2", "repos", true), ("ts", "projects,og", false)], &[], &[]);
157 let core = deploy.jobs.iter().find(|j| j.id == "core").unwrap();
158 assert!(!core.fail_fast);
159 assert_eq!(core.max_parallel, Some(4));
160 let mut contexts = Map::new();
161 contexts.insert("needs".into(), json!({ "plan": { "result": "success", "outputs": outputs } }));
162 let scope = Scope { contexts: &contexts, status: Status::Success, hash_files: None };
163 let value = expr::interpolate_value(core.matrix.as_ref().unwrap(), &scope).unwrap();
164 let jobs = matrix::expand(&value).unwrap();
165 let groups: Vec<(&str, &str, bool)> = jobs
166 .iter()
167 .map(|c| (c["group"].as_str().unwrap(), c["units"].as_str().unwrap(), c["rust"].as_bool().unwrap()))
168 .collect();
169 assert_eq!(groups, [("rust-1", "events,work", true), ("rust-2", "repos", true), ("ts", "projects,og", false)]);
170}
171
172#[test]
173fn deploy_builds_rust_on_the_larger_machine_with_its_target_cached() {
174 let deploy = read("deploy.yml");
175 for stage in ["core", "edge", "front"] {
176 let job = deploy.jobs.iter().find(|j| j.id == stage).unwrap();
177 let on = |rust: bool| {
178 let mut contexts = Map::new();
179 contexts.insert("matrix".into(), json!({ "group": "g", "units": "u", "rust": rust }));
180 let scope = Scope { contexts: &contexts, status: Status::Success, hash_files: None };
181 expr::interpolate_value(&job.runs_on, &scope).unwrap()
182 };
183 assert_eq!(on(true), json!("g1t-4core"), "{stage}");
184 assert_eq!(on(false), json!("ubuntu-latest"), "{stage}");
185 }
186 let source = std::fs::read_to_string(workflows_dir().join("deploy.yml")).unwrap();
187 assert!(source.contains("target/wasm32-unknown-unknown/release"));
188 assert!(source.contains("!target/**/incremental"));
189}
190
191#[test]
192fn the_runner_base_rebuilds_weekly_on_a_machine_with_docker() {
193 let base = read("runner-base.yml");
194 assert!(base.trigger("schedule").is_some());
195 assert!(base.trigger("workflow_dispatch").is_some());
196 assert!(base.trigger("push").unwrap().branches.allows("main"));
197 let job = base.jobs.iter().find(|j| j.id == "build").unwrap();
198 assert_eq!(job.runs_on, json!(["self-hosted", "docker"]));
199}