Skip to content

Commit 6fb7a7d

Browse files
committed
Open thread a week prior to meeting - post update on issues without updates the next thursday
1 parent f1ae8be commit 6fb7a7d

File tree

4 files changed

+138
-17
lines changed

4 files changed

+138
-17
lines changed

src/db.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,16 +187,32 @@ pub async fn schedule_jobs(db: &DbClient, jobs: Vec<JobSchedule>) -> anyhow::Res
187187
let mut upcoming = job.schedule.upcoming(Utc).take(1);
188188

189189
if let Some(scheduled_at) = upcoming.next() {
190-
if let Err(_) = get_job_by_name_and_scheduled_at(&db, job.name, &scheduled_at).await {
191-
// mean there's no job already in the db with that name and scheduled_at
192-
insert_job(&db, job.name, &scheduled_at, &job.metadata).await?;
193-
}
190+
schedule_job(db, job.name, job.metadata, scheduled_at).await?;
194191
}
195192
}
196193

197194
Ok(())
198195
}
199196

197+
pub async fn schedule_job(
198+
db: &DbClient,
199+
job_name: &str,
200+
job_metadata: serde_json::Value,
201+
when: chrono::DateTime<Utc>,
202+
) -> anyhow::Result<()> {
203+
let all_jobs = jobs();
204+
if !all_jobs.iter().any(|j| j.name() == job_name) {
205+
anyhow::bail!("Job {} does not exist in the current job list.", job_name);
206+
}
207+
208+
if let Err(_) = get_job_by_name_and_scheduled_at(&db, job_name, &when).await {
209+
// mean there's no job already in the db with that name and scheduled_at
210+
insert_job(&db, job_name, &when, &job_metadata).await?;
211+
}
212+
213+
Ok(())
214+
}
215+
200216
pub async fn run_scheduled_jobs(ctx: &Context, db: &DbClient) -> anyhow::Result<()> {
201217
let jobs = get_jobs_to_execute(&db).await.unwrap();
202218
tracing::trace!("jobs to execute: {:#?}", jobs);

src/handlers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ mod review_submitted;
4545
mod rfc_helper;
4646
pub mod rustc_commits;
4747
mod shortcut;
48-
mod types_planning_updates;
48+
pub mod types_planning_updates;
4949

5050
pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
5151
let config = config::get(&ctx.github, event.repo()).await;

src/handlers/types_planning_updates.rs

Lines changed: 97 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,89 @@
1+
use crate::db::schedule_job;
12
use crate::github;
23
use crate::jobs::Job;
34
use crate::zulip::BOT_EMAIL;
45
use crate::zulip::{to_zulip_id, MembersApiResponse};
56
use anyhow::{format_err, Context as _};
67
use async_trait::async_trait;
7-
use chrono::{Duration, Utc};
8+
use chrono::{Datelike, Duration, NaiveTime, TimeZone, Utc};
9+
use serde::{Deserialize, Serialize};
810

9-
pub struct TypesPlanningUpdatesJob;
11+
const TYPES_REPO: &'static str = "rust-lang/types-team";
12+
13+
pub struct TypesPlanningMeetingThreadOpenJob;
1014

1115
#[async_trait]
12-
impl Job for TypesPlanningUpdatesJob {
16+
impl Job for TypesPlanningMeetingThreadOpenJob {
1317
fn name(&self) -> &'static str {
14-
"types_planning_updates"
18+
"types_planning_meeting_thread_open"
1519
}
1620

1721
async fn run(&self, ctx: &super::Context, _metadata: &serde_json::Value) -> anyhow::Result<()> {
18-
request_updates(ctx).await?;
22+
// On the last week of the month, we open a thread on zulip for the next Monday
23+
let today = chrono::Utc::now().date().naive_utc();
24+
let first_monday = today + chrono::Duration::days(7);
25+
let meeting_date_string = first_monday.format("%Y-%m-%d").to_string();
26+
let message = format!("\
27+
Hello @*T-types/meetings*. Monthly planning meeting in one week.\n\
28+
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\
29+
Extra reminders will be sent later this week.");
30+
let zulip_req = crate::zulip::MessageApiRequest {
31+
recipient: crate::zulip::Recipient::Stream {
32+
id: 326132,
33+
topic: &format!("{meeting_date_string} planning meeting"),
34+
},
35+
content: &message,
36+
};
37+
zulip_req.send(&ctx.github.raw()).await?;
38+
39+
// Then, we want to schedule the next Thursday after this
40+
let mut thursday = today;
41+
while thursday.weekday().num_days_from_monday() != 3 {
42+
thursday = thursday.succ();
43+
}
44+
let thursday_at_noon =
45+
Utc.from_utc_datetime(&thursday.and_time(NaiveTime::from_hms(12, 0, 0)));
46+
let metadata = serde_json::value::to_value(PlanningMeetingUpdatesPingMetadata {
47+
date_string: meeting_date_string,
48+
})
49+
.unwrap();
50+
schedule_job(
51+
&*ctx.db.get().await,
52+
TypesPlanningMeetingUpdatesPing.name(),
53+
metadata,
54+
thursday_at_noon,
55+
)
56+
.await?;
57+
1958
Ok(())
2059
}
2160
}
2261

23-
const TYPES_REPO: &'static str = "rust-lang/types-team";
62+
#[derive(Serialize, Deserialize)]
63+
pub struct PlanningMeetingUpdatesPingMetadata {
64+
pub date_string: String,
65+
}
66+
67+
pub struct TypesPlanningMeetingUpdatesPing;
68+
69+
#[async_trait]
70+
impl Job for TypesPlanningMeetingUpdatesPing {
71+
fn name(&self) -> &'static str {
72+
"types_planning_meeting_updates_ping"
73+
}
74+
75+
async fn run(&self, ctx: &super::Context, metadata: &serde_json::Value) -> anyhow::Result<()> {
76+
let metadata = serde_json::from_value(metadata.clone())?;
77+
// On the thursday before the first monday, we want to ping for updates
78+
request_updates(ctx, metadata).await?;
79+
Ok(())
80+
}
81+
}
2482

25-
pub async fn request_updates(ctx: &super::Context) -> anyhow::Result<()> {
83+
pub async fn request_updates(
84+
ctx: &super::Context,
85+
metadata: PlanningMeetingUpdatesPingMetadata,
86+
) -> anyhow::Result<()> {
2687
let gh = &ctx.github;
2788
let types_repo = gh.repository(TYPES_REPO).await?;
2889

@@ -36,19 +97,28 @@ pub async fn request_updates(ctx: &super::Context) -> anyhow::Result<()> {
3697
.await
3798
.with_context(|| "Unable to get issues.")?;
3899

100+
let mut issues_needs_updates = vec![];
39101
for issue in issues {
102+
// Github doesn't have a nice way to get the *last* comment; we would have to paginate all comments to get it.
103+
// For now, just bail out if there are more than 100 comments (if this ever becomes a problem, we will have to fix).
40104
let comments = issue.get_first100_comments(gh).await?;
41105
if comments.len() >= 100 {
42106
anyhow::bail!(
43107
"Encountered types tracking issue with 100 or more comments; needs implementation."
44108
);
45109
}
46-
let older_than_28_days = comments
110+
111+
// If there are any comments in the past 7 days, we consider this "updated". We *could* be more clever, but
112+
// this is fine under the assumption that tracking issues should only contain updates.
113+
let older_than_7_days = comments
47114
.last()
48-
.map_or(true, |c| c.updated_at < (Utc::now() - Duration::days(28)));
49-
if !older_than_28_days {
115+
.map_or(true, |c| c.updated_at < (Utc::now() - Duration::days(7)));
116+
if !older_than_7_days {
50117
continue;
51118
}
119+
// In the future, we should reach out to specific people in charge of specific issues. For now, because our tracking
120+
// method is crude and will over-estimate the issues that need updates.
121+
/*
52122
let mut dmed_assignee = false;
53123
for assignee in issue.assignees {
54124
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<()> {
85155
};
86156
zulip_req.send(&ctx.github.raw()).await?;
87157
}
158+
*/
159+
issues_needs_updates.push(format!("- [Issue #{}]({})", issue.number, issue.html_url));
88160
}
89161

162+
let issue_list = issues_needs_updates.join("\n");
163+
164+
let message = format!("The following issues still need updates:\n\n{issue_list}");
165+
166+
let meeting_date_string = metadata.date_string;
167+
let zulip_req = crate::zulip::MessageApiRequest {
168+
recipient: crate::zulip::Recipient::Stream {
169+
id: 326132,
170+
topic: &format!("{meeting_date_string} planning meeting"),
171+
},
172+
content: &message,
173+
};
174+
zulip_req.send(&ctx.github.raw()).await?;
175+
90176
Ok(())
91177
}
92178

179+
#[allow(unused)] // Needed for commented out bit above
93180
async fn zulip_id_and_email(
94181
ctx: &super::Context,
95182
github_id: i64,

src/jobs.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,14 @@ use cron::Schedule;
4848

4949
use crate::{
5050
db::jobs::JobSchedule,
51-
handlers::{docs_update::DocsUpdateJob, rustc_commits::RustcCommitsJob, Context},
51+
handlers::{
52+
docs_update::DocsUpdateJob,
53+
rustc_commits::RustcCommitsJob,
54+
types_planning_updates::{
55+
TypesPlanningMeetingThreadOpenJob, TypesPlanningMeetingUpdatesPing,
56+
},
57+
Context,
58+
},
5259
};
5360

5461
// 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;
6168

6269
// The default jobs to schedule, repeatedly.
6370
pub fn jobs() -> Vec<Box<dyn Job + Send + Sync>> {
64-
vec![Box::new(DocsUpdateJob), Box::new(RustcCommitsJob)]
71+
vec![
72+
Box::new(DocsUpdateJob),
73+
Box::new(RustcCommitsJob),
74+
Box::new(TypesPlanningMeetingThreadOpenJob),
75+
Box::new(TypesPlanningMeetingUpdatesPing),
76+
]
6577
}
6678

6779
pub fn default_jobs() -> Vec<JobSchedule> {
@@ -78,6 +90,12 @@ pub fn default_jobs() -> Vec<JobSchedule> {
7890
schedule: Schedule::from_str("* 0,30 * * * * *").unwrap(),
7991
metadata: serde_json::Value::Null,
8092
},
93+
JobSchedule {
94+
name: TypesPlanningMeetingThreadOpenJob.name(),
95+
// Last Monday of every month
96+
schedule: Schedule::from_str("0 0 12 ? * 2L *").unwrap(),
97+
metadata: serde_json::Value::Null,
98+
},
8199
]
82100
}
83101

0 commit comments

Comments
 (0)