Skip to content

Commit 46975c9

Browse files
authored
Merge pull request #858 from Kobzol/approval-upgrade
Upgrade tentative approvals if the PR was already fully approved
2 parents 7ebb8fb + dfbba15 commit 46975c9

3 files changed

Lines changed: 112 additions & 30 deletions

File tree

‎src/bors/approval.rs‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ pub(super) fn check_unknown_reviewers(repo: &RepositoryState, approvers: &str) -
1818
.collect()
1919
}
2020

21+
/// Note that can be attached to an approval comment.
22+
pub enum ApprovalNote {
23+
/// The pull request was force approved while its PR CI is failing.
24+
PrCiIsFailing,
25+
/// A previously fully approved PR was approved again *tentatively*.
26+
/// This tentative approval was automatically upgraded to a full approval.
27+
TentativeApprovalUpgraded,
28+
}
29+
2130
/// Finalize an approval and prepare the pull request for the merge queue.
2231
///
2332
/// Clears any failed auto build so it can be retried, wakes the merge queue, posts the approval
@@ -30,17 +39,17 @@ pub(super) async fn finalize_approval(
3039
approver: &str,
3140
priority: Option<u32>,
3241
merge_queue_tx: &MergeQueueSender,
33-
failed_pr_ci: bool,
42+
note: Option<ApprovalNote>,
3443
) -> anyhow::Result<()> {
3544
let unknown_reviewers = check_unknown_reviewers(repo, approver);
36-
let was_failed = pr
45+
let had_failed_auto_build = pr
3746
.db
3847
.auto_build
3948
.as_ref()
4049
.map(|b| b.status.is_failure())
4150
.unwrap_or(false);
4251
// Re-approval should act as a retry
43-
if was_failed {
52+
if had_failed_auto_build {
4453
ctx.db.clear_auto_build(pr.db).await?;
4554
}
4655

@@ -74,8 +83,8 @@ pub(super) async fn finalize_approval(
7483
approver,
7584
unknown_reviewers,
7685
tree_state,
77-
was_failed,
78-
failed_pr_ci,
86+
had_failed_auto_build,
87+
note,
7988
),
8089
&ctx.db,
8190
)
@@ -142,6 +151,6 @@ pub(super) async fn try_resolve_tentative_approval(
142151
}
143152

144153
ctx.db.confirm_tentative_approval(pr.db).await?;
145-
finalize_approval(ctx, repo, pr, approver, priority, merge_queue_tx, false).await?;
154+
finalize_approval(ctx, repo, pr, approver, priority, merge_queue_tx, None).await?;
146155
Ok(TentativeApprovalOutcome::Resolved)
147156
}

‎src/bors/comment.rs‎

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::bors::approval::ApprovalNote;
12
use crate::bors::command::CommandPrefix;
23
use crate::bors::{FailedWorkflowRun, WorkflowRun};
34
use crate::database::PullRequestModel;
@@ -296,8 +297,8 @@ pub fn approved_comment(
296297
reviewer: &str,
297298
unknown_reviewers: Vec<String>,
298299
tree_state: TreeState,
299-
was_failed: bool,
300-
failed_pr_ci: bool,
300+
had_failed_auto_build: bool,
301+
note: Option<ApprovalNote>,
301302
) -> Comment {
302303
let approve_emoji = if is_holiday_season() {
303304
"star2"
@@ -312,20 +313,38 @@ It is now in the [queue]({web_url}/queue/{}) for this repository.
312313
repo.name()
313314
);
314315

315-
if was_failed {
316+
if had_failed_auto_build {
316317
writeln!(
317318
comment,
318319
"\nA failed build status on this PR was cleared due to the approval."
319320
)
320321
.unwrap();
321322
}
322323

323-
if failed_pr_ci {
324-
writeln!(
325-
comment,
326-
"\n> [!WARNING]\n> This PR was force-approved despite failing PR CI."
327-
)
328-
.unwrap();
324+
if let Some(note) = note {
325+
match note {
326+
ApprovalNote::PrCiIsFailing => {
327+
writeln!(
328+
comment,
329+
"\n> [!WARNING]\n> This PR was force-approved despite failing PR CI."
330+
)
331+
.unwrap();
332+
}
333+
ApprovalNote::TentativeApprovalUpgraded => {
334+
writeln!(
335+
comment,
336+
r#"
337+
> [!WARNING]
338+
> This PR was already fully approved previously, so this tentative approval was treated as a full approval. If you want to instead downgrade the PR to be only tentatively approved, unapprove it first and then re-approve it again:
339+
> ```
340+
> @bors r-
341+
> @bors r+
342+
> ```
343+
"#
344+
)
345+
.unwrap();
346+
}
347+
}
329348
}
330349

331350
if !unknown_reviewers.is_empty() {

‎src/bors/handlers/review.rs‎

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::bors::RepositoryState;
22
use crate::bors::approval::{
3-
TentativeApprovalOutcome, check_unknown_reviewers, finalize_approval,
3+
ApprovalNote, TentativeApprovalOutcome, check_unknown_reviewers, finalize_approval,
44
try_resolve_tentative_approval,
55
};
66
use crate::bors::command::{Approver, CommandPrefix, Delegatee};
@@ -84,6 +84,18 @@ pub(super) async fn command_approve(
8484
sha: pr.github.head.sha.to_string(),
8585
};
8686

87+
// It is possible that the PR was already (fully) approved before.
88+
// If we are now doing a tentative approval, we will "upgrade" it to a full approval, which is
89+
// usually what the user wants.
90+
// This situation should be very rare anyway.
91+
let already_approved = pr.db.is_approved();
92+
let (approval_mode, approval_upgraded) =
93+
if already_approved && matches!(approval_mode, ApprovalMode::Tentative) {
94+
(ApprovalMode::Eager, true)
95+
} else {
96+
(approval_mode, false)
97+
};
98+
8799
db.approve(
88100
pr.db,
89101
approval_info,
@@ -130,18 +142,27 @@ pub(super) async fn command_approve(
130142
Ok(())
131143
}
132144
ApprovalMode::Eager => {
133-
let failed_pr_ci = match repo_state
134-
.client
135-
.get_workflow_runs_for_commit_sha(WorkflowSource::PullRequest(pr.github))
136-
.await
137-
{
138-
Ok(runs) => runs.iter().any(|run| run.status == WorkflowStatus::Failure),
139-
Err(error) => {
140-
tracing::warn!(
141-
"Failed to get pull request CI status for commit {}: {error:?}",
142-
pr.github.head.sha
143-
);
144-
false
145+
let note = if approval_upgraded {
146+
Some(ApprovalNote::TentativeApprovalUpgraded)
147+
} else {
148+
let pr_ci_fails = match repo_state
149+
.client
150+
.get_workflow_runs_for_commit_sha(WorkflowSource::PullRequest(pr.github))
151+
.await
152+
{
153+
Ok(runs) => runs.iter().any(|run| run.status == WorkflowStatus::Failure),
154+
Err(error) => {
155+
tracing::warn!(
156+
"Failed to get pull request CI status for commit {}: {error:?}",
157+
pr.github.head.sha
158+
);
159+
false
160+
}
161+
};
162+
if pr_ci_fails {
163+
Some(ApprovalNote::PrCiIsFailing)
164+
} else {
165+
None
145166
}
146167
};
147168
finalize_approval(
@@ -151,7 +172,7 @@ pub(super) async fn command_approve(
151172
&approver,
152173
priority,
153174
merge_queue_tx,
154-
failed_pr_ci,
175+
note,
155176
)
156177
.await
157178
}
@@ -2302,7 +2323,7 @@ labels_blocking_approval = ["proposed-final-comment-period", "final-comment-peri
23022323
ctx.start_auto_build(()).await?;
23032324
ctx.workflow_full_failure(ctx.auto_workflow()).await?;
23042325
ctx.expect_comments((), 1).await; // build failed
2305-
ctx.post_comment("@bors r+").await?;
2326+
ctx.post_comment("@bors r+ force").await?;
23062327
insta::assert_snapshot!(ctx.get_next_comment_text(()).await?, @"
23072328
:pushpin: Commit pr-1-sha has been approved by `default-user`
23082329
@@ -2316,4 +2337,37 @@ labels_blocking_approval = ["proposed-final-comment-period", "final-comment-peri
23162337
})
23172338
.await;
23182339
}
2340+
2341+
#[sqlx::test(migrator = "crate::MIGRATOR")]
2342+
async fn tentative_approval_upgrade(pool: sqlx::PgPool) {
2343+
run_test(pool, async |ctx: &mut BorsTester| {
2344+
// Full approval
2345+
ctx.post_comment("@bors r+ force").await?;
2346+
ctx.expect_comments((), 1).await;
2347+
2348+
// Cause PR CI to fail
2349+
ctx.pr_workflow_failure(ctx.pr_ci_workflow(())).await?;
2350+
2351+
// Tentative approval
2352+
ctx.post_comment("@bors r+").await?;
2353+
insta::assert_snapshot!(ctx.get_next_comment_text(()).await?, @"
2354+
:pushpin: Commit pr-1-sha has been approved by `default-user`
2355+
2356+
It is now in the [queue](https://bors-test.com/queue/borstest) for this repository.
2357+
2358+
> [!WARNING]
2359+
> This PR was already fully approved previously, so this tentative approval was treated as a full approval. If you want to instead downgrade the PR to be only tentatively approved, unapprove it first and then re-approve it again:
2360+
> ```
2361+
> @bors r-
2362+
> @bors r+
2363+
> ```
2364+
");
2365+
2366+
// The tentative approval should not unapprove the PR
2367+
ctx.pr(()).await.expect_approved_by(&User::default_pr_author().name);
2368+
2369+
Ok(())
2370+
})
2371+
.await;
2372+
}
23192373
}

0 commit comments

Comments
 (0)