Skip to content

Commit a75633d

Browse files
committed
Import pull request assignments into triagebot
General overview at: #1753 - Added a new DB table with the fields to track how many PRs are assigned to a contributor - Initial DB table population with a one-off job, manually run.
1 parent c52016a commit a75633d

File tree

11 files changed

+313
-9
lines changed

11 files changed

+313
-9
lines changed

github-graphql/PullRequestsOpen.gql

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
query PullRequestsOpen ($repo_owner: String!, $repo_name: String!, $after: String) {
2+
repository(owner: $repo_owner, name: $repo_name) {
3+
pullRequests(first: 100, after: $after, states:OPEN) {
4+
pageInfo {
5+
hasNextPage
6+
endCursor
7+
}
8+
nodes {
9+
number
10+
updatedAt
11+
createdAt
12+
assignees(first: 10) {
13+
nodes {
14+
login
15+
databaseId
16+
}
17+
}
18+
labels(first:5, orderBy:{field:NAME, direction:DESC}) {
19+
nodes {
20+
name
21+
}
22+
}
23+
}
24+
}
25+
}
26+
}

github-graphql/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# How to use GraphQL with Rust
2+
3+
# GUI Clients (Electron apps)
4+
5+
Use a client to experiment and build your GraphQL query/mutation.
6+
7+
https://insomnia.rest/download
8+
9+
https://docs.usebruno.com
10+
11+
Once you're happy with the result, save your query in a `<query>.gql` file in this directory. It will serve as
12+
documentation on how to reproduce the Rust boilerplate.
13+
14+
# Cynic CLI
15+
16+
Introspect a schema and save it locally:
17+
18+
```sh
19+
cynic introspect \
20+
-H "User-Agent: cynic/3.4.3" \
21+
-H "Authorization: Bearer [GITHUB_TOKEN]" \
22+
"https://api.github.com/graphql" \
23+
-o schemas/github.graphql
24+
```
25+
26+
Execute a GraphQL query/mutation and save locally the Rust boilerplate:
27+
28+
``` sh
29+
cynic querygen --schema schemas/github.graphql --query query.gql
30+
```
31+

github-graphql/src/lib.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ pub mod queries {
8989
#[derive(cynic::QueryFragment, Debug)]
9090
pub struct User {
9191
pub login: String,
92+
pub database_id: Option<i32>,
9293
}
9394

9495
#[derive(cynic::QueryFragment, Debug)]
@@ -385,3 +386,44 @@ pub mod project_items {
385386
pub date: Option<Date>,
386387
}
387388
}
389+
390+
/// Retrieve all pull requests waiting on review from T-compiler
391+
/// GraphQL query: see file github-graphql/PullRequestsOpen.gql
392+
pub mod pull_requests_open {
393+
use crate::queries::{LabelConnection, PullRequestConnection, UserConnection};
394+
395+
use super::queries::DateTime;
396+
use super::schema;
397+
398+
#[derive(cynic::QueryVariables, Clone, Debug)]
399+
pub struct PullRequestsOpenVariables<'a> {
400+
pub repo_owner: &'a str,
401+
pub repo_name: &'a str,
402+
pub after: Option<String>,
403+
}
404+
405+
#[derive(cynic::QueryFragment, Debug)]
406+
#[cynic(graphql_type = "Query", variables = "PullRequestsOpenVariables")]
407+
pub struct PullRequestsOpen {
408+
#[arguments(owner: $repo_owner, name: $repo_name)]
409+
pub repository: Option<Repository>,
410+
}
411+
412+
#[derive(cynic::QueryFragment, Debug)]
413+
#[cynic(variables = "PullRequestsOpenVariables")]
414+
pub struct Repository {
415+
#[arguments(first: 100, after: $after, states: "OPEN")]
416+
pub pull_requests: PullRequestConnection,
417+
}
418+
419+
#[derive(cynic::QueryFragment, Debug)]
420+
pub struct PullRequest {
421+
pub number: i32,
422+
pub updated_at: DateTime,
423+
pub created_at: DateTime,
424+
#[arguments(first: 10)]
425+
pub assignees: UserConnection,
426+
#[arguments(first: 5, orderBy: { direction: "DESC", field: "NAME" })]
427+
pub labels: Option<LabelConnection>,
428+
}
429+
}

src/db.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,18 @@ CREATE TABLE jobs (
320320
);
321321
",
322322
"
323-
CREATE UNIQUE INDEX jobs_name_scheduled_at_unique_index
323+
CREATE UNIQUE INDEX jobs_name_scheduled_at_unique_index
324324
ON jobs (
325325
name, scheduled_at
326326
);
327327
",
328+
"
329+
CREATE table review_prefs (
330+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
331+
user_id BIGINT REFERENCES users(user_id),
332+
assigned_prs INT[] NOT NULL DEFAULT array[]::INT[]
333+
);",
334+
"
335+
CREATE UNIQUE INDEX review_prefs_user_id ON review_prefs(user_id);
336+
",
328337
];

src/db/notifications.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ pub struct Notification {
1515
pub team_name: Option<String>,
1616
}
1717

18-
pub async fn record_username(db: &DbClient, user_id: u64, username: String) -> anyhow::Result<()> {
18+
/// Add a new user (if not existing)
19+
pub async fn record_username(db: &DbClient, user_id: u64, username: &str) -> anyhow::Result<()> {
1920
db.execute(
2021
"INSERT INTO users (user_id, username) VALUES ($1, $2) ON CONFLICT DO NOTHING",
2122
&[&(user_id as i64), &username],

src/github.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2592,6 +2592,90 @@ async fn project_items_by_status(
25922592
Ok(all_items)
25932593
}
25942594

2595+
/// Retrieve all pull requests in status OPEN that are not drafts
2596+
pub async fn retrieve_pull_requests(
2597+
repo: &Repository,
2598+
client: &GithubClient,
2599+
) -> anyhow::Result<Vec<(User, i32)>> {
2600+
use cynic::QueryBuilder;
2601+
use github_graphql::pull_requests_open::{PullRequestsOpen, PullRequestsOpenVariables};
2602+
2603+
let repo_owner = repo.owner();
2604+
let repo_name = repo.name();
2605+
2606+
let mut prs = vec![];
2607+
2608+
let mut vars = PullRequestsOpenVariables {
2609+
repo_owner,
2610+
repo_name,
2611+
after: None,
2612+
};
2613+
loop {
2614+
let query = PullRequestsOpen::build(vars.clone());
2615+
let req = client.post(&client.graphql_url);
2616+
let req = req.json(&query);
2617+
2618+
let data: cynic::GraphQlResponse<PullRequestsOpen> = client.json(req).await?;
2619+
if let Some(errors) = data.errors {
2620+
anyhow::bail!("There were graphql errors. {:?}", errors);
2621+
}
2622+
let repository = data
2623+
.data
2624+
.ok_or_else(|| anyhow::anyhow!("No data returned."))?
2625+
.repository
2626+
.ok_or_else(|| anyhow::anyhow!("No repository."))?;
2627+
prs.extend(repository.pull_requests.nodes);
2628+
2629+
let page_info = repository.pull_requests.page_info;
2630+
if !page_info.has_next_page || page_info.end_cursor.is_none() {
2631+
break;
2632+
}
2633+
vars.after = page_info.end_cursor;
2634+
}
2635+
2636+
let mut prs_processed: Vec<_> = vec![];
2637+
let _: Vec<_> = prs
2638+
.into_iter()
2639+
.filter_map(|pr| {
2640+
if pr.is_draft {
2641+
return None;
2642+
}
2643+
2644+
// exclude rollup PRs
2645+
let labels = pr
2646+
.labels
2647+
.map(|l| l.nodes)
2648+
.unwrap_or_default()
2649+
.into_iter()
2650+
.map(|node| node.name)
2651+
.collect::<Vec<_>>();
2652+
if labels.iter().any(|label| label == "rollup") {
2653+
return None;
2654+
}
2655+
2656+
let _: Vec<_> = pr
2657+
.assignees
2658+
.nodes
2659+
.iter()
2660+
.map(|user| {
2661+
let user_id = user.database_id.expect("checked") as u64;
2662+
prs_processed.push((
2663+
User {
2664+
login: user.login.clone(),
2665+
id: Some(user_id),
2666+
},
2667+
pr.number,
2668+
));
2669+
})
2670+
.collect();
2671+
Some(true)
2672+
})
2673+
.collect();
2674+
prs_processed.sort_by(|a, b| a.0.id.cmp(&b.0.id));
2675+
2676+
Ok(prs_processed)
2677+
}
2678+
25952679
pub enum DesignMeetingStatus {
25962680
Proposed,
25972681
Scheduled,

src/handlers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ mod notification;
3939
mod notify_zulip;
4040
mod ping;
4141
mod prioritize;
42+
pub mod pull_requests_assignment_update;
4243
mod relabel;
4344
mod review_requested;
4445
mod review_submitted;

src/handlers/notification.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ pub async fn handle(ctx: &Context, event: &Event) -> anyhow::Result<()> {
9292
continue;
9393
}
9494

95-
if let Err(err) = notifications::record_username(&client, user.id.unwrap(), user.login)
95+
if let Err(err) = notifications::record_username(&client, user.id.unwrap(), &user.login)
9696
.await
9797
.context("failed to record username")
9898
{
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use std::collections::HashMap;
2+
3+
use crate::db::notifications::record_username;
4+
use crate::github::retrieve_pull_requests;
5+
use crate::jobs::Job;
6+
use anyhow::Context as _;
7+
use async_trait::async_trait;
8+
use tokio_postgres::Client as DbClient;
9+
10+
pub struct PullRequestAssignmentUpdate;
11+
12+
#[async_trait]
13+
impl Job for PullRequestAssignmentUpdate {
14+
fn name(&self) -> &'static str {
15+
"pull_request_assignment_update"
16+
}
17+
18+
async fn run(&self, ctx: &super::Context, _metadata: &serde_json::Value) -> anyhow::Result<()> {
19+
let db = ctx.db.get().await;
20+
let gh = &ctx.github;
21+
22+
tracing::trace!("starting pull_request_assignment_update");
23+
24+
let rust_repo = gh.repository("rust-lang/rust").await?;
25+
let prs = retrieve_pull_requests(&rust_repo, &gh).await?;
26+
27+
// delete all PR assignments before populating
28+
init_table(&db).await?;
29+
30+
// aggregate by user first
31+
let aggregated = prs.into_iter().fold(HashMap::new(), |mut acc, (user, pr)| {
32+
let (_, prs) = acc
33+
.entry(user.id.unwrap())
34+
.or_insert_with(|| (user, Vec::new()));
35+
prs.push(pr);
36+
acc
37+
});
38+
39+
// populate the table
40+
for (_user_id, (assignee, prs)) in &aggregated {
41+
let assignee_id = assignee.id.expect("checked");
42+
let _ = record_username(&db, assignee_id, &assignee.login).await;
43+
create_team_member_workqueue(&db, assignee_id, &prs).await?;
44+
}
45+
46+
Ok(())
47+
}
48+
}
49+
50+
/// Truncate the review prefs table
51+
async fn init_table(db: &DbClient) -> anyhow::Result<u64> {
52+
let res = db
53+
.execute("UPDATE review_prefs SET assigned_prs='{}';", &[])
54+
.await?;
55+
Ok(res)
56+
}
57+
58+
/// Create a team member work queue
59+
async fn create_team_member_workqueue(
60+
db: &DbClient,
61+
user_id: u64,
62+
prs: &Vec<i32>,
63+
) -> anyhow::Result<u64, anyhow::Error> {
64+
let q = "
65+
INSERT INTO review_prefs (user_id, assigned_prs) VALUES ($1, $2)
66+
ON CONFLICT (user_id)
67+
DO UPDATE SET assigned_prs = $2
68+
WHERE review_prefs.user_id=$1";
69+
db.execute(q, &[&(user_id as i64), prs])
70+
.await
71+
.context("Insert DB error")
72+
}

src/jobs.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ use crate::{
5858
},
5959
};
6060

61-
// How often new cron-based jobs will be placed in the queue.
62-
// This is the minimum period *between* a single cron task's executions.
61+
/// How often new cron-based jobs will be placed in the queue.
62+
/// This is the minimum period *between* a single cron task's executions.
6363
pub const JOB_SCHEDULING_CADENCE_IN_SECS: u64 = 1800;
6464

65-
// How often the database is inspected for jobs which need to execute.
66-
// This is the granularity at which events will occur.
65+
/// How often the database is inspected for jobs which need to execute.
66+
/// This is the granularity at which events will occur.
6767
pub const JOB_PROCESSING_CADENCE_IN_SECS: u64 = 60;
6868

6969
// The default jobs to schedule, repeatedly.
@@ -119,7 +119,7 @@ fn jobs_defined() {
119119
unique_all_job_names.dedup();
120120
assert_eq!(all_job_names, unique_all_job_names);
121121

122-
// Also ensure that our defalt jobs are release jobs
122+
// Also ensure that our default jobs are release jobs
123123
let default_jobs = default_jobs();
124124
default_jobs
125125
.iter()

0 commit comments

Comments
 (0)