Skip to content

Commit 0129fcc

Browse files
authored
Merge pull request #1781 from apiraino/add-webhook-to-update-pr-workload-queues
Add webhook handler to update PR workload queues
2 parents e9fe690 + 1e7a875 commit 0129fcc

File tree

5 files changed

+137
-2
lines changed

5 files changed

+137
-2
lines changed

src/config.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ lazy_static::lazy_static! {
1515
RwLock::new(HashMap::new());
1616
}
1717

18+
// This struct maps each possible option of the triagebot.toml.
19+
// See documentation of options at: https://forge.rust-lang.org/triagebot/pr-assignment.html#configuration
20+
// When adding a new config option to the triagebot.toml, it must be also mapped here
21+
// Will be used by the `issue_handlers!()` or `command_handlers!()` macros.
1822
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
1923
#[serde(rename_all = "kebab-case")]
2024
#[serde(deny_unknown_fields)]
@@ -39,6 +43,7 @@ pub(crate) struct Config {
3943
// We want this validation to run even without the entry in the config file
4044
#[serde(default = "ValidateConfig::default")]
4145
pub(crate) validate_config: Option<ValidateConfig>,
46+
pub(crate) pr_tracking: Option<ReviewPrefsConfig>,
4247
}
4348

4449
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -317,6 +322,12 @@ pub(crate) struct GitHubReleasesConfig {
317322
pub(crate) changelog_branch: String,
318323
}
319324

325+
#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
326+
pub(crate) struct ReviewPrefsConfig {
327+
#[serde(default)]
328+
_empty: (),
329+
}
330+
320331
fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
321332
let cache = CONFIG_CACHE.read().unwrap();
322333
cache.get(repo).and_then(|(config, fetch_time)| {
@@ -463,6 +474,7 @@ mod tests {
463474
mentions: None,
464475
no_merges: None,
465476
validate_config: Some(ValidateConfig {}),
477+
pr_tracking: None,
466478
}
467479
);
468480
}

src/db.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ CREATE table review_prefs (
332332
assigned_prs INT[] NOT NULL DEFAULT array[]::INT[]
333333
);",
334334
"
335+
CREATE EXTENSION intarray;
335336
CREATE UNIQUE INDEX review_prefs_user_id ON review_prefs(user_id);
336337
",
337338
];

src/github.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,11 @@ pub struct Issue {
285285
///
286286
/// Example: `https://github.com/octocat/Hello-World/pull/1347`
287287
pub html_url: String,
288+
// User performing an `action`
288289
pub user: User,
289290
pub labels: Vec<Label>,
291+
// Users assigned to the issue/pr after `action` has been performed
292+
// These are NOT the same as `IssueEvent.assignee`
290293
pub assignees: Vec<User>,
291294
/// Indicator if this is a pull request.
292295
///
@@ -953,8 +956,14 @@ pub enum IssuesAction {
953956
Unpinned,
954957
Closed,
955958
Reopened,
956-
Assigned,
957-
Unassigned,
959+
Assigned {
960+
/// Github users assigned to the issue / pull request
961+
assignee: User,
962+
},
963+
Unassigned {
964+
/// Github users removed from the issue / pull request
965+
assignee: User,
966+
},
958967
Labeled {
959968
/// The label added from the issue
960969
label: Label,

src/handlers.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ mod note;
3838
mod notification;
3939
mod notify_zulip;
4040
mod ping;
41+
pub mod pr_tracking;
4142
mod prioritize;
4243
pub mod pull_requests_assignment_update;
4344
mod relabel;
@@ -168,6 +169,7 @@ issue_handlers! {
168169
no_merges,
169170
notify_zulip,
170171
review_requested,
172+
pr_tracking,
171173
validate_config,
172174
}
173175

src/handlers/pr_tracking.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
//! This module updates the PR workqueue of the Rust project contributors
2+
//!
3+
//! Purpose:
4+
//!
5+
//! - Adds the PR to the workqueue of one team member (when the PR has been assigned)
6+
//! - Removes the PR from the workqueue of one team member (when the PR is unassigned or closed)
7+
8+
use crate::{
9+
config::ReviewPrefsConfig,
10+
db::notifications::record_username,
11+
github::{IssuesAction, IssuesEvent},
12+
handlers::Context,
13+
};
14+
use anyhow::Context as _;
15+
use tokio_postgres::Client as DbClient;
16+
17+
pub(super) struct ReviewPrefsInput {}
18+
19+
pub(super) async fn parse_input(
20+
_ctx: &Context,
21+
event: &IssuesEvent,
22+
config: Option<&ReviewPrefsConfig>,
23+
) -> Result<Option<ReviewPrefsInput>, String> {
24+
// NOTE: this config check MUST exist. Else, the triagebot will emit an error
25+
// about this feature not being enabled
26+
if config.is_none() {
27+
return Ok(None);
28+
};
29+
30+
// Execute this handler only if this is a PR ...
31+
if !event.issue.is_pr() {
32+
return Ok(None);
33+
}
34+
35+
// ... and if the action is an assignment or unassignment with an assignee
36+
match event.action {
37+
IssuesAction::Assigned { .. } | IssuesAction::Unassigned { .. } => {
38+
Ok(Some(ReviewPrefsInput {}))
39+
}
40+
_ => Ok(None),
41+
}
42+
}
43+
44+
pub(super) async fn handle_input<'a>(
45+
ctx: &Context,
46+
_config: &ReviewPrefsConfig,
47+
event: &IssuesEvent,
48+
_inputs: ReviewPrefsInput,
49+
) -> anyhow::Result<()> {
50+
let db_client = ctx.db.get().await;
51+
52+
// extract the assignee matching the assignment or unassignment enum variants or return and ignore this handler
53+
let IssuesEvent {
54+
action: IssuesAction::Assigned { assignee } | IssuesAction::Unassigned { assignee },
55+
..
56+
} = event
57+
else {
58+
return Ok(());
59+
};
60+
61+
// ensure the team member object of this action exists in the `users` table
62+
record_username(&db_client, assignee.id.unwrap(), &assignee.login)
63+
.await
64+
.context("failed to record username")?;
65+
66+
if matches!(event.action, IssuesAction::Unassigned { .. }) {
67+
delete_pr_from_workqueue(&db_client, assignee.id.unwrap(), event.issue.number)
68+
.await
69+
.context("Failed to remove PR from workqueue")?;
70+
}
71+
72+
if matches!(event.action, IssuesAction::Assigned { .. }) {
73+
upsert_pr_into_workqueue(&db_client, assignee.id.unwrap(), event.issue.number)
74+
.await
75+
.context("Failed to add PR to workqueue")?;
76+
}
77+
78+
Ok(())
79+
}
80+
81+
/// Add a PR to the workqueue of a team member.
82+
/// Ensures no accidental PR duplicates.
83+
async fn upsert_pr_into_workqueue(
84+
db: &DbClient,
85+
user_id: u64,
86+
pr: u64,
87+
) -> anyhow::Result<u64, anyhow::Error> {
88+
let q = "
89+
INSERT INTO review_prefs
90+
(user_id, assigned_prs) VALUES ($1, $2)
91+
ON CONFLICT (user_id)
92+
DO UPDATE SET assigned_prs = uniq(sort(array_append(review_prefs.assigned_prs, $3)));";
93+
db.execute(q, &[&(user_id as i64), &vec![pr as i32], &(pr as i32)])
94+
.await
95+
.context("Upsert DB error")
96+
}
97+
98+
/// Delete a PR from the workqueue of a team member
99+
async fn delete_pr_from_workqueue(
100+
db: &DbClient,
101+
user_id: u64,
102+
pr: u64,
103+
) -> anyhow::Result<u64, anyhow::Error> {
104+
let q = "
105+
UPDATE review_prefs r
106+
SET assigned_prs = array_remove(r.assigned_prs, $2)
107+
WHERE r.user_id = $1;";
108+
db.execute(q, &[&(user_id as i64), &(pr as i32)])
109+
.await
110+
.context("Update DB error")
111+
}

0 commit comments

Comments
 (0)