Skip to content

Commit b265ac9

Browse files
committed
accept backports from zulip
1 parent 5c4f2b1 commit b265ac9

File tree

5 files changed

+126
-8
lines changed

5 files changed

+126
-8
lines changed

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
@@ -36,3 +36,7 @@ where
3636
AppError(err.into())
3737
}
3838
}
39+
40+
pub fn contains_any(haystack: &[&str], needles: &[&str]) -> bool {
41+
needles.iter().any(|needle| haystack.contains(needle))
42+
}

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,29 @@ 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+
const BACKPORT_CHANNELS: [&str; 2] = ["beta", "stable"];
48+
const BACKPORT_VERBS_APPROVE: [&str; 4] = ["accept", "accepted", "approve", "approved"];
49+
const BACKPORT_VERBS_DECLINE: [&str; 2] = ["decline", "declined"];
3350

3451
#[derive(Debug, serde::Deserialize)]
3552
pub struct Request {
@@ -302,10 +319,77 @@ async fn handle_command<'a>(
302319
.map_err(|e| format_err!("Failed to await at this time: {e:?}")),
303320
StreamCommand::PingGoals(args) => ping_goals_cmd(ctx, gh_id, message_data, &args).await,
304321
StreamCommand::DocsUpdate => trigger_docs_update(message_data, &ctx.zulip),
322+
StreamCommand::Backport(args) => {
323+
accept_decline_backport(message_data, &ctx.octocrab, &ctx.zulip, &args).await
324+
}
305325
}
306326
}
307327
}
308328

329+
// TODO: shorter variant of this command (f.e. `backport accept` or even `accept`) that infers everything from the Message payload
330+
async fn accept_decline_backport(
331+
message_data: &Message,
332+
octo_client: &Octocrab,
333+
zulip_client: &ZulipClient,
334+
args_data: &BackportArgs,
335+
) -> anyhow::Result<Option<String>> {
336+
let message = message_data.clone();
337+
let args = args_data.clone();
338+
let stream_id = message.stream_id.unwrap();
339+
let subject = message.subject.unwrap();
340+
let verb = args.verb.to_lowercase();
341+
let octo_client = octo_client.clone();
342+
343+
// Repository owner and name are hardcoded
344+
// This command is only used in this repository
345+
let repo_owner = "rust-lang";
346+
let repo_name = "rust";
347+
348+
// validate command parameters
349+
if !contains_any(&[args.channel.to_lowercase().as_str()], &BACKPORT_CHANNELS) {
350+
return Err(anyhow::anyhow!(
351+
"Parser error: unknown channel (allowed: {BACKPORT_CHANNELS:?})."
352+
));
353+
}
354+
355+
let message_body = if contains_any(&[verb.as_str()], &BACKPORT_VERBS_APPROVE) {
356+
BACKPORT_APPROVED
357+
} else if contains_any(&[verb.as_str()], &BACKPORT_VERBS_DECLINE) {
358+
BACKPORT_DECLINED
359+
} else {
360+
return Err(anyhow::anyhow!(
361+
"Parser error: unknown verb (allowed: {BACKPORT_VERBS_APPROVE:?} or {BACKPORT_VERBS_DECLINE:?})"
362+
));
363+
};
364+
365+
// TODO: factor out the Zulip "URL encoder" to make it practical to use
366+
let zulip_send_req = crate::zulip::MessageApiRequest {
367+
recipient: Recipient::Stream {
368+
id: stream_id,
369+
topic: &subject,
370+
},
371+
content: "",
372+
};
373+
let zulip_link = zulip_send_req.url(zulip_client);
374+
375+
let message_body = message_body
376+
.replace("{args.channel}", args.channel.as_str())
377+
.replace("{args.verb}", verb.as_str())
378+
.replace("{zulip_link}", zulip_link.as_str());
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
// Add a fake first argument, which is expected by clap
@@ -265,6 +278,26 @@ mod tests {
265278
assert_eq!(parse_stream(&["await"]), StreamCommand::EndTopic);
266279
}
267280

281+
#[test]
282+
fn backports_command() {
283+
assert_eq!(
284+
parse_stream(&["backport", "beta", "accept", "123456"]),
285+
StreamCommand::Backport(BackportArgs {
286+
channel: "beta".to_string(),
287+
verb: "accept".to_string(),
288+
pr_num: 123456
289+
})
290+
);
291+
assert_eq!(
292+
parse_stream(&["backport", "stable", "decline", "123456"]),
293+
StreamCommand::Backport(BackportArgs {
294+
channel: "stable".to_string(),
295+
verb: "decline".to_string(),
296+
pr_num: 123456
297+
})
298+
);
299+
}
300+
268301
fn parse_chat(input: &[&str]) -> ChatCommand {
269302
parse_cli::<ChatCommand, _>(input.into_iter().copied()).unwrap()
270303
}

0 commit comments

Comments
 (0)