Incident Creation Workflows & Signal Architecture¤
This document provides a comprehensive overview of the incident creation workflows, signal flows, and event chains in FireFighter.
Table of Contents¤
- Overview
- Signal Architecture
- Current Workflow: P1-P3 (Critical Incidents)
- Current Workflow: P4-P5 (Normal Incidents)
- Signal Handlers Overview
- Key Differences Between Workflows
- Future: Unified Workflow Design
Overview¤
FireFighter currently implements two distinct workflows for incident creation, based on priority:
- P1-P3 (Critical): Creates
Incident→ Creates Slack Channel → Creates Jira Ticket - P4-P5 (Normal): Creates only
JiraTicket(noIncidentin database)
This design means P4-P5 incidents do not appear in the /incident/ web UI list view, as they don't exist as Incident objects.
Signal Architecture¤
Available Signals¤
FireFighter uses Django signals to decouple incident workflow steps. Here are all available signals:
# src/firefighter/incidents/signals.py
incident_created = django.dispatch.Signal()
"""Signal sent when an incident is created
Args:
incident (Incident): The incident that was created
"""
incident_updated = django.dispatch.Signal()
"""Signal sent when an incident is updated
Args:
sender (str): The sender identifier (e.g., "update_status", "update_roles")
incident (Incident): The incident that was updated
incident_update (IncidentUpdate): The incident update that was created
updated_fields (list[str]): The fields that were updated
old_priority (Priority, optional): The old priority if changed
"""
incident_key_events_updated = django.dispatch.Signal()
"""Signal sent when an incident's key events are updated
Args:
incident (Incident): The incident whose key events were updated
"""
incident_closed = django.dispatch.Signal()
"""Signal sent when an incident is closed
Args:
sender (Any): The sender identifier
incident (Incident): The incident that was closed
"""
postmortem_created = django.dispatch.Signal()
"""Signal sent when a postmortem is created
Args:
sender (str): The sender identifier
incident (Incident): The incident for which postmortem was created
"""
get_invites = django.dispatch.Signal()
"""Signal sent to retrieve the list of users to invite
Args:
incident (Incident): The incident for which to retrieve invites
Returns:
users (list[User]): The list of users to invite
"""
create_incident_conversation = django.dispatch.Signal()
"""Signal sent to create a Slack conversation for an incident
Args:
incident (Incident): The incident for which to create conversation
"""
# src/firefighter/slack/signals/__init__.py
incident_channel_done = django.dispatch.Signal()
"""Signal sent when Slack channel creation is complete
Args:
incident (Incident): The incident
channel (IncidentChannel): The created Slack channel
**kwargs: Additional context (jira_extra_fields, impacts_data, etc.)
"""
Signal Flow Diagram¤
graph TD
A[User Submits Form] --> B{Priority?}
B -->|P1-P3| C[UnifiedIncidentForm._trigger_critical_incident_workflow]
B -->|P4-P5| D[UnifiedIncidentForm._trigger_normal_incident_workflow]
C --> E[Incident.objects.declare]
E --> F[Incident created in DB]
F --> G[create_incident_conversation.send]
G --> H[Slack Channel Created]
H --> I[incident_channel_done.send]
I --> J1[create_ticket handler]
I --> J2[incident_channel_done_bookmarks handler]
I --> J3[sev1_process_reminder handler]
I --> J4[send_roles_message_in_conversation handler]
J1 --> K[JiraTicket created in DB]
K --> L[Link: JiraTicket.incident = Incident]
D --> M[prepare_jira_fields]
M --> N[jira_client.create_issue]
N --> O[process_jira_issue]
O --> P[JiraTicket created in DB]
P --> Q[NO Incident in DB]
style F fill:#28a745,stroke:#1e7e34,stroke-width:3px,color:#fff
style H fill:#17a2b8,stroke:#117a8b,stroke-width:3px,color:#fff
style K fill:#ffc107,stroke:#d39e00,stroke-width:3px,color:#000
style L fill:#28a745,stroke:#1e7e34,stroke-width:3px,color:#fff
style P fill:#ffc107,stroke:#d39e00,stroke-width:3px,color:#000
style Q fill:#dc3545,stroke:#bd2130,stroke-width:3px,color:#fff Current Workflow: P1-P3 (Critical Incidents)¤
Overview¤
Critical incidents (P1-P3) follow a complete workflow that creates: 1. An Incident object in the database 2. A Slack channel for communication 3. A Jira ticket linked to the incident
Step-by-Step Flow¤
sequenceDiagram
participant User
participant Form as UnifiedIncidentForm
participant DB as Database
participant Slack as Slack API
participant Jira as Jira API
User->>Form: Submit incident (P1-P3)
Form->>Form: _trigger_critical_incident_workflow()
Note over Form: Step 1: Create Incident
Form->>DB: Incident.objects.declare()
DB-->>Form: incident object
Note over Form: Step 2: Request Slack Channel
Form->>Form: create_incident_conversation.send(incident)
Note over Slack: Signal Handler: create_conversation()
Form->>Slack: Create channel
Slack-->>Form: channel object
Form->>DB: Save IncidentChannel
Note over Form: Step 3: Signal channel ready
Form->>Form: incident_channel_done.send(incident, channel, jira_extra_fields, impacts_data)
Note over Jira: Signal Handler: create_ticket()
Form->>Jira: Create Jira issue
Jira-->>Form: issue_data
Form->>DB: JiraTicket.objects.create(incident=incident)
Note over Slack: Multiple incident_channel_done handlers
Form->>Slack: Add bookmarks
Form->>Slack: Send roles message
Form->>Slack: Send P1 process reminder (if applicable)
Form->>Slack: Send incident announcement
Form-->>User: Incident created successfully Code Implementation¤
File: src/firefighter/incidents/forms/unified_incident.py (Lines 305-352)
def _trigger_critical_incident_workflow(
self,
creator: User,
impacts_data: dict[str, ImpactLevel],
) -> None:
"""Create a critical incident (P1-P3) with Slack channel and roles.
Workflow:
1. Create Incident in database
2. Signal: create_incident_conversation → Creates Slack channel
3. Signal: incident_channel_done → Creates Jira ticket + setup channel
"""
# Prepare cleaned data
cleaned_data_copy = dict(self.cleaned_data)
# Handle multi-environment: store all in custom_fields
environments = cleaned_data_copy.pop("environment", [])
if isinstance(environments, list) and environments:
cleaned_data_copy["environment"] = environments[0]
cleaned_data_copy.setdefault("custom_fields", {})
cleaned_data_copy["custom_fields"]["environments"] = [env.value for env in environments]
# Step 1: Create Incident in DB
incident = Incident.objects.declare(created_by=creator, **cleaned_data_copy)
# Prepare Jira extra fields (passed to signal handlers)
jira_extra_fields = {
"zendesk_ticket_id": self.cleaned_data.get("zendesk_ticket_id", ""),
"seller_contract_id": self.cleaned_data.get("seller_contract_id", ""),
"zoho_desk_ticket_id": self.cleaned_data.get("zoho_desk_ticket_id", ""),
"is_key_account": self.cleaned_data.get("is_key_account"),
"is_seller_in_golden_list": self.cleaned_data.get("is_seller_in_golden_list"),
"platforms": [p.value for p in self.cleaned_data.get("platform", [])],
"environments": [env.value for env in environments] if environments else [],
}
# Step 2: Signal to create Slack channel
# This will trigger:
# - Slack channel creation
# - incident_channel_done signal → Jira ticket creation
create_incident_conversation.send(
"incident_created",
incident=incident,
jira_extra_fields=jira_extra_fields,
impacts_data=impacts_data,
)
Signal Handlers Triggered¤
1. create_incident_conversation Signal¤
Handler: Creates Slack channel and triggers incident_channel_done
Location: Slack channel creation logic
2. incident_channel_done Signal¤
Multiple handlers listen to this signal:
Handler: create_ticket()¤
File: src/firefighter/raid/signals/incident_created.py (Lines 28-136)
@receiver(signal=incident_channel_done)
def create_ticket(
sender: Any, incident: Incident, channel: IncidentChannel, **kwargs: Any
) -> JiraTicket:
# Extract context from kwargs
jira_extra_fields = kwargs.get("jira_extra_fields", {})
impacts_data = kwargs.get("impacts_data", {})
# Get Jira user
jira_user = get_jira_user_from_user(incident.created_by)
# Build enhanced description
description = f"""{incident.description}
🧯 This incident has been created for a critical incident.
📦 Incident category: {incident.incident_category.name}
{incident.priority.emoji} Priority: {incident.priority.name}
"""
# Get all environments (multi-environment support)
environments = jira_extra_fields.get("environments") or \
incident.custom_fields.get("environments", []) or \
[incident.environment.value]
platforms = jira_extra_fields.get("platforms", ["platform-All"])
# Prepare Jira fields using common function
jira_fields = prepare_jira_fields(
title=incident.title,
description=description,
priority=incident.priority.value,
reporter=jira_user.id,
incident_category=incident.incident_category.name,
environments=environments,
platforms=platforms,
impacts_data=impacts_data,
optional_fields={
"zendesk_ticket_id": jira_extra_fields.get("zendesk_ticket_id", ""),
"seller_contract_id": jira_extra_fields.get("seller_contract_id", ""),
"zoho_desk_ticket_id": jira_extra_fields.get("zoho_desk_ticket_id", ""),
"is_key_account": jira_extra_fields.get("is_key_account"),
"is_seller_in_golden_list": jira_extra_fields.get("is_seller_in_golden_list"),
},
)
# Create Jira issue
issue = client.create_issue(**jira_fields)
# Add watchers and links
client.jira.add_watcher(issue=issue_id, watcher=jira_user.id)
client.jira.add_simple_link(issue=issue_id, object={"url": incident.status_page_url, ...})
client.jira.add_simple_link(issue=issue_id, object={"url": channel.link, ...})
# Create JiraTicket in DB, linked to Incident
jira_ticket = JiraTicket.objects.create(**issue, incident=incident)
# Add Jira bookmark to Slack channel
channel.add_bookmark(title="Jira ticket", link=jira_ticket.url, emoji=":jira_new:")
# Send announcement in channel
channel.send_message_and_save(SlackMessageIncidentDeclaredAnnouncement(incident))
return jira_ticket
Handler: incident_channel_done_bookmarks()¤
File: src/firefighter/slack/signals/handle_incident_channel_done.py (Lines 24-33)
@receiver(signal=incident_channel_done)
def incident_channel_done_bookmarks(
incident: Incident, channel: IncidentChannel, **kwargs: Any
) -> None:
"""Add incident page bookmark to Slack channel"""
channel.add_bookmark(
title="Incident Page",
link=incident.status_page_url + "?utm_medium=...",
emoji=SLACK_APP_EMOJI,
)
Handler: sev1_process_reminder()¤
File: src/firefighter/slack/signals/handle_incident_channel_done.py (Lines 37-42)
@receiver(incident_channel_done)
def sev1_process_reminder(
incident: Incident, channel: IncidentChannel, **kwargs: Never
) -> None:
"""Send communication process reminder for P1 PRD incidents"""
if incident.priority.value == 1 and incident.environment.value == "PRD":
channel.send_message_and_save(SlackMessageIncidentComProcess(incident))
Handler: send_roles_message_in_conversation()¤
File: src/firefighter/slack/signals/handle_incident_channel_done.py (Lines 45-54)
@receiver(incident_channel_done)
def send_roles_message_in_conversation(
incident: Incident, channel: IncidentChannel, **kwargs: Any
) -> None:
"""Send initial roles message in Slack channel"""
update_roles_message = SlackMessageIncidentRolesUpdated(
incident=incident,
incident_update=None,
first_update=True,
)
incident.conversation.send_message_and_save(update_roles_message)
Database State After P1-P3 Creation¤
incidents_incident:
id: 123
title: "Production API down"
priority: P1 (value=1)
environment: PRD
custom_fields: {"environments": ["PRD", "STG"]}
...
slack_incident_channel:
id: 1
incident_id: 123 # OneToOne relationship
channel_id: "C12345"
name: "inc-123-prd-stg-production-api-down"
...
raid_jiraticket:
id: 456
incident_id: 123 # Links to Incident
key: "INC-789"
url: "https://jira.example.com/browse/INC-789"
...
Current Workflow: P4-P5 (Normal Incidents)¤
Overview¤
Normal incidents (P4-P5) follow a simplified workflow that: 1. Does NOT create an Incident object in the database 2. Does NOT create a Slack channel 3. Does create a Jira ticket directly
This means P4-P5 incidents are invisible in the /incident/ web UI.
Step-by-Step Flow¤
sequenceDiagram
participant User
participant Form as UnifiedIncidentForm
participant DB as Database
participant Jira as Jira API
participant Slack as Slack API (DM only)
User->>Form: Submit incident (P4-P5)
Form->>Form: _trigger_normal_incident_workflow()
Note over Form: Step 1: Prepare Jira fields
Form->>Form: prepare_jira_fields(...)
Note over Form: Step 2: Create Jira issue directly
Form->>Jira: jira_client.create_issue()
Jira-->>Form: issue_data
Note over Form: Step 3: Process Jira issue
Form->>Form: process_jira_issue(issue_data)
Form->>DB: JiraTicket.objects.create(incident=None)
Note over Slack: Step 4: Alert user via DM
Form->>Slack: Send private message to creator
Form->>Slack: Post in raid_alert channels
Form-->>User: Jira ticket created (no Incident in DB)
Note over DB: ❌ NO Incident object created
Note over DB: ❌ NO Slack channel created
Note over DB: ✅ JiraTicket created (incident_id = NULL) Code Implementation¤
File: src/firefighter/incidents/forms/unified_incident.py (Lines 354-407)
def _trigger_normal_incident_workflow(
self,
creator: User,
impacts_data: dict[str, ImpactLevel],
) -> None:
"""Create a normal incident (P4-P5) with Jira ticket only.
Workflow:
1. Prepare Jira fields
2. Create Jira issue via API
3. Create JiraTicket in DB (incident=None)
4. Send Slack notifications (DMs + raid_alert channels)
Note: Does NOT create Incident object or Slack channel.
"""
# Get Jira user
jira_user = get_jira_user_from_user(creator)
# Prepare environments and platforms
environments = self.cleaned_data.get("environment", [])
if isinstance(environments, list) and environments:
environments_values = [env.value for env in environments]
else:
environments_values = [environments.value] if environments else []
platforms = [p.value for p in self.cleaned_data.get("platform", [])]
# Prepare Jira fields using common function
jira_fields = prepare_jira_fields(
title=self.cleaned_data["title"],
description=self.cleaned_data["description"],
priority=self.cleaned_data["priority"].value,
reporter=jira_user.id,
incident_category=self.cleaned_data["incident_category"].name,
environments=environments_values,
platforms=platforms,
impacts_data=impacts_data,
optional_fields={
"zendesk_ticket_id": self.cleaned_data.get("zendesk_ticket_id", ""),
"seller_contract_id": self.cleaned_data.get("seller_contract_id", ""),
"zoho_desk_ticket_id": self.cleaned_data.get("zoho_desk_ticket_id", ""),
"is_key_account": self.cleaned_data.get("is_key_account"),
"is_seller_in_golden_list": self.cleaned_data.get("is_seller_in_golden_list"),
"suggested_team_routing": self.cleaned_data.get("suggested_team_routing"),
},
)
# Create Jira issue directly (no Incident object created)
issue_data = jira_client.create_issue(**jira_fields)
# Process Jira issue: create JiraTicket in DB (incident=None)
process_jira_issue(
issue_data=issue_data,
user=creator,
jira_user=jira_user,
impacts_data=impacts_data,
)
Function: process_jira_issue()¤
File: src/firefighter/raid/forms.py (Lines 60-75)
def process_jira_issue(
issue_data: JiraObject,
user: User,
jira_user: JiraUser,
impacts_data: dict[str, ImpactLevel],
*args: Never,
**kwargs: Never,
) -> None:
"""Process a Jira issue for P4-P5 incidents.
Creates JiraTicket in DB without linking to an Incident.
Sends Slack notifications via DM and raid_alert channels.
"""
# Create JiraTicket in DB (incident=None)
jira_ticket = JiraTicket.objects.create(**issue_data)
# Save impact levels
impacts_form = SelectImpactForm(impacts_data)
impacts_form.save(incident=jira_ticket)
# Set up Jira watchers
set_jira_ticket_watchers_raid(jira_ticket)
# Send Slack alerts (DM to creator + raid_alert channels)
alert_slack_new_jira_ticket(jira_ticket)
Function: alert_slack_new_jira_ticket()¤
File: src/firefighter/raid/forms.py (Lines 51-108)
def alert_slack_new_jira_ticket(
jira_ticket: JiraTicket,
reporter_user: User | None = None,
reporter_email: str | None = None,
) -> None:
"""Send Slack notifications for new P4-P5 Jira tickets.
- Sends DM to reporter (if they have Slack enabled)
- Posts in raid_alert channels based on priority and domain
"""
# Validation: this is only for P4-P5 (no incident)
if hasattr(jira_ticket, "incident") and jira_ticket.incident:
raise ValueError("This is a critical incident, not a raid incident.")
reporter_user = reporter_user or jira_ticket.reporter.user
reporter_email = reporter_email or reporter_user.email
user_domain = get_domain_from_email(reporter_email)
# Create Slack message
message = SlackMessageRaidCreatedIssue(jira_ticket, reporter_user=reporter_user)
# Send DM to reporter
if reporter_user.slack_user and reporter_user.slack_user.slack_id:
reporter_user.slack_user.send_private_message(message, unfurl_links=False)
# Get alert channels based on priority and domain
channels = get_internal_alert_conversations(jira_ticket)
if user_domain:
channels.union(get_partner_alert_conversations(user_domain))
# Post in all raid_alert channels
for channel in channels:
channel.send_message_and_save(message)
Database State After P4-P5 Creation¤
incidents_incident:
(empty - no record created)
slack_incident_channel:
(empty - no channel created)
raid_jiraticket:
id: 789
incident_id: NULL # ❌ Not linked to any Incident
key: "SBI-123"
url: "https://jira.example.com/browse/SBI-123"
...
Signal Handlers Overview¤
Summary Table¤
| Signal | Sender | Triggered By | Handlers | Purpose |
|---|---|---|---|---|
create_incident_conversation | "incident_created" | P1-P3 workflow | Slack channel creation logic | Create Slack channel |
incident_channel_done | N/A | After Slack channel created | create_ticket()incident_channel_done_bookmarks()sev1_process_reminder()send_roles_message_in_conversation() | Setup channel & create Jira ticket |
incident_updated | "update_status" "update_roles" etc. | Incident updates | incident_updated_update_status_handler()incident_updated_check_dowmgrade_handler()incident_updated_update_roles_handler()incident_updated_reinvite_handler() | Sync Slack channel with incident changes |
incident_key_events_updated | N/A | Key events form submission | incident_key_events_updated_handler() | Post key events message in channel |
incident_closed | Various | Incident closure | Slack notification handlers | Notify about closure |
postmortem_created | Various | Postmortem creation | Confluence/Slack handlers | Create postmortem doc |
Handler Protection for Incidents Without Channels¤
All incident_updated signal handlers have been protected to handle incidents without Slack channels:
File: src/firefighter/slack/signals/incident_updated.py
@receiver(signal=incident_updated, sender="update_status")
def incident_updated_update_status_handler(...) -> None:
# Skip Slack operations for incidents without channels (e.g., P4-P5)
if not hasattr(incident, "conversation"):
logger.debug(f"Skipping Slack channel update for incident {incident.id} (no conversation)")
return
# Proceed with Slack operations...
incident.conversation.rename_if_needed()
incident.conversation.set_incident_channel_topic()
# ...
This protection allows future support for P4-P5 incidents to be stored as Incident objects without requiring Slack channels.
Key Differences Between Workflows¤
| Aspect | P1-P3 (Critical) | P4-P5 (Normal) |
|---|---|---|
| Incident in DB | ✅ Created | ❌ Not created |
| Slack Channel | ✅ Created | ❌ Not created |
| Jira Ticket | ✅ Created (via signal) | ✅ Created (directly) |
| Jira → Incident Link | ✅ JiraTicket.incident = incident | ❌ JiraTicket.incident = NULL |
| Roles Assignment | ✅ Yes | ❌ No |
| Appears in /incident/ | ✅ Yes | ❌ No |
| Slack Notifications | ✅ Channel messages | ⚠️ DMs + raid_alert channels only |
| Signal Flow | create_incident_conversation → incident_channel_done | None (direct creation) |
Future: Unified Workflow Design¤
Goal¤
Create a single unified workflow for all priorities (P1-P5) that: - Always creates an Incident object in the database - Conditionally creates Slack channels (P1-P3 only) - Always creates Jira tickets (all priorities) - Makes P4-P5 visible in /incident/ web UI
Proposed Unified Flow¤
sequenceDiagram
participant User
participant Form as UnifiedIncidentForm
participant DB as Database
participant Slack as Slack API
participant Jira as Jira API
User->>Form: Submit incident (any priority)
Form->>Form: _trigger_unified_incident_workflow()
Note over Form: Step 1: Always create Incident
Form->>DB: Incident.objects.declare()
DB-->>Form: incident object
Note over Form: Step 2: Conditionally create Slack channel
Form->>Form: Check priority: should_create_slack_channel()?
alt P1-P3 (Critical)
Form->>Form: create_incident_conversation.send(incident)
Form->>Slack: Create channel
Slack-->>Form: channel object
Form->>DB: Save IncidentChannel
Form->>Slack: Add bookmarks, send roles message, etc.
else P4-P5 (Normal)
Note over Form: Skip Slack channel creation
end
Note over Form: Step 3: Always create Jira ticket (unified)
Form->>Form: _create_jira_ticket(incident, impacts_data)
Form->>Form: prepare_jira_fields()
Form->>Jira: jira_client.create_issue()
Jira-->>Form: issue_data
Form->>DB: JiraTicket.objects.create(incident=incident)
alt Has Slack Channel (P1-P3)
Form->>Slack: Add Jira bookmark to channel
Form->>Slack: Send incident announcement
else No Channel (P4-P5)
Form->>Slack: Send DM to creator
Form->>Slack: Post in raid_alert channels
end
Form-->>User: Incident created successfully
Note over DB: ✅ Incident ALWAYS created (all priorities)
Note over DB: ✅ Slack channel ONLY for P1-P3
Note over DB: ✅ JiraTicket ALWAYS created and linked to Incident Implementation Changes Required¤
1. Modify UnifiedIncidentForm¤
Replace two workflows with one unified workflow:
def save(self, creator: User, impacts_data: dict[str, ImpactLevel]) -> Incident:
"""Unified workflow for all priorities."""
# Step 1: Always create Incident in DB (ALL priorities)
cleaned_data_copy = dict(self.cleaned_data)
# Handle multi-environment
environments = cleaned_data_copy.pop("environment", [])
if isinstance(environments, list) and environments:
cleaned_data_copy["environment"] = environments[0]
cleaned_data_copy.setdefault("custom_fields", {})
cleaned_data_copy["custom_fields"]["environments"] = [env.value for env in environments]
incident = Incident.objects.declare(created_by=creator, **cleaned_data_copy)
# Step 2: Conditionally create Slack channel (P1-P3 only)
if self.should_create_slack_channel():
self._create_slack_channel(incident, impacts_data)
# Step 3: Always create Jira ticket (ALL priorities) - UNIFIED
self._create_jira_ticket(incident, creator, impacts_data)
return incident
def should_create_slack_channel(self) -> bool:
"""Determine if incident needs Slack channel based on priority."""
priority = self.cleaned_data.get("priority")
return priority and priority.value < 4 # P1-P3 only
def _create_slack_channel(
self,
incident: Incident,
impacts_data: dict[str, ImpactLevel]
) -> None:
"""Create Slack channel for P1-P3 incidents."""
# Signal to create Slack channel (triggers bookmarks, roles message, etc.)
create_incident_conversation.send(
"incident_created",
incident=incident,
impacts_data=impacts_data,
)
def _create_jira_ticket(
self,
incident: Incident,
creator: User,
impacts_data: dict[str, ImpactLevel],
) -> JiraTicket:
"""Create Jira ticket for ALL priorities - UNIFIED method."""
# Get Jira user
jira_user = get_jira_user_from_user(creator)
# Prepare environments and platforms
environments = self.cleaned_data.get("environment", [])
if isinstance(environments, list) and environments:
environments_values = [env.value for env in environments]
else:
environments_values = [environments.value] if environments else []
platforms = [p.value for p in self.cleaned_data.get("platform", [])]
# Build description (enhanced for P1-P3, simple for P4-P5)
if hasattr(incident, "conversation"):
# P1-P3: Enhanced description with links
description = f"""{incident.description}
🧯 This incident has been created for a critical incident.
📦 Incident category: {incident.incident_category.name}
{incident.priority.emoji} Priority: {incident.priority.name}
"""
else:
# P4-P5: Simple description
description = incident.description
# Prepare Jira fields using common function (UNIFIED for all priorities)
jira_fields = prepare_jira_fields(
title=incident.title,
description=description,
priority=incident.priority.value,
reporter=jira_user.id,
incident_category=incident.incident_category.name,
environments=environments_values,
platforms=platforms,
impacts_data=impacts_data,
optional_fields={
"zendesk_ticket_id": self.cleaned_data.get("zendesk_ticket_id", ""),
"seller_contract_id": self.cleaned_data.get("seller_contract_id", ""),
"zoho_desk_ticket_id": self.cleaned_data.get("zoho_desk_ticket_id", ""),
"is_key_account": self.cleaned_data.get("is_key_account"),
"is_seller_in_golden_list": self.cleaned_data.get("is_seller_in_golden_list"),
"suggested_team_routing": self.cleaned_data.get("suggested_team_routing"),
},
)
# Create Jira issue via API (UNIFIED)
issue_data = jira_client.create_issue(**jira_fields)
# Create JiraTicket in DB, linked to Incident (UNIFIED)
jira_ticket = JiraTicket.objects.create(**issue_data, incident=incident)
# Save impact levels
impacts_form = SelectImpactForm(impacts_data)
impacts_form.save(incident=jira_ticket)
# Set up Jira watchers
set_jira_ticket_watchers(jira_ticket, jira_user)
# Add Jira links
add_jira_links(jira_ticket, incident)
# Send Slack notifications based on channel presence
if hasattr(incident, "conversation"):
# P1-P3: Add bookmark and send announcement in channel
incident.conversation.add_bookmark(
title="Jira ticket",
link=jira_ticket.url,
emoji=":jira_new:",
)
incident.conversation.send_message_and_save(
SlackMessageIncidentDeclaredAnnouncement(incident)
)
else:
# P4-P5: Send DM and raid_alert notifications
alert_slack_new_jira_ticket(jira_ticket)
return jira_ticket
Key improvements: - ✅ Single _create_jira_ticket() method for all priorities - ✅ Same prepare_jira_fields() call for P1-P5 - ✅ Same JiraTicket.objects.create(incident=incident) for all - ✅ Conditional Slack notifications based on channel presence - ✅ Much simpler logic - no workflow duplication
2. Remove Signal-Based Jira Creation for P1-P3¤
With the unified approach, we no longer need the create_ticket() signal handler:
Before: P1-P3 created Jira via incident_channel_done signal After: All priorities create Jira directly in _create_jira_ticket()
# ❌ DELETE: src/firefighter/raid/signals/incident_created.py
# The entire signal handler can be removed since Jira creation
# is now unified in UnifiedIncidentForm._create_jira_ticket()
@receiver(signal=incident_channel_done) # ❌ No longer needed
def create_ticket(...) -> JiraTicket:
# This entire handler can be deleted
pass
3. Simplify process_jira_issue()¤
This function can be completely removed or simplified to just a helper for Jira webhooks:
# ⚠️ OPTION 1: Remove entirely (recommended)
# All logic moved to UnifiedIncidentForm._create_jira_ticket()
# ⚠️ OPTION 2: Keep only for Jira webhook processing
def process_jira_issue_from_webhook(
issue_data: JiraObject,
user: User,
impacts_data: dict[str, ImpactLevel],
) -> JiraTicket:
"""Process Jira issue updates from webhooks only."""
# Minimal logic for webhook-triggered updates
pass
4. Benefits of Unified Workflow¤
✅ Simplified codebase: One workflow instead of two ✅ Single Jira creation method: _create_jira_ticket() for ALL priorities ✅ No signal-based Jira creation: Direct call instead of incident_channel_done signal ✅ Consistent data model: All incidents in database ✅ Better visibility: P4-P5 appear in /incident/ list ✅ Easier maintenance: Changes in one place ✅ Less code: Remove create_ticket() signal handler and process_jira_issue() ✅ Future-proof: Easy to add new priority levels or features ✅ Signal handlers protected: Already handle incidents without channels
5. Code Reduction Summary¤
Files that can be simplified/removed: - ❌ src/firefighter/raid/signals/incident_created.py - Remove create_ticket() handler (~110 lines) - ❌ src/firefighter/raid/forms.py - Remove process_jira_issue() (~15 lines) - ✅ src/firefighter/incidents/forms/unified_incident.py - Replace 2 workflows with 1 unified workflow (~100 lines saved)
Total code reduction: ~225 lines removed, simpler architecture
References¤
Last Updated: 2025-10-22 Status: Current workflows documented, unified workflow design proposed