Skip to content

Commit 5861437

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

File tree

5 files changed

+125
-10
lines changed

5 files changed

+125
-10
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/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: 86 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,34 @@ 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+
fn get_text_backport_approved(channel: &str, verb: &str, zulip_link: &str) -> String {
37+
format!("
38+
{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.
39+
40+
@rustbot label +{channel}-accepted")
41+
}
42+
43+
fn get_text_backport_declined(channel: &str, verb: &str, zulip_link: &str) -> String {
44+
format!(
45+
"
46+
{channel} backport {verb} as per compiler team [on Zulip]({zulip_link}).
47+
48+
@rustbot label -{channel}-nominated"
49+
)
50+
}
51+
52+
const BACKPORT_CHANNELS: [&str; 2] = ["beta", "stable"];
53+
const BACKPORT_VERBS_APPROVE: [&str; 4] = ["accept", "accepted", "approve", "approved"];
54+
const BACKPORT_VERBS_DECLINE: [&str; 2] = ["decline", "declined"];
3355

3456
#[derive(Debug, serde::Deserialize)]
3557
pub struct Request {
@@ -302,10 +324,72 @@ async fn handle_command<'a>(
302324
.map_err(|e| format_err!("Failed to await at this time: {e:?}")),
303325
StreamCommand::PingGoals(args) => ping_goals_cmd(ctx, gh_id, message_data, &args).await,
304326
StreamCommand::DocsUpdate => trigger_docs_update(message_data, &ctx.zulip),
327+
StreamCommand::Backport(args) => {
328+
accept_decline_backport(message_data, &ctx.octocrab, &ctx.zulip, &args).await
329+
}
305330
}
306331
}
307332
}
308333

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