Skip to content

Commit 78f8362

Browse files
authored
Merge pull request #365 from Kobzol/test-failed-job-list
Show a sample of failed jobs when a build fails
2 parents b57e62b + 16a6647 commit 78f8362

File tree

11 files changed

+374
-81
lines changed

11 files changed

+374
-81
lines changed

src/bors/comment.rs

Lines changed: 60 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,57 @@ 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+
.flat_map(|w| w.failed_jobs)
119+
.collect();
120+
failed_jobs.sort_by(|l, r| l.name.cmp(&r.name));
121+
122+
if !failed_jobs.is_empty() {
123+
write!(msg, ". Failed {}:\n\n", pluralize("job", failed_jobs.len())).unwrap();
124+
125+
let max_jobs_to_show = 5;
126+
for job in failed_jobs.iter().take(max_jobs_to_show) {
127+
let logs_url = job.html_url.to_string();
128+
let extended_logs_url = format!(
129+
"https://triage.rust-lang.org/gha-logs/{}/{}/{}",
130+
repo.owner(),
131+
repo.name(),
132+
job.id
133+
);
134+
writeln!(
135+
msg,
136+
"- `{}` ([web logs]({}), [extended logs]({}))",
137+
job.name, logs_url, extended_logs_url
138+
)
139+
.unwrap();
140+
}
141+
if failed_jobs.len() > max_jobs_to_show {
142+
let remaining = failed_jobs.len() - max_jobs_to_show;
143+
writeln!(
144+
msg,
145+
"- (and {remaining} other {})",
146+
pluralize("job", remaining)
147+
)
148+
.unwrap();
149+
}
150+
}
151+
}
152+
153+
Comment::new(msg)
104154
}
105155

106156
pub fn try_build_started_comment(

src/bors/handlers/trybuild.rs

Lines changed: 47 additions & 9 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,
@@ -413,9 +417,10 @@ mod tests {
413417
use crate::github::CommitSha;
414418
use crate::tests::BorsTester;
415419
use crate::tests::mocks::{
416-
BorsBuilder, Comment, GitHubState, User, WorkflowEvent, WorkflowRunData, default_pr_number,
417-
default_repo_name, run_test,
420+
BorsBuilder, Comment, GitHubState, User, WorkflowEvent, WorkflowJob, WorkflowRunData,
421+
default_pr_number, default_repo_name, run_test,
418422
};
423+
use octocrab::models::JobId;
419424
use octocrab::params::checks::{CheckRunConclusion, CheckRunStatus};
420425

421426
#[sqlx::test]
@@ -446,16 +451,49 @@ mod tests {
446451
tester.workflow_full_failure(tester.try_branch()).await?;
447452
insta::assert_snapshot!(
448453
tester.get_comment().await?,
449-
@r###"
450-
:broken_heart: Test failed
451-
- [Workflow1](https://github.com/workflows/Workflow1/1) :x:
452-
"###
454+
@":broken_heart: Test failed ([Workflow1](https://github.com/workflows/Workflow1/1))"
453455
);
454456
Ok(())
455457
})
456458
.await;
457459
}
458460

461+
#[sqlx::test]
462+
async fn try_failure_job(pool: sqlx::PgPool) {
463+
run_test(pool, async |tester: &mut BorsTester| {
464+
tester.post_comment("@bors try").await?;
465+
tester.expect_comments(1).await;
466+
467+
let mut workflow = WorkflowRunData::from(tester.try_branch());
468+
workflow.jobs.push(WorkflowJob {
469+
id: JobId(42),
470+
status: WorkflowStatus::Failure,
471+
});
472+
workflow.jobs.push(WorkflowJob {
473+
id: JobId(50),
474+
status: WorkflowStatus::Failure,
475+
});
476+
workflow.jobs.push(WorkflowJob {
477+
id: JobId(51),
478+
status: WorkflowStatus::Success,
479+
});
480+
481+
tester.default_repo().lock().update_workflow_run(workflow.clone(), WorkflowStatus::Pending);
482+
tester.workflow_full_failure(workflow).await?;
483+
insta::assert_snapshot!(
484+
tester.get_comment().await?,
485+
@r"
486+
:broken_heart: Test failed ([Workflow1](https://github.com/workflows/Workflow1/1)). Failed jobs:
487+
488+
- `Job 42` ([web logs](https://github.com/job-logs/42), [extended logs](https://triage.rust-lang.org/gha-logs/rust-lang/borstest/42))
489+
- `Job 50` ([web logs](https://github.com/job-logs/50), [extended logs](https://triage.rust-lang.org/gha-logs/rust-lang/borstest/50))
490+
"
491+
);
492+
Ok(())
493+
})
494+
.await;
495+
}
496+
459497
#[sqlx::test]
460498
async fn try_no_permissions(pool: sqlx::PgPool) {
461499
run_test(pool, async |tester| {

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.

0 commit comments

Comments
 (0)