Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new PR review assignment #1745

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ GITHUB_WEBHOOK_SECRET=MUST_BE_CONFIGURED
# `RUSTC_LOG` is not required to run the application, but it makes local development easier
# RUST_LOG=MUST_BE_CONFIGURED

# Flag to enable the new pull request assignment workflow (see pr_prefs_backoffice.md).
# If this env var is UNSET, the old pull request assignment is used (basically random assignment).
# If this env var is SET, the new pull request assignment reads the Rust contributors preferences and assigns PRs accordingly.
USE_NEW_PR_ASSIGNMENT=yes

# A comma separated list of teams that are allowed to use the new PR assignment workflow.
# Used to limit the number of users during the test phase.
# Team name matches names in the rust-lang/team repository:
# https://github.com/rust-lang/team/tree/master/teams
NEW_PR_ASSIGNMENT_TEAMS=compiler,compiler-contributors

# If you are running a bot on non-rustbot account,
# this allows to configure that username which the bot will respond to.
# For example write blahblahblah here, if you want for this bot to
Expand Down
102 changes: 102 additions & 0 deletions src/bin/import-users-from-github.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use reqwest::Client;
use tracing::{debug, info};
// TODO: this should become an HTTP API
// use triagebot::db::notifications::record_username;
use triagebot::github;
use triagebot::github::User;

// TODO: these should become HTTP APIs
// use triagebot::handlers::review_prefs::{add_prefs, delete_prefs, get_prefs};

// Import and synchronization:
// 1. Download teams and retrieve those listed in $NEW_PR_ASSIGNMENT_TEAMS
// 2. Add missing team members to the review preferences table
// 3. Delete preferences for members not present in the team roster anymore

#[tokio::main(flavor = "current_thread")]
async fn main() -> anyhow::Result<()> {
dotenv::dotenv().ok();
tracing_subscriber::fmt::init();

let gh = github::GithubClient::new_with_default_token(Client::new());
let teams_data = triagebot::team_data::teams(&gh).await?;

// 1. get team members
let x = std::env::var("NEW_PR_ASSIGNMENT_TEAMS")
.expect("NEW_PR_ASSIGNMENT_TEAMS env var must be set");
let allowed_teams = x.split(",").collect::<Vec<&str>>();
info!("Will download members for teams: {:?}", allowed_teams);
for team in &allowed_teams {
let members = teams_data.teams.get(*team).unwrap();
let team_members = members
.members
.iter()
.map(|tm| User {
login: tm.github.clone(),
id: Some(tm.github_id as i64),
})
.collect::<Vec<_>>();
debug!("Team {} members loaded: {:?}", team, team_members);

// get team members review capacity
let team_review_prefs = get_prefs(
&db_client,
&team_members
.iter()
.map(|tm| tm.login.clone())
.collect::<Vec<String>>(),
"apiraino",
true,
)
.await;

// 2. Add missing team members to the review preferences table
for member in &team_members {
if !team_review_prefs
.iter()
.find(|rec| rec.username == member.login)
.is_some()
{
debug!(
"Team member {:?} was NOT found in the prefs DB table",
member
);

// ensure this person exists in the users DB table first
let team_member = team_members
.iter()
.find(|m| m.login == member.login)
.expect(&format!(
"Could not find member {:?} in team {}",
member, team
));
let _ = record_username(&db_client, team_member.id.unwrap() as i64, &member.login)
.await;

// Create a record in the review_capacity DB table for this member with some defaults
let _ = add_prefs(&db_client, team_member.id.unwrap() as i64).await?;
info!("Added team member {}", &team_member.login);
}
}

// 3. delete prefs for members not present in the team roster anymore
let removed_members = team_review_prefs
.iter()
.filter(|tm| {
!team_members.contains(&User {
id: Some(tm.user_id),
login: tm.username.clone(),
})
})
.map(|tm| tm.user_id)
.collect::<Vec<i64>>();
if !removed_members.is_empty() {
let _ = delete_prefs(&db_client, &removed_members).await?;
info!("Delete preferences for team members {:?}", &removed_members);
}
info!("Finished updating review prefs for team {}", team);
}

info!("Import/Sync job finished");
Ok(())
}
8 changes: 8 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ pub(crate) struct Config {
pub(crate) note: Option<NoteConfig>,
pub(crate) mentions: Option<MentionsConfig>,
pub(crate) no_merges: Option<NoMergesConfig>,
pub(crate) review_prefs: Option<ReviewPrefsConfig>,
}

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
Expand Down Expand Up @@ -152,6 +153,12 @@ pub(crate) struct ShortcutConfig {
_empty: (),
}

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

#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
pub(crate) struct PrioritizeConfig {
pub(crate) label: String,
Expand Down Expand Up @@ -431,6 +438,7 @@ mod tests {
review_requested: None,
mentions: None,
no_merges: None,
review_prefs: None
}
);
}
Expand Down
15 changes: 15 additions & 0 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,5 +272,20 @@ CREATE UNIQUE INDEX jobs_name_scheduled_at_unique_index
ON jobs (
name, scheduled_at
);
",
"
CREATE table review_capacity (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id BIGINT REFERENCES users(user_id),
active boolean NOT NULL DEFAULT true,
assigned_prs INT[] NOT NULL DEFAULT array[]::INT[],
max_assigned_prs INTEGER NOT NULL DEFAULT 5,
num_assigned_prs INTEGER,
pto_date_start date,
pto_date_end date,
allow_ping_after_days INTEGER NOT NULL DEFAULT 15,
publish_prefs boolean NOT NULL DEFAULT false,
checksum TEXT
);
",
];
2 changes: 1 addition & 1 deletion src/db/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub struct Notification {
pub team_name: Option<String>,
}

pub async fn record_username(db: &DbClient, user_id: i64, username: String) -> anyhow::Result<()> {
pub async fn record_username(db: &DbClient, user_id: i64, username: &str) -> anyhow::Result<()> {
db.execute(
"INSERT INTO users (user_id, username) VALUES ($1, $2) ON CONFLICT DO NOTHING",
&[&user_id, &username],
Expand Down
4 changes: 4 additions & 0 deletions src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ mod notify_zulip;
mod ping;
mod prioritize;
mod relabel;
pub mod review_prefs;
mod review_requested;
mod review_submitted;
mod rfc_helper;
Expand Down Expand Up @@ -158,13 +159,16 @@ macro_rules! issue_handlers {
//
// This is for events that happen only on issues (e.g. label changes).
// Each module in the list must contain the functions `parse_input` and `handle_input`.
// - `parse_input` should parse and validate the input, return an object with everything needed to perform an action
// - `handle_input`: performs the action (optionally) using the input object received
issue_handlers! {
assign,
autolabel,
major_change,
mentions,
no_merges,
notify_zulip,
review_prefs,
review_requested,
}

Expand Down
Loading