1+ use std:: fmt:: Display ;
2+
3+ use crate :: jobs:: Job ;
14use crate :: zulip:: api:: Recipient ;
25use crate :: {
36 config:: MajorChangeConfig ,
@@ -6,7 +9,10 @@ use crate::{
69 interactions:: ErrorComment ,
710} ;
811use anyhow:: Context as _;
12+ use async_trait:: async_trait;
13+ use chrono:: { DateTime , Duration , Utc } ;
914use parser:: command:: second:: SecondCommand ;
15+ use serde:: { Deserialize , Serialize } ;
1016use tracing as log;
1117
1218#[ derive( Clone , PartialEq , Eq , Debug ) ]
@@ -274,6 +280,42 @@ pub(super) async fn handle_command(
274280 false ,
275281 )
276282 . await
283+ . context ( "unable to process second command" ) ?;
284+
285+ // Add MCP to the MCP acceptence queue to be close automaticaly
286+ if let Some ( waiting_period) = & config. waiting_period {
287+ let seconded_at = Utc :: now ( ) ;
288+ let accept_at = if issue. repository ( ) . full_repo_name ( ) == "rust-lang/triagebot" {
289+ // Hack for the triagebot repo, so we can test more quickly
290+ seconded_at + Duration :: minutes ( 5 )
291+ } else {
292+ seconded_at + Duration :: days ( ( * waiting_period) . into ( ) )
293+ } ;
294+
295+ let major_change_seconded = MajorChangeSeconded {
296+ repo : issue. repository ( ) . full_repo_name ( ) ,
297+ issue : issue. number ,
298+ seconded_at,
299+ accept_at,
300+ } ;
301+
302+ tracing:: info!(
303+ "major_change inserting to acceptence queue: {:?}" ,
304+ & major_change_seconded
305+ ) ;
306+
307+ crate :: db:: schedule_job (
308+ & * ctx. db . get ( ) . await ,
309+ MAJOR_CHANGE_ACCEPTENCE_JOB_NAME ,
310+ serde_json:: to_value ( major_change_seconded)
311+ . context ( "unable to serialize the major change metadata" ) ?,
312+ accept_at,
313+ )
314+ . await
315+ . context ( "failed to add the major change to the automatic acceptance queue" ) ?;
316+ }
317+
318+ Ok ( ( ) )
277319}
278320
279321async fn handle (
@@ -362,3 +404,183 @@ fn zulip_topic_from_issue(issue: &ZulipGitHubReference) -> String {
362404 _ => format ! ( "{} {}" , issue. title, topic_ref) ,
363405 }
364406}
407+
408+ #[ derive( Debug ) ]
409+ enum SecondedLogicError {
410+ NotYetAcceptenceTime {
411+ accept_at : DateTime < Utc > ,
412+ now : DateTime < Utc > ,
413+ } ,
414+ IssueNotReady {
415+ draft : bool ,
416+ open : bool ,
417+ } ,
418+ ConcernsLabelSet ,
419+ NoMajorChangeConfig ,
420+ }
421+
422+ impl std:: error:: Error for SecondedLogicError { }
423+
424+ impl Display for SecondedLogicError {
425+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
426+ match self {
427+ SecondedLogicError :: NotYetAcceptenceTime { accept_at, now } => {
428+ write ! ( f, "not yet acceptence time ({accept_at} < {now})" )
429+ }
430+ SecondedLogicError :: IssueNotReady { draft, open } => {
431+ write ! ( f, "issue is not ready (draft: {draft}; open: {open})" )
432+ }
433+ SecondedLogicError :: ConcernsLabelSet => write ! ( f, "concerns label set" ) ,
434+ SecondedLogicError :: NoMajorChangeConfig => write ! ( f, "no `[major_change]` config" ) ,
435+ }
436+ }
437+ }
438+
439+ #[ derive( Debug , Serialize , Deserialize ) ]
440+ #[ cfg_attr( test, derive( PartialEq , Eq , Clone ) ) ]
441+ struct MajorChangeSeconded {
442+ repo : String ,
443+ issue : u64 ,
444+ seconded_at : DateTime < Utc > ,
445+ accept_at : DateTime < Utc > ,
446+ }
447+
448+ const MAJOR_CHANGE_ACCEPTENCE_JOB_NAME : & str = "major_change_acceptence" ;
449+
450+ pub ( crate ) struct MajorChangeAcceptenceJob ;
451+
452+ #[ async_trait]
453+ impl Job for MajorChangeAcceptenceJob {
454+ fn name ( & self ) -> & ' static str {
455+ MAJOR_CHANGE_ACCEPTENCE_JOB_NAME
456+ }
457+
458+ async fn run ( & self , ctx : & super :: Context , metadata : & serde_json:: Value ) -> anyhow:: Result < ( ) > {
459+ let major_change: MajorChangeSeconded = serde_json:: from_value ( metadata. clone ( ) )
460+ . context ( "unable to deserialize the metadata in major change acceptence job" ) ?;
461+
462+ let now = Utc :: now ( ) ;
463+
464+ match process_seconded ( & ctx, & major_change, now) . await {
465+ Ok ( ( ) ) => {
466+ tracing:: info!(
467+ "{}: major change ({:?}) as been accepted, remove from the queue" ,
468+ self . name( ) ,
469+ & major_change,
470+ ) ;
471+ }
472+ Err ( err) if err. downcast_ref :: < SecondedLogicError > ( ) . is_some ( ) => {
473+ tracing:: error!(
474+ "{}: major change ({:?}) has a logical error (no retry): {err}" ,
475+ self . name( ) ,
476+ & major_change,
477+ ) ;
478+ // exit job succesfully, so it's not retried
479+ }
480+ Err ( err) => {
481+ tracing:: error!(
482+ "{}: major change ({:?}) is in error: {err}" ,
483+ self . name( ) ,
484+ & major_change,
485+ ) ;
486+ return Err ( err) ; // so it is retried
487+ }
488+ }
489+
490+ Ok ( ( ) )
491+ }
492+ }
493+
494+ async fn process_seconded (
495+ ctx : & super :: Context ,
496+ major_change : & MajorChangeSeconded ,
497+ now : DateTime < Utc > ,
498+ ) -> anyhow:: Result < ( ) > {
499+ if major_change. accept_at < now {
500+ anyhow:: bail!( SecondedLogicError :: NotYetAcceptenceTime {
501+ accept_at: major_change. accept_at,
502+ now
503+ } ) ;
504+ }
505+
506+ let repo = ctx
507+ . github
508+ . repository ( & major_change. repo )
509+ . await
510+ . context ( "failed retrieving the repository informations" ) ?;
511+
512+ let config = crate :: config:: get ( & ctx. github , & repo)
513+ . await
514+ . context ( "failed to get triagebot configuration" ) ?;
515+
516+ let config = config
517+ . major_change
518+ . as_ref ( )
519+ . ok_or ( SecondedLogicError :: NoMajorChangeConfig ) ?;
520+
521+ let issue = repo
522+ . get_issue ( & ctx. github , major_change. issue )
523+ . await
524+ . context ( "unable to get the associated issue" ) ?;
525+
526+ if issue
527+ . labels
528+ . iter ( )
529+ . any ( |l| Some ( & l. name ) == config. concerns_label . as_ref ( ) )
530+ {
531+ anyhow:: bail!( SecondedLogicError :: ConcernsLabelSet ) ;
532+ }
533+
534+ if !issue. is_open ( ) || issue. draft {
535+ anyhow:: bail!( SecondedLogicError :: IssueNotReady {
536+ draft: issue. draft,
537+ open: issue. is_open( )
538+ } ) ;
539+ }
540+
541+ if !issue. labels . iter ( ) . any ( |l| l. name == config. accept_label ) {
542+ // Only post the comment if the accept_label isn't set yet, we may be in a retry
543+ issue
544+ . post_comment (
545+ & ctx. github ,
546+ "The final comment period is now complete, this major change is now accepted.\n \n As the automated representative, I would like to thank the author for their work and everyone else who contributed to this major change proposal."
547+ )
548+ . await
549+ . context ( "unable to post the acceptance comment" ) ?;
550+ }
551+ issue
552+ . add_labels (
553+ & ctx. github ,
554+ vec ! [ Label {
555+ name: config. accept_label. clone( ) ,
556+ } ] ,
557+ )
558+ . await
559+ . context ( "unable to add the accept label" ) ?;
560+ issue
561+ . remove_label ( & ctx. github , & config. second_label )
562+ . await
563+ . context ( "unable to remove the second label" ) ?;
564+ issue
565+ . close ( & ctx. github )
566+ . await
567+ . context ( "unable to close the issue" ) ?;
568+
569+ Ok ( ( ) )
570+ }
571+
572+ #[ test]
573+ fn major_change_queue_serialize ( ) {
574+ let original = MajorChangeSeconded {
575+ repo : "rust-lang/rust" . to_string ( ) ,
576+ issue : 1245 ,
577+ seconded_at : Utc :: now ( ) ,
578+ accept_at : Utc :: now ( ) ,
579+ } ;
580+
581+ let value = serde_json:: to_value ( original. clone ( ) ) . unwrap ( ) ;
582+
583+ let deserialized = serde_json:: from_value ( value) . unwrap ( ) ;
584+
585+ assert_eq ! ( original, deserialized) ;
586+ }
0 commit comments