Skip to content

Commit 9ce791d

Browse files
committed
accept backports from zulip
This patch adds a new Zulip command: `@triagebot backport <channel> <action> <PR>` Example: - `@triagebot backport stable accept 123456` - `@triagebot backport beta decline 654321` This can be used to post on GitHub a comment when T-compiler accepts/decline to backport a patch that fixes a regression. Limitations: - This command is only available in the `rust-lang/rust` repository - There is no check on the PR number (in case of mistypes, the comment will go elsewhere)
1 parent c6b8549 commit 9ce791d

File tree

2 files changed

+156
-3
lines changed

2 files changed

+156
-3
lines changed

src/zulip.rs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,47 @@ use crate::utils::pluralize;
1717
use crate::zulip::api::{MessageApiResponse, Recipient};
1818
use crate::zulip::client::ZulipClient;
1919
use crate::zulip::commands::{
20-
ChatCommand, LookupCmd, PingGoalsArgs, StreamCommand, WorkqueueCmd, WorkqueueLimit, parse_cli,
20+
BackportChannelArgs, BackportVerbArgs, ChatCommand, LookupCmd, PingGoalsArgs, StreamCommand,
21+
WorkqueueCmd, WorkqueueLimit, parse_cli,
2122
};
2223
use anyhow::{Context as _, format_err};
2324
use axum::Json;
2425
use axum::extract::State;
2526
use axum::extract::rejection::JsonRejection;
2627
use axum::response::IntoResponse;
28+
use commands::BackportArgs;
2729
use itertools::Itertools;
30+
use octocrab::Octocrab;
2831
use rust_team_data::v1::{TeamKind, TeamMember};
2932
use std::cmp::Reverse;
3033
use std::fmt::Write as _;
3134
use std::sync::Arc;
3235
use subtle::ConstantTimeEq;
33-
use tracing as log;
36+
use tracing::log;
37+
38+
fn get_text_backport_approved(
39+
channel: &BackportChannelArgs,
40+
verb: &BackportVerbArgs,
41+
zulip_link: &str,
42+
) -> String {
43+
format!("
44+
{channel} backport {verb} as per compiler team [on Zulip]({zulip_link}). A backport PR will be authored by the release team at the end of the current development cycle. Backport labels are handled by them.
45+
46+
@rustbot label +{channel}-accepted")
47+
}
48+
49+
fn get_text_backport_declined(
50+
channel: &BackportChannelArgs,
51+
verb: &BackportVerbArgs,
52+
zulip_link: &str,
53+
) -> String {
54+
format!(
55+
"
56+
{channel} backport {verb} as per compiler team [on Zulip]({zulip_link}).
57+
58+
@rustbot label -{channel}-nominated -{channel}-accepted"
59+
)
60+
}
3461

3562
#[derive(Debug, serde::Deserialize)]
3663
pub struct Request {
@@ -317,6 +344,9 @@ async fn handle_command<'a>(
317344
ping_goals_cmd(ctx, gh_id, message_data, &args).await
318345
}
319346
StreamCommand::DocsUpdate => trigger_docs_update(message_data, &ctx.zulip),
347+
StreamCommand::Backport(args) => {
348+
accept_decline_backport(message_data, &ctx.octocrab, &ctx.zulip, &args).await
349+
}
320350
};
321351
}
322352

@@ -325,6 +355,57 @@ async fn handle_command<'a>(
325355
}
326356
}
327357

358+
// TODO: shorter variant of this command (f.e. `backport accept` or even `accept`) that infers everything from the Message payload
359+
async fn accept_decline_backport(
360+
message_data: &Message,
361+
octo_client: &Octocrab,
362+
zulip_client: &ZulipClient,
363+
args_data: &BackportArgs,
364+
) -> anyhow::Result<Option<String>> {
365+
let message = message_data.clone();
366+
let args = args_data.clone();
367+
let stream_id = message.stream_id.unwrap();
368+
let subject = message.subject.unwrap();
369+
370+
// Repository owner and name are hardcoded
371+
// This command is only used in this repository
372+
let repo_owner = "rust-lang";
373+
let repo_name = "rust";
374+
375+
// TODO: factor out the Zulip "URL encoder" to make it practical to use
376+
let zulip_send_req = crate::zulip::MessageApiRequest {
377+
recipient: Recipient::Stream {
378+
id: stream_id,
379+
topic: &subject,
380+
},
381+
content: "",
382+
};
383+
384+
// 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
385+
// See: https://rust-lang.zulipchat.com/#narrow/channel/122653-zulip/topic/.22near.22.20parameter.20in.20payload.20of.20send.20message.20API
386+
let zulip_link = zulip_send_req.url(zulip_client);
387+
388+
let message_body = match args.verb {
389+
BackportVerbArgs::Accept
390+
| BackportVerbArgs::Accepted
391+
| BackportVerbArgs::Approve
392+
| BackportVerbArgs::Approved => {
393+
get_text_backport_approved(&args.channel, &args.verb, &zulip_link)
394+
}
395+
BackportVerbArgs::Decline | BackportVerbArgs::Declined => {
396+
get_text_backport_declined(&args.channel, &args.verb, &zulip_link)
397+
}
398+
};
399+
400+
let _ = octo_client
401+
.issues(repo_owner, repo_name)
402+
.create_comment(args.pr_num, &message_body)
403+
.await
404+
.with_context(|| anyhow::anyhow!("unable to post comment on #{}", args.pr_num))?;
405+
406+
Ok(None)
407+
}
408+
328409
async fn ping_goals_cmd(
329410
ctx: Arc<Context>,
330411
gh_id: u64,

src/zulip/commands.rs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::db::notifications::Identifier;
22
use crate::db::review_prefs::RotationMode;
3+
use crate::github::PullRequestNumber;
34
use clap::{ColorChoice, Parser};
45
use std::num::NonZeroU32;
56
use std::str::FromStr;
@@ -161,8 +162,10 @@ pub enum StreamCommand {
161162
Read,
162163
/// Ping project goal owners.
163164
PingGoals(PingGoalsArgs),
164-
/// Update docs
165+
/// Update docs.
165166
DocsUpdate,
167+
/// Accept or decline a backport.
168+
Backport(BackportArgs),
166169
}
167170

168171
#[derive(clap::Parser, Debug, PartialEq, Clone)]
@@ -173,6 +176,55 @@ pub struct PingGoalsArgs {
173176
pub next_update: String,
174177
}
175178

179+
/// Backport release channels
180+
#[derive(Clone, clap::ValueEnum, Debug, PartialEq)]
181+
pub enum BackportChannelArgs {
182+
Beta,
183+
Stable,
184+
}
185+
186+
impl std::fmt::Display for BackportChannelArgs {
187+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
188+
match &self {
189+
BackportChannelArgs::Beta => write!(f, "beta"),
190+
BackportChannelArgs::Stable => write!(f, "stable"),
191+
}
192+
}
193+
}
194+
195+
/// Backport verbs
196+
#[derive(Clone, clap::ValueEnum, Debug, PartialEq)]
197+
pub enum BackportVerbArgs {
198+
Accept,
199+
Accepted,
200+
Approve,
201+
Approved,
202+
Decline,
203+
Declined,
204+
}
205+
206+
impl std::fmt::Display for BackportVerbArgs {
207+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
208+
match &self {
209+
BackportVerbArgs::Accept
210+
| BackportVerbArgs::Accepted
211+
| BackportVerbArgs::Approve
212+
| BackportVerbArgs::Approved => write!(f, "approved"),
213+
BackportVerbArgs::Decline | BackportVerbArgs::Declined => write!(f, "declined"),
214+
}
215+
}
216+
}
217+
218+
#[derive(clap::Parser, Debug, PartialEq, Clone)]
219+
pub struct BackportArgs {
220+
/// Release channel this backport is pointing to. Allowed: "beta" or "stable".
221+
pub channel: BackportChannelArgs,
222+
/// Accept or decline this backport? Allowed: "accept", "accepted", "approve", "approved", "decline", "declined".
223+
pub verb: BackportVerbArgs,
224+
/// PR to be backported
225+
pub pr_num: PullRequestNumber,
226+
}
227+
176228
/// Helper function to parse CLI arguments without any colored help or error output.
177229
pub fn parse_cli<'a, T: Parser, I: Iterator<Item = &'a str>>(input: I) -> anyhow::Result<T> {
178230
fn allow_title_case(sub: clap::Command) -> clap::Command {
@@ -292,6 +344,26 @@ mod tests {
292344
assert_eq!(parse_stream(&["await"]), StreamCommand::EndTopic);
293345
}
294346

347+
#[test]
348+
fn backports_command() {
349+
assert_eq!(
350+
parse_stream(&["backport", "beta", "accept", "123456"]),
351+
StreamCommand::Backport(BackportArgs {
352+
channel: BackportChannelArgs::Beta,
353+
verb: BackportVerbArgs::Accept,
354+
pr_num: 123456
355+
})
356+
);
357+
assert_eq!(
358+
parse_stream(&["backport", "stable", "decline", "123456"]),
359+
StreamCommand::Backport(BackportArgs {
360+
channel: BackportChannelArgs::Stable,
361+
verb: BackportVerbArgs::Decline,
362+
pr_num: 123456
363+
})
364+
);
365+
}
366+
295367
fn parse_chat(input: &[&str]) -> ChatCommand {
296368
parse_cli::<ChatCommand, _>(input.into_iter().copied()).unwrap()
297369
}

0 commit comments

Comments
 (0)