Skip to content

Commit 08378e2

Browse files
authored
Merge pull request #115 from vohoanglong0107/feat-unapprove-on-edit
feat: unapprove on PR's base changed
2 parents 83e7c77 + 6da5a4f commit 08378e2

File tree

7 files changed

+292
-7
lines changed

7 files changed

+292
-7
lines changed

src/bors/event.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use crate::database::{WorkflowStatus, WorkflowType};
2-
use crate::github::{CommitSha, GithubRepoName, GithubUser, PullRequestNumber};
2+
use crate::github::{CommitSha, GithubRepoName, GithubUser, PullRequest, PullRequestNumber};
33
use octocrab::models::RunId;
44

55
#[derive(Debug)]
66
pub enum BorsRepositoryEvent {
77
/// A comment was posted on a pull request.
88
Comment(PullRequestComment),
9+
/// When the pull request is edited by its author
10+
PullRequestEdited(PullRequestEdited),
911
/// A workflow run on Github Actions or a check run from external CI system has been started.
1012
WorkflowStarted(WorkflowStarted),
1113
/// A workflow run on Github Actions or a check run from external CI system has been completed.
@@ -22,6 +24,7 @@ impl BorsRepositoryEvent {
2224
BorsRepositoryEvent::WorkflowStarted(workflow) => &workflow.repository,
2325
BorsRepositoryEvent::WorkflowCompleted(workflow) => &workflow.repository,
2426
BorsRepositoryEvent::CheckSuiteCompleted(payload) => &payload.repository,
27+
BorsRepositoryEvent::PullRequestEdited(payload) => &payload.repository,
2528
}
2629
}
2730
}
@@ -50,6 +53,13 @@ pub struct PullRequestComment {
5053
pub text: String,
5154
}
5255

56+
#[derive(Debug)]
57+
pub struct PullRequestEdited {
58+
pub repository: GithubRepoName,
59+
pub pull_request: PullRequest,
60+
pub from_base_sha: Option<CommitSha>,
61+
}
62+
5363
#[derive(Debug)]
5464
pub struct WorkflowStarted {
5565
pub repository: GithubRepoName,

src/bors/handlers/mod.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use crate::bors::event::{BorsGlobalEvent, BorsRepositoryEvent, PullRequestCommen
88
use crate::bors::handlers::help::command_help;
99
use crate::bors::handlers::ping::command_ping;
1010
use crate::bors::handlers::refresh::refresh_repository;
11-
use crate::bors::handlers::review::{command_approve, command_unapprove};
11+
use crate::bors::handlers::review::{
12+
command_approve, command_unapprove, handle_pull_request_edited,
13+
};
1214
use crate::bors::handlers::trybuild::{command_try_build, command_try_cancel, TRY_BRANCH_NAME};
1315
use crate::bors::handlers::workflow::{
1416
handle_check_suite_completed, handle_workflow_completed, handle_workflow_started,
@@ -112,6 +114,14 @@ pub async fn handle_bors_repository_event(
112114
.instrument(span.clone())
113115
.await?;
114116
}
117+
BorsRepositoryEvent::PullRequestEdited(payload) => {
118+
let span =
119+
tracing::info_span!("Pull request edited", repo = payload.repository.to_string());
120+
121+
handle_pull_request_edited(repo, db, payload)
122+
.instrument(span.clone())
123+
.await?;
124+
}
115125
}
116126
Ok(())
117127
}

src/bors/handlers/review.rs

Lines changed: 157 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
use std::sync::Arc;
22

33
use crate::bors::command::Approver;
4+
use crate::bors::event::PullRequestEdited;
45
use crate::bors::handlers::labels::handle_label_trigger;
56
use crate::bors::Comment;
67
use crate::bors::RepositoryState;
8+
use crate::github::CommitSha;
79
use crate::github::GithubUser;
810
use crate::github::LabelTrigger;
911
use crate::github::PullRequest;
12+
use crate::github::PullRequestNumber;
1013
use crate::permissions::PermissionType;
1114
use crate::PgDbClient;
1215

@@ -52,6 +55,29 @@ pub(super) async fn command_unapprove(
5255
notify_of_unapproval(&repo_state, pr).await
5356
}
5457

58+
pub(super) async fn handle_pull_request_edited(
59+
repo_state: Arc<RepositoryState>,
60+
db: Arc<PgDbClient>,
61+
payload: PullRequestEdited,
62+
) -> anyhow::Result<()> {
63+
// If the base branch has changed, unapprove the PR
64+
let Some(_) = payload.from_base_sha else {
65+
return Ok(());
66+
};
67+
68+
let pr_model = db
69+
.get_or_create_pull_request(repo_state.repository(), payload.pull_request.number)
70+
.await?;
71+
if pr_model.approved_by.is_none() {
72+
return Ok(());
73+
}
74+
75+
let pr_number = payload.pull_request.number;
76+
db.unapprove(repo_state.repository(), pr_number).await?;
77+
handle_label_trigger(&repo_state, pr_number, LabelTrigger::Unapproved).await?;
78+
notify_of_edited_pr(&repo_state, pr_number, payload.pull_request.head.sha).await
79+
}
80+
5581
fn sufficient_approve_permission(repo: Arc<RepositoryState>, author: &GithubUser) -> bool {
5682
repo.permissions
5783
.load()
@@ -135,12 +161,30 @@ async fn notify_of_unapproval(repo: &RepositoryState, pr: &PullRequest) -> anyho
135161
.await
136162
}
137163

164+
async fn notify_of_edited_pr(
165+
repo: &RepositoryState,
166+
pr_number: PullRequestNumber,
167+
head_sha: CommitSha,
168+
) -> anyhow::Result<()> {
169+
repo.client
170+
.post_comment(
171+
pr_number,
172+
Comment::new(format!(
173+
r#":warning: The base branch changed to `{}`, and the
174+
PR will need to be re-approved."#,
175+
head_sha
176+
)),
177+
)
178+
.await
179+
}
180+
138181
#[cfg(test)]
139182
mod tests {
140183
use crate::{
141184
github::PullRequestNumber,
142185
tests::mocks::{
143-
default_pr_number, default_repo_name, BorsBuilder, BorsTester, Permissions, User, World,
186+
default_pr_number, default_repo_name, BorsBuilder, BorsTester, Permissions,
187+
PullRequestChangeEvent, User, World,
144188
},
145189
};
146190

@@ -226,7 +270,6 @@ approve = ["+approved"]
226270
}
227271

228272
#[sqlx::test]
229-
#[tracing_test::traced_test]
230273
async fn unapprove(pool: sqlx::PgPool) {
231274
let world = World::default();
232275
world.default_repo().lock().set_config(
@@ -264,6 +307,118 @@ approve = ["+approved"]
264307
.await;
265308
}
266309

310+
#[sqlx::test]
311+
async fn unapprove_on_base_edited(pool: sqlx::PgPool) {
312+
let world = World::default();
313+
world.default_repo().lock().set_config(
314+
r#"
315+
[labels]
316+
approve = ["+approved"]
317+
"#,
318+
);
319+
BorsBuilder::new(pool)
320+
.world(world)
321+
.run_test(|mut tester| async {
322+
tester.post_comment("@bors r+").await?;
323+
assert_eq!(
324+
tester.get_comment().await?,
325+
format!(
326+
"Commit pr-{}-sha has been approved by `{}`",
327+
default_pr_number(),
328+
User::default_user().name
329+
),
330+
);
331+
tester
332+
.edit_pull_request(
333+
default_pr_number(),
334+
PullRequestChangeEvent {
335+
from_base_sha: Some("main-sha".to_string()),
336+
},
337+
)
338+
.await?;
339+
340+
assert_eq!(
341+
tester.get_comment().await?,
342+
format!(
343+
r#":warning: The base branch changed to `pr-{}-sha`, and the
344+
PR will need to be re-approved."#,
345+
default_pr_number()
346+
)
347+
);
348+
check_pr_unapproved(&tester, default_pr_number().into()).await;
349+
Ok(tester)
350+
})
351+
.await;
352+
}
353+
354+
#[sqlx::test]
355+
async fn edit_pr_do_nothing_when_base_not_edited(pool: sqlx::PgPool) {
356+
let world = World::default();
357+
world.default_repo().lock().set_config(
358+
r#"
359+
[labels]
360+
approve = ["+approved"]
361+
"#,
362+
);
363+
BorsBuilder::new(pool)
364+
.world(world)
365+
.run_test(|mut tester| async {
366+
tester.post_comment("@bors r+").await?;
367+
assert_eq!(
368+
tester.get_comment().await?,
369+
format!(
370+
"Commit pr-{}-sha has been approved by `{}`",
371+
default_pr_number(),
372+
User::default_user().name
373+
),
374+
);
375+
tester
376+
.edit_pull_request(
377+
default_pr_number(),
378+
PullRequestChangeEvent {
379+
from_base_sha: None,
380+
},
381+
)
382+
.await?;
383+
384+
check_pr_approved_by(
385+
&tester,
386+
default_pr_number().into(),
387+
&User::default_user().name,
388+
)
389+
.await;
390+
Ok(tester)
391+
})
392+
.await;
393+
}
394+
395+
#[sqlx::test]
396+
async fn edit_pr_do_nothing_when_not_approved(pool: sqlx::PgPool) {
397+
let world = World::default();
398+
world.default_repo().lock().set_config(
399+
r#"
400+
[labels]
401+
approve = ["+approved"]
402+
"#,
403+
);
404+
BorsBuilder::new(pool)
405+
.world(world)
406+
.run_test(|mut tester| async {
407+
tester
408+
.edit_pull_request(
409+
default_pr_number(),
410+
PullRequestChangeEvent {
411+
from_base_sha: Some("main-sha".to_string()),
412+
},
413+
)
414+
.await?;
415+
416+
// No comment should be posted
417+
Ok(tester)
418+
})
419+
.await;
420+
}
421+
267422
async fn check_pr_approved_by(
268423
tester: &BorsTester,
269424
pr_number: PullRequestNumber,

src/github/webhook.rs

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use axum::http::{HeaderMap, HeaderValue, StatusCode};
88
use axum::{async_trait, RequestExt};
99
use hmac::{Hmac, Mac};
1010
use octocrab::models::events::payload::{
11-
IssueCommentEventAction, IssueCommentEventPayload, PullRequestReviewCommentEventAction,
11+
IssueCommentEventAction, IssueCommentEventPayload, PullRequestEventAction,
12+
PullRequestEventChangesFrom, PullRequestReviewCommentEventAction,
1213
PullRequestReviewCommentEventPayload,
1314
};
1415
use octocrab::models::pulls::{PullRequest, Review};
@@ -18,7 +19,7 @@ use sha2::Sha256;
1819

1920
use crate::bors::event::{
2021
BorsEvent, BorsGlobalEvent, BorsRepositoryEvent, CheckSuiteCompleted, PullRequestComment,
21-
WorkflowCompleted, WorkflowStarted,
22+
PullRequestEdited, WorkflowCompleted, WorkflowStarted,
2223
};
2324
use crate::database::{WorkflowStatus, WorkflowType};
2425
use crate::github::server::ServerStateRef;
@@ -90,6 +91,26 @@ pub struct WebhookPullRequestReviewEvent<'a> {
9091
sender: Author,
9192
}
9293

94+
/// Similar to PullRequestEvent from octocrab, but changes field also includes base sha.
95+
/// https://docs.github.com/en/webhooks/webhook-events-and-payloads#pull_request
96+
#[derive(Debug, serde::Deserialize)]
97+
struct WebhookPullRequestEvent {
98+
action: PullRequestEventAction,
99+
pull_request: PullRequest,
100+
changes: Option<WebhookPullRequestChanges>,
101+
repository: Repository,
102+
}
103+
104+
#[derive(Debug, serde::Deserialize)]
105+
struct WebhookPullRequestChanges {
106+
base: Option<WebhookPullRequestBaseChanges>,
107+
}
108+
109+
#[derive(Debug, serde::Deserialize)]
110+
struct WebhookPullRequestBaseChanges {
111+
sha: Option<PullRequestEventChangesFrom>,
112+
}
113+
93114
/// axum extractor for GitHub webhook events.
94115
#[derive(Debug)]
95116
pub struct GitHubWebhook(pub BorsEvent);
@@ -149,6 +170,7 @@ fn parse_webhook_event(request: Parts, body: &[u8]) -> anyhow::Result<Option<Bor
149170

150171
match event_type.as_bytes() {
151172
b"issue_comment" => parse_issue_comment_event(body),
173+
b"pull_request" => parse_pull_request_events(body),
152174
b"pull_request_review" => parse_pull_request_review_events(body),
153175
b"pull_request_review_comment" => parse_pull_request_review_comment_events(body),
154176
b"installation_repositories" | b"installation" => Ok(Some(BorsEvent::Global(
@@ -179,6 +201,30 @@ fn parse_issue_comment_event(body: &[u8]) -> anyhow::Result<Option<BorsEvent>> {
179201
}
180202
}
181203

204+
fn parse_pull_request_events(body: &[u8]) -> anyhow::Result<Option<BorsEvent>> {
205+
let payload: WebhookPullRequestEvent = serde_json::from_slice(body)?;
206+
let repository_name = parse_repository_name(&payload.repository)?;
207+
if payload.action == PullRequestEventAction::Edited {
208+
let Some(changes) = payload.changes else {
209+
return Err(anyhow::anyhow!(
210+
"Edited pull request event should have `changes` field"
211+
));
212+
};
213+
Ok(Some(BorsEvent::Repository(
214+
BorsRepositoryEvent::PullRequestEdited(PullRequestEdited {
215+
repository: repository_name,
216+
pull_request: payload.pull_request.into(),
217+
from_base_sha: changes
218+
.base
219+
.and_then(|base| base.sha)
220+
.map(|sha| CommitSha(sha.from)),
221+
}),
222+
)))
223+
} else {
224+
Ok(None)
225+
}
226+
}
227+
182228
fn parse_pull_request_review_events(body: &[u8]) -> anyhow::Result<Option<BorsEvent>> {
183229
let payload: WebhookPullRequestReviewEvent = serde_json::from_slice(body)?;
184230
if payload.action == "submitted" {

src/tests/mocks/bors.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ use crate::{
2828
ServerState, WebhookSecret,
2929
};
3030

31+
use super::pull_request::{GitHubPullRequestEventPayload, PullRequestChangeEvent};
32+
3133
pub struct BorsBuilder {
3234
world: World,
3335
pool: PgPool,
@@ -266,6 +268,18 @@ impl BorsTester {
266268
self.webhook_check_suite(check_suite.into()).await
267269
}
268270

271+
pub async fn edit_pull_request(
272+
&mut self,
273+
pr_number: u64,
274+
changes: PullRequestChangeEvent,
275+
) -> anyhow::Result<()> {
276+
self.send_webhook(
277+
"pull_request",
278+
GitHubPullRequestEventPayload::new(pr_number, "edited".to_string(), Some(changes)),
279+
)
280+
.await
281+
}
282+
269283
async fn webhook_comment(&mut self, comment: Comment) -> anyhow::Result<()> {
270284
self.send_webhook(
271285
"issue_comment",

src/tests/mocks/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub use bors::BorsTester;
1818
pub use comment::Comment;
1919
pub use permissions::Permissions;
2020
pub use pull_request::default_pr_number;
21+
pub use pull_request::PullRequestChangeEvent;
2122
pub use repository::default_repo_name;
2223
pub use repository::Branch;
2324
pub use repository::Repo;

0 commit comments

Comments
 (0)