diff --git a/src/github.rs b/src/github.rs index 5c4c4e3a6..4d63f4c27 100644 --- a/src/github.rs +++ b/src/github.rs @@ -1338,9 +1338,6 @@ impl IssuesEvent { } } -#[derive(Debug, serde::Deserialize)] -struct PullRequestEventFields {} - #[derive(Debug, serde::Deserialize)] pub struct WorkflowRunJob { pub name: String, diff --git a/src/zulip.rs b/src/zulip.rs index 1aacc825f..97146c206 100644 --- a/src/zulip.rs +++ b/src/zulip.rs @@ -17,20 +17,47 @@ use crate::utils::pluralize; use crate::zulip::api::{MessageApiResponse, Recipient}; use crate::zulip::client::ZulipClient; use crate::zulip::commands::{ - ChatCommand, LookupCmd, PingGoalsArgs, StreamCommand, WorkqueueCmd, WorkqueueLimit, parse_cli, + BackportChannelArgs, BackportVerbArgs, ChatCommand, LookupCmd, PingGoalsArgs, StreamCommand, + WorkqueueCmd, WorkqueueLimit, parse_cli, }; use anyhow::{Context as _, format_err}; use axum::Json; use axum::extract::State; use axum::extract::rejection::JsonRejection; use axum::response::IntoResponse; +use commands::BackportArgs; use itertools::Itertools; +use octocrab::Octocrab; use rust_team_data::v1::{TeamKind, TeamMember}; use std::cmp::Reverse; use std::fmt::Write as _; use std::sync::Arc; use subtle::ConstantTimeEq; -use tracing as log; +use tracing::log; + +fn get_text_backport_approved( + channel: &BackportChannelArgs, + verb: &BackportVerbArgs, + zulip_link: &str, +) -> String { + format!(" +{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. + +@rustbot label +{channel:?}-accepted") +} + +fn get_text_backport_declined( + channel: &BackportChannelArgs, + verb: &BackportVerbArgs, + zulip_link: &str, +) -> String { + format!( + " +{channel:?} backport {verb:?} as per compiler team [on Zulip]({zulip_link}). + +@rustbot label -{channel:?}-nominated" + ) +} #[derive(Debug, serde::Deserialize)] pub struct Request { @@ -296,10 +323,68 @@ async fn handle_command<'a>( .map_err(|e| format_err!("Failed to await at this time: {e:?}")), StreamCommand::PingGoals(args) => ping_goals_cmd(ctx, gh_id, message_data, &args).await, StreamCommand::DocsUpdate => trigger_docs_update(message_data, &ctx.zulip), + StreamCommand::Backport(args) => { + accept_decline_backport(message_data, &ctx.octocrab, &ctx.zulip, &args).await + } } } } +// TODO: shorter variant of this command (f.e. `backport accept` or even `accept`) that infers everything from the Message payload +async fn accept_decline_backport( + message_data: &Message, + octo_client: &Octocrab, + zulip_client: &ZulipClient, + args_data: &BackportArgs, +) -> anyhow::Result> { + let message = message_data.clone(); + let args = args_data.clone(); + let stream_id = message.stream_id.unwrap(); + let subject = message.subject.unwrap(); + let octo_client = octo_client.clone(); + + // Repository owner and name are hardcoded + // This command is only used in this repository + let repo_owner = "rust-lang"; + let repo_name = "rust"; + + // TODO: factor out the Zulip "URL encoder" to make it practical to use + let zulip_send_req = crate::zulip::MessageApiRequest { + recipient: Recipient::Stream { + id: stream_id, + topic: &subject, + }, + content: "", + }; + + // 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 + // See: https://rust-lang.zulipchat.com/#narrow/channel/122653-zulip/topic/.22near.22.20parameter.20in.20payload.20of.20send.20message.20API + let zulip_link = zulip_send_req.url(zulip_client); + + let message_body = match args.verb { + BackportVerbArgs::Accept + | BackportVerbArgs::Accepted + | BackportVerbArgs::Approve + | BackportVerbArgs::Approved => { + get_text_backport_approved(&args.channel, &args.verb, &zulip_link) + } + BackportVerbArgs::Decline | BackportVerbArgs::Declined => { + get_text_backport_declined(&args.channel, &args.verb, &zulip_link) + } + }; + + let res = octo_client + .issues(repo_owner, repo_name) + .create_comment(args.pr_num, &message_body) + .await + .with_context(|| anyhow::anyhow!("unable to post comment on #{}", args.pr_num)); + if res.is_err() { + tracing::error!("failed to post comment: {0:?}", res.err()); + } + + Ok(Some("".to_string())) +} + async fn ping_goals_cmd( ctx: Arc, gh_id: u64, diff --git a/src/zulip/commands.rs b/src/zulip/commands.rs index ddd23a684..5cef371e2 100644 --- a/src/zulip/commands.rs +++ b/src/zulip/commands.rs @@ -1,5 +1,6 @@ use crate::db::notifications::Identifier; use crate::db::review_prefs::RotationMode; +use crate::github::PullRequestNumber; use clap::{ColorChoice, Parser}; use std::num::NonZeroU32; use std::str::FromStr; @@ -161,8 +162,10 @@ pub enum StreamCommand { Read, /// Ping project goal owners. PingGoals(PingGoalsArgs), - /// Update docs + /// Update docs. DocsUpdate, + /// Accept or decline a backport. + Backport(BackportArgs), } #[derive(clap::Parser, Debug, PartialEq, Clone)] @@ -173,6 +176,34 @@ pub struct PingGoalsArgs { pub next_update: String, } +/// Backport release channels +#[derive(Clone, clap::ValueEnum, Debug, PartialEq)] +pub enum BackportChannelArgs { + Beta, + Stable, +} + +/// Backport verbs +#[derive(Clone, clap::ValueEnum, Debug, PartialEq)] +pub enum BackportVerbArgs { + Accept, + Accepted, + Approve, + Approved, + Decline, + Declined, +} + +#[derive(clap::Parser, Debug, PartialEq, Clone)] +pub struct BackportArgs { + /// Release channel this backport is pointing to. Allowed: "beta" or "stable". + pub channel: BackportChannelArgs, + /// Accept or decline this backport? Allowed: "accept", "accepted", "approve", "approved", "decline", "declined". + pub verb: BackportVerbArgs, + /// PR to be backported + pub pr_num: PullRequestNumber, +} + /// Helper function to parse CLI arguments without any colored help or error output. pub fn parse_cli<'a, T: Parser, I: Iterator>(input: I) -> anyhow::Result { fn allow_title_case(sub: clap::Command) -> clap::Command { @@ -292,6 +323,26 @@ mod tests { assert_eq!(parse_stream(&["await"]), StreamCommand::EndTopic); } + #[test] + fn backports_command() { + assert_eq!( + parse_stream(&["backport", "beta", "accept", "123456"]), + StreamCommand::Backport(BackportArgs { + channel: BackportChannelArgs::Beta, + verb: BackportVerbArgs::Accept, + pr_num: 123456 + }) + ); + assert_eq!( + parse_stream(&["backport", "stable", "decline", "123456"]), + StreamCommand::Backport(BackportArgs { + channel: BackportChannelArgs::Stable, + verb: BackportVerbArgs::Decline, + pr_num: 123456 + }) + ); + } + fn parse_chat(input: &[&str]) -> ChatCommand { parse_cli::(input.into_iter().copied()).unwrap() }