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.
Files changed (32) hide show
  1. firefighter/_version.py +2 -2
  2. firefighter/confluence/models.py +16 -1
  3. firefighter/incidents/management/__init__.py +1 -0
  4. firefighter/incidents/management/commands/__init__.py +1 -0
  5. firefighter/incidents/management/commands/backdate_incident_mitigated.py +94 -0
  6. firefighter/incidents/management/commands/test_postmortem_reminders.py +113 -0
  7. firefighter/incidents/migrations/0030_add_mitigated_at_field.py +22 -0
  8. firefighter/incidents/models/incident.py +43 -8
  9. firefighter/jira_app/service_postmortem.py +13 -0
  10. firefighter/jira_app/signals/postmortem_created.py +108 -46
  11. firefighter/jira_app/templates/jira/postmortem/impact.txt +9 -4
  12. firefighter/slack/messages/slack_messages.py +234 -18
  13. firefighter/slack/migrations/0009_add_postmortem_reminder_periodic_task.py +60 -0
  14. firefighter/slack/rules.py +22 -0
  15. firefighter/slack/tasks/send_postmortem_reminders.py +127 -0
  16. firefighter/slack/views/modals/close.py +113 -3
  17. firefighter/slack/views/modals/closure_reason.py +39 -15
  18. firefighter/slack/views/modals/postmortem.py +75 -7
  19. firefighter/slack/views/modals/update_status.py +4 -4
  20. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/METADATA +1 -1
  21. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/RECORD +32 -24
  22. firefighter_tests/test_incidents/test_incident_urls.py +4 -0
  23. firefighter_tests/test_incidents/test_models/test_incident_model.py +109 -1
  24. firefighter_tests/test_incidents/test_views/test_incident_detail_view.py +4 -0
  25. firefighter_tests/test_slack/messages/test_slack_messages.py +4 -0
  26. firefighter_tests/test_slack/views/modals/test_close.py +4 -0
  27. firefighter_tests/test_slack/views/modals/test_closure_reason_modal.py +109 -26
  28. firefighter_tests/test_slack/views/modals/test_postmortem_modal.py +72 -0
  29. firefighter_tests/test_slack/views/modals/test_update_status.py +45 -51
  30. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/WHEEL +0 -0
  31. {firefighter_incident-0.0.26.dist-info → firefighter_incident-0.0.28.dist-info}/entry_points.txt +0 -0
  32. {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
- SectionBlock(
113
- text="2. Edit your post-mortem on Confluence",
114
- accessory=ButtonElement(
115
- text="Edit post-mortem",
116
- value=self.incident.postmortem_for.page_edit_url,
117
- url=self.incident.postmortem_for.page_edit_url,
118
- action_id="open_link",
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
- return [SectionBlock(text=self.get_text())]
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
+ ]
@@ -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
- ) -> bool | None:
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[changed_key].id
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