Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 3e67637

Browse files
authoredMar 11, 2025
Rollup merge of #138307 - Kobzol:citool-alias, r=marcoieni
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 07f33e2 + 16c08f6 commit 3e67637

File tree

7 files changed

+361
-242
lines changed

7 files changed

+361
-242
lines changed
 

‎src/ci/citool/Cargo.lock

Lines changed: 7 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
#[cfg(test)]
2+
mod tests;
3+
4+
use std::collections::BTreeMap;
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+
/// Find `auto` jobs that correspond to the passed `pattern`.
69+
/// Patterns are matched using the glob syntax.
70+
/// For example `dist-*` matches all jobs starting with `dist-`.
71+
fn find_auto_jobs_by_pattern(&self, pattern: &str) -> Vec<Job> {
72+
self.auto_jobs
73+
.iter()
74+
.filter(|j| glob_match::glob_match(pattern, &j.name))
75+
.cloned()
76+
.collect()
77+
}
78+
}
79+
80+
pub fn load_job_db(db: &str) -> anyhow::Result<JobDatabase> {
81+
let mut db: Value = serde_yaml::from_str(&db)?;
82+
83+
// We need to expand merge keys (<<), because serde_yaml can't deal with them
84+
// `apply_merge` only applies the merge once, so do it a few times to unwrap nested merges.
85+
db.apply_merge()?;
86+
db.apply_merge()?;
87+
88+
let db: JobDatabase = serde_yaml::from_value(db)?;
89+
Ok(db)
90+
}
91+
92+
/// Representation of a job outputted to a GitHub Actions workflow.
93+
#[derive(serde::Serialize, Debug)]
94+
struct GithubActionsJob {
95+
/// The main identifier of the job, used by CI scripts to determine what should be executed.
96+
name: String,
97+
/// Helper label displayed in GitHub Actions interface, containing the job name and a run type
98+
/// prefix (PR/try/auto).
99+
full_name: String,
100+
os: String,
101+
env: BTreeMap<String, serde_json::Value>,
102+
#[serde(skip_serializing_if = "Option::is_none")]
103+
continue_on_error: Option<bool>,
104+
#[serde(skip_serializing_if = "Option::is_none")]
105+
free_disk: Option<bool>,
106+
}
107+
108+
/// Skip CI jobs that are not supposed to be executed on the given `channel`.
109+
fn skip_jobs(jobs: Vec<Job>, channel: &str) -> Vec<Job> {
110+
jobs.into_iter()
111+
.filter(|job| {
112+
job.only_on_channel.is_none() || job.only_on_channel.as_deref() == Some(channel)
113+
})
114+
.collect()
115+
}
116+
117+
/// Type of workflow that is being executed on CI
118+
#[derive(Debug)]
119+
pub enum RunType {
120+
/// Workflows that run after a push to a PR branch
121+
PullRequest,
122+
/// Try run started with @bors try
123+
TryJob { job_patterns: Option<Vec<String>> },
124+
/// Merge attempt workflow
125+
AutoJob,
126+
}
127+
128+
/// Maximum number of custom try jobs that can be requested in a single
129+
/// `@bors try` request.
130+
const MAX_TRY_JOBS_COUNT: usize = 20;
131+
132+
fn calculate_jobs(
133+
run_type: &RunType,
134+
db: &JobDatabase,
135+
channel: &str,
136+
) -> anyhow::Result<Vec<GithubActionsJob>> {
137+
let (jobs, prefix, base_env) = match run_type {
138+
RunType::PullRequest => (db.pr_jobs.clone(), "PR", &db.envs.pr_env),
139+
RunType::TryJob { job_patterns } => {
140+
let jobs = if let Some(patterns) = job_patterns {
141+
let mut jobs: Vec<Job> = vec![];
142+
let mut unknown_patterns = vec![];
143+
for pattern in patterns {
144+
let matched_jobs = db.find_auto_jobs_by_pattern(pattern);
145+
if matched_jobs.is_empty() {
146+
unknown_patterns.push(pattern.clone());
147+
} else {
148+
for job in matched_jobs {
149+
if !jobs.iter().any(|j| j.name == job.name) {
150+
jobs.push(job);
151+
}
152+
}
153+
}
154+
}
155+
if !unknown_patterns.is_empty() {
156+
return Err(anyhow::anyhow!(
157+
"Patterns `{}` did not match any auto jobs",
158+
unknown_patterns.join(", ")
159+
));
160+
}
161+
if jobs.len() > MAX_TRY_JOBS_COUNT {
162+
return Err(anyhow::anyhow!(
163+
"It is only possible to schedule up to {MAX_TRY_JOBS_COUNT} custom jobs, received {} custom jobs expanded from {} pattern(s)",
164+
jobs.len(),
165+
patterns.len()
166+
));
167+
}
168+
jobs
169+
} else {
170+
db.try_jobs.clone()
171+
};
172+
(jobs, "try", &db.envs.try_env)
173+
}
174+
RunType::AutoJob => (db.auto_jobs.clone(), "auto", &db.envs.auto_env),
175+
};
176+
let jobs = skip_jobs(jobs, channel);
177+
let jobs = jobs
178+
.into_iter()
179+
.map(|job| {
180+
let mut env: BTreeMap<String, serde_json::Value> = crate::yaml_map_to_json(base_env);
181+
env.extend(crate::yaml_map_to_json(&job.env));
182+
let full_name = format!("{prefix} - {}", job.name);
183+
184+
GithubActionsJob {
185+
name: job.name,
186+
full_name,
187+
os: job.os,
188+
env,
189+
continue_on_error: job.continue_on_error,
190+
free_disk: job.free_disk,
191+
}
192+
})
193+
.collect();
194+
195+
Ok(jobs)
196+
}
197+
198+
pub fn calculate_job_matrix(
199+
db: JobDatabase,
200+
gh_ctx: GitHubContext,
201+
channel: &str,
202+
) -> anyhow::Result<()> {
203+
let run_type = gh_ctx.get_run_type().ok_or_else(|| {
204+
anyhow::anyhow!("Cannot determine the type of workflow that is being executed")
205+
})?;
206+
eprintln!("Run type: {run_type:?}");
207+
208+
let jobs = calculate_jobs(&run_type, &db, channel)?;
209+
if jobs.is_empty() {
210+
return Err(anyhow::anyhow!("Computed job list is empty"));
211+
}
212+
213+
let run_type = match run_type {
214+
RunType::PullRequest => "pr",
215+
RunType::TryJob { .. } => "try",
216+
RunType::AutoJob => "auto",
217+
};
218+
219+
eprintln!("Output");
220+
eprintln!("jobs={jobs:?}");
221+
eprintln!("run_type={run_type}");
222+
println!("jobs={}", serde_json::to_string(&jobs)?);
223+
println!("run_type={run_type}");
224+
225+
Ok(())
226+
}
227+
228+
pub fn find_linux_job<'a>(jobs: &'a [Job], name: &str) -> anyhow::Result<&'a Job> {
229+
let Some(job) = jobs.iter().find(|j| j.name == name) else {
230+
let available_jobs: Vec<&Job> = jobs.iter().filter(|j| j.is_linux()).collect();
231+
let mut available_jobs =
232+
available_jobs.iter().map(|j| j.name.to_string()).collect::<Vec<_>>();
233+
available_jobs.sort();
234+
return Err(anyhow::anyhow!(
235+
"Job {name} not found. The following jobs are available:\n{}",
236+
available_jobs.join(", ")
237+
));
238+
};
239+
if !job.is_linux() {
240+
return Err(anyhow::anyhow!("Only Linux jobs can be executed locally"));
241+
}
242+
243+
Ok(job)
244+
}

‎src/ci/citool/src/jobs/tests.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use crate::jobs::{JobDatabase, load_job_db};
2+
3+
#[test]
4+
fn lookup_job_pattern() {
5+
let db = load_job_db(
6+
r#"
7+
envs:
8+
pr:
9+
try:
10+
auto:
11+
12+
pr:
13+
try:
14+
auto:
15+
- name: dist-a
16+
os: ubuntu
17+
env: {}
18+
- name: dist-a-alt
19+
os: ubuntu
20+
env: {}
21+
- name: dist-b
22+
os: ubuntu
23+
env: {}
24+
- name: dist-b-alt
25+
os: ubuntu
26+
env: {}
27+
- name: test-a
28+
os: ubuntu
29+
env: {}
30+
- name: test-a-alt
31+
os: ubuntu
32+
env: {}
33+
- name: test-i686
34+
os: ubuntu
35+
env: {}
36+
- name: dist-i686
37+
os: ubuntu
38+
env: {}
39+
- name: test-msvc-i686-1
40+
os: ubuntu
41+
env: {}
42+
- name: test-msvc-i686-2
43+
os: ubuntu
44+
env: {}
45+
"#,
46+
)
47+
.unwrap();
48+
check_pattern(&db, "dist-*", &["dist-a", "dist-a-alt", "dist-b", "dist-b-alt", "dist-i686"]);
49+
check_pattern(&db, "*-alt", &["dist-a-alt", "dist-b-alt", "test-a-alt"]);
50+
check_pattern(&db, "dist*-alt", &["dist-a-alt", "dist-b-alt"]);
51+
check_pattern(
52+
&db,
53+
"*i686*",
54+
&["test-i686", "dist-i686", "test-msvc-i686-1", "test-msvc-i686-2"],
55+
);
56+
}
57+
58+
#[track_caller]
59+
fn check_pattern(db: &JobDatabase, pattern: &str, expected: &[&str]) {
60+
let jobs =
61+
db.find_auto_jobs_by_pattern(pattern).into_iter().map(|j| j.name).collect::<Vec<_>>();
62+
63+
assert_eq!(jobs, expected);
64+
}

‎src/ci/citool/src/main.rs

Lines changed: 26 additions & 231 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod cpu_usage;
22
mod datadog;
3+
mod jobs;
34
mod merge_report;
45
mod metrics;
56
mod utils;
@@ -10,10 +11,12 @@ use std::process::Command;
1011

1112
use anyhow::Context;
1213
use clap::Parser;
14+
use jobs::JobDatabase;
1315
use serde_yaml::Value;
1416

1517
use crate::cpu_usage::load_cpu_usage;
1618
use crate::datadog::upload_datadog_metric;
19+
use crate::jobs::RunType;
1720
use crate::merge_report::post_merge_report;
1821
use crate::metrics::postprocess_metrics;
1922
use crate::utils::load_env_var;
@@ -22,104 +25,6 @@ const CI_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/..");
2225
const DOCKER_DIRECTORY: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../docker");
2326
const JOBS_YML_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../github-actions/jobs.yml");
2427

25-
/// Representation of a job loaded from the `src/ci/github-actions/jobs.yml` file.
26-
#[derive(serde::Deserialize, Debug, Clone)]
27-
struct Job {
28-
/// Name of the job, e.g. mingw-check
29-
name: String,
30-
/// GitHub runner on which the job should be executed
31-
os: String,
32-
env: BTreeMap<String, Value>,
33-
/// Should the job be only executed on a specific channel?
34-
#[serde(default)]
35-
only_on_channel: Option<String>,
36-
/// Rest of attributes that will be passed through to GitHub actions
37-
#[serde(flatten)]
38-
extra_keys: BTreeMap<String, Value>,
39-
}
40-
41-
impl Job {
42-
fn is_linux(&self) -> bool {
43-
self.os.contains("ubuntu")
44-
}
45-
46-
/// By default, the Docker image of a job is based on its name.
47-
/// However, it can be overridden by its IMAGE environment variable.
48-
fn image(&self) -> String {
49-
self.env
50-
.get("IMAGE")
51-
.map(|v| v.as_str().expect("IMAGE value should be a string").to_string())
52-
.unwrap_or_else(|| self.name.clone())
53-
}
54-
}
55-
56-
#[derive(serde::Deserialize, Debug)]
57-
struct JobEnvironments {
58-
#[serde(rename = "pr")]
59-
pr_env: BTreeMap<String, Value>,
60-
#[serde(rename = "try")]
61-
try_env: BTreeMap<String, Value>,
62-
#[serde(rename = "auto")]
63-
auto_env: BTreeMap<String, Value>,
64-
}
65-
66-
#[derive(serde::Deserialize, Debug)]
67-
struct JobDatabase {
68-
#[serde(rename = "pr")]
69-
pr_jobs: Vec<Job>,
70-
#[serde(rename = "try")]
71-
try_jobs: Vec<Job>,
72-
#[serde(rename = "auto")]
73-
auto_jobs: Vec<Job>,
74-
75-
/// Shared environments for the individual run types.
76-
envs: JobEnvironments,
77-
}
78-
79-
impl JobDatabase {
80-
fn find_auto_job_by_name(&self, name: &str) -> Option<Job> {
81-
self.auto_jobs.iter().find(|j| j.name == name).cloned()
82-
}
83-
}
84-
85-
fn load_job_db(path: &Path) -> anyhow::Result<JobDatabase> {
86-
let db = utils::read_to_string(path)?;
87-
let mut db: Value = serde_yaml::from_str(&db)?;
88-
89-
// We need to expand merge keys (<<), because serde_yaml can't deal with them
90-
// `apply_merge` only applies the merge once, so do it a few times to unwrap nested merges.
91-
db.apply_merge()?;
92-
db.apply_merge()?;
93-
94-
let db: JobDatabase = serde_yaml::from_value(db)?;
95-
Ok(db)
96-
}
97-
98-
/// Representation of a job outputted to a GitHub Actions workflow.
99-
#[derive(serde::Serialize, Debug)]
100-
struct GithubActionsJob {
101-
/// The main identifier of the job, used by CI scripts to determine what should be executed.
102-
name: String,
103-
/// Helper label displayed in GitHub Actions interface, containing the job name and a run type
104-
/// prefix (PR/try/auto).
105-
full_name: String,
106-
os: String,
107-
env: BTreeMap<String, serde_json::Value>,
108-
#[serde(flatten)]
109-
extra_keys: BTreeMap<String, serde_json::Value>,
110-
}
111-
112-
/// Type of workflow that is being executed on CI
113-
#[derive(Debug)]
114-
enum RunType {
115-
/// Workflows that run after a push to a PR branch
116-
PullRequest,
117-
/// Try run started with @bors try
118-
TryJob { custom_jobs: Option<Vec<String>> },
119-
/// Merge attempt workflow
120-
AutoJob,
121-
}
122-
12328
struct GitHubContext {
12429
event_name: String,
12530
branch_ref: String,
@@ -130,24 +35,31 @@ impl GitHubContext {
13035
fn get_run_type(&self) -> Option<RunType> {
13136
match (self.event_name.as_str(), self.branch_ref.as_str()) {
13237
("pull_request", _) => Some(RunType::PullRequest),
133-
("push", "refs/heads/try-perf") => Some(RunType::TryJob { custom_jobs: None }),
38+
("push", "refs/heads/try-perf") => Some(RunType::TryJob { job_patterns: None }),
13439
("push", "refs/heads/try" | "refs/heads/automation/bors/try") => {
135-
let custom_jobs = self.get_custom_jobs();
136-
let custom_jobs = if !custom_jobs.is_empty() { Some(custom_jobs) } else { None };
137-
Some(RunType::TryJob { custom_jobs })
40+
let patterns = self.get_try_job_patterns();
41+
let patterns = if !patterns.is_empty() { Some(patterns) } else { None };
42+
Some(RunType::TryJob { job_patterns: patterns })
13843
}
13944
("push", "refs/heads/auto") => Some(RunType::AutoJob),
14045
_ => None,
14146
}
14247
}
14348

144-
/// Tries to parse names of specific CI jobs that should be executed in the form of
145-
/// try-job: <job-name>
146-
/// from the commit message of the passed GitHub context.
147-
fn get_custom_jobs(&self) -> Vec<String> {
49+
/// Tries to parse patterns of CI jobs that should be executed
50+
/// from the commit message of the passed GitHub context
51+
///
52+
/// They can be specified in the form of
53+
/// try-job: <job-pattern>
54+
/// or
55+
/// try-job: `<job-pattern>`
56+
/// (to avoid GitHub rendering the glob patterns as Markdown)
57+
fn get_try_job_patterns(&self) -> Vec<String> {
14858
if let Some(ref msg) = self.commit_message {
14959
msg.lines()
15060
.filter_map(|line| line.trim().strip_prefix("try-job: "))
61+
// Strip backticks if present
62+
.map(|l| l.trim_matches('`'))
15163
.map(|l| l.trim().to_string())
15264
.collect()
15365
} else {
@@ -164,15 +76,6 @@ fn load_github_ctx() -> anyhow::Result<GitHubContext> {
16476
Ok(GitHubContext { event_name, branch_ref: load_env_var("GITHUB_REF")?, commit_message })
16577
}
16678

167-
/// Skip CI jobs that are not supposed to be executed on the given `channel`.
168-
fn skip_jobs(jobs: Vec<Job>, channel: &str) -> Vec<Job> {
169-
jobs.into_iter()
170-
.filter(|job| {
171-
job.only_on_channel.is_none() || job.only_on_channel.as_deref() == Some(channel)
172-
})
173-
.collect()
174-
}
175-
17679
fn yaml_map_to_json(map: &BTreeMap<String, Value>) -> BTreeMap<String, serde_json::Value> {
17780
map.into_iter()
17881
.map(|(key, value)| {
@@ -184,124 +87,13 @@ fn yaml_map_to_json(map: &BTreeMap<String, Value>) -> BTreeMap<String, serde_jso
18487
.collect()
18588
}
18689

187-
/// Maximum number of custom try jobs that can be requested in a single
188-
/// `@bors try` request.
189-
const MAX_TRY_JOBS_COUNT: usize = 20;
190-
191-
fn calculate_jobs(
192-
run_type: &RunType,
193-
db: &JobDatabase,
194-
channel: &str,
195-
) -> anyhow::Result<Vec<GithubActionsJob>> {
196-
let (jobs, prefix, base_env) = match run_type {
197-
RunType::PullRequest => (db.pr_jobs.clone(), "PR", &db.envs.pr_env),
198-
RunType::TryJob { custom_jobs } => {
199-
let jobs = if let Some(custom_jobs) = custom_jobs {
200-
if custom_jobs.len() > MAX_TRY_JOBS_COUNT {
201-
return Err(anyhow::anyhow!(
202-
"It is only possible to schedule up to {MAX_TRY_JOBS_COUNT} custom jobs, received {} custom jobs",
203-
custom_jobs.len()
204-
));
205-
}
206-
207-
let mut jobs = vec![];
208-
let mut unknown_jobs = vec![];
209-
for custom_job in custom_jobs {
210-
if let Some(job) = db.find_auto_job_by_name(custom_job) {
211-
jobs.push(job);
212-
} else {
213-
unknown_jobs.push(custom_job.clone());
214-
}
215-
}
216-
if !unknown_jobs.is_empty() {
217-
return Err(anyhow::anyhow!(
218-
"Custom job(s) `{}` not found in auto jobs",
219-
unknown_jobs.join(", ")
220-
));
221-
}
222-
jobs
223-
} else {
224-
db.try_jobs.clone()
225-
};
226-
(jobs, "try", &db.envs.try_env)
227-
}
228-
RunType::AutoJob => (db.auto_jobs.clone(), "auto", &db.envs.auto_env),
229-
};
230-
let jobs = skip_jobs(jobs, channel);
231-
let jobs = jobs
232-
.into_iter()
233-
.map(|job| {
234-
let mut env: BTreeMap<String, serde_json::Value> = yaml_map_to_json(base_env);
235-
env.extend(yaml_map_to_json(&job.env));
236-
let full_name = format!("{prefix} - {}", job.name);
237-
238-
GithubActionsJob {
239-
name: job.name,
240-
full_name,
241-
os: job.os,
242-
env,
243-
extra_keys: yaml_map_to_json(&job.extra_keys),
244-
}
245-
})
246-
.collect();
247-
248-
Ok(jobs)
249-
}
250-
251-
fn calculate_job_matrix(
252-
db: JobDatabase,
253-
gh_ctx: GitHubContext,
254-
channel: &str,
255-
) -> anyhow::Result<()> {
256-
let run_type = gh_ctx.get_run_type().ok_or_else(|| {
257-
anyhow::anyhow!("Cannot determine the type of workflow that is being executed")
258-
})?;
259-
eprintln!("Run type: {run_type:?}");
260-
261-
let jobs = calculate_jobs(&run_type, &db, channel)?;
262-
if jobs.is_empty() {
263-
return Err(anyhow::anyhow!("Computed job list is empty"));
264-
}
265-
266-
let run_type = match run_type {
267-
RunType::PullRequest => "pr",
268-
RunType::TryJob { .. } => "try",
269-
RunType::AutoJob => "auto",
270-
};
271-
272-
eprintln!("Output");
273-
eprintln!("jobs={jobs:?}");
274-
eprintln!("run_type={run_type}");
275-
println!("jobs={}", serde_json::to_string(&jobs)?);
276-
println!("run_type={run_type}");
277-
278-
Ok(())
279-
}
280-
281-
fn find_linux_job<'a>(jobs: &'a [Job], name: &str) -> anyhow::Result<&'a Job> {
282-
let Some(job) = jobs.iter().find(|j| j.name == name) else {
283-
let available_jobs: Vec<&Job> = jobs.iter().filter(|j| j.is_linux()).collect();
284-
let mut available_jobs =
285-
available_jobs.iter().map(|j| j.name.to_string()).collect::<Vec<_>>();
286-
available_jobs.sort();
287-
return Err(anyhow::anyhow!(
288-
"Job {name} not found. The following jobs are available:\n{}",
289-
available_jobs.join(", ")
290-
));
291-
};
292-
if !job.is_linux() {
293-
return Err(anyhow::anyhow!("Only Linux jobs can be executed locally"));
294-
}
295-
296-
Ok(job)
297-
}
298-
29990
fn run_workflow_locally(db: JobDatabase, job_type: JobType, name: String) -> anyhow::Result<()> {
30091
let jobs = match job_type {
30192
JobType::Auto => &db.auto_jobs,
30293
JobType::PR => &db.pr_jobs,
30394
};
304-
let job = find_linux_job(jobs, &name).with_context(|| format!("Cannot find job {name}"))?;
95+
let job =
96+
jobs::find_linux_job(jobs, &name).with_context(|| format!("Cannot find job {name}"))?;
30597

30698
let mut custom_env: BTreeMap<String, String> = BTreeMap::new();
30799
// Replicate src/ci/scripts/setup-environment.sh
@@ -385,7 +177,7 @@ enum Args {
385177
}
386178

387179
#[derive(clap::ValueEnum, Clone)]
388-
enum JobType {
180+
pub enum JobType {
389181
/// Merge attempt ("auto") job
390182
Auto,
391183
/// Pull request job
@@ -395,7 +187,10 @@ enum JobType {
395187
fn main() -> anyhow::Result<()> {
396188
let args = Args::parse();
397189
let default_jobs_file = Path::new(JOBS_YML_PATH);
398-
let load_db = |jobs_path| load_job_db(jobs_path).context("Cannot load jobs.yml");
190+
let load_db = |jobs_path| {
191+
let db = utils::read_to_string(jobs_path)?;
192+
Ok::<_, anyhow::Error>(jobs::load_job_db(&db).context("Cannot load jobs.yml")?)
193+
};
399194

400195
match args {
401196
Args::CalculateJobMatrix { jobs_file } => {
@@ -407,7 +202,7 @@ fn main() -> anyhow::Result<()> {
407202
.trim()
408203
.to_string();
409204

410-
calculate_job_matrix(load_db(jobs_path)?, gh_ctx, &channel)
205+
jobs::calculate_job_matrix(load_db(jobs_path)?, gh_ctx, &channel)
411206
.context("Failed to calculate job matrix")?;
412207
}
413208
Args::RunJobLocally { job_type, name } => {

‎src/ci/citool/src/merge_report.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::collections::HashMap;
44
use anyhow::Context;
55
use build_helper::metrics::{JsonRoot, TestOutcome};
66

7-
use crate::JobDatabase;
7+
use crate::jobs::JobDatabase;
88
use crate::metrics::get_test_suites;
99

1010
type Sha = String;

‎src/doc/rustc-dev-guide/src/tests/ci.md

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,29 +133,37 @@ There are several use-cases for try builds:
133133
Again, a working compiler build is needed for this, which can be produced by
134134
the [dist-x86_64-linux] CI job.
135135
- Run a specific CI job (e.g. Windows tests) on a PR, to quickly test if it
136-
passes the test suite executed by that job. You can select which CI jobs will
137-
be executed in the try build by adding up to 10 lines containing `try-job:
138-
<name of job>` to the PR description. All such specified jobs will be executed
139-
in the try build once the `@bors try` command is used on the PR. If no try
140-
jobs are specified in this way, the jobs defined in the `try` section of
141-
[`jobs.yml`] will be executed by default.
136+
passes the test suite executed by that job.
137+
138+
You can select which CI jobs will
139+
be executed in the try build by adding lines containing `try-job:
140+
<job pattern>` to the PR description. All such specified jobs will be executed
141+
in the try build once the `@bors try` command is used on the PR. If no try
142+
jobs are specified in this way, the jobs defined in the `try` section of
143+
[`jobs.yml`] will be executed by default.
144+
145+
Each pattern can either be an exact name of a job or a glob pattern that matches multiple jobs,
146+
for example `*msvc*` or `*-alt`. You can start at most 20 jobs in a single try build. When using
147+
glob patterns, you might want to wrap them in backticks (`` ` ``) to avoid GitHub rendering
148+
the pattern as Markdown.
142149

143150
> **Using `try-job` PR description directives**
144151
>
145-
> 1. Identify which set of try-jobs (max 10) you would like to exercise. You can
152+
> 1. Identify which set of try-jobs you would like to exercise. You can
146153
> find the name of the CI jobs in [`jobs.yml`].
147154
>
148-
> 2. Amend PR description to include (usually at the end of the PR description)
149-
> e.g.
155+
> 2. Amend PR description to include a set of patterns (usually at the end
156+
> of the PR description), for example:
150157
>
151158
> ```text
152159
> This PR fixes #123456.
153160
>
154161
> try-job: x86_64-msvc
155162
> try-job: test-various
163+
> try-job: `*-alt`
156164
> ```
157165
>
158-
> Each `try-job` directive must be on its own line.
166+
> Each `try-job` pattern must be on its own line.
159167
>
160168
> 3. Run the prescribed try jobs with `@bors try`. As aforementioned, this
161169
> requires the user to either (1) have `try` permissions or (2) be delegated

0 commit comments

Comments
 (0)
Please sign in to comment.