Skip to content

Commit 787f56f

Browse files
committed
Show a sample of failed jobs after a build fails
1 parent b57e62b commit 787f56f

File tree

7 files changed

+188
-44
lines changed

7 files changed

+188
-44
lines changed

src/bors/comment.rs

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
use itertools::Itertools;
2+
use octocrab::models::workflows::Job;
13
use serde::Serialize;
24

5+
use crate::bors::FailedWorkflowRun;
36
use crate::bors::command::CommandPrefix;
47
use crate::database::BuildModel;
8+
use crate::github::GithubRepoName;
9+
use crate::utils::text::pluralize;
510
use crate::{
611
database::{WorkflowModel, WorkflowStatus},
712
github::CommitSha,
@@ -29,13 +34,14 @@ impl Comment {
2934

3035
pub fn render(&self) -> String {
3136
if let Some(metadata) = &self.metadata {
32-
return format!(
37+
format!(
3338
"{}\n<!-- homu: {} -->",
3439
self.text,
3540
serde_json::to_string(metadata).unwrap()
36-
);
41+
)
42+
} else {
43+
self.text.clone()
3744
}
38-
self.text.clone()
3945
}
4046
}
4147

@@ -94,13 +100,58 @@ pub fn try_build_cancelled_comment(workflow_urls: impl Iterator<Item = String>)
94100
Comment::new(try_build_cancelled_comment)
95101
}
96102

97-
pub fn workflow_failed_comment(workflows: &[WorkflowModel]) -> Comment {
98-
let workflows_status = list_workflows_status(workflows);
99-
Comment::new(format!(
100-
r#":broken_heart: Test failed
101-
{}"#,
102-
workflows_status
103-
))
103+
pub fn build_failed_comment(
104+
repo: &GithubRepoName,
105+
failed_workflows: Vec<FailedWorkflowRun>,
106+
) -> Comment {
107+
use std::fmt::Write;
108+
109+
let mut msg = ":broken_heart: Test failed".to_string();
110+
let mut workflow_links = failed_workflows
111+
.iter()
112+
.map(|w| format!("[{}]({})", w.workflow_run.name, w.workflow_run.url));
113+
if !failed_workflows.is_empty() {
114+
write!(msg, " ({})", workflow_links.join(", ")).unwrap();
115+
116+
let mut failed_jobs: Vec<Job> = failed_workflows
117+
.into_iter()
118+
.map(|w| w.failed_jobs)
119+
.flatten()
120+
.collect();
121+
failed_jobs.sort_by(|l, r| l.name.cmp(&r.name));
122+
123+
if !failed_jobs.is_empty() {
124+
write!(msg, ". Failed {}:\n\n", pluralize("job", failed_jobs.len())).unwrap();
125+
126+
let max_jobs_to_show = 5;
127+
for job in failed_jobs.iter().take(max_jobs_to_show) {
128+
let logs_url = job.html_url.to_string();
129+
let extended_logs_url = format!(
130+
"https://triage.rust-lang.org/gha-logs/{}/{}/{}",
131+
repo.owner(),
132+
repo.name(),
133+
job.id
134+
);
135+
writeln!(
136+
msg,
137+
"- `{}` ([web logs]({}), [extended logs]({}))",
138+
job.name, logs_url, extended_logs_url
139+
)
140+
.unwrap();
141+
}
142+
if failed_jobs.len() > max_jobs_to_show {
143+
let remaining = failed_jobs.len() - max_jobs_to_show;
144+
writeln!(
145+
msg,
146+
"- (and {remaining} other {})",
147+
pluralize("job", remaining)
148+
)
149+
.unwrap();
150+
}
151+
}
152+
}
153+
154+
Comment::new(msg)
104155
}
105156

106157
pub fn try_build_started_comment(

src/bors/handlers/trybuild.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ use crate::bors::comment::{
1010
cant_find_last_parent_comment, merge_conflict_comment, try_build_started_comment,
1111
};
1212
use crate::bors::handlers::labels::handle_label_trigger;
13-
use crate::database::RunId;
1413
use crate::database::{BuildModel, BuildStatus, PullRequestModel};
1514
use crate::github::GithubRepoName;
1615
use crate::github::api::client::GithubRepositoryClient;
@@ -20,6 +19,7 @@ use crate::permissions::PermissionType;
2019
use crate::utils::text::suppress_github_mentions;
2120
use anyhow::{Context, anyhow};
2221
use itertools::Itertools;
22+
use octocrab::models::CheckRunId;
2323
use octocrab::params::checks::CheckRunConclusion;
2424
use octocrab::params::checks::CheckRunOutput;
2525
use octocrab::params::checks::CheckRunStatus;
@@ -312,8 +312,12 @@ pub async fn cancel_build_workflows(
312312
db: &PgDbClient,
313313
build: &BuildModel,
314314
check_run_conclusion: CheckRunConclusion,
315-
) -> anyhow::Result<Vec<RunId>> {
315+
) -> anyhow::Result<Vec<octocrab::models::RunId>> {
316316
let pending_workflows = db.get_pending_workflows_for_build(build).await?;
317+
let pending_workflows: Vec<octocrab::models::RunId> = pending_workflows
318+
.into_iter()
319+
.map(|id| octocrab::models::RunId(id.0))
320+
.collect();
317321

318322
tracing::info!("Cancelling workflows {:?}", pending_workflows);
319323
client.cancel_workflows(&pending_workflows).await?;
@@ -324,7 +328,7 @@ pub async fn cancel_build_workflows(
324328
if let Some(check_run_id) = build.check_run_id {
325329
if let Err(error) = client
326330
.update_check_run(
327-
check_run_id as u64,
331+
CheckRunId(check_run_id as u64),
328332
CheckRunStatus::Completed,
329333
Some(check_run_conclusion),
330334
None,
@@ -446,10 +450,7 @@ mod tests {
446450
tester.workflow_full_failure(tester.try_branch()).await?;
447451
insta::assert_snapshot!(
448452
tester.get_comment().await?,
449-
@r###"
450-
:broken_heart: Test failed
451-
- [Workflow1](https://github.com/workflows/Workflow1/1) :x:
452-
"###
453+
@":broken_heart: Test failed ([Workflow1](https://github.com/workflows/Workflow1/1))"
453454
);
454455
Ok(())
455456
})

src/bors/handlers/workflow.rs

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
use std::sync::Arc;
2-
use std::time::Duration;
3-
4-
use octocrab::params::checks::CheckRunConclusion;
5-
use octocrab::params::checks::CheckRunStatus;
6-
71
use crate::PgDbClient;
8-
use crate::bors::comment::{try_build_succeeded_comment, workflow_failed_comment};
2+
use crate::bors::comment::{build_failed_comment, try_build_succeeded_comment};
93
use crate::bors::event::{WorkflowRunCompleted, WorkflowRunStarted};
104
use crate::bors::handlers::is_bors_observed_branch;
115
use crate::bors::handlers::labels::handle_label_trigger;
126
use crate::bors::merge_queue::AUTO_BRANCH_NAME;
137
use crate::bors::merge_queue::MergeQueueSender;
14-
use crate::bors::{RepositoryState, WorkflowRun};
15-
use crate::database::{BuildStatus, WorkflowStatus};
8+
use crate::bors::{FailedWorkflowRun, RepositoryState, WorkflowRun};
9+
use crate::database::{BuildStatus, WorkflowModel, WorkflowStatus};
1610
use crate::github::LabelTrigger;
11+
use octocrab::models::CheckRunId;
12+
use octocrab::models::workflows::{Conclusion, Job, Status};
13+
use octocrab::params::checks::CheckRunConclusion;
14+
use octocrab::params::checks::CheckRunStatus;
15+
use std::sync::Arc;
16+
use std::time::Duration;
1717

1818
pub(super) async fn handle_workflow_started(
1919
db: Arc<PgDbClient>,
@@ -203,20 +203,40 @@ async fn maybe_complete_build(
203203

204204
if let Err(error) = repo
205205
.client
206-
.update_check_run(check_run_id as u64, status, conclusion, None)
206+
.update_check_run(CheckRunId(check_run_id as u64), status, conclusion, None)
207207
.await
208208
{
209209
tracing::error!("Could not update check run {check_run_id}: {error:?}");
210210
}
211211
}
212212

213213
db_workflow_runs.sort_by(|a, b| a.name.cmp(&b.name));
214+
214215
let message = if !has_failure {
215-
tracing::info!("Workflow succeeded");
216+
tracing::info!("Build succeeded");
216217
try_build_succeeded_comment(&db_workflow_runs, payload.commit_sha, &build)
217218
} else {
218-
tracing::info!("Workflow failed");
219-
workflow_failed_comment(&db_workflow_runs)
219+
// Download failed jobs
220+
let mut workflow_runs: Vec<FailedWorkflowRun> = vec![];
221+
for workflow_run in db_workflow_runs {
222+
let failed_jobs = match get_failed_jobs(&repo, &workflow_run).await {
223+
Ok(jobs) => jobs,
224+
Err(error) => {
225+
tracing::error!(
226+
"Cannot download jobs for workflow run {}: {error:?}",
227+
workflow_run.run_id
228+
);
229+
vec![]
230+
}
231+
};
232+
workflow_runs.push(FailedWorkflowRun {
233+
workflow_run,
234+
failed_jobs,
235+
})
236+
}
237+
238+
tracing::info!("Build failed");
239+
build_failed_comment(repo.repository(), workflow_runs)
220240
};
221241
repo.client.post_comment(pr.number, message).await?;
222242

@@ -228,6 +248,29 @@ async fn maybe_complete_build(
228248
Ok(())
229249
}
230250

251+
/// Return failed jobs from the given workflow run.
252+
async fn get_failed_jobs(
253+
repo: &RepositoryState,
254+
workflow_run: &WorkflowModel,
255+
) -> anyhow::Result<Vec<Job>> {
256+
let jobs = repo
257+
.client
258+
.get_jobs_for_workflow_run(workflow_run.run_id.into())
259+
.await?;
260+
Ok(jobs
261+
.into_iter()
262+
.filter(|j| {
263+
j.status == Status::Failed || {
264+
j.status == Status::Completed
265+
&& matches!(
266+
j.conclusion,
267+
Some(Conclusion::Failure | Conclusion::Cancelled | Conclusion::TimedOut)
268+
)
269+
}
270+
})
271+
.collect())
272+
}
273+
231274
#[cfg(test)]
232275
mod tests {
233276
use crate::database::WorkflowStatus;
@@ -376,11 +419,7 @@ mod tests {
376419
tester.workflow_event(WorkflowEvent::failure(w2)).await?;
377420
insta::assert_snapshot!(
378421
tester.get_comment().await?,
379-
@r"
380-
:broken_heart: Test failed
381-
- [Workflow1](https://github.com/workflows/Workflow1/1) :question:
382-
- [Workflow1](https://github.com/workflows/Workflow1/2) :x:
383-
"
422+
@":broken_heart: Test failed ([Workflow1](https://github.com/workflows/Workflow1/1), [Workflow1](https://github.com/workflows/Workflow1/2))"
384423
);
385424
Ok(())
386425
})

src/bors/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ pub use comment::Comment;
88
pub use context::BorsContext;
99
pub use handlers::{handle_bors_global_event, handle_bors_repository_event};
1010
use octocrab::models::RunId;
11+
use octocrab::models::workflows::Job;
1112
use serde::Serialize;
1213

1314
use crate::config::RepositoryConfig;
@@ -25,7 +26,7 @@ mod handlers;
2526
pub mod merge_queue;
2627
pub mod mergeable_queue;
2728

28-
use crate::database::WorkflowStatus;
29+
use crate::database::{WorkflowModel, WorkflowStatus};
2930
pub use command::CommandPrefix;
3031

3132
#[cfg(test)]
@@ -47,6 +48,11 @@ pub struct WorkflowRun {
4748
pub status: WorkflowStatus,
4849
}
4950

51+
pub struct FailedWorkflowRun {
52+
pub workflow_run: WorkflowModel,
53+
pub failed_jobs: Vec<Job>,
54+
}
55+
5056
/// An access point to a single repository.
5157
/// Can be used to query permissions for the repository, and also to perform various
5258
/// actions using the stored client.

src/github/api/client.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
use anyhow::Context;
22
use octocrab::Octocrab;
33
use octocrab::models::checks::CheckRun;
4-
use octocrab::models::{App, CheckSuiteId, Repository};
4+
use octocrab::models::{App, CheckRunId, CheckSuiteId, Repository, RunId};
55
use octocrab::params::checks::{CheckRunConclusion, CheckRunOutput, CheckRunStatus};
66
use std::fmt::Debug;
77
use tracing::log;
88

99
use crate::bors::event::PullRequestComment;
1010
use crate::bors::{Comment, WorkflowRun};
1111
use crate::config::{CONFIG_FILE_PATH, RepositoryConfig};
12-
use crate::database::{RunId, WorkflowStatus};
12+
use crate::database::WorkflowStatus;
1313
use crate::github::api::base_github_html_url;
1414
use crate::github::api::operations::{
1515
ForcePush, MergeError, create_check_run, merge_branches, set_branch_to_commit, update_check_run,
1616
};
1717
use crate::github::{CommitSha, GithubRepoName, PullRequest, PullRequestNumber};
1818
use crate::utils::timing::{measure_network_request, perform_network_request_with_retry};
1919
use futures::TryStreamExt;
20+
use octocrab::models::workflows::Job;
2021
use serde::de::DeserializeOwned;
2122

2223
/// Provides access to a single app installation (repository) using the GitHub API.
@@ -187,7 +188,7 @@ impl GithubRepositoryClient {
187188
/// Update a check run with the given check run ID.
188189
pub async fn update_check_run(
189190
&self,
190-
check_run_id: u64,
191+
check_run_id: CheckRunId,
191192
status: CheckRunStatus,
192193
conclusion: Option<CheckRunConclusion>,
193194
output: Option<CheckRunOutput>,
@@ -207,7 +208,7 @@ impl GithubRepositoryClient {
207208
) -> anyhow::Result<Vec<WorkflowRun>> {
208209
#[derive(serde::Deserialize, Debug)]
209210
struct WorkflowRunResponse {
210-
id: octocrab::models::RunId,
211+
id: RunId,
211212
status: String,
212213
conclusion: Option<String>,
213214
}
@@ -255,6 +256,29 @@ impl GithubRepositoryClient {
255256
.await?
256257
}
257258

259+
/// Find all jobs for the latest execution of a workflow run with the given ID.
260+
pub async fn get_jobs_for_workflow_run(&self, run_id: RunId) -> anyhow::Result<Vec<Job>> {
261+
let response = self
262+
.client
263+
.workflows(&self.repo_name.owner, &self.repo_name.name)
264+
.list_jobs(run_id)
265+
.per_page(100)
266+
.send()
267+
.await?;
268+
269+
let mut jobs = Vec::with_capacity(
270+
response
271+
.total_count
272+
.map(|v| v as usize)
273+
.unwrap_or(response.items.len()),
274+
);
275+
let mut stream = std::pin::pin!(response.into_stream(&self.client));
276+
while let Some(job) = stream.try_next().await? {
277+
jobs.push(job);
278+
}
279+
Ok(jobs)
280+
}
281+
258282
/// Cancels Github Actions workflows.
259283
pub async fn cancel_workflows(&self, run_ids: &[RunId]) -> anyhow::Result<()> {
260284
measure_network_request("cancel_workflows", || async {

src/github/api/operations.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ pub async fn create_check_run(
201201

202202
pub async fn update_check_run(
203203
repo: &GithubRepositoryClient,
204-
check_run_id: u64,
204+
check_run_id: CheckRunId,
205205
status: CheckRunStatus,
206206
conclusion: Option<CheckRunConclusion>,
207207
output: Option<CheckRunOutput>,
@@ -210,9 +210,7 @@ pub async fn update_check_run(
210210
.client()
211211
.checks(repo.repository().owner(), repo.repository().name());
212212

213-
let mut request = checks
214-
.update_check_run(CheckRunId(check_run_id))
215-
.status(status);
213+
let mut request = checks.update_check_run(check_run_id).status(status);
216214

217215
if let Some(conclusion) = conclusion {
218216
request = request.conclusion(conclusion);

0 commit comments

Comments
 (0)