Skip to content

Commit 14d1076

Browse files
authored
Merge pull request #428 from Sakib25800/retry-cmd
Add `@bors retry`
2 parents 9ebd817 + a8c1406 commit 14d1076

File tree

13 files changed

+210
-28
lines changed

13 files changed

+210
-28
lines changed

.sqlx/query-aa9f3483df0d2ab722603300368e19a9ae88785203c49bc4622a704d9bef220d.json

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/bors/command/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,7 @@ pub enum BorsCommand {
129129
OpenTree,
130130
/// Set the tree closed with a priority level.
131131
TreeClosed(Priority),
132+
/// Clear a failed auto build status from an approved PR.
133+
/// This will cause the merge queue to attempt to start a new auto build and retry merging the PR again.
134+
Retry,
132135
}

src/bors/command/parser.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ const DEFAULT_PARSERS: &[ParserFn] = &[
142142
parser_info,
143143
parser_help,
144144
parser_ping,
145+
parser_retry,
145146
parser_tree_ops,
146147
];
147148

@@ -168,7 +169,7 @@ fn parse_command(input: &str, parsers: &[ParserFn]) -> ParseResult {
168169
}
169170
}
170171

171-
fn parse_parts(input: &str) -> Result<Vec<CommandPart>, CommandParseError> {
172+
fn parse_parts(input: &str) -> Result<Vec<CommandPart<'_>>, CommandParseError> {
172173
let mut parts = vec![];
173174
let mut seen_keys = HashSet::new();
174175

@@ -411,6 +412,15 @@ fn parser_info(command: &CommandPart<'_>, _parts: &[CommandPart<'_>]) -> ParseRe
411412
}
412413
}
413414

415+
/// Parses `@bors retry`
416+
fn parser_retry(command: &CommandPart<'_>, _parts: &[CommandPart<'_>]) -> ParseResult {
417+
if let CommandPart::Bare("retry") = command {
418+
Some(Ok(BorsCommand::Retry))
419+
} else {
420+
None
421+
}
422+
}
423+
414424
/// Parses `@bors treeclosed-`, `@bors treeopen` and `@bors treeclosed=<priority>`
415425
fn parser_tree_ops(command: &CommandPart<'_>, _parts: &[CommandPart<'_>]) -> ParseResult {
416426
match command {
@@ -1195,6 +1205,20 @@ for the crater",
11951205
assert!(matches!(cmds[0], Ok(BorsCommand::Info)));
11961206
}
11971207

1208+
#[test]
1209+
fn parse_retry() {
1210+
let cmds = parse_commands("@bors retry");
1211+
assert_eq!(cmds.len(), 1);
1212+
assert!(matches!(cmds[0], Ok(BorsCommand::Retry)));
1213+
}
1214+
1215+
#[test]
1216+
fn parse_retry_unknown_arg() {
1217+
let cmds = parse_commands("@bors retry xyz");
1218+
assert_eq!(cmds.len(), 1);
1219+
assert!(matches!(cmds[0], Ok(BorsCommand::Retry)));
1220+
}
1221+
11981222
#[test]
11991223
fn parse_try_cancel() {
12001224
let cmds = parse_commands("@bors try cancel");

src/bors/handlers/help.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ fn format_help() -> &'static str {
3939
BorsCommand::SetRollupMode(_) => {}
4040
BorsCommand::OpenTree => {}
4141
BorsCommand::TreeClosed(_) => {}
42+
BorsCommand::Retry => {}
4243
}
4344

4445
r#"
@@ -65,6 +66,7 @@ You can use the following commands:
6566
- Optionally, you can specify a `<parent>` SHA with which will the PR be merged. You can specify `parent=last` to use the same parent SHA as the previous try build.
6667
- Optionally, you can select a comma-separated list of CI `<jobs>` to run in the try build.
6768
- `try cancel`: Cancel a running try build
69+
- `retry`: Clear a failed auto build status from an approved PR. This will cause the merge queue to attempt to start a new auto build and retry merging the PR again.
6870
- `info`: Get information about the current PR
6971
7072
## Repository management
@@ -109,6 +111,7 @@ mod tests {
109111
- Optionally, you can specify a `<parent>` SHA with which will the PR be merged. You can specify `parent=last` to use the same parent SHA as the previous try build.
110112
- Optionally, you can select a comma-separated list of CI `<jobs>` to run in the try build.
111113
- `try cancel`: Cancel a running try build
114+
- `retry`: Clear a failed auto build status from an approved PR. This will cause the merge queue to attempt to start a new auto build and retry merging the PR again.
112115
- `info`: Get information about the current PR
113116
114117
## Repository management

src/bors/handlers/mod.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::bors::handlers::refresh::{
1515
refresh_pending_builds, reload_mergeability_status, reload_repository_config,
1616
reload_repository_permissions,
1717
};
18+
use crate::bors::handlers::retry::command_retry;
1819
use crate::bors::handlers::review::{
1920
command_approve, command_close_tree, command_open_tree, command_unapprove,
2021
};
@@ -44,6 +45,7 @@ mod labels;
4445
mod ping;
4546
mod pr_events;
4647
mod refresh;
48+
mod retry;
4749
mod review;
4850
mod trybuild;
4951
mod workflow;
@@ -393,6 +395,12 @@ async fn handle_comment(
393395
let repo = Arc::clone(&repo);
394396
let database = Arc::clone(&database);
395397
let result = match command {
398+
BorsCommand::Retry => {
399+
let span = tracing::info_span!("Retry");
400+
command_retry(repo, database, pr, &comment.author, &merge_queue_tx)
401+
.instrument(span)
402+
.await
403+
}
396404
BorsCommand::Approve {
397405
approver,
398406
priority,

src/bors/handlers/retry.rs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
use std::sync::Arc;
2+
3+
use crate::PgDbClient;
4+
use crate::bors::handlers::{PullRequestData, deny_request, has_permission};
5+
use crate::bors::merge_queue::MergeQueueSender;
6+
use crate::bors::{Comment, RepositoryState};
7+
use crate::github::{GithubUser, PullRequestNumber};
8+
use crate::permissions::PermissionType;
9+
10+
pub(super) async fn command_retry(
11+
repo_state: Arc<RepositoryState>,
12+
db: Arc<PgDbClient>,
13+
pr: PullRequestData<'_>,
14+
author: &GithubUser,
15+
merge_queue_tx: &MergeQueueSender,
16+
) -> anyhow::Result<()> {
17+
if !has_permission(&repo_state, author, pr, PermissionType::Review).await? {
18+
deny_request(&repo_state, pr.number(), author, PermissionType::Review).await?;
19+
return Ok(());
20+
}
21+
22+
let pr_model = pr.db;
23+
24+
if pr_model.is_stalled() {
25+
db.clear_auto_build(pr_model).await?;
26+
merge_queue_tx.notify().await?;
27+
} else {
28+
notify_of_invalid_retry_state(&repo_state, pr.number()).await?;
29+
}
30+
31+
Ok(())
32+
}
33+
34+
async fn notify_of_invalid_retry_state(
35+
repo: &RepositoryState,
36+
pr_number: PullRequestNumber,
37+
) -> anyhow::Result<()> {
38+
repo.client
39+
.post_comment(
40+
pr_number,
41+
Comment::new(":exclamation: You can only retry pull requests that are approved and have a previously failed auto build".to_string())
42+
)
43+
.await?;
44+
Ok(())
45+
}
46+
47+
#[cfg(test)]
48+
mod tests {
49+
use crate::tests::{BorsTester, Comment, User, run_test};
50+
51+
#[sqlx::test]
52+
async fn retry_command_insufficient_privileges(pool: sqlx::PgPool) {
53+
run_test(pool, async |tester: &mut BorsTester| {
54+
tester
55+
.post_comment(Comment::from("@bors retry").with_author(User::unprivileged()))
56+
.await?;
57+
insta::assert_snapshot!(
58+
tester.get_comment_text(()).await?,
59+
@"@unprivileged-user: :key: Insufficient privileges: not in review users"
60+
);
61+
Ok(())
62+
})
63+
.await;
64+
}
65+
66+
#[sqlx::test]
67+
async fn retry_invalid_state_error(pool: sqlx::PgPool) {
68+
run_test(pool, async |tester: &mut BorsTester| {
69+
tester.post_comment(Comment::from("@bors retry")).await?;
70+
insta::assert_snapshot!(
71+
tester.get_comment_text(()).await?,
72+
@":exclamation: You can only retry pull requests that are approved and have a previously failed auto build"
73+
);
74+
Ok(())
75+
})
76+
.await;
77+
}
78+
79+
#[sqlx::test]
80+
async fn retry_auto_build(pool: sqlx::PgPool) {
81+
run_test(pool, async |tester: &mut BorsTester| {
82+
tester.approve(()).await?;
83+
tester.start_auto_build(()).await?;
84+
tester
85+
.workflow_full_failure(tester.auto_branch().await)
86+
.await?;
87+
tester.expect_comments((), 1).await;
88+
tester.post_comment(Comment::from("@bors retry")).await?;
89+
tester.wait_for_pr((), |pr| pr.auto_build.is_none()).await?;
90+
tester.process_merge_queue().await;
91+
insta::assert_snapshot!(
92+
tester.get_comment_text(()).await?,
93+
@":hourglass: Testing commit pr-1-sha with merge merge-1-pr-1..."
94+
);
95+
Ok(())
96+
})
97+
.await;
98+
}
99+
}

src/bors/handlers/workflow.rs

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,20 @@ pub(super) async fn handle_workflow_completed(
7878
if let Some(running_time) = payload.running_time {
7979
let running_time =
8080
chrono::Duration::to_std(&running_time).unwrap_or(Duration::from_secs(0));
81-
if let Some(min_ci_time) = repo.config.load().min_ci_time {
82-
if running_time < min_ci_time {
83-
tracing::warn!(
84-
"Workflow running time is less than the minimum CI duration: workflow time ({}s) < min time ({}s). Marking it as a failure",
85-
running_time.as_secs_f64(),
86-
min_ci_time.as_secs_f64()
87-
);
88-
payload.status = WorkflowStatus::Failure;
89-
error_context = Some(format!(
90-
"A workflow was considered to be a failure because it took only `{}s`. The minimum duration for CI workflows is configured to be `{}s`.",
91-
running_time.as_secs_f64(),
92-
min_ci_time.as_secs_f64()
93-
))
94-
}
81+
if let Some(min_ci_time) = repo.config.load().min_ci_time
82+
&& running_time < min_ci_time
83+
{
84+
tracing::warn!(
85+
"Workflow running time is less than the minimum CI duration: workflow time ({}s) < min time ({}s). Marking it as a failure",
86+
running_time.as_secs_f64(),
87+
min_ci_time.as_secs_f64()
88+
);
89+
payload.status = WorkflowStatus::Failure;
90+
error_context = Some(format!(
91+
"A workflow was considered to be a failure because it took only `{}s`. The minimum duration for CI workflows is configured to be `{}s`.",
92+
running_time.as_secs_f64(),
93+
min_ci_time.as_secs_f64()
94+
))
9595
}
9696
}
9797

@@ -375,19 +375,16 @@ pub async fn cancel_build(
375375
.await
376376
.map_err(CancelBuildError::FailedToCancelWorkflows)?;
377377

378-
if let Some(check_run_id) = build.check_run_id {
379-
if let Err(error) = client
378+
if let Some(check_run_id) = build.check_run_id
379+
&& let Err(error) = client
380380
.update_check_run(
381381
CheckRunId(check_run_id as u64),
382382
CheckRunStatus::Completed,
383383
Some(check_run_conclusion),
384384
)
385385
.await
386-
{
387-
tracing::error!(
388-
"Could not update check run {check_run_id} for build {build:?}: {error:?}"
389-
);
390-
}
386+
{
387+
tracing::error!("Could not update check run {check_run_id} for build {build:?}: {error:?}");
391388
}
392389

393390
Ok(pending_workflows)

src/database/client.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use crate::github::PullRequestNumber;
1111
use crate::github::{CommitSha, GithubRepoName};
1212

1313
use super::operations::{
14-
approve_pull_request, create_build, create_pull_request, create_workflow,
14+
approve_pull_request, clear_auto_build, create_build, create_pull_request, create_workflow,
1515
delegate_pull_request, delete_tagged_bot_comment, find_build, find_pr_by_build,
1616
get_nonclosed_pull_requests, get_pending_builds, get_prs_with_unknown_mergeability_state,
1717
get_pull_request, get_repository, get_repository_by_name, get_tagged_bot_comments,
@@ -49,6 +49,10 @@ impl PgDbClient {
4949
unapprove_pull_request(&self.pool, pr.id).await
5050
}
5151

52+
pub async fn clear_auto_build(&self, pr: &PullRequestModel) -> anyhow::Result<()> {
53+
clear_auto_build(&self.pool, pr.id).await
54+
}
55+
5256
pub async fn set_priority(&self, pr: &PullRequestModel, priority: u32) -> anyhow::Result<()> {
5357
set_pr_priority(&self.pool, pr.id, priority).await
5458
}

src/database/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,15 @@ impl PullRequestModel {
381381
ApprovalStatus::NotApproved => None,
382382
}
383383
}
384+
385+
/// Approved but with a failed auto build.
386+
pub fn is_stalled(&self) -> bool {
387+
self.is_approved()
388+
&& self
389+
.auto_build
390+
.as_ref()
391+
.is_some_and(|build| build.status.is_failure())
392+
}
384393
}
385394

386395
/// Describes whether a workflow is a Github Actions workflow or if it's a job from some external

src/database/operations.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,3 +1092,19 @@ pub(crate) async fn delete_tagged_bot_comment(
10921092
})
10931093
.await
10941094
}
1095+
1096+
pub(crate) async fn clear_auto_build(
1097+
executor: impl PgExecutor<'_>,
1098+
pr_id: i32,
1099+
) -> anyhow::Result<()> {
1100+
measure_db_query("clear_pr_builds", || async {
1101+
sqlx::query!(
1102+
"UPDATE pull_request SET auto_build_id = NULL WHERE id = $1",
1103+
pr_id
1104+
)
1105+
.execute(executor)
1106+
.await?;
1107+
Ok(())
1108+
})
1109+
.await
1110+
}

0 commit comments

Comments
 (0)