瀏覽代碼

Open thread a week prior to meeting - post update on issues without updates the next thursday

Jack Huey 1 年之前
父節點
當前提交
6fb7a7d39a
共有 4 個文件被更改,包括 138 次插入17 次删除
  1. 20 4
      src/db.rs
  2. 1 1
      src/handlers.rs
  3. 97 10
      src/handlers/types_planning_updates.rs
  4. 20 2
      src/jobs.rs

+ 20 - 4
src/db.rs

@@ -187,16 +187,32 @@ pub async fn schedule_jobs(db: &DbClient, jobs: Vec<JobSchedule>) -> anyhow::Res
         let mut upcoming = job.schedule.upcoming(Utc).take(1);
         let mut upcoming = job.schedule.upcoming(Utc).take(1);
 
 
         if let Some(scheduled_at) = upcoming.next() {
         if let Some(scheduled_at) = upcoming.next() {
-            if let Err(_) = get_job_by_name_and_scheduled_at(&db, job.name, &scheduled_at).await {
-                // mean there's no job already in the db with that name and scheduled_at
-                insert_job(&db, job.name, &scheduled_at, &job.metadata).await?;
-            }
+            schedule_job(db, job.name, job.metadata, scheduled_at).await?;
         }
         }
     }
     }
 
 
     Ok(())
     Ok(())
 }
 }
 
 
+pub async fn schedule_job(
+    db: &DbClient,
+    job_name: &str,
+    job_metadata: serde_json::Value,
+    when: chrono::DateTime<Utc>,
+) -> anyhow::Result<()> {
+    let all_jobs = jobs();
+    if !all_jobs.iter().any(|j| j.name() == job_name) {
+        anyhow::bail!("Job {} does not exist in the current job list.", job_name);
+    }
+
+    if let Err(_) = get_job_by_name_and_scheduled_at(&db, job_name, &when).await {
+        // mean there's no job already in the db with that name and scheduled_at
+        insert_job(&db, job_name, &when, &job_metadata).await?;
+    }
+
+    Ok(())
+}
+
 pub async fn run_scheduled_jobs(ctx: &Context, db: &DbClient) -> anyhow::Result<()> {
 pub async fn run_scheduled_jobs(ctx: &Context, db: &DbClient) -> anyhow::Result<()> {
     let jobs = get_jobs_to_execute(&db).await.unwrap();
     let jobs = get_jobs_to_execute(&db).await.unwrap();
     tracing::trace!("jobs to execute: {:#?}", jobs);
     tracing::trace!("jobs to execute: {:#?}", jobs);

+ 1 - 1
src/handlers.rs

@@ -45,7 +45,7 @@ mod review_submitted;
 mod rfc_helper;
 mod rfc_helper;
 pub mod rustc_commits;
 pub mod rustc_commits;
 mod shortcut;
 mod shortcut;
-mod types_planning_updates;
+pub mod types_planning_updates;
 
 
 pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
 pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
     let config = config::get(&ctx.github, event.repo()).await;
     let config = config::get(&ctx.github, event.repo()).await;

+ 97 - 10
src/handlers/types_planning_updates.rs

@@ -1,28 +1,89 @@
+use crate::db::schedule_job;
 use crate::github;
 use crate::github;
 use crate::jobs::Job;
 use crate::jobs::Job;
 use crate::zulip::BOT_EMAIL;
 use crate::zulip::BOT_EMAIL;
 use crate::zulip::{to_zulip_id, MembersApiResponse};
 use crate::zulip::{to_zulip_id, MembersApiResponse};
 use anyhow::{format_err, Context as _};
 use anyhow::{format_err, Context as _};
 use async_trait::async_trait;
 use async_trait::async_trait;
-use chrono::{Duration, Utc};
+use chrono::{Datelike, Duration, NaiveTime, TimeZone, Utc};
+use serde::{Deserialize, Serialize};
 
 
-pub struct TypesPlanningUpdatesJob;
+const TYPES_REPO: &'static str = "rust-lang/types-team";
+
+pub struct TypesPlanningMeetingThreadOpenJob;
 
 
 #[async_trait]
 #[async_trait]
-impl Job for TypesPlanningUpdatesJob {
+impl Job for TypesPlanningMeetingThreadOpenJob {
     fn name(&self) -> &'static str {
     fn name(&self) -> &'static str {
-        "types_planning_updates"
+        "types_planning_meeting_thread_open"
     }
     }
 
 
     async fn run(&self, ctx: &super::Context, _metadata: &serde_json::Value) -> anyhow::Result<()> {
     async fn run(&self, ctx: &super::Context, _metadata: &serde_json::Value) -> anyhow::Result<()> {
-        request_updates(ctx).await?;
+        // On the last week of the month, we open a thread on zulip for the next Monday
+        let today = chrono::Utc::now().date().naive_utc();
+        let first_monday = today + chrono::Duration::days(7);
+        let meeting_date_string = first_monday.format("%Y-%m-%d").to_string();
+        let message = format!("\
+            Hello @*T-types/meetings*. Monthly planning meeting in one week.\n\
+            This is a reminder to update the current [roadmap tracking issues](https://github.com/rust-lang/types-team/issues?q=is%3Aissue+is%3Aopen+label%3Aroadmap-tracking-issue).\n\
+            Extra reminders will be sent later this week.");
+        let zulip_req = crate::zulip::MessageApiRequest {
+            recipient: crate::zulip::Recipient::Stream {
+                id: 326132,
+                topic: &format!("{meeting_date_string} planning meeting"),
+            },
+            content: &message,
+        };
+        zulip_req.send(&ctx.github.raw()).await?;
+
+        // Then, we want to schedule the next Thursday after this
+        let mut thursday = today;
+        while thursday.weekday().num_days_from_monday() != 3 {
+            thursday = thursday.succ();
+        }
+        let thursday_at_noon =
+            Utc.from_utc_datetime(&thursday.and_time(NaiveTime::from_hms(12, 0, 0)));
+        let metadata = serde_json::value::to_value(PlanningMeetingUpdatesPingMetadata {
+            date_string: meeting_date_string,
+        })
+        .unwrap();
+        schedule_job(
+            &*ctx.db.get().await,
+            TypesPlanningMeetingUpdatesPing.name(),
+            metadata,
+            thursday_at_noon,
+        )
+        .await?;
+
         Ok(())
         Ok(())
     }
     }
 }
 }
 
 
-const TYPES_REPO: &'static str = "rust-lang/types-team";
+#[derive(Serialize, Deserialize)]
+pub struct PlanningMeetingUpdatesPingMetadata {
+    pub date_string: String,
+}
+
+pub struct TypesPlanningMeetingUpdatesPing;
+
+#[async_trait]
+impl Job for TypesPlanningMeetingUpdatesPing {
+    fn name(&self) -> &'static str {
+        "types_planning_meeting_updates_ping"
+    }
+
+    async fn run(&self, ctx: &super::Context, metadata: &serde_json::Value) -> anyhow::Result<()> {
+        let metadata = serde_json::from_value(metadata.clone())?;
+        // On the thursday before the first monday, we want to ping for updates
+        request_updates(ctx, metadata).await?;
+        Ok(())
+    }
+}
 
 
-pub async fn request_updates(ctx: &super::Context) -> anyhow::Result<()> {
+pub async fn request_updates(
+    ctx: &super::Context,
+    metadata: PlanningMeetingUpdatesPingMetadata,
+) -> anyhow::Result<()> {
     let gh = &ctx.github;
     let gh = &ctx.github;
     let types_repo = gh.repository(TYPES_REPO).await?;
     let types_repo = gh.repository(TYPES_REPO).await?;
 
 
@@ -36,19 +97,28 @@ pub async fn request_updates(ctx: &super::Context) -> anyhow::Result<()> {
         .await
         .await
         .with_context(|| "Unable to get issues.")?;
         .with_context(|| "Unable to get issues.")?;
 
 
+    let mut issues_needs_updates = vec![];
     for issue in issues {
     for issue in issues {
+        // Github doesn't have a nice way to get the *last* comment; we would have to paginate all comments to get it.
+        // For now, just bail out if there are more than 100 comments (if this ever becomes a problem, we will have to fix).
         let comments = issue.get_first100_comments(gh).await?;
         let comments = issue.get_first100_comments(gh).await?;
         if comments.len() >= 100 {
         if comments.len() >= 100 {
             anyhow::bail!(
             anyhow::bail!(
                 "Encountered types tracking issue with 100 or more comments; needs implementation."
                 "Encountered types tracking issue with 100 or more comments; needs implementation."
             );
             );
         }
         }
-        let older_than_28_days = comments
+
+        // If there are any comments in the past 7 days, we consider this "updated". We *could* be more clever, but
+        // this is fine under the assumption that tracking issues should only contain updates.
+        let older_than_7_days = comments
             .last()
             .last()
-            .map_or(true, |c| c.updated_at < (Utc::now() - Duration::days(28)));
-        if !older_than_28_days {
+            .map_or(true, |c| c.updated_at < (Utc::now() - Duration::days(7)));
+        if !older_than_7_days {
             continue;
             continue;
         }
         }
+        // In the future, we should reach out to specific people in charge of specific issues. For now, because our tracking
+        // method is crude and will over-estimate the issues that need updates.
+        /*
         let mut dmed_assignee = false;
         let mut dmed_assignee = false;
         for assignee in issue.assignees {
         for assignee in issue.assignees {
             let zulip_id_and_email = zulip_id_and_email(ctx, assignee.id.unwrap()).await?;
             let zulip_id_and_email = zulip_id_and_email(ctx, assignee.id.unwrap()).await?;
@@ -85,11 +155,28 @@ pub async fn request_updates(ctx: &super::Context) -> anyhow::Result<()> {
             };
             };
             zulip_req.send(&ctx.github.raw()).await?;
             zulip_req.send(&ctx.github.raw()).await?;
         }
         }
+        */
+        issues_needs_updates.push(format!("- [Issue #{}]({})", issue.number, issue.html_url));
     }
     }
 
 
+    let issue_list = issues_needs_updates.join("\n");
+
+    let message = format!("The following issues still need updates:\n\n{issue_list}");
+
+    let meeting_date_string = metadata.date_string;
+    let zulip_req = crate::zulip::MessageApiRequest {
+        recipient: crate::zulip::Recipient::Stream {
+            id: 326132,
+            topic: &format!("{meeting_date_string} planning meeting"),
+        },
+        content: &message,
+    };
+    zulip_req.send(&ctx.github.raw()).await?;
+
     Ok(())
     Ok(())
 }
 }
 
 
+#[allow(unused)] // Needed for commented out bit above
 async fn zulip_id_and_email(
 async fn zulip_id_and_email(
     ctx: &super::Context,
     ctx: &super::Context,
     github_id: i64,
     github_id: i64,

+ 20 - 2
src/jobs.rs

@@ -48,7 +48,14 @@ use cron::Schedule;
 
 
 use crate::{
 use crate::{
     db::jobs::JobSchedule,
     db::jobs::JobSchedule,
-    handlers::{docs_update::DocsUpdateJob, rustc_commits::RustcCommitsJob, Context},
+    handlers::{
+        docs_update::DocsUpdateJob,
+        rustc_commits::RustcCommitsJob,
+        types_planning_updates::{
+            TypesPlanningMeetingThreadOpenJob, TypesPlanningMeetingUpdatesPing,
+        },
+        Context,
+    },
 };
 };
 
 
 // How often new cron-based jobs will be placed in the queue.
 // How often new cron-based jobs will be placed in the queue.
@@ -61,7 +68,12 @@ pub const JOB_PROCESSING_CADENCE_IN_SECS: u64 = 60;
 
 
 // The default jobs to schedule, repeatedly.
 // The default jobs to schedule, repeatedly.
 pub fn jobs() -> Vec<Box<dyn Job + Send + Sync>> {
 pub fn jobs() -> Vec<Box<dyn Job + Send + Sync>> {
-    vec![Box::new(DocsUpdateJob), Box::new(RustcCommitsJob)]
+    vec![
+        Box::new(DocsUpdateJob),
+        Box::new(RustcCommitsJob),
+        Box::new(TypesPlanningMeetingThreadOpenJob),
+        Box::new(TypesPlanningMeetingUpdatesPing),
+    ]
 }
 }
 
 
 pub fn default_jobs() -> Vec<JobSchedule> {
 pub fn default_jobs() -> Vec<JobSchedule> {
@@ -78,6 +90,12 @@ pub fn default_jobs() -> Vec<JobSchedule> {
             schedule: Schedule::from_str("* 0,30 * * * * *").unwrap(),
             schedule: Schedule::from_str("* 0,30 * * * * *").unwrap(),
             metadata: serde_json::Value::Null,
             metadata: serde_json::Value::Null,
         },
         },
+        JobSchedule {
+            name: TypesPlanningMeetingThreadOpenJob.name(),
+            // Last Monday of every month
+            schedule: Schedule::from_str("0 0 12 ? * 2L *").unwrap(),
+            metadata: serde_json::Value::Null,
+        },
     ]
     ]
 }
 }