Skip to content

Commit d308586

Browse files
committed
Auto merge of rust-lang#138307 - Kobzol:citool-alias, r=<try>
Allow specifying glob patterns for try jobs This PR modifies the `try-job` lookup logic to allow glob patterns. So you can e.g. request all MSVC-related jobs with `try-job: *msvc*`. Best reviewed commit by commit. r? `@marcoieni` try-job: *msvc*
2 parents 2b285cd + d32fddf commit d308586

File tree

6 files changed

+344
-236
lines changed

6 files changed

+344
-236
lines changed

src/ci/citool/Cargo.lock

+7
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ dependencies = [
107107
"build_helper",
108108
"clap",
109109
"csv",
110+
"glob-match",
110111
"insta",
111112
"serde",
112113
"serde_json",
@@ -308,6 +309,12 @@ dependencies = [
308309
"wasi",
309310
]
310311

312+
[[package]]
313+
name = "glob-match"
314+
version = "0.2.1"
315+
source = "registry+https://github.com./rust-lang/crates.io-index"
316+
checksum = "9985c9503b412198aa4197559e9a318524ebc4519c229bfa05a535828c950b9d"
317+
311318
[[package]]
312319
name = "hashbrown"
313320
version = "0.15.2"

src/ci/citool/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ edition = "2021"
77
anyhow = "1"
88
clap = { version = "4.5", features = ["derive"] }
99
csv = "1"
10+
glob-match = "0.2"
1011
serde = { version = "1", features = ["derive"] }
1112
serde_yaml = "0.9"
1213
serde_json = "1"

src/ci/citool/src/jobs.rs

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

0 commit comments

Comments
 (0)