@@ -11,6 +11,7 @@ use crate::{
11
11
use anyhow:: Context as _;
12
12
use async_trait:: async_trait;
13
13
use chrono:: { DateTime , Duration , Utc } ;
14
+ use futures:: TryStreamExt as _;
14
15
use parser:: command:: second:: SecondCommand ;
15
16
use serde:: { Deserialize , Serialize } ;
16
17
use tracing as log;
@@ -452,13 +453,27 @@ enum SecondedLogicError {
452
453
accept_at : DateTime < Utc > ,
453
454
now : DateTime < Utc > ,
454
455
} ,
456
+ IssueStateChanged {
457
+ at : DateTime < Utc > ,
458
+ draft : bool ,
459
+ open : bool ,
460
+ } ,
455
461
IssueNotReady {
456
462
draft : bool ,
457
463
open : bool ,
458
464
} ,
459
- NotAMajorChange ,
465
+ EnablingLabelAbsent ,
466
+ EnablingLabelRemoved {
467
+ at : DateTime < Utc > ,
468
+ } ,
460
469
SecondLabelAbsent ,
470
+ SecondLabelRemoved {
471
+ at : DateTime < Utc > ,
472
+ } ,
461
473
ConcernsLabelPresent ,
474
+ ConcernsLabelAdded {
475
+ at : DateTime < Utc > ,
476
+ } ,
462
477
NoMajorChangeConfig ,
463
478
}
464
479
@@ -470,12 +485,27 @@ impl Display for SecondedLogicError {
470
485
SecondedLogicError :: NotYetAcceptenceTime { accept_at, now } => {
471
486
write ! ( f, "not yet acceptence time ({accept_at} > {now})" )
472
487
}
488
+ SecondedLogicError :: IssueStateChanged { at, draft, open } => {
489
+ write ! (
490
+ f,
491
+ "issue state changed at {at} (draft: {draft}; open: {open})"
492
+ )
493
+ }
473
494
SecondedLogicError :: IssueNotReady { draft, open } => {
474
495
write ! ( f, "issue is not ready (draft: {draft}; open: {open})" )
475
496
}
476
- SecondedLogicError :: NotAMajorChange => write ! ( f, "not a major change" ) ,
497
+ SecondedLogicError :: EnablingLabelAbsent => write ! ( f, "enabling label is absent" ) ,
498
+ SecondedLogicError :: EnablingLabelRemoved { at } => {
499
+ write ! ( f, "enabling label removed at {at}" )
500
+ }
477
501
SecondedLogicError :: SecondLabelAbsent => write ! ( f, "second label is absent" ) ,
502
+ SecondedLogicError :: SecondLabelRemoved { at } => {
503
+ write ! ( f, "second labek removed at {at}" )
504
+ }
478
505
SecondedLogicError :: ConcernsLabelPresent => write ! ( f, "concerns label set" ) ,
506
+ SecondedLogicError :: ConcernsLabelAdded { at } => {
507
+ write ! ( f, "concerns label added at {at}" )
508
+ }
479
509
SecondedLogicError :: NoMajorChangeConfig => write ! ( f, "no `[major_change]` config" ) ,
480
510
}
481
511
}
@@ -568,24 +598,87 @@ async fn process_seconded(
568
598
. await
569
599
. context ( "unable to get the associated issue" ) ?;
570
600
571
- if !issue. labels . iter ( ) . any ( |l| l. name == config. enabling_label ) {
572
- anyhow:: bail!( SecondedLogicError :: NotAMajorChange ) ;
573
- }
601
+ {
602
+ // Static checks against the timeline to block the acceptance if the state changed between
603
+ // the second and now (like concerns added, issue closed, ...).
604
+ //
605
+ // Note that checking the timeline is important as a concern could have been added and
606
+ // resolved by the time we run, missing that this job should not run.
607
+
608
+ let ( org, repo) = major_change
609
+ . repo
610
+ . split_once ( '/' )
611
+ . context ( "unable to split org/repo" ) ?;
612
+ let timeline = ctx
613
+ . octocrab
614
+ . issues ( org, repo)
615
+ . list_timeline_events ( major_change. issue )
616
+ . per_page ( 100 )
617
+ . send ( )
618
+ . await
619
+ . context ( "unable to get the timeline for the issue" ) ?
620
+ . into_stream ( & ctx. octocrab ) ;
621
+ let mut timeline = std:: pin:: pin!( timeline) ;
574
622
575
- if !issue. labels . iter ( ) . any ( |l| l. name == config. second_label ) {
576
- anyhow:: bail!( SecondedLogicError :: SecondLabelAbsent ) ;
577
- }
623
+ while let Some ( event) = timeline. try_next ( ) . await ? {
624
+ use octocrab:: models:: Event ;
625
+
626
+ let Some ( at) = event. created_at else {
627
+ // event has no associated time, can't do anything about it
628
+ continue ;
629
+ } ;
578
630
579
- let concerns_label = config. concerns_label . as_ref ( ) ;
580
- if issue. labels . iter ( ) . any ( |l| Some ( & l. name ) == concerns_label) {
581
- anyhow:: bail!( SecondedLogicError :: ConcernsLabelPresent ) ;
631
+ if at <= major_change. seconded_at {
632
+ // event is before the second, ignore it
633
+ continue ;
634
+ }
635
+
636
+ if event. event == Event :: Unlabeled {
637
+ let label = event. label . context ( "unlabeled event without label" ) ?;
638
+
639
+ if label. name == config. enabling_label {
640
+ anyhow:: bail!( SecondedLogicError :: EnablingLabelRemoved { at } ) ;
641
+ } else if label. name == config. second_label {
642
+ anyhow:: bail!( SecondedLogicError :: SecondLabelRemoved { at } ) ;
643
+ }
644
+ } else if event. event == Event :: Labeled {
645
+ let label = event. label . context ( "labeled event without label" ) ?;
646
+
647
+ if Some ( & label. name ) == config. concerns_label . as_ref ( ) {
648
+ anyhow:: bail!( SecondedLogicError :: ConcernsLabelAdded { at } )
649
+ }
650
+ } else if event. event == Event :: Closed || event. event == Event :: ConvertToDraft {
651
+ anyhow:: bail!( SecondedLogicError :: IssueStateChanged {
652
+ at,
653
+ draft: event. event == Event :: ConvertToDraft ,
654
+ open: event. event == Event :: Closed ,
655
+ } ) ;
656
+ }
657
+ }
582
658
}
583
659
584
- if !issue. is_open ( ) || issue. draft {
585
- anyhow:: bail!( SecondedLogicError :: IssueNotReady {
586
- draft: issue. draft,
587
- open: issue. is_open( )
588
- } ) ;
660
+ {
661
+ // Sanity checks to make sure the final state is still all right
662
+
663
+ if !issue. labels . iter ( ) . any ( |l| l. name == config. enabling_label ) {
664
+ anyhow:: bail!( SecondedLogicError :: EnablingLabelAbsent ) ;
665
+ }
666
+
667
+ if !issue. labels . iter ( ) . any ( |l| l. name == config. second_label ) {
668
+ anyhow:: bail!( SecondedLogicError :: SecondLabelAbsent ) ;
669
+ }
670
+
671
+ let concerns_label = config. concerns_label . as_ref ( ) ;
672
+ if issue. labels . iter ( ) . any ( |l| Some ( & l. name ) == concerns_label) {
673
+ anyhow:: bail!( SecondedLogicError :: ConcernsLabelPresent ) ;
674
+ }
675
+
676
+ if !issue. is_open ( ) || issue. draft {
677
+ anyhow:: bail!( SecondedLogicError :: IssueNotReady {
678
+ draft: issue. draft,
679
+ open: issue. is_open( )
680
+ } ) ;
681
+ }
589
682
}
590
683
591
684
if !issue. labels . iter ( ) . any ( |l| l. name == config. accept_label ) {
0 commit comments