@@ -13,7 +13,7 @@ use crate::handlers::docs_update::docs_update;
1313use crate :: handlers:: pr_tracking:: get_assigned_prs;
1414use crate :: handlers:: project_goals:: { self , ping_project_goals_owners} ;
1515use crate :: interactions:: ErrorComment ;
16- use crate :: utils:: pluralize;
16+ use crate :: utils:: { contains_any , pluralize} ;
1717use crate :: zulip:: api:: { MessageApiResponse , Recipient } ;
1818use crate :: zulip:: client:: ZulipClient ;
1919use crate :: zulip:: commands:: {
@@ -24,12 +24,29 @@ use axum::Json;
2424use axum:: extract:: State ;
2525use axum:: extract:: rejection:: JsonRejection ;
2626use axum:: response:: IntoResponse ;
27+ use commands:: BackportArgs ;
28+ use octocrab:: Octocrab ;
2729use rust_team_data:: v1:: { TeamKind , TeamMember } ;
2830use std:: cmp:: Reverse ;
2931use std:: fmt:: Write as _;
3032use std:: sync:: Arc ;
3133use 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 ) ]
3552pub 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+
309395async fn ping_goals_cmd (
310396 ctx : Arc < Context > ,
311397 gh_id : u64 ,
0 commit comments