Skip to content

Commit 6ece1de

Browse files
committed
Add job summary links to post-merge report
This should make it much easier to investigate the individual job changes.
1 parent 1a48634 commit 6ece1de

File tree

3 files changed

+152
-10
lines changed

3 files changed

+152
-10
lines changed

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

+35-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use build_helper::metrics::{
77
format_build_steps,
88
};
99

10+
use crate::github::JobInfoResolver;
1011
use crate::metrics;
1112
use crate::metrics::{JobMetrics, JobName, get_test_suites};
1213
use crate::utils::{output_details, pluralize};
@@ -185,13 +186,19 @@ fn render_table(suites: BTreeMap<String, TestSuiteRecord>) -> String {
185186
}
186187

187188
/// Outputs a report of test differences between the `parent` and `current` commits.
188-
pub fn output_test_diffs(job_metrics: &HashMap<JobName, JobMetrics>) {
189+
pub fn output_test_diffs(
190+
job_metrics: &HashMap<JobName, JobMetrics>,
191+
job_info_resolver: &mut JobInfoResolver,
192+
) {
189193
let aggregated_test_diffs = aggregate_test_diffs(&job_metrics);
190-
report_test_diffs(aggregated_test_diffs);
194+
report_test_diffs(aggregated_test_diffs, job_metrics, job_info_resolver);
191195
}
192196

193197
/// Prints the ten largest differences in bootstrap durations.
194-
pub fn output_largest_duration_changes(job_metrics: &HashMap<JobName, JobMetrics>) {
198+
pub fn output_largest_duration_changes(
199+
job_metrics: &HashMap<JobName, JobMetrics>,
200+
job_info_resolver: &mut JobInfoResolver,
201+
) {
195202
struct Entry<'a> {
196203
job: &'a JobName,
197204
before: Duration,
@@ -230,9 +237,9 @@ pub fn output_largest_duration_changes(job_metrics: &HashMap<JobName, JobMetrics
230237
println!("# Job duration changes");
231238
for (index, entry) in changes.into_iter().take(10).enumerate() {
232239
println!(
233-
"{}. `{}`: {:.1}s -> {:.1}s ({:.1}%)",
240+
"{}. {}: {:.1}s -> {:.1}s ({:.1}%)",
234241
index + 1,
235-
entry.job,
242+
format_job_link(job_info_resolver, job_metrics, entry.job),
236243
entry.before.as_secs_f64(),
237244
entry.after.as_secs_f64(),
238245
entry.change
@@ -400,7 +407,11 @@ fn generate_test_name(name: &str) -> String {
400407
}
401408

402409
/// Prints test changes in Markdown format to stdout.
403-
fn report_test_diffs(diff: AggregatedTestDiffs) {
410+
fn report_test_diffs(
411+
diff: AggregatedTestDiffs,
412+
job_metrics: &HashMap<JobName, JobMetrics>,
413+
job_info_resolver: &mut JobInfoResolver,
414+
) {
404415
println!("# Test differences");
405416
if diff.diffs.is_empty() {
406417
println!("No test diffs found");
@@ -521,9 +532,26 @@ fn report_test_diffs(diff: AggregatedTestDiffs) {
521532
println!(
522533
"- {}: {}",
523534
format_job_group(group as u64),
524-
jobs.iter().map(|j| format!("`{j}`")).collect::<Vec<_>>().join(", ")
535+
jobs.iter()
536+
.map(|j| format_job_link(job_info_resolver, job_metrics, j))
537+
.collect::<Vec<_>>()
538+
.join(", ")
525539
);
526540
}
527541
},
528542
);
529543
}
544+
545+
/// Tries to get a GitHub Actions job summary URL from the resolver.
546+
/// If it is not available, just wraps the job name in backticks.
547+
fn format_job_link(
548+
job_info_resolver: &mut JobInfoResolver,
549+
job_metrics: &HashMap<JobName, JobMetrics>,
550+
job_name: &str,
551+
) -> String {
552+
job_metrics
553+
.get(job_name)
554+
.and_then(|metrics| job_info_resolver.get_job_summary_link(job_name, &metrics.current))
555+
.map(|summary_url| format!("[{job_name}]({summary_url})"))
556+
.unwrap_or_else(|| format!("`{job_name}`"))
557+
}

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

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use std::collections::HashMap;
2+
3+
use anyhow::Context;
4+
use build_helper::metrics::{CiMetadata, JsonRoot};
5+
6+
pub struct GitHubClient;
7+
8+
impl GitHubClient {
9+
fn get_workflow_run_jobs(
10+
&self,
11+
repo: &str,
12+
workflow_run_id: u64,
13+
) -> anyhow::Result<Vec<GitHubJob>> {
14+
let req = ureq::get(format!(
15+
"https://api.github.com/repos/{repo}/actions/runs/{workflow_run_id}/jobs?per_page=100"
16+
))
17+
.header("User-Agent", "rust-lang/rust/citool")
18+
.header("Accept", "application/vnd.github+json")
19+
.header("X-GitHub-Api-Version", "2022-11-28")
20+
.call()
21+
.context("cannot get workflow job list")?;
22+
23+
let status = req.status();
24+
let mut body = req.into_body();
25+
if status.is_success() {
26+
// This API response is actually paged, but we assume for now that there are at
27+
// most 100 jobs per workflow.
28+
let response = body
29+
.read_json::<WorkflowRunJobsResponse>()
30+
.context("cannot deserialize workflow run jobs response")?;
31+
// The CI job names have a prefix, e.g. `auto - foo`. We remove the prefix here to
32+
// normalize the job name.
33+
Ok(response
34+
.jobs
35+
.into_iter()
36+
.map(|mut job| {
37+
job.name = job
38+
.name
39+
.split_once(" - ")
40+
.map(|res| res.1.to_string())
41+
.unwrap_or_else(|| job.name);
42+
job
43+
})
44+
.collect())
45+
} else {
46+
Err(anyhow::anyhow!(
47+
"Cannot get jobs of workflow run {workflow_run_id}: {status}\n{}",
48+
body.read_to_string()?
49+
))
50+
}
51+
}
52+
}
53+
54+
#[derive(serde::Deserialize)]
55+
struct WorkflowRunJobsResponse {
56+
jobs: Vec<GitHubJob>,
57+
}
58+
59+
#[derive(serde::Deserialize)]
60+
struct GitHubJob {
61+
name: String,
62+
id: u64,
63+
}
64+
65+
/// Can be used to resolve information about GitHub Actions jobs.
66+
/// Caches results internally to avoid too unnecessary GitHub API calls.
67+
pub struct JobInfoResolver {
68+
client: GitHubClient,
69+
// Workflow run ID -> jobs
70+
workflow_job_cache: HashMap<u64, Vec<GitHubJob>>,
71+
}
72+
73+
impl JobInfoResolver {
74+
pub fn new() -> Self {
75+
Self { client: GitHubClient, workflow_job_cache: Default::default() }
76+
}
77+
78+
/// Get a link to a job summary for the given job name and bootstrap execution.
79+
pub fn get_job_summary_link(&mut self, job_name: &str, metrics: &JsonRoot) -> Option<String> {
80+
metrics.ci_metadata.as_ref().and_then(|metadata| {
81+
self.get_job_id(metadata, job_name).map(|job_id| {
82+
format!(
83+
"https://github.com/{}/actions/runs/{}#summary-{job_id}",
84+
metadata.repository, metadata.workflow_run_id
85+
)
86+
})
87+
})
88+
}
89+
90+
fn get_job_id(&mut self, ci_metadata: &CiMetadata, job_name: &str) -> Option<u64> {
91+
if let Some(job) = self
92+
.workflow_job_cache
93+
.get(&ci_metadata.workflow_run_id)
94+
.and_then(|jobs| jobs.iter().find(|j| j.name == job_name))
95+
{
96+
return Some(job.id);
97+
}
98+
99+
let jobs = self
100+
.client
101+
.get_workflow_run_jobs(&ci_metadata.repository, ci_metadata.workflow_run_id)
102+
.inspect_err(|e| eprintln!("Cannot download workflow jobs: {e:?}"))
103+
.ok()?;
104+
let job_id = jobs.iter().find(|j| j.name == job_name).map(|j| j.id);
105+
// Save the cache even if the job name was not found, it could be useful for further lookups
106+
self.workflow_job_cache.insert(ci_metadata.workflow_run_id, jobs);
107+
job_id
108+
}
109+
}

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod analysis;
22
mod cpu_usage;
33
mod datadog;
4+
mod github;
45
mod jobs;
56
mod metrics;
67
mod utils;
@@ -18,6 +19,7 @@ use serde_yaml::Value;
1819
use crate::analysis::{output_largest_duration_changes, output_test_diffs};
1920
use crate::cpu_usage::load_cpu_usage;
2021
use crate::datadog::upload_datadog_metric;
22+
use crate::github::JobInfoResolver;
2123
use crate::jobs::RunType;
2224
use crate::metrics::{JobMetrics, download_auto_job_metrics, download_job_metrics, load_metrics};
2325
use crate::utils::load_env_var;
@@ -145,6 +147,7 @@ fn postprocess_metrics(
145147
) -> anyhow::Result<()> {
146148
let metrics = load_metrics(&metrics_path)?;
147149

150+
let mut job_info_resolver = JobInfoResolver::new();
148151
if let (Some(parent), Some(job_name)) = (parent, job_name) {
149152
// This command is executed also on PR builds, which might not have parent metrics
150153
// available, because some PR jobs don't run on auto builds, and PR jobs do not upload metrics
@@ -160,7 +163,7 @@ fn postprocess_metrics(
160163
job_name,
161164
JobMetrics { parent: Some(parent_metrics), current: metrics },
162165
)]);
163-
output_test_diffs(&job_metrics);
166+
output_test_diffs(&job_metrics, &mut job_info_resolver);
164167
return Ok(());
165168
}
166169
Err(error) => {
@@ -180,8 +183,10 @@ fn post_merge_report(db: JobDatabase, current: String, parent: String) -> anyhow
180183
let metrics = download_auto_job_metrics(&db, &parent, &current)?;
181184

182185
println!("\nComparing {parent} (parent) -> {current} (this PR)\n");
183-
output_test_diffs(&metrics);
184-
output_largest_duration_changes(&metrics);
186+
187+
let mut job_info_resolver = JobInfoResolver::new();
188+
output_test_diffs(&metrics, &mut job_info_resolver);
189+
output_largest_duration_changes(&metrics, &mut job_info_resolver);
185190

186191
Ok(())
187192
}

0 commit comments

Comments
 (0)