Skip to content

Commit 6d99fff

Browse files
committed
accept backports from zulip
1 parent 885b6ed commit 6d99fff

File tree

6 files changed

+137
-11
lines changed

6 files changed

+137
-11
lines changed

src/github.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,9 +1319,6 @@ impl IssuesEvent {
13191319
}
13201320
}
13211321

1322-
#[derive(Debug, serde::Deserialize)]
1323-
struct PullRequestEventFields {}
1324-
13251322
#[derive(Debug, serde::Deserialize)]
13261323
pub struct WorkflowRunJob {
13271324
pub name: String,

src/handlers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ impl fmt::Display for HandlerError {
2929

3030
mod assign;
3131
mod autolabel;
32-
mod backport;
32+
pub mod backport;
3333
mod bot_pull_requests;
3434
mod check_commits;
3535
mod close;

src/handlers/backport.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use std::sync::LazyLock;
44
use crate::config::BackportConfig;
55
use crate::github::{IssuesAction, IssuesEvent, Label};
66
use crate::handlers::Context;
7+
use crate::utils::contains_any;
78
use anyhow::Context as AnyhowContext;
89
use futures::future::join_all;
910
use regex::Regex;
@@ -204,10 +205,6 @@ pub(super) async fn handle_input(
204205
Ok(())
205206
}
206207

207-
fn contains_any(haystack: &[&str], needles: &[&str]) -> bool {
208-
needles.iter().any(|needle| haystack.contains(needle))
209-
}
210-
211208
#[cfg(test)]
212209
mod tests {
213210
use crate::handlers::backport::CLOSES_ISSUE_REGEXP;

src/utils.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ pub(crate) async fn is_repo_autorized(
6161

6262
Ok(true)
6363
}
64+
65+
pub fn contains_any(haystack: &[&str], needles: &[&str]) -> bool {
66+
needles.iter().any(|needle| haystack.contains(needle))
67+
}

src/zulip.rs

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use crate::handlers::docs_update::docs_update;
1313
use crate::handlers::pr_tracking::get_assigned_prs;
1414
use crate::handlers::project_goals::{self, ping_project_goals_owners};
1515
use crate::interactions::ErrorComment;
16-
use crate::utils::pluralize;
16+
use crate::utils::{contains_any, pluralize};
1717
use crate::zulip::api::{MessageApiResponse, Recipient};
1818
use crate::zulip::client::ZulipClient;
1919
use crate::zulip::commands::{
@@ -24,12 +24,45 @@ use axum::Json;
2424
use axum::extract::State;
2525
use axum::extract::rejection::JsonRejection;
2626
use axum::response::IntoResponse;
27+
use commands::BackportArgs;
28+
use octocrab::Octocrab;
2729
use rust_team_data::v1::{TeamKind, TeamMember};
2830
use std::cmp::Reverse;
2931
use std::fmt::Write as _;
3032
use std::sync::Arc;
3133
use subtle::ConstantTimeEq;
32-
use tracing as log;
34+
use tracing::log;
35+
36+
// const BACKPORT_APPROVED: &str = "
37+
// {args.channel} backport {args.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.
38+
39+
// @rustbot label +{args.channel}-accepted
40+
// ";
41+
// const BACKPORT_DECLINED: &str = "
42+
// {args.channel} backport {args.verb} as per compiler team [on Zulip]({zulip_link}).
43+
44+
// @rustbot label -{args.channel}-nominated
45+
// ";
46+
47+
fn get_text_backport_approved(channel: &str, verb: &str, zulip_link: &str) -> String {
48+
format!("
49+
{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.
50+
51+
@rustbot label +{channel}-accepted")
52+
}
53+
54+
fn get_text_backport_declined(channel: &str, verb: &str, zulip_link: &str) -> String {
55+
format!(
56+
"
57+
{channel} backport {verb} as per compiler team [on Zulip]({zulip_link}).
58+
59+
@rustbot label -{channel}-nominated"
60+
)
61+
}
62+
63+
const BACKPORT_CHANNELS: [&str; 2] = ["beta", "stable"];
64+
const BACKPORT_VERBS_APPROVE: [&str; 4] = ["accept", "accepted", "approve", "approved"];
65+
const BACKPORT_VERBS_DECLINE: [&str; 2] = ["decline", "declined"];
3366

3467
#[derive(Debug, serde::Deserialize)]
3568
pub struct Request {
@@ -302,10 +335,72 @@ async fn handle_command<'a>(
302335
.map_err(|e| format_err!("Failed to await at this time: {e:?}")),
303336
StreamCommand::PingGoals(args) => ping_goals_cmd(ctx, gh_id, message_data, &args).await,
304337
StreamCommand::DocsUpdate => trigger_docs_update(message_data, &ctx.zulip),
338+
StreamCommand::Backport(args) => {
339+
accept_decline_backport(message_data, &ctx.octocrab, &ctx.zulip, &args).await
340+
}
305341
}
306342
}
307343
}
308344

345+
// TODO: shorter variant of this command (f.e. `backport accept` or even `accept`) that infers everything from the Message payload
346+
async fn accept_decline_backport(
347+
message_data: &Message,
348+
octo_client: &Octocrab,
349+
zulip_client: &ZulipClient,
350+
args_data: &BackportArgs,
351+
) -> anyhow::Result<Option<String>> {
352+
let message = message_data.clone();
353+
let args = args_data.clone();
354+
let stream_id = message.stream_id.unwrap();
355+
let subject = message.subject.unwrap();
356+
let verb = args.verb.to_lowercase();
357+
let octo_client = octo_client.clone();
358+
359+
// Repository owner and name are hardcoded
360+
// This command is only used in this repository
361+
let repo_owner = "rust-lang";
362+
let repo_name = "rust";
363+
364+
// validate command parameters
365+
if !contains_any(&[args.channel.to_lowercase().as_str()], &BACKPORT_CHANNELS) {
366+
return Err(anyhow::anyhow!(
367+
"Parser error: unknown channel (allowed: {BACKPORT_CHANNELS:?})."
368+
));
369+
}
370+
371+
// TODO: factor out the Zulip "URL encoder" to make it practical to use
372+
let zulip_send_req = crate::zulip::MessageApiRequest {
373+
recipient: Recipient::Stream {
374+
id: stream_id,
375+
topic: &subject,
376+
},
377+
content: "",
378+
};
379+
let zulip_link = zulip_send_req.url(zulip_client);
380+
381+
let message_body = if contains_any(&[verb.as_str()], &BACKPORT_VERBS_APPROVE) {
382+
get_text_backport_approved(&args.channel, &verb, &zulip_link)
383+
} else if contains_any(&[verb.as_str()], &BACKPORT_VERBS_DECLINE) {
384+
get_text_backport_declined(&args.channel, &verb, &zulip_link)
385+
} else {
386+
return Err(anyhow::anyhow!(
387+
"Parser error: unknown verb (allowed: {BACKPORT_VERBS_APPROVE:?} or {BACKPORT_VERBS_DECLINE:?})"
388+
));
389+
};
390+
391+
tokio::spawn(async move {
392+
let res = octo_client
393+
.issues(repo_owner, repo_name)
394+
.create_comment(args.pr_num, &message_body)
395+
.await
396+
.context("unable to post comment on #{args.pr_num}");
397+
if res.is_err() {
398+
tracing::error!("failed to post comment: {0:?}", res.err());
399+
}
400+
});
401+
Ok(Some("".to_string()))
402+
}
403+
309404
async fn ping_goals_cmd(
310405
ctx: Arc<Context>,
311406
gh_id: u64,

src/zulip/commands.rs

Lines changed: 34 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,16 @@ pub struct PingGoalsArgs {
173176
pub next_update: String,
174177
}
175178

179+
#[derive(clap::Parser, Debug, PartialEq, Clone)]
180+
pub struct BackportArgs {
181+
/// Release channel this backport is pointing to. Allowed: "beta" or "stable".
182+
pub channel: String,
183+
/// Accept or decline this backport? Allowed: "accept", "accepted", "approve", "approved", "decline", "declined".
184+
pub verb: String,
185+
/// PR to be backported
186+
pub pr_num: PullRequestNumber,
187+
}
188+
176189
/// Helper function to parse CLI arguments without any colored help or error output.
177190
pub fn parse_cli<'a, T: Parser, I: Iterator<Item = &'a str>>(input: I) -> anyhow::Result<T> {
178191
fn allow_title_case(sub: clap::Command) -> clap::Command {
@@ -292,6 +305,26 @@ mod tests {
292305
assert_eq!(parse_stream(&["await"]), StreamCommand::EndTopic);
293306
}
294307

308+
#[test]
309+
fn backports_command() {
310+
assert_eq!(
311+
parse_stream(&["backport", "beta", "accept", "123456"]),
312+
StreamCommand::Backport(BackportArgs {
313+
channel: "beta".to_string(),
314+
verb: "accept".to_string(),
315+
pr_num: 123456
316+
})
317+
);
318+
assert_eq!(
319+
parse_stream(&["backport", "stable", "decline", "123456"]),
320+
StreamCommand::Backport(BackportArgs {
321+
channel: "stable".to_string(),
322+
verb: "decline".to_string(),
323+
pr_num: 123456
324+
})
325+
);
326+
}
327+
295328
fn parse_chat(input: &[&str]) -> ChatCommand {
296329
parse_cli::<ChatCommand, _>(input.into_iter().copied()).unwrap()
297330
}

0 commit comments

Comments
 (0)