Skip to content

Commit 57e94e9

Browse files
committed
Add automatic closing of major change after their waiting period
1 parent 8c613ad commit 57e94e9

File tree

4 files changed

+231
-2
lines changed

4 files changed

+231
-2
lines changed

src/config.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,9 @@ pub(crate) struct MajorChangeConfig {
400400
pub(crate) meeting_label: String,
401401
/// This label signals there are concern(s) about the proposal.
402402
pub(crate) concerns_label: Option<String>,
403+
/// An optional duration (in days) for the waiting period after second for the
404+
/// major change to become automaticaly accepted.
405+
pub(crate) waiting_period: Option<u16>,
403406
/// The Zulip stream ID where the messages about the status of
404407
/// the major changed should be relayed.
405408
pub(crate) zulip_stream: u64,

src/handlers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ mod concern;
3434
pub mod docs_update;
3535
mod github_releases;
3636
mod issue_links;
37-
mod major_change;
37+
pub(crate) mod major_change;
3838
mod mentions;
3939
mod merge_conflicts;
4040
mod milestone_prs;

src/handlers/major_change.rs

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::fmt::Display;
2+
3+
use crate::jobs::Job;
14
use crate::zulip::api::Recipient;
25
use crate::{
36
config::MajorChangeConfig,
@@ -6,7 +9,10 @@ use crate::{
69
interactions::ErrorComment,
710
};
811
use anyhow::Context as _;
12+
use async_trait::async_trait;
13+
use chrono::{DateTime, Duration, Utc};
914
use parser::command::second::SecondCommand;
15+
use serde::{Deserialize, Serialize};
1016
use 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

279321
async 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\nAs 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+
}

src/jobs.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ use cron::Schedule;
4949
use crate::handlers::pull_requests_assignment_update::PullRequestAssignmentUpdate;
5050
use crate::{
5151
db::jobs::JobSchedule,
52-
handlers::{docs_update::DocsUpdateJob, rustc_commits::RustcCommitsJob, Context},
52+
handlers::{
53+
docs_update::DocsUpdateJob, major_change::MajorChangeAcceptenceJob,
54+
rustc_commits::RustcCommitsJob, Context,
55+
},
5356
};
5457

5558
/// How often new cron-based jobs will be placed in the queue.
@@ -66,6 +69,7 @@ pub fn jobs() -> Vec<Box<dyn Job + Send + Sync>> {
6669
Box::new(DocsUpdateJob),
6770
Box::new(RustcCommitsJob),
6871
Box::new(PullRequestAssignmentUpdate),
72+
Box::new(MajorChangeAcceptenceJob),
6973
]
7074
}
7175

0 commit comments

Comments
 (0)