firefighter-incident 0.0.26__py3-none-any.whl → 0.0.28__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- firefighter/_version.py +2 -2
- firefighter/confluence/models.py +16 -1
- firefighter/incidents/management/__init__.py +1 -0
- firefighter/incidents/management/commands/__init__.py +1 -0
- firefighter/incidents/management/commands/backdate_incident_mitigated.py +94 -0
- firefighter/incidents/management/commands/test_postmortem_reminders.py +113 -0
- firefighter/incidents/migrations/0030_add_mitigated_at_field.py +22 -0
- firefighter/incidents/models/incident.py +43 -8
- firefighter/jira_app/service_postmortem.py +13 -0
- firefighter/jira_app/signals/postmortem_created.py +108 -46
- firefighter/jira_app/templates/jira/postmortem/impact.txt +9 -4
- firefighter/slack/messages/slack_messages.py +234 -18
- firefighter/slack/migrations/0009_add_postmortem_reminder_periodic_task.py +60 -0
- firefighter/slack/rules.py +22 -0
- firefighter/slack/tasks/send_postmortem_reminders.py +127 -0
- firefighter/slack/views/modals/close.py +113 -3
- firefighter/slack/views/modals/closure_reason.py +39 -15
- firefighter/slack/views/modals/postmortem.py +75 -7
- firefighter/slack/views/modals/update_status.py +4 -4
- {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/METADATA +1 -1
- {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/RECORD +32 -24
- firefighter_tests/test_incidents/test_incident_urls.py +4 -0
- firefighter_tests/test_incidents/test_models/test_incident_model.py +109 -1
- firefighter_tests/test_incidents/test_views/test_incident_detail_view.py +4 -0
- firefighter_tests/test_slack/messages/test_slack_messages.py +4 -0
- firefighter_tests/test_slack/views/modals/test_close.py +4 -0
- firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +109 -26
- firefighter_tests/test_slack/views/modals/test_postmortem_modal.py +72 -0
- firefighter_tests/test_slack/views/modals/test_update_status.py +45 -51
- {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/WHEEL +0 -0
- {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/entry_points.txt +0 -0
- {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/licenses/LICENSE +0 -0
|
@@ -109,15 +109,36 @@ class SlackMessageIncidentPostMortemReminder(SlackMessageSurface):
|
|
|
109
109
|
action_id=UpdateStatusModal.open_action,
|
|
110
110
|
),
|
|
111
111
|
),
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
# Add post-mortem editing options based on available services
|
|
115
|
+
if hasattr(self.incident, "postmortem_for") and self.incident.postmortem_for:
|
|
116
|
+
blocks.append(
|
|
117
|
+
SectionBlock(
|
|
118
|
+
text="2. Edit your post-mortem on Confluence",
|
|
119
|
+
accessory=ButtonElement(
|
|
120
|
+
text="Edit post-mortem",
|
|
121
|
+
value=self.incident.postmortem_for.page_edit_url,
|
|
122
|
+
url=self.incident.postmortem_for.page_edit_url,
|
|
123
|
+
action_id="open_link",
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
elif hasattr(self.incident, "jira_postmortem_for") and self.incident.jira_postmortem_for:
|
|
128
|
+
jira_pm = self.incident.jira_postmortem_for
|
|
129
|
+
blocks.append(
|
|
130
|
+
SectionBlock(
|
|
131
|
+
text="2. Edit your post-mortem on Jira",
|
|
132
|
+
accessory=ButtonElement(
|
|
133
|
+
text=f"Edit Jira post-mortem ({jira_pm.jira_issue_key})",
|
|
134
|
+
url=jira_pm.issue_url,
|
|
135
|
+
action_id="open_link",
|
|
136
|
+
),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Continue with remaining steps
|
|
141
|
+
blocks.extend([
|
|
121
142
|
SectionBlock(
|
|
122
143
|
text=f"3. Submit the key events to {APP_DISPLAY_NAME}",
|
|
123
144
|
**accessory_kwargs,
|
|
@@ -138,7 +159,8 @@ class SlackMessageIncidentPostMortemReminder(SlackMessageSurface):
|
|
|
138
159
|
)
|
|
139
160
|
]
|
|
140
161
|
),
|
|
141
|
-
]
|
|
162
|
+
])
|
|
163
|
+
|
|
142
164
|
if POSTMORTEM_HELP_URL:
|
|
143
165
|
blocks.insert(
|
|
144
166
|
4,
|
|
@@ -184,14 +206,30 @@ class SlackMessageIncidentFixedNextActions(SlackMessageSurface):
|
|
|
184
206
|
),
|
|
185
207
|
),
|
|
186
208
|
DividerBlock(),
|
|
187
|
-
ContextBlock(
|
|
188
|
-
elements=[
|
|
189
|
-
MarkdownTextObject(
|
|
190
|
-
text="A post-mortem is *not* required for this incident.\nIf you want to create one, use `/incident postmortem` to create a new post-mortem page on Confluence."
|
|
191
|
-
)
|
|
192
|
-
]
|
|
193
|
-
),
|
|
194
209
|
]
|
|
210
|
+
|
|
211
|
+
# Add post-mortem context message if any post-mortem service is enabled
|
|
212
|
+
enable_confluence = getattr(settings, "ENABLE_CONFLUENCE", False)
|
|
213
|
+
enable_jira_postmortem = getattr(settings, "ENABLE_JIRA_POSTMORTEM", False)
|
|
214
|
+
|
|
215
|
+
if enable_confluence or enable_jira_postmortem:
|
|
216
|
+
postmortem_text = "A post-mortem is *not* required for this incident.\nIf you want to create one, use `/incident postmortem` to create a new post-mortem page"
|
|
217
|
+
|
|
218
|
+
if enable_confluence and enable_jira_postmortem:
|
|
219
|
+
postmortem_text += " on Confluence or Jira."
|
|
220
|
+
elif enable_confluence:
|
|
221
|
+
postmortem_text += " on Confluence."
|
|
222
|
+
elif enable_jira_postmortem:
|
|
223
|
+
postmortem_text += " on Jira."
|
|
224
|
+
|
|
225
|
+
blocks.append(
|
|
226
|
+
ContextBlock(
|
|
227
|
+
elements=[
|
|
228
|
+
MarkdownTextObject(text=postmortem_text)
|
|
229
|
+
]
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
195
233
|
return blocks
|
|
196
234
|
|
|
197
235
|
|
|
@@ -612,7 +650,185 @@ class SlackMessageIncidentPostMortemCreated(SlackMessageSurface):
|
|
|
612
650
|
return "\n".join(parts)
|
|
613
651
|
|
|
614
652
|
def get_blocks(self) -> list[Block]:
|
|
615
|
-
|
|
653
|
+
blocks: list[Block] = [SectionBlock(text=self.get_text())]
|
|
654
|
+
|
|
655
|
+
# Add documentation link if Jira post-mortem exists
|
|
656
|
+
if hasattr(self.incident, "jira_postmortem_for") and self.incident.jira_postmortem_for:
|
|
657
|
+
blocks.append(
|
|
658
|
+
SectionBlock(
|
|
659
|
+
text="Need guidance on how to fill Post-Mortems in Jira? See our documentation",
|
|
660
|
+
accessory=ButtonElement(
|
|
661
|
+
text="Open documentation",
|
|
662
|
+
url="https://manomano.atlassian.net/wiki/spaces/TC/pages/5639635000/How+to+fill+Post-Mortems+in+Jira",
|
|
663
|
+
value="jira_postmortem_documentation",
|
|
664
|
+
action_id="open_link",
|
|
665
|
+
),
|
|
666
|
+
)
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
return blocks
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
class SlackMessageIncidentPostMortemCreatedAnnouncement(SlackMessageSurface):
|
|
673
|
+
"""Message to announce post-mortem creation in #critical-incidents (tech_incidents tag)."""
|
|
674
|
+
|
|
675
|
+
id = "ff_incident_postmortem_created_announcement"
|
|
676
|
+
incident: Incident
|
|
677
|
+
|
|
678
|
+
def __init__(self, incident: Incident) -> None:
|
|
679
|
+
self.incident = incident
|
|
680
|
+
super().__init__()
|
|
681
|
+
|
|
682
|
+
def get_text(self) -> str:
|
|
683
|
+
"""Generate announcement text for critical incidents channel."""
|
|
684
|
+
return f"📔 Post-mortem created for {self.incident.priority} incident #{self.incident.id}: {self.incident.title}"
|
|
685
|
+
|
|
686
|
+
def get_blocks(self) -> list[Block]:
|
|
687
|
+
fields = [
|
|
688
|
+
f"{self.incident.priority.emoji} *Priority:* {self.incident.priority.name}",
|
|
689
|
+
f":calendar: *Created at:* {date_time(self.incident.created_at)}",
|
|
690
|
+
f":slack: *Channel:* <#{self.incident.conversation.channel_id}>",
|
|
691
|
+
]
|
|
692
|
+
|
|
693
|
+
# Add Confluence link if available
|
|
694
|
+
if hasattr(self.incident, "postmortem_for"):
|
|
695
|
+
fields.append(
|
|
696
|
+
f":confluence: <{self.incident.postmortem_for.page_url}|*Confluence Post-Mortem*>"
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# Add Jira link if available
|
|
700
|
+
if hasattr(self.incident, "jira_postmortem_for"):
|
|
701
|
+
jira_pm = self.incident.jira_postmortem_for
|
|
702
|
+
fields.append(
|
|
703
|
+
f":jira_new: <{jira_pm.issue_url}|*Jira Post-Mortem ({jira_pm.jira_issue_key})*>"
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
blocks: list[Block] = [
|
|
707
|
+
SectionBlock(
|
|
708
|
+
text=f"📔 *Post-mortem created for incident #{self.incident.id}*"
|
|
709
|
+
),
|
|
710
|
+
SectionBlock(text=f"*{shorten(self.incident.title, 2995, placeholder='...')}*"),
|
|
711
|
+
DividerBlock(),
|
|
712
|
+
SectionBlock(fields=fields),
|
|
713
|
+
]
|
|
714
|
+
|
|
715
|
+
return blocks
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
class SlackMessagePostMortemReminder5Days(SlackMessageSurface):
|
|
719
|
+
"""Reminder message sent 5 days after incident reaches MITIGATED status.
|
|
720
|
+
|
|
721
|
+
Sent to both the incident channel and #critical-incidents (tech_incidents tag).
|
|
722
|
+
"""
|
|
723
|
+
|
|
724
|
+
id = "ff_incident_postmortem_reminder_5days"
|
|
725
|
+
incident: Incident
|
|
726
|
+
|
|
727
|
+
def __init__(self, incident: Incident) -> None:
|
|
728
|
+
self.incident = incident
|
|
729
|
+
super().__init__()
|
|
730
|
+
|
|
731
|
+
def get_text(self) -> str:
|
|
732
|
+
return f"⏰ Reminder: Post-mortem for incident #{self.incident.id} needs to be completed"
|
|
733
|
+
|
|
734
|
+
def get_blocks(self) -> list[Block]:
|
|
735
|
+
blocks: list[Block] = [
|
|
736
|
+
HeaderBlock(
|
|
737
|
+
text=PlainTextObject(
|
|
738
|
+
text="⏰ Post-mortem Reminder ⏰",
|
|
739
|
+
emoji=True,
|
|
740
|
+
)
|
|
741
|
+
),
|
|
742
|
+
SectionBlock(
|
|
743
|
+
text="This incident was mitigated 5 days ago. The post-mortem *must* be completed to close the incident."
|
|
744
|
+
),
|
|
745
|
+
DividerBlock(),
|
|
746
|
+
]
|
|
747
|
+
|
|
748
|
+
# Add links to post-mortems
|
|
749
|
+
pm_links = []
|
|
750
|
+
if hasattr(self.incident, "postmortem_for"):
|
|
751
|
+
pm_links.append(
|
|
752
|
+
ButtonElement(
|
|
753
|
+
text="Open Post-Mortem (Confluence)",
|
|
754
|
+
url=self.incident.postmortem_for.page_edit_url,
|
|
755
|
+
action_id="open_link",
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
if hasattr(self.incident, "jira_postmortem_for"):
|
|
760
|
+
jira_pm = self.incident.jira_postmortem_for
|
|
761
|
+
pm_links.append(
|
|
762
|
+
ButtonElement(
|
|
763
|
+
text=f"Open Jira Post-Mortem ({jira_pm.jira_issue_key})",
|
|
764
|
+
url=jira_pm.issue_url,
|
|
765
|
+
action_id="open_link",
|
|
766
|
+
)
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
if pm_links:
|
|
770
|
+
blocks.append(ActionsBlock(elements=pm_links))
|
|
771
|
+
|
|
772
|
+
# Add action buttons
|
|
773
|
+
blocks.extend([
|
|
774
|
+
DividerBlock(),
|
|
775
|
+
SectionBlock(
|
|
776
|
+
text="Update the incident status or close it once the post-mortem is complete.",
|
|
777
|
+
accessory=ButtonElement(
|
|
778
|
+
text="Update status",
|
|
779
|
+
value=str(self.incident.id),
|
|
780
|
+
action_id=UpdateStatusModal.open_action,
|
|
781
|
+
),
|
|
782
|
+
),
|
|
783
|
+
])
|
|
784
|
+
|
|
785
|
+
return blocks
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
class SlackMessagePostMortemReminder5DaysAnnouncement(SlackMessageSurface):
|
|
789
|
+
"""Announcement version of 5-day reminder for #critical-incidents channel."""
|
|
790
|
+
|
|
791
|
+
id = "ff_incident_postmortem_reminder_5days_announcement"
|
|
792
|
+
incident: Incident
|
|
793
|
+
|
|
794
|
+
def __init__(self, incident: Incident) -> None:
|
|
795
|
+
self.incident = incident
|
|
796
|
+
super().__init__()
|
|
797
|
+
|
|
798
|
+
def get_text(self) -> str:
|
|
799
|
+
return f"⏰ Post-mortem reminder for {self.incident.priority} incident #{self.incident.id}: {self.incident.title}"
|
|
800
|
+
|
|
801
|
+
def get_blocks(self) -> list[Block]:
|
|
802
|
+
fields = [
|
|
803
|
+
f"{self.incident.priority.emoji} *Priority:* {self.incident.priority.name}",
|
|
804
|
+
f":calendar: *Mitigated:* {date_time(self.incident.mitigated_at) if self.incident.mitigated_at else 'Unknown'}",
|
|
805
|
+
f":slack: *Channel:* <#{self.incident.conversation.channel_id}>",
|
|
806
|
+
]
|
|
807
|
+
|
|
808
|
+
# Add post-mortem links
|
|
809
|
+
if hasattr(self.incident, "postmortem_for"):
|
|
810
|
+
fields.append(
|
|
811
|
+
f":confluence: <{self.incident.postmortem_for.page_url}|*Confluence Post-Mortem*>"
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
if hasattr(self.incident, "jira_postmortem_for"):
|
|
815
|
+
jira_pm = self.incident.jira_postmortem_for
|
|
816
|
+
fields.append(
|
|
817
|
+
f":jira_new: <{jira_pm.issue_url}|*Jira Post-Mortem ({jira_pm.jira_issue_key})*>"
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
blocks: list[Block] = [
|
|
821
|
+
SectionBlock(
|
|
822
|
+
text=f"⏰ *Post-mortem reminder for incident #{self.incident.id}*"
|
|
823
|
+
),
|
|
824
|
+
SectionBlock(
|
|
825
|
+
text=f"*{shorten(self.incident.title, 2995, placeholder='...')}*\n_This incident was mitigated 5 days ago. Post-mortem completion is overdue._"
|
|
826
|
+
),
|
|
827
|
+
DividerBlock(),
|
|
828
|
+
SectionBlock(fields=fields),
|
|
829
|
+
]
|
|
830
|
+
|
|
831
|
+
return blocks
|
|
616
832
|
|
|
617
833
|
|
|
618
834
|
class SlackMessageIncidentDuringOffHours(SlackMessageSurface):
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Generated by Django 4.2.21 on 2025-12-19 14:05
|
|
2
|
+
|
|
3
|
+
from django.db import migrations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_postmortem_reminder_task(apps, schema_editor):
|
|
7
|
+
"""Create periodic task for post-mortem reminders.
|
|
8
|
+
|
|
9
|
+
Runs twice daily at 10:00 AM and 3:00 PM to check for incidents mitigated 5+ days ago.
|
|
10
|
+
"""
|
|
11
|
+
CrontabSchedule = apps.get_model("django_celery_beat", "CrontabSchedule")
|
|
12
|
+
PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask")
|
|
13
|
+
|
|
14
|
+
# Create crontab schedule for 10:00 AM and 3:00 PM (Europe/Paris timezone)
|
|
15
|
+
# Crontab format: minute hour day_of_week day_of_month month_of_year
|
|
16
|
+
# hour='10,15' means run at both 10:00 and 15:00
|
|
17
|
+
# Using Europe/Paris timezone handles DST automatically
|
|
18
|
+
schedule, _ = CrontabSchedule.objects.get_or_create(
|
|
19
|
+
minute="0",
|
|
20
|
+
hour="10,15",
|
|
21
|
+
day_of_week="*",
|
|
22
|
+
day_of_month="*",
|
|
23
|
+
month_of_year="*",
|
|
24
|
+
timezone="Europe/Paris",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Create the periodic task
|
|
28
|
+
PeriodicTask.objects.get_or_create(
|
|
29
|
+
name="Send post-mortem reminders for mitigated incidents",
|
|
30
|
+
defaults={
|
|
31
|
+
"task": "slack.send_postmortem_reminders",
|
|
32
|
+
"crontab": schedule,
|
|
33
|
+
"enabled": True,
|
|
34
|
+
"description": "Send reminders for incidents mitigated 5+ days ago that still need post-mortem completion (runs at 10 AM and 3 PM)",
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def remove_postmortem_reminder_task(apps, schema_editor):
|
|
40
|
+
"""Remove the periodic task on migration rollback."""
|
|
41
|
+
PeriodicTask = apps.get_model("django_celery_beat", "PeriodicTask")
|
|
42
|
+
|
|
43
|
+
PeriodicTask.objects.filter(
|
|
44
|
+
task="slack.send_postmortem_reminders"
|
|
45
|
+
).delete()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Migration(migrations.Migration):
|
|
49
|
+
|
|
50
|
+
dependencies = [
|
|
51
|
+
("slack", "0008_alter_conversation_incident_categories_and_more"),
|
|
52
|
+
("django_celery_beat", "0019_alter_periodictasks_options"),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
operations = [
|
|
56
|
+
migrations.RunPython(
|
|
57
|
+
create_postmortem_reminder_task,
|
|
58
|
+
reverse_code=remove_postmortem_reminder_task,
|
|
59
|
+
),
|
|
60
|
+
]
|
firefighter/slack/rules.py
CHANGED
|
@@ -52,3 +52,25 @@ def should_publish_in_it_deploy_channel(incident: Incident) -> bool:
|
|
|
52
52
|
and not incident.private
|
|
53
53
|
and incident.incident_category.deploy_warning
|
|
54
54
|
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def should_publish_pm_in_general_channel(incident: Incident) -> bool:
|
|
58
|
+
"""Determine if post-mortem creation should be announced in #critical-incidents.
|
|
59
|
+
|
|
60
|
+
Post-mortems are announced for P1-P3 production incidents that are not private
|
|
61
|
+
and require a post-mortem.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
incident: The incident for which a post-mortem was created.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
True if the post-mortem creation should be announced in tech_incidents channel.
|
|
68
|
+
"""
|
|
69
|
+
return (
|
|
70
|
+
incident.priority is not None
|
|
71
|
+
and incident.priority.value <= 3
|
|
72
|
+
and incident.environment is not None
|
|
73
|
+
and incident.environment.value == "PRD"
|
|
74
|
+
and not incident.private
|
|
75
|
+
and incident.needs_postmortem
|
|
76
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Celery task to send post-mortem reminders for incidents.
|
|
2
|
+
|
|
3
|
+
This task runs periodically to check for incidents that were mitigated
|
|
4
|
+
5 days ago and still need their post-mortem completed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import timedelta
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
from celery import shared_task
|
|
14
|
+
from django.conf import settings
|
|
15
|
+
from django.utils import timezone
|
|
16
|
+
|
|
17
|
+
from firefighter.incidents.enums import IncidentStatus
|
|
18
|
+
from firefighter.incidents.models.incident import Incident
|
|
19
|
+
from firefighter.slack.models.conversation import Conversation
|
|
20
|
+
from firefighter.slack.rules import should_publish_pm_in_general_channel
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from firefighter.slack.models import Message
|
|
24
|
+
|
|
25
|
+
if settings.ENABLE_SLACK:
|
|
26
|
+
from firefighter.slack.messages.slack_messages import (
|
|
27
|
+
SlackMessagePostMortemReminder5Days,
|
|
28
|
+
SlackMessagePostMortemReminder5DaysAnnouncement,
|
|
29
|
+
)
|
|
30
|
+
from firefighter.slack.models import Message
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Days after mitigation to send reminder
|
|
35
|
+
POSTMORTEM_REMINDER_DAYS = 5
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@shared_task(name="slack.send_postmortem_reminders")
|
|
39
|
+
def send_postmortem_reminders() -> None:
|
|
40
|
+
"""Send post-mortem completion reminders for incidents mitigated 5+ days ago.
|
|
41
|
+
|
|
42
|
+
This task:
|
|
43
|
+
1. Finds incidents that were mitigated at least 5 days ago
|
|
44
|
+
2. Filters for incidents that still need their post-mortem completed
|
|
45
|
+
3. Sends reminders to both the incident channel and #critical-incidents
|
|
46
|
+
4. Tracks sent reminders to avoid duplicates
|
|
47
|
+
"""
|
|
48
|
+
# Calculate the cutoff date (5 days ago)
|
|
49
|
+
cutoff_date = timezone.now() - timedelta(days=POSTMORTEM_REMINDER_DAYS)
|
|
50
|
+
|
|
51
|
+
# Get incidents that:
|
|
52
|
+
# - Were mitigated at least 5 days ago
|
|
53
|
+
# - Still need post-mortem (P1-P3)
|
|
54
|
+
# - Are in MITIGATED or POST_MORTEM status (not yet closed)
|
|
55
|
+
# - Are not ignored
|
|
56
|
+
incidents_needing_reminder = Incident.objects.filter(
|
|
57
|
+
mitigated_at__lte=cutoff_date,
|
|
58
|
+
mitigated_at__isnull=False,
|
|
59
|
+
_status__in=[
|
|
60
|
+
IncidentStatus.MITIGATED.value,
|
|
61
|
+
IncidentStatus.POST_MORTEM.value,
|
|
62
|
+
],
|
|
63
|
+
priority__needs_postmortem=True,
|
|
64
|
+
ignore=False,
|
|
65
|
+
).select_related("conversation", "priority", "environment")
|
|
66
|
+
|
|
67
|
+
logger.info(
|
|
68
|
+
f"Found {incidents_needing_reminder.count()} incidents needing post-mortem reminders"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
for incident in incidents_needing_reminder:
|
|
72
|
+
# Check if we already sent a reminder for this incident
|
|
73
|
+
if Message.objects.filter(
|
|
74
|
+
ff_type=SlackMessagePostMortemReminder5Days.id,
|
|
75
|
+
incident=incident,
|
|
76
|
+
).exists():
|
|
77
|
+
logger.debug(
|
|
78
|
+
f"Skipping incident #{incident.id} - reminder already sent"
|
|
79
|
+
)
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Skip if no conversation
|
|
83
|
+
if not hasattr(incident, "conversation") or not incident.conversation:
|
|
84
|
+
logger.warning(
|
|
85
|
+
f"Incident #{incident.id} has no conversation, skipping reminder"
|
|
86
|
+
)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
# Send reminder to incident channel
|
|
90
|
+
try:
|
|
91
|
+
reminder_message = SlackMessagePostMortemReminder5Days(incident)
|
|
92
|
+
incident.conversation.send_message_and_save(reminder_message)
|
|
93
|
+
logger.info(
|
|
94
|
+
f"Sent post-mortem reminder to incident #{incident.id} channel"
|
|
95
|
+
)
|
|
96
|
+
except Exception:
|
|
97
|
+
logger.exception(
|
|
98
|
+
f"Failed to send post-mortem reminder to incident #{incident.id} channel"
|
|
99
|
+
)
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
# Send announcement to #critical-incidents if applicable
|
|
103
|
+
if should_publish_pm_in_general_channel(incident):
|
|
104
|
+
try:
|
|
105
|
+
tech_incidents_conversation = Conversation.objects.get_or_none(
|
|
106
|
+
tag="tech_incidents"
|
|
107
|
+
)
|
|
108
|
+
if tech_incidents_conversation:
|
|
109
|
+
announcement = SlackMessagePostMortemReminder5DaysAnnouncement(
|
|
110
|
+
incident
|
|
111
|
+
)
|
|
112
|
+
tech_incidents_conversation.send_message_and_save(announcement)
|
|
113
|
+
logger.info(
|
|
114
|
+
f"Sent post-mortem reminder to tech_incidents for incident #{incident.id}"
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
logger.warning(
|
|
118
|
+
"Could not find tech_incidents conversation! Is there a channel with tag tech_incidents?"
|
|
119
|
+
)
|
|
120
|
+
except Exception:
|
|
121
|
+
logger.exception(
|
|
122
|
+
f"Failed to send post-mortem reminder to tech_incidents for incident #{incident.id}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
logger.info(
|
|
126
|
+
f"Post-mortem reminder task completed. Processed {incidents_needing_reminder.count()} incidents."
|
|
127
|
+
)
|
|
@@ -104,7 +104,9 @@ class CloseModal(
|
|
|
104
104
|
]
|
|
105
105
|
),
|
|
106
106
|
]
|
|
107
|
-
elif reason[0] == "STATUS_NOT_POST_MORTEM"
|
|
107
|
+
elif reason[0] == "STATUS_NOT_POST_MORTEM" and getattr(
|
|
108
|
+
settings, "ENABLE_CONFLUENCE", False
|
|
109
|
+
):
|
|
108
110
|
reason_blocks += [
|
|
109
111
|
SectionBlock(
|
|
110
112
|
text=f":warning: *Status is not _{IncidentStatus.POST_MORTEM.label}_* :warning:\n"
|
|
@@ -148,6 +150,44 @@ class CloseModal(
|
|
|
148
150
|
]
|
|
149
151
|
),
|
|
150
152
|
]
|
|
153
|
+
elif reason[0] == "JIRA_POSTMORTEM_NOT_READY" and getattr(
|
|
154
|
+
settings, "ENABLE_JIRA", False
|
|
155
|
+
):
|
|
156
|
+
reason_blocks += [
|
|
157
|
+
SectionBlock(
|
|
158
|
+
text=":warning: *Jira post-mortem not ready* :warning:\n"
|
|
159
|
+
),
|
|
160
|
+
ContextBlock(
|
|
161
|
+
elements=[
|
|
162
|
+
MarkdownTextObject(
|
|
163
|
+
text=f"{reason[1]}\nPlease move the Jira post-mortem to the Ready status before closing the incident."
|
|
164
|
+
)
|
|
165
|
+
]
|
|
166
|
+
),
|
|
167
|
+
ActionsBlock(
|
|
168
|
+
elements=[
|
|
169
|
+
ButtonElement(
|
|
170
|
+
text="Open Jira post-mortem",
|
|
171
|
+
action_id="open_link",
|
|
172
|
+
url=incident.jira_postmortem_for.issue_url,
|
|
173
|
+
style="primary",
|
|
174
|
+
),
|
|
175
|
+
]
|
|
176
|
+
),
|
|
177
|
+
]
|
|
178
|
+
elif reason[0] == "JIRA_POSTMORTEM_STATUS_UNKNOWN":
|
|
179
|
+
reason_blocks += [
|
|
180
|
+
SectionBlock(
|
|
181
|
+
text=":warning: *Could not verify Jira post-mortem status* :warning:\n"
|
|
182
|
+
),
|
|
183
|
+
ContextBlock(
|
|
184
|
+
elements=[
|
|
185
|
+
MarkdownTextObject(
|
|
186
|
+
text=f"{reason[1]}\nPlease check the Jira post-mortem status before closing the incident."
|
|
187
|
+
)
|
|
188
|
+
]
|
|
189
|
+
),
|
|
190
|
+
]
|
|
151
191
|
elif reason[0] == "STATUS_NOT_MITIGATED":
|
|
152
192
|
reason_blocks += [
|
|
153
193
|
SectionBlock(
|
|
@@ -178,6 +218,74 @@ class CloseModal(
|
|
|
178
218
|
]
|
|
179
219
|
),
|
|
180
220
|
]
|
|
221
|
+
elif reason[0] == "POSTMORTEM_NOT_READY":
|
|
222
|
+
reason_blocks += [
|
|
223
|
+
SectionBlock(
|
|
224
|
+
text=":warning: *Post-mortem is not Ready* :warning:\n"
|
|
225
|
+
),
|
|
226
|
+
ContextBlock(
|
|
227
|
+
elements=[
|
|
228
|
+
MarkdownTextObject(
|
|
229
|
+
text="The linked Jira post-mortem must be in the *Ready* status before closing the incident.\n"
|
|
230
|
+
+ reason[1]
|
|
231
|
+
)
|
|
232
|
+
]
|
|
233
|
+
),
|
|
234
|
+
ActionsBlock(
|
|
235
|
+
elements=[
|
|
236
|
+
ButtonElement(
|
|
237
|
+
text="Open Jira post-mortem",
|
|
238
|
+
action_id="open_link",
|
|
239
|
+
url=(
|
|
240
|
+
getattr(
|
|
241
|
+
incident.jira_postmortem_for,
|
|
242
|
+
"issue_url",
|
|
243
|
+
None,
|
|
244
|
+
)
|
|
245
|
+
if hasattr(incident, "jira_postmortem_for")
|
|
246
|
+
else None
|
|
247
|
+
),
|
|
248
|
+
style="primary",
|
|
249
|
+
),
|
|
250
|
+
ButtonElement(
|
|
251
|
+
text="Update status",
|
|
252
|
+
action_id=UpdateStatusModal.push_action,
|
|
253
|
+
value=str(incident.id),
|
|
254
|
+
),
|
|
255
|
+
]
|
|
256
|
+
),
|
|
257
|
+
]
|
|
258
|
+
elif reason[0] == "POSTMORTEM_STATUS_UNKNOWN":
|
|
259
|
+
reason_blocks += [
|
|
260
|
+
SectionBlock(
|
|
261
|
+
text=":warning: *Could not verify post-mortem status* :warning:\n"
|
|
262
|
+
),
|
|
263
|
+
ContextBlock(
|
|
264
|
+
elements=[
|
|
265
|
+
MarkdownTextObject(
|
|
266
|
+
text="We could not verify the Jira post-mortem status. Please open it in Jira to confirm it is Ready before closing."
|
|
267
|
+
)
|
|
268
|
+
]
|
|
269
|
+
),
|
|
270
|
+
ActionsBlock(
|
|
271
|
+
elements=[
|
|
272
|
+
ButtonElement(
|
|
273
|
+
text="Open Jira post-mortem",
|
|
274
|
+
action_id="open_link",
|
|
275
|
+
url=(
|
|
276
|
+
getattr(
|
|
277
|
+
incident.jira_postmortem_for,
|
|
278
|
+
"issue_url",
|
|
279
|
+
None,
|
|
280
|
+
)
|
|
281
|
+
if hasattr(incident, "jira_postmortem_for")
|
|
282
|
+
else None
|
|
283
|
+
),
|
|
284
|
+
style="primary",
|
|
285
|
+
),
|
|
286
|
+
]
|
|
287
|
+
),
|
|
288
|
+
]
|
|
181
289
|
|
|
182
290
|
return View(
|
|
183
291
|
type="modal",
|
|
@@ -227,7 +335,7 @@ class CloseModal(
|
|
|
227
335
|
|
|
228
336
|
def handle_modal_fn( # type: ignore[override]
|
|
229
337
|
self, ack: Ack, body: dict[str, Any], incident: Incident, user: User
|
|
230
|
-
) ->
|
|
338
|
+
) -> bool | None:
|
|
231
339
|
"""Handle response from /incident close modal."""
|
|
232
340
|
# Check if this should be handled by closure reason modal
|
|
233
341
|
closure_result = handle_close_modal_callback(ack, body, incident, user)
|
|
@@ -243,7 +351,9 @@ class CloseModal(
|
|
|
243
351
|
update_kwargs = {}
|
|
244
352
|
for changed_key in form.changed_data:
|
|
245
353
|
if changed_key == "incident_category":
|
|
246
|
-
update_kwargs["incident_category_id"] = form.cleaned_data[
|
|
354
|
+
update_kwargs["incident_category_id"] = form.cleaned_data[
|
|
355
|
+
changed_key
|
|
356
|
+
].id
|
|
247
357
|
if changed_key in {"description", "title", "message"}:
|
|
248
358
|
update_kwargs[changed_key] = form.cleaned_data[changed_key]
|
|
249
359
|
# Check can close
|