Skip to content

Commit 27ba888

Browse files
authored
Merge pull request #2337 from apiraino/assign-priority-from-zulip
Assign priority to issues from Zulip
2 parents 2db04e7 + a7f7eb2 commit 27ba888

File tree

4 files changed

+151
-16
lines changed

4 files changed

+151
-16
lines changed

src/zulip.rs

Lines changed: 116 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ use crate::db::review_prefs::{
1010
upsert_repo_review_prefs, upsert_team_review_prefs, upsert_user_review_prefs,
1111
};
1212
use crate::db::users::DbUser;
13-
use crate::github;
1413
use crate::github::queries::user_comments_in_org::UserComment;
1514
use crate::github::queries::user_prs::PullRequestState;
1615
use crate::github::queries::user_prs::UserPullRequest;
16+
use crate::github::{self, Repository};
1717
use crate::handlers::Context;
1818
use crate::handlers::docs_update::docs_update;
1919
use crate::handlers::pr_tracking::{ReviewerWorkqueue, get_assigned_prs};
@@ -23,8 +23,8 @@ use crate::utils::pluralize;
2323
use crate::zulip::api::{MessageApiResponse, Recipient};
2424
use crate::zulip::client::ZulipClient;
2525
use crate::zulip::commands::{
26-
BackportChannelArgs, BackportVerbArgs, ChatCommand, LookupCmd, PingGoalsArgs, StreamCommand,
27-
WorkqueueCmd, WorkqueueLimit, parse_cli,
26+
AssignPrioArgs, BackportChannelArgs, BackportVerbArgs, ChatCommand, IssuePrio, LookupCmd,
27+
PingGoalsArgs, StreamCommand, WorkqueueCmd, WorkqueueLimit, parse_cli,
2828
};
2929
use anyhow::{Context as _, format_err};
3030
use axum::Json;
@@ -377,6 +377,9 @@ async fn handle_command<'a>(
377377
} => user_info_cmd(&ctx, gh_id, &username, &organization)
378378
.await
379379
.map(Some),
380+
StreamCommand::AssignPriority(args) => {
381+
assign_issue_prio(ctx, message_data, &args).await
382+
}
380383
};
381384
}
382385

@@ -424,18 +427,8 @@ async fn accept_decline_backport(
424427
)));
425428
}
426429

427-
// TODO: factor out the Zulip "URL encoder" to make it practical to use
428-
let zulip_send_req = crate::zulip::MessageApiRequest {
429-
recipient: Recipient::Stream {
430-
id: stream_id,
431-
topic: &subject,
432-
},
433-
content: "",
434-
};
435-
436-
// NOTE: the Zulip Message API cannot yet pin exactly a single message so the link in the GitHub comment will be to the whole topic
437-
// See: https://rust-lang.zulipchat.com/#narrow/channel/122653-zulip/topic/.22near.22.20parameter.20in.20payload.20of.20send.20message.20API
438-
let zulip_link = zulip_send_req.url(zulip_client);
430+
let zulip_link =
431+
crate::zulip::MessageApiRequest::new(stream_id, &subject, "").url(zulip_client);
439432

440433
let (message_body, approve_backport) = match args.verb {
441434
BackportVerbArgs::Accept
@@ -488,6 +481,99 @@ async fn accept_decline_backport(
488481
Ok(None)
489482
}
490483

484+
async fn assign_issue_prio(
485+
ctx: Arc<Context>,
486+
message_data: &Message,
487+
args_data: &AssignPrioArgs,
488+
) -> anyhow::Result<Option<String>> {
489+
let message = message_data.clone();
490+
let args = args_data.clone();
491+
let stream_id = message.stream_id.unwrap();
492+
let subject = message.subject.unwrap();
493+
let zulip_client = &ctx.zulip;
494+
495+
// Repository owner and name are hardcoded
496+
// This command is only used in this repository
497+
let repo_owner = std::env::var("MAIN_GH_REPO_OWNER").unwrap_or("rust-lang".to_string());
498+
let repo_name = std::env::var("MAIN_GH_REPO_NAME").unwrap_or("rust".to_string());
499+
500+
let repository = Repository {
501+
full_name: format!("{}/{}", repo_owner, repo_name),
502+
default_branch: "main".to_string(),
503+
fork: false,
504+
parent: None,
505+
};
506+
507+
// Ensure this is an issue and not a pull request
508+
let issue = repository
509+
.get_issue(&ctx.github, args.issue_num)
510+
.await
511+
.context(format!("Could not retrieve #{}", args.issue_num))?;
512+
if issue.pull_request.is_some() {
513+
return Ok(Some(format!(
514+
"Error: #{} is a pull request (must be an issue)",
515+
args.issue_num
516+
)));
517+
}
518+
519+
let zulip_link =
520+
crate::zulip::MessageApiRequest::new(stream_id, &subject, "").url(zulip_client);
521+
522+
// Remove I-prioritize and all P-* labels (if any)
523+
issue
524+
.remove_labels(
525+
&ctx.github,
526+
vec![
527+
github::Label {
528+
name: "I-prioritize".to_string(),
529+
},
530+
github::Label {
531+
name: "P-low".to_string(),
532+
},
533+
github::Label {
534+
name: "P-medium".to_string(),
535+
},
536+
github::Label {
537+
name: "P-high".to_string(),
538+
},
539+
github::Label {
540+
name: "P-critical".to_string(),
541+
},
542+
],
543+
)
544+
.await
545+
.context("failed to remove labels from the issue")?;
546+
547+
// if just removing priority, nothing else to do
548+
if args.prio == IssuePrio::None {
549+
return Ok(None);
550+
}
551+
552+
// post a comment on GitHub
553+
let message_body = format!(
554+
"Assigning P-{} (discussion on [Zulip]({zulip_link})).",
555+
args.prio
556+
);
557+
558+
issue
559+
.post_comment(&ctx.github, &message_body)
560+
.await
561+
.with_context(|| anyhow::anyhow!("Unable to post comment on #{}", args.issue_num))?;
562+
563+
// Add the specified priority label
564+
issue
565+
.add_labels(
566+
&ctx.github,
567+
vec![github::Label {
568+
name: format!("P-{}", args.prio),
569+
}],
570+
)
571+
.await
572+
.context(format!("failed to add labels to issue #{}", args.issue_num))?;
573+
574+
Ok(None)
575+
}
576+
491577
async fn ping_goals_cmd(
492578
ctx: Arc<Context>,
493579
gh_id: u64,
@@ -1358,11 +1444,25 @@ async fn lookup_zulip_username(ctx: &Context, gh_username: &str) -> anyhow::Resu
13581444

13591445
#[derive(serde::Serialize)]
13601446
pub(crate) struct MessageApiRequest<'a> {
1447+
/// The recipient of the message. Could be DM or a Stream
13611448
pub(crate) recipient: Recipient<'a>,
1449+
/// The content of the message
13621450
pub(crate) content: &'a str,
13631451
}
13641452

1365-
impl MessageApiRequest<'_> {
1453+
impl<'a> MessageApiRequest<'_> {
1454+
pub fn new(stream_id: u64, subject: &'a str, content: &'a str) -> MessageApiRequest<'a> {
1455+
MessageApiRequest {
1456+
recipient: Recipient::Stream {
1457+
id: stream_id,
1458+
topic: subject,
1459+
},
1460+
content,
1461+
}
1462+
}
1463+
1464+
// note: the Zulip Message API cannot yet pin exactly a single message so the link in the GitHub comment will be to the whole topic
1465+
// See: https://rust-lang.zulipchat.com/#narrow/channel/122653-zulip/topic/.22near.22.20parameter.20in.20payload.20of.20send.20message.20API
13661466
pub fn url(&self, zulip: &ZulipClient) -> String {
13671467
self.recipient.url(zulip)
13681468
}

src/zulip/api.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
//! This module implements parts of the Zulip REST API that we use
2+
//! Documentation: https://zulip.com/api/send-message
3+
14
use crate::zulip::client::ZulipClient;
25
use std::collections::HashMap;
36

src/zulip/client.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
//! This module implements parts of the Zulip REST API that we use
2+
//! Documentation: https://zulip.com/api/send-message
3+
14
use crate::zulip::Recipient;
25
use crate::zulip::api::{MessageApiResponse, ZulipUser, ZulipUsers};
36
use anyhow::Context;

src/zulip/commands.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ pub enum StreamCommand {
208208
#[arg(long = "org", default_value = "rust-lang")]
209209
organization: String,
210210
},
211+
/// Label assignment: add one of `P-{low,medium,high,critical}` and remove `I-prioritize`
212+
AssignPriority(AssignPrioArgs),
211213
}
212214

213215
#[derive(clap::Parser, Debug, PartialEq, Clone)]
@@ -267,6 +269,33 @@ pub struct BackportArgs {
267269
pub pr_num: PullRequestNumber,
268270
}
269271

272+
#[derive(clap::Parser, Debug, PartialEq, Clone)]
273+
pub struct AssignPrioArgs {
274+
/// Issue target of the prioritization
275+
pub issue_num: PullRequestNumber,
276+
/// Issue priority. Allowed: "low", "medium", "high", "critical", "none" (to just remove the prioritization)
277+
pub prio: IssuePrio,
278+
}
279+
280+
/// Priority labels
281+
#[derive(Clone, clap::ValueEnum, Debug, PartialEq)]
282+
pub enum IssuePrio {
283+
Low,
284+
Medium,
285+
High,
286+
Critical,
287+
None,
288+
}
289+
290+
impl std::fmt::Display for IssuePrio {
291+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
292+
match &self {
293+
Self::None => write!(f, ""),
294+
_ => write!(f, "{}", format!("{:?}", self).to_lowercase()),
295+
}
296+
}
297+
}
298+
270299
/// Helper function to parse CLI arguments without any colored help or error output.
271300
pub fn parse_cli<'a, T: Parser, I: Iterator<Item = &'a str>>(input: I) -> anyhow::Result<T> {
272301
fn allow_title_case(sub: clap::Command) -> clap::Command {

0 commit comments

Comments
 (0)