Skip to content

Commit

Permalink
Add webhook handler to update PR workload queues
Browse files Browse the repository at this point in the history
  • Loading branch information
apiraino committed Mar 7, 2024
1 parent 4f833ca commit 685e739
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 0 deletions.
8 changes: 8 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ pub(crate) struct Config {
// We want this validation to run even without the entry in the config file
#[serde(default = "ValidateConfig::default")]
pub(crate) validate_config: Option<ValidateConfig>,
pub(crate) pr_tracking: Option<ReviewPrefsConfig>,
}

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
Expand Down Expand Up @@ -317,6 +318,12 @@ pub(crate) struct GitHubReleasesConfig {
pub(crate) changelog_branch: String,
}

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
pub(crate) struct ReviewPrefsConfig {
#[serde(default)]
_empty: (),
}

fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
let cache = CONFIG_CACHE.read().unwrap();
cache.get(repo).and_then(|(config, fetch_time)| {
Expand Down Expand Up @@ -463,6 +470,7 @@ mod tests {
mentions: None,
no_merges: None,
validate_config: Some(ValidateConfig {}),
pr_tracking: None,
}
);
}
Expand Down
1 change: 1 addition & 0 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ CREATE table review_prefs (
assigned_prs INT[] NOT NULL DEFAULT array[]::INT[]
);",
"
CREATE EXTENSION intarray;
CREATE UNIQUE INDEX review_prefs_user_id ON review_prefs(user_id);
",
];
5 changes: 5 additions & 0 deletions src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,11 @@ pub struct Issue {
///
/// Example: `https://github.com/octocat/Hello-World/pull/1347`
pub html_url: String,
// User performing an `action`
pub user: User,
pub labels: Vec<Label>,
// Users assigned to the issue/pr after `action` has been performed
// These are NOT the same as `IssueEvent.assignee`
pub assignees: Vec<User>,
/// Indicator if this is a pull request.
///
Expand Down Expand Up @@ -990,6 +993,8 @@ pub struct IssuesEvent {
pub repository: Repository,
/// The GitHub user that triggered the event.
pub sender: User,
/// Assignee affected by this `action`
pub assignee: User,
}

#[derive(Debug, serde::Deserialize)]
Expand Down
2 changes: 2 additions & 0 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ mod note;
mod notification;
mod notify_zulip;
mod ping;
pub mod pr_tracking;
mod prioritize;
pub mod pull_requests_assignment_update;
mod relabel;
Expand Down Expand Up @@ -168,6 +169,7 @@ issue_handlers! {
no_merges,
notify_zulip,
review_requested,
pr_tracking,
validate_config,
}

Expand Down
105 changes: 105 additions & 0 deletions src/handlers/pr_tracking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//! This module updates the PR workqueue of the Rust project contributors
//!
//! Purpose:
//!
//! - Adds the PR to the workqueue of one team member (when the PR has been assigned)
//! - Removes the PR from the workqueue of one team member (when the PR is unassigned or closed)
use crate::{
config::ReviewPrefsConfig,
db::notifications::record_username,
github::{IssuesAction, IssuesEvent},
handlers::Context,
};
use anyhow::Context as _;
use tokio_postgres::Client as DbClient;

pub(super) struct ReviewPrefsInput {}

pub(super) async fn parse_input(
_ctx: &Context,
event: &IssuesEvent,
config: Option<&ReviewPrefsConfig>,
) -> Result<Option<ReviewPrefsInput>, String> {
// NOTE: this config check MUST exist. Else, the triagebot will emit an error
// about this feature not being enabled
if config.is_none() {
return Ok(None);
};

// Execute this handler only if this is a PR
// and if the action is an assignment or unassignment
if !event.issue.is_pr()
|| !matches!(
event.action,
IssuesAction::Assigned | IssuesAction::Unassigned
)
{
return Ok(None);
}
Ok(Some(ReviewPrefsInput {}))
}

pub(super) async fn handle_input<'a>(
ctx: &Context,
_config: &ReviewPrefsConfig,
event: &IssuesEvent,
_inputs: ReviewPrefsInput,
) -> anyhow::Result<()> {
let db_client = ctx.db.get().await;

// ensure the team member involved in this action exists in the `users` table
record_username(
&db_client,
event.assignee.id.unwrap(),
&event.assignee.login,
)
.await
.context("failed to record username")?;

if event.action == IssuesAction::Unassigned {
delete_pr_from_workqueue(&db_client, event.assignee.id.unwrap(), event.issue.number)
.await
.context("Failed to remove PR from workqueue")?;
}

if event.action == IssuesAction::Assigned {
upsert_pr_into_workqueue(&db_client, event.assignee.id.unwrap(), event.issue.number)
.await
.context("Failed to add PR to workqueue")?;
}

Ok(())
}

/// Add a PR to the workqueue of a team member.
/// Ensures no accidental PR duplicates.
async fn upsert_pr_into_workqueue(
db: &DbClient,
user_id: u64,
pr: u64,
) -> anyhow::Result<u64, anyhow::Error> {
let q = "
INSERT INTO review_prefs
(user_id, assigned_prs) VALUES ($1, $2)
ON CONFLICT (user_id)
DO UPDATE SET assigned_prs = uniq(sort(array_append(review_prefs.assigned_prs, $3)));";
db.execute(q, &[&(user_id as i64), &vec![pr as i32], &(pr as i32)])
.await
.context("Upsert DB error")
}

/// Delete a PR from the workqueue of a team member
async fn delete_pr_from_workqueue(
db: &DbClient,
user_id: u64,
pr: u64,
) -> anyhow::Result<u64, anyhow::Error> {
let q = "
UPDATE review_prefs r
SET assigned_prs = array_remove(r.assigned_prs, $2)
WHERE r.user_id = $1;";
db.execute(q, &[&(user_id as i64), &(pr as i32)])
.await
.context("Update DB error")
}

0 comments on commit 685e739

Please sign in to comment.