Skip to content

Commit d7b0278

Browse files
YuriiMotovtiangolo
andauthored
✨ Add configurable reminder before closing issue (#39)
Co-authored-by: Sebastián Ramírez <[email protected]>
1 parent 0a9b900 commit d7b0278

File tree

3 files changed

+128
-5
lines changed

3 files changed

+128
-5
lines changed

README.md

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,11 @@ Imagine this JSON config:
8383
},
8484
"waiting": {
8585
"delay": 691200,
86-
"message": "Closing after 8 days of waiting for the additional info requested."
86+
"message": "Closing after 8 days of waiting for the additional info requested.",
87+
"reminder": {
88+
"before": "P3D",
89+
"message": "Heads-up: this will be closed in ~3 days unless there’s new activity."
90+
}
8791
},
8892
"needs-tests": {
8993
"delay": 691200,
@@ -130,7 +134,13 @@ Then, if:
130134
* the label was added _after_ the last comment
131135
* the last comment was addded more than `691200` seconds (8 days) ago
132136

133-
...the GitHub action would close the issue with:
137+
...the GitHub action would send a reminder on day 5 (because the delay is 8 days and the reminder is set to 3 days before closing):
138+
139+
```markdown
140+
Heads-up: this will be closed in ~3 days unless there’s new activity.
141+
```
142+
143+
...and if there is still no activity, it would finally close the issue with:
134144

135145
```markdown
136146
Closing after 10 days of waiting for the additional info requested.
@@ -174,6 +184,30 @@ After this GitHub action closes an issue it can also automatically remove the la
174184

175185
By default it is false, and doesn't remove the label from the issue.
176186

187+
### Reminder
188+
189+
Each label can also define an optional reminder with:
190+
191+
* `before`: How long before the issue/PR would be closed to send the reminder.
192+
Must be shorter than the main `delay`.
193+
Supports ISO 8601 durations (e.g. `P3D`) or seconds.
194+
* `message`: The text to post as a comment.
195+
196+
The reminder is just a comment, it does not close the issue or PR.
197+
198+
Example:
199+
200+
```json
201+
"waiting": {
202+
"delay": 691200,
203+
"message": "Closing after 8 days of waiting for the additional info requested.",
204+
"reminder": {
205+
"before": "P3D",
206+
"message": "Heads-up: this will be closed in ~3 days unless there’s new activity."
207+
}
208+
}
209+
210+
177211
### Defaults
178212
179213
By default, any config has:
@@ -187,6 +221,7 @@ Assuming the original issue was solved, it will be automatically closed now.
187221

188222
* `remove_label_on_comment`: True. If someone adds a comment after you added the label, it will remove the label from the issue.
189223
* `remove_label_on_close`: False. After this GitHub action closes the issue it would also remove the label from the issue.
224+
* `reminder`: None. No reminder will be sent unless explicitly configured.
190225

191226
### Config in the action
192227

@@ -239,7 +274,11 @@ jobs:
239274
},
240275
"waiting": {
241276
"delay": 691200,
242-
"message": "Closing after 8 days of waiting for the additional info requested."
277+
"message": "Closing after 8 days of waiting for the additional info requested.",
278+
"reminder": {
279+
"before": "P3D",
280+
"message": "Heads-up: this will be closed in ~3 days unless there’s new activity."
281+
}
243282
}
244283
}
245284
```
@@ -316,7 +355,11 @@ jobs:
316355
"delay": 691200,
317356
"message": "Closing after 8 days of waiting for the additional info requested.",
318357
"remove_label_on_comment": true,
319-
"remove_label_on_close": true
358+
"remove_label_on_close": true,
359+
"reminder": {
360+
"before": "P3D",
361+
"message": "Heads-up: this will be closed in ~3 days unless there’s new activity."
362+
}
320363
}
321364
}
322365
```
@@ -402,6 +445,7 @@ Then, this action, by running every night (or however you configure it) will, fo
402445
* Check if the issue has one of the configured labels.
403446
* Check if the label was added _after_ the last comment.
404447
* If not, remove the label (configurable).
448+
* If a reminder is configured and its time has arrived, post the reminder comment.
405449
* Check if the current date-time is more than the configured *delay* to wait for the user to reply back (configurable).
406450
* Then, if all that matches, it will add a comment with a message (configurable).
407451
* And then it will close the issue.

app/main.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,25 @@
22
from datetime import datetime, timedelta, timezone
33
from pathlib import Path
44
from typing import Dict, List, Optional, Set
5+
from typing_extensions import Literal
56

67
from github import Github
8+
from github.PaginatedList import PaginatedList
9+
from github.IssueComment import IssueComment
710
from github.Issue import Issue
811
from github.IssueEvent import IssueEvent
912
from pydantic import BaseModel, SecretStr, validator
1013
from pydantic_settings import BaseSettings
1114

15+
REMINDER_MARKER = "<!-- reminder -->"
16+
17+
18+
class Reminder(BaseModel):
19+
message: str = (
20+
"This will be closed automatically soon if there's no further activity."
21+
)
22+
before: timedelta = timedelta(days=1)
23+
1224

1325
class KeywordMeta(BaseModel):
1426
delay: timedelta = timedelta(days=10)
@@ -17,6 +29,7 @@ class KeywordMeta(BaseModel):
1729
)
1830
remove_label_on_comment: bool = True
1931
remove_label_on_close: bool = False
32+
reminder: Optional[Reminder] = None
2033

2134

2235
class Settings(BaseSettings):
@@ -42,9 +55,26 @@ class PartialGitHubEvent(BaseModel):
4255
pull_request: Optional[PartialGitHubEventIssue] = None
4356

4457

58+
def filter_comments(
59+
comments: PaginatedList[IssueComment], include: Literal["regular", "reminder"]
60+
) -> list[IssueComment]:
61+
if include == "regular":
62+
return [
63+
comment
64+
for comment in comments
65+
if not comment.body.startswith(REMINDER_MARKER)
66+
]
67+
elif include == "reminder":
68+
return [
69+
comment for comment in comments if comment.body.startswith(REMINDER_MARKER)
70+
]
71+
else:
72+
raise ValueError(f"Unsupported value of include ({include})")
73+
74+
4575
def get_last_interaction_date(issue: Issue) -> Optional[datetime]:
4676
last_date: Optional[datetime] = None
47-
comments = list(issue.get_comments())
77+
comments = filter_comments(issue.get_comments(), include="regular")
4878
if issue.pull_request:
4979
pr = issue.as_pull_request()
5080
commits = list(pr.get_commits())
@@ -87,6 +117,19 @@ def get_last_event_for_label(
87117
return last_event
88118

89119

120+
def get_last_reminder_date(issue: Issue) -> Optional[datetime]:
121+
"""Get date of last reminder message was sent"""
122+
last_date: Optional[datetime] = None
123+
comments = filter_comments(issue.get_comments(), include="reminder")
124+
comment_dates = [comment.created_at for comment in comments]
125+
for item_date in comment_dates:
126+
if not last_date:
127+
last_date = item_date
128+
elif item_date > last_date:
129+
last_date = item_date
130+
return last_date
131+
132+
90133
def close_issue(
91134
*, issue: Issue, keyword_meta: KeywordMeta, keyword: str, label_strs: Set[str]
92135
) -> None:
@@ -106,6 +149,7 @@ def process_issue(*, issue: Issue, settings: Settings) -> None:
106149
events = list(issue.get_events())
107150
labeled_events = get_labeled_events(events)
108151
last_date = get_last_interaction_date(issue)
152+
last_reminder_date = get_last_reminder_date(issue)
109153
now = datetime.now(timezone.utc)
110154
for keyword, keyword_meta in settings.input_config.items():
111155
# Check closable delay, if enough time passed and the issue could be closed
@@ -116,6 +160,19 @@ def process_issue(*, issue: Issue, settings: Settings) -> None:
116160
keyword_event = get_last_event_for_label(
117161
labeled_events=labeled_events, label=keyword
118162
)
163+
# Check if we need to send a reminder
164+
need_send_reminder = False
165+
if keyword_meta.reminder and keyword_event:
166+
scheduled_close_date = keyword_event.created_at + keyword_meta.delay
167+
remind_time = ( # Time point after which we should send reminder
168+
scheduled_close_date - keyword_meta.reminder.before
169+
)
170+
need_send_reminder = (
171+
(now > remind_time) # It's time to send reminder
172+
and ( # .. and it hasn't been sent yet
173+
not last_reminder_date or (last_reminder_date < remind_time)
174+
)
175+
)
119176
if last_date and keyword_event and last_date > keyword_event.created_at:
120177
logging.info(
121178
f"Not closing as the last comment was written after adding the "
@@ -124,6 +181,11 @@ def process_issue(*, issue: Issue, settings: Settings) -> None:
124181
if keyword_meta.remove_label_on_comment:
125182
logging.info(f'Removing label: "{keyword}"')
126183
issue.remove_from_labels(keyword)
184+
elif need_send_reminder and keyword_meta.reminder:
185+
message = keyword_meta.reminder.message
186+
logging.info(f"Sending reminder: #{issue.number} with message: {message}")
187+
issue.create_comment(f"{REMINDER_MARKER}\n{message}")
188+
break
127189
elif closable_delay:
128190
close_issue(
129191
issue=issue,

schema.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@
3333
"title": "Remove Label On Close",
3434
"default": false,
3535
"type": "boolean"
36+
},
37+
"reminder": {
38+
"title": "Reminder",
39+
"type": "object",
40+
"properties": {
41+
"before": {
42+
"title": "Before",
43+
"default": 86400.0,
44+
"type": "number",
45+
"format": "time-delta"
46+
},
47+
"message": {
48+
"title": "Message",
49+
"default": "This will be closed automatically soon if there's no further activity.",
50+
"type": "string"
51+
}
52+
}
3653
}
3754
}
3855
}

0 commit comments

Comments
 (0)