Skip to content

Commit a42cf9d

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

File tree

5 files changed

+128
-8
lines changed

5 files changed

+128
-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: 88 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,79 @@ 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+
// validate command parameters
344+
if !contains_any(&[args.channel.to_lowercase().as_str()], &BACKPORT_CHANNELS) {
345+
return Err(anyhow::anyhow!(
346+
"Parser error: unknown channel (allowed: {BACKPORT_CHANNELS:?})."
347+
));
348+
}
349+
350+
let message_body = if contains_any(&[verb.as_str()], &BACKPORT_VERBS_APPROVE) {
351+
BACKPORT_APPROVED
352+
} else if contains_any(&[verb.as_str()], &BACKPORT_VERBS_DECLINE) {
353+
BACKPORT_DECLINED
354+
} else {
355+
return Err(anyhow::anyhow!(
356+
"Parser error: unknown verb (allowed: {BACKPORT_VERBS_APPROVE:?} or {BACKPORT_VERBS_DECLINE:?})"
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 = message_body
371+
.replace("{args.channel}", args.channel.as_str())
372+
.replace("{args.verb}", verb.as_str())
373+
.replace("{zulip_link}", zulip_link.as_str());
374+
375+
// TODO: can I get repo owner and name from somewhere - without doing HTTP requests?
376+
// let repo_owner = "rust";
377+
// let repo_name = "rust";
378+
let repo_owner = "apiraino";
379+
let repo_name = "test-triagebot";
380+
381+
tokio::spawn(async move {
382+
let res = octo_client
383+
.issues(repo_owner, repo_name)
384+
.create_comment(args.pr_num, &message_body)
385+
.await
386+
.context("unable to post comment on #{args.pr_num}");
387+
// XXX: was is successful? Silently fail or return a feedback on Zulip?
388+
if res.is_err() {
389+
tracing::error!("failed to post comment: {0:?}", res.err());
390+
}
391+
});
392+
Ok(Some("".to_string()))
393+
}
394+
309395
async fn ping_goals_cmd(
310396
ctx: Arc<Context>,
311397
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)