Skip to content

Commit c1f7a9a

Browse files
committed
Use timeline to detect state change in major change acceptance job
1 parent 2aaad35 commit c1f7a9a

File tree

1 file changed

+109
-16
lines changed

1 file changed

+109
-16
lines changed

src/handlers/major_change.rs

Lines changed: 109 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::{
1111
use anyhow::Context as _;
1212
use async_trait::async_trait;
1313
use chrono::{DateTime, Duration, Utc};
14+
use futures::TryStreamExt as _;
1415
use parser::command::second::SecondCommand;
1516
use serde::{Deserialize, Serialize};
1617
use tracing as log;
@@ -452,13 +453,27 @@ enum SecondedLogicError {
452453
accept_at: DateTime<Utc>,
453454
now: DateTime<Utc>,
454455
},
456+
IssueStateChanged {
457+
at: DateTime<Utc>,
458+
draft: bool,
459+
open: bool,
460+
},
455461
IssueNotReady {
456462
draft: bool,
457463
open: bool,
458464
},
459-
NotAMajorChange,
465+
EnablingLabelAbsent,
466+
EnablingLabelRemoved {
467+
at: DateTime<Utc>,
468+
},
460469
SecondLabelAbsent,
470+
SecondLabelRemoved {
471+
at: DateTime<Utc>,
472+
},
461473
ConcernsLabelPresent,
474+
ConcernsLabelAdded {
475+
at: DateTime<Utc>,
476+
},
462477
NoMajorChangeConfig,
463478
}
464479

@@ -470,12 +485,27 @@ impl Display for SecondedLogicError {
470485
SecondedLogicError::NotYetAcceptenceTime { accept_at, now } => {
471486
write!(f, "not yet acceptence time ({accept_at} > {now})")
472487
}
488+
SecondedLogicError::IssueStateChanged { at, draft, open } => {
489+
write!(
490+
f,
491+
"issue state changed at {at} (draft: {draft}; open: {open})"
492+
)
493+
}
473494
SecondedLogicError::IssueNotReady { draft, open } => {
474495
write!(f, "issue is not ready (draft: {draft}; open: {open})")
475496
}
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+
}
477501
SecondedLogicError::SecondLabelAbsent => write!(f, "second label is absent"),
502+
SecondedLogicError::SecondLabelRemoved { at } => {
503+
write!(f, "second labek removed at {at}")
504+
}
478505
SecondedLogicError::ConcernsLabelPresent => write!(f, "concerns label set"),
506+
SecondedLogicError::ConcernsLabelAdded { at } => {
507+
write!(f, "concerns label added at {at}")
508+
}
479509
SecondedLogicError::NoMajorChangeConfig => write!(f, "no `[major_change]` config"),
480510
}
481511
}
@@ -568,24 +598,87 @@ async fn process_seconded(
568598
.await
569599
.context("unable to get the associated issue")?;
570600

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);
574622

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+
};
578630

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+
}
582658
}
583659

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+
}
589682
}
590683

591684
if !issue.labels.iter().any(|l| l.name == config.accept_label) {

0 commit comments

Comments
 (0)