Skip to content

Commit 52295ac

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 assumes PRs 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 1d62ded commit 52295ac

File tree

2 files changed

+139
-3
lines changed

2 files changed

+139
-3
lines changed

src/zulip.rs

Lines changed: 87 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 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"
59+
)
60+
}
3461

3562
#[derive(Debug, serde::Deserialize)]
3663
pub struct Request {
@@ -296,10 +323,68 @@ async fn handle_command<'a>(
296323
.map_err(|e| format_err!("Failed to await at this time: {e:?}")),
297324
StreamCommand::PingGoals(args) => ping_goals_cmd(ctx, gh_id, message_data, &args).await,
298325
StreamCommand::DocsUpdate => trigger_docs_update(message_data, &ctx.zulip),
326+
StreamCommand::Backport(args) => {
327+
accept_decline_backport(message_data, &ctx.octocrab, &ctx.zulip, &args).await
328+
}
299329
}
300330
}
301331
}
302332

333+
// TODO: shorter variant of this command (f.e. `backport accept` or even `accept`) that infers everything from the Message payload
334+
async fn accept_decline_backport(
335+
message_data: &Message,
336+
octo_client: &Octocrab,
337+
zulip_client: &ZulipClient,
338+
args_data: &BackportArgs,
339+
) -> anyhow::Result<Option<String>> {
340+
let message = message_data.clone();
341+
let args = args_data.clone();
342+
let stream_id = message.stream_id.unwrap();
343+
let subject = message.subject.unwrap();
344+
let octo_client = octo_client.clone();
345+
346+
// Repository owner and name are hardcoded
347+
// This command is only used in this repository
348+
let repo_owner = "rust-lang";
349+
let repo_name = "rust";
350+
351+
// TODO: factor out the Zulip "URL encoder" to make it practical to use
352+
let zulip_send_req = crate::zulip::MessageApiRequest {
353+
recipient: Recipient::Stream {
354+
id: stream_id,
355+
topic: &subject,
356+
},
357+
content: "",
358+
};
359+
360+
// 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
361+
// See: https://rust-lang.zulipchat.com/#narrow/channel/122653-zulip/topic/.22near.22.20parameter.20in.20payload.20of.20send.20message.20API
362+
let zulip_link = zulip_send_req.url(zulip_client);
363+
364+
let message_body = match args.verb {
365+
BackportVerbArgs::Accept
366+
| BackportVerbArgs::Accepted
367+
| BackportVerbArgs::Approve
368+
| BackportVerbArgs::Approved => {
369+
get_text_backport_approved(&args.channel, &args.verb, &zulip_link)
370+
}
371+
BackportVerbArgs::Decline | BackportVerbArgs::Declined => {
372+
get_text_backport_declined(&args.channel, &args.verb, &zulip_link)
373+
}
374+
};
375+
376+
let res = octo_client
377+
.issues(repo_owner, repo_name)
378+
.create_comment(args.pr_num, &message_body)
379+
.await
380+
.with_context(|| anyhow::anyhow!("unable to post comment on #{}", args.pr_num));
381+
if res.is_err() {
382+
tracing::error!("failed to post comment: {0:?}", res.err());
383+
}
384+
385+
Ok(Some("".to_string()))
386+
}
387+
303388
async fn ping_goals_cmd(
304389
ctx: Arc<Context>,
305390
gh_id: u64,

src/zulip/commands.rs

Lines changed: 52 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,34 @@ 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+
/// Backport verbs
187+
#[derive(Clone, clap::ValueEnum, Debug, PartialEq)]
188+
pub enum BackportVerbArgs {
189+
Accept,
190+
Accepted,
191+
Approve,
192+
Approved,
193+
Decline,
194+
Declined,
195+
}
196+
197+
#[derive(clap::Parser, Debug, PartialEq, Clone)]
198+
pub struct BackportArgs {
199+
/// Release channel this backport is pointing to. Allowed: "beta" or "stable".
200+
pub channel: BackportChannelArgs,
201+
/// Accept or decline this backport? Allowed: "accept", "accepted", "approve", "approved", "decline", "declined".
202+
pub verb: BackportVerbArgs,
203+
/// PR to be backported
204+
pub pr_num: PullRequestNumber,
205+
}
206+
176207
/// Helper function to parse CLI arguments without any colored help or error output.
177208
pub fn parse_cli<'a, T: Parser, I: Iterator<Item = &'a str>>(input: I) -> anyhow::Result<T> {
178209
fn allow_title_case(sub: clap::Command) -> clap::Command {
@@ -292,6 +323,26 @@ mod tests {
292323
assert_eq!(parse_stream(&["await"]), StreamCommand::EndTopic);
293324
}
294325

326+
#[test]
327+
fn backports_command() {
328+
assert_eq!(
329+
parse_stream(&["backport", "beta", "accept", "123456"]),
330+
StreamCommand::Backport(BackportArgs {
331+
channel: BackportChannelArgs::Beta,
332+
verb: BackportVerbArgs::Accept,
333+
pr_num: 123456
334+
})
335+
);
336+
assert_eq!(
337+
parse_stream(&["backport", "stable", "decline", "123456"]),
338+
StreamCommand::Backport(BackportArgs {
339+
channel: BackportChannelArgs::Stable,
340+
verb: BackportVerbArgs::Decline,
341+
pr_num: 123456
342+
})
343+
);
344+
}
345+
295346
fn parse_chat(input: &[&str]) -> ChatCommand {
296347
parse_cli::<ChatCommand, _>(input.into_iter().copied()).unwrap()
297348
}

0 commit comments

Comments
 (0)