Skip to content

Commit 0412507

Browse files
committed
Move job handling to a separate module
1 parent 7ca7675 commit 0412507

File tree

3 files changed

+242
-230
lines changed

3 files changed

+242
-230
lines changed

Diff for: src/ci/citool/src/jobs.rs

+233
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use crate::{GitHubContext, utils};
2+
use serde_yaml::Value;
3+
use std::collections::BTreeMap;
4+
use std::path::Path;
5+
6+
use serde_yaml::Value;
7+
8+
use crate::GitHubContext;
9+
10+
/// Representation of a job loaded from the `src/ci/github-actions/jobs.yml` file.
11+
#[derive(serde::Deserialize, Debug, Clone)]
12+
pub struct Job {
13+
/// Name of the job, e.g. mingw-check
14+
pub name: String,
15+
/// GitHub runner on which the job should be executed
16+
pub os: String,
17+
pub env: BTreeMap<String, Value>,
18+
/// Should the job be only executed on a specific channel?
19+
#[serde(default)]
20+
pub only_on_channel: Option<String>,
21+
/// Do not cancel the whole workflow if this job fails.
22+
#[serde(default)]
23+
pub continue_on_error: Option<bool>,
24+
/// Free additional disk space in the job, by removing unused packages.
25+
#[serde(default)]
26+
pub free_disk: Option<bool>,
27+
}
28+
29+
impl Job {
30+
/// By default, the Docker image of a job is based on its name.
31+
/// However, it can be overridden by its IMAGE environment variable.
32+
pub fn image(&self) -> String {
33+
self.env
34+
.get("IMAGE")
35+
.map(|v| v.as_str().expect("IMAGE value should be a string").to_string())
36+
.unwrap_or_else(|| self.name.clone())
37+
}
38+
39+
fn is_linux(&self) -> bool {
40+
self.os.contains("ubuntu")
41+
}
42+
}
43+
44+
#[derive(serde::Deserialize, Debug)]
45+
struct JobEnvironments {
46+
#[serde(rename = "pr")]
47+
pr_env: BTreeMap<String, Value>,
48+
#[serde(rename = "try")]
49+
try_env: BTreeMap<String, Value>,
50+
#[serde(rename = "auto")]
51+
auto_env: BTreeMap<String, Value>,
52+
}
53+
54+
#[derive(serde::Deserialize, Debug)]
55+
pub struct JobDatabase {
56+
#[serde(rename = "pr")]
57+
pub pr_jobs: Vec<Job>,
58+
#[serde(rename = "try")]
59+
pub try_jobs: Vec<Job>,
60+
#[serde(rename = "auto")]
61+
pub auto_jobs: Vec<Job>,
62+
63+
/// Shared environments for the individual run types.
64+
envs: JobEnvironments,
65+
}
66+
67+
impl JobDatabase {
68+
fn find_auto_job_by_name(&self, name: &str) -> Option<Job> {
69+
self.auto_jobs.iter().find(|j| j.name == name).cloned()
70+
}
71+
}
72+
73+
pub fn load_job_db(path: &Path) -> anyhow::Result<JobDatabase> {
74+
let db = utils::read_to_string(path)?;
75+
let mut db: Value = serde_yaml::from_str(&db)?;
76+
77+
// We need to expand merge keys (<<), because serde_yaml can't deal with them
78+
// `apply_merge` only applies the merge once, so do it a few times to unwrap nested merges.
79+
db.apply_merge()?;
80+
db.apply_merge()?;
81+
82+
let db: JobDatabase = serde_yaml::from_value(db)?;
83+
Ok(db)
84+
}
85+
86+
/// Representation of a job outputted to a GitHub Actions workflow.
87+
#[derive(serde::Serialize, Debug)]
88+
struct GithubActionsJob {
89+
/// The main identifier of the job, used by CI scripts to determine what should be executed.
90+
name: String,
91+
/// Helper label displayed in GitHub Actions interface, containing the job name and a run type
92+
/// prefix (PR/try/auto).
93+
full_name: String,
94+
os: String,
95+
env: BTreeMap<String, serde_json::Value>,
96+
#[serde(skip_serializing_if = "Option::is_none")]
97+
continue_on_error: Option<bool>,
98+
#[serde(skip_serializing_if = "Option::is_none")]
99+
free_disk: Option<bool>,
100+
}
101+
102+
/// Skip CI jobs that are not supposed to be executed on the given `channel`.
103+
fn skip_jobs(jobs: Vec<Job>, channel: &str) -> Vec<Job> {
104+
jobs.into_iter()
105+
.filter(|job| {
106+
job.only_on_channel.is_none() || job.only_on_channel.as_deref() == Some(channel)
107+
})
108+
.collect()
109+
}
110+
111+
/// Type of workflow that is being executed on CI
112+
#[derive(Debug)]
113+
pub enum RunType {
114+
/// Workflows that run after a push to a PR branch
115+
PullRequest,
116+
/// Try run started with @bors try
117+
TryJob { custom_jobs: Option<Vec<String>> },
118+
/// Merge attempt workflow
119+
AutoJob,
120+
}
121+
122+
/// Maximum number of custom try jobs that can be requested in a single
123+
/// `@bors try` request.
124+
const MAX_TRY_JOBS_COUNT: usize = 20;
125+
126+
fn calculate_jobs(
127+
run_type: &RunType,
128+
db: &JobDatabase,
129+
channel: &str,
130+
) -> anyhow::Result<Vec<GithubActionsJob>> {
131+
let (jobs, prefix, base_env) = match run_type {
132+
RunType::PullRequest => (db.pr_jobs.clone(), "PR", &db.envs.pr_env),
133+
RunType::TryJob { custom_jobs } => {
134+
let jobs = if let Some(custom_jobs) = custom_jobs {
135+
if custom_jobs.len() > MAX_TRY_JOBS_COUNT {
136+
return Err(anyhow::anyhow!(
137+
"It is only possible to schedule up to {MAX_TRY_JOBS_COUNT} custom jobs, received {} custom jobs",
138+
custom_jobs.len()
139+
));
140+
}
141+
142+
let mut jobs = vec![];
143+
let mut unknown_jobs = vec![];
144+
for custom_job in custom_jobs {
145+
if let Some(job) = db.find_auto_job_by_name(custom_job) {
146+
jobs.push(job);
147+
} else {
148+
unknown_jobs.push(custom_job.clone());
149+
}
150+
}
151+
if !unknown_jobs.is_empty() {
152+
return Err(anyhow::anyhow!(
153+
"Custom job(s) `{}` not found in auto jobs",
154+
unknown_jobs.join(", ")
155+
));
156+
}
157+
jobs
158+
} else {
159+
db.try_jobs.clone()
160+
};
161+
(jobs, "try", &db.envs.try_env)
162+
}
163+
RunType::AutoJob => (db.auto_jobs.clone(), "auto", &db.envs.auto_env),
164+
};
165+
let jobs = skip_jobs(jobs, channel);
166+
let jobs = jobs
167+
.into_iter()
168+
.map(|job| {
169+
let mut env: BTreeMap<String, serde_json::Value> = crate::yaml_map_to_json(base_env);
170+
env.extend(crate::yaml_map_to_json(&job.env));
171+
let full_name = format!("{prefix} - {}", job.name);
172+
173+
GithubActionsJob {
174+
name: job.name,
175+
full_name,
176+
os: job.os,
177+
env,
178+
continue_on_error: job.continue_on_error,
179+
free_disk: job.free_disk,
180+
}
181+
})
182+
.collect();
183+
184+
Ok(jobs)
185+
}
186+
187+
pub fn calculate_job_matrix(
188+
db: JobDatabase,
189+
gh_ctx: GitHubContext,
190+
channel: &str,
191+
) -> anyhow::Result<()> {
192+
let run_type = gh_ctx.get_run_type().ok_or_else(|| {
193+
anyhow::anyhow!("Cannot determine the type of workflow that is being executed")
194+
})?;
195+
eprintln!("Run type: {run_type:?}");
196+
197+
let jobs = calculate_jobs(&run_type, &db, channel)?;
198+
if jobs.is_empty() {
199+
return Err(anyhow::anyhow!("Computed job list is empty"));
200+
}
201+
202+
let run_type = match run_type {
203+
RunType::PullRequest => "pr",
204+
RunType::TryJob { .. } => "try",
205+
RunType::AutoJob => "auto",
206+
};
207+
208+
eprintln!("Output");
209+
eprintln!("jobs={jobs:?}");
210+
eprintln!("run_type={run_type}");
211+
println!("jobs={}", serde_json::to_string(&jobs)?);
212+
println!("run_type={run_type}");
213+
214+
Ok(())
215+
}
216+
217+
pub fn find_linux_job<'a>(jobs: &'a [Job], name: &str) -> anyhow::Result<&'a Job> {
218+
let Some(job) = jobs.iter().find(|j| j.name == name) else {
219+
let available_jobs: Vec<&Job> = jobs.iter().filter(|j| j.is_linux()).collect();
220+
let mut available_jobs =
221+
available_jobs.iter().map(|j| j.name.to_string()).collect::<Vec<_>>();
222+
available_jobs.sort();
223+
return Err(anyhow::anyhow!(
224+
"Job {name} not found. The following jobs are available:\n{}",
225+
available_jobs.join(", ")
226+
));
227+
};
228+
if !job.is_linux() {
229+
return Err(anyhow::anyhow!("Only Linux jobs can be executed locally"));
230+
}
231+
232+
Ok(job)
233+
}

0 commit comments

Comments
 (0)