Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
420841a
Sync from Jira to Impact
juanjogarciatorres43 Jan 15, 2026
8bd0f29
Impact Jira double sync
juanjogarciatorres43 Jan 20, 2026
0c7e2ec
Sync from Jira to Impact
juanjogarciatorres43 Jan 15, 2026
facc13f
Impact Jira double sync
juanjogarciatorres43 Jan 20, 2026
0f5cef3
Merge branch 'feat/impact_jira_status_sync' of https://github.com/Man…
juanjogarciatorres43 Jan 22, 2026
9920156
double sync for status and priority
juanjogarciatorres43 Jan 22, 2026
d432d3a
Loop prevention and normalization
juanjogarciatorres43 Jan 23, 2026
775c82b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 23, 2026
01a4fac
Update documentation
juanjogarciatorres43 Jan 23, 2026
a3927a0
Merge branch 'feat/impact_jira_status_sync' of https://github.com/Man…
juanjogarciatorres43 Jan 23, 2026
2b1cffd
Fix ruff errors
juanjogarciatorres43 Jan 23, 2026
0b0b720
Fix unit tests
juanjogarciatorres43 Jan 23, 2026
eaf3fe8
Fix test_jira_status_sync.py timeout
juanjogarciatorres43 Jan 23, 2026
83f8976
Fix lint-mypy
juanjogarciatorres43 Jan 23, 2026
735967d
Fix test_cannot_close_from_postmortem_without_key_events
juanjogarciatorres43 Jan 23, 2026
cfa1bd0
Fix test_update_status
juanjogarciatorres43 Jan 23, 2026
87f0205
Fix issues raised by AI
juanjogarciatorres43 Feb 4, 2026
300e0a7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 4, 2026
58ba339
Fix tests
juanjogarciatorres43 Feb 4, 2026
ecf234c
Merge branch 'feat/impact_jira_status_sync' of https://github.com/Man…
juanjogarciatorres43 Feb 4, 2026
2cf8887
Fix import
juanjogarciatorres43 Feb 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 86 additions & 55 deletions docs/architecture/jira-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@
The RAID module provides comprehensive bidirectional synchronization between Impact incidents and JIRA tickets, ensuring data consistency across both platforms.

✅ **Applies to all P1-P5**:

- All priorities create both `Incident` objects AND JIRA tickets
- The JIRA integration works identically for all priorities

✅ **Double sync (both directions)**:

- Impact → Jira: on incident updates (status, priority, title, description, commander) via `incident_updated` signals; admin saves fall back to post_save handlers for status/priority
- Jira → Impact: on Jira webhooks (status, priority, mapped fields) via webhook handlers
- Loop-prevention cache ensures a change coming from one side is not re-sent back immediately

See [incident-workflow.md](incident-workflow.md) for architecture overview.

## Synchronization Architecture
Expand Down Expand Up @@ -37,25 +44,29 @@ See [incident-workflow.md](incident-workflow.md) for architecture overview.
Centralizes all JIRA field preparation for both P1-P3 and P4-P5 workflows.

**P1-P3 (Critical)**:

- Trigger: `incident_channel_done` signal
- Handler: `src/firefighter/raid/signals/incident_created.py`
- Flow: Create Incident → Create Slack channel → Signal triggers JIRA ticket

**P4-P5 (Normal)**:

- Trigger: Form submission
- Handler: `UnifiedIncidentForm._trigger_normal_incident_workflow()`
- Flow: Direct call to `jira_client.create_issue()`

### Custom Fields Mapping

**Always Passed**:

- `customfield_11049` (environments): List of env values (PRD, STG, INT)
- P1-P3: First environment only
- P4-P5: All selected environments
- `customfield_10201` (platform): Platform value (platform-FR, platform-All, etc.)
- `customfield_10936` (business_impact): Computed from impacts_data

**Impact-Specific**:

- Customer: `zendesk_ticket_id`
- Seller: `seller_contract_id`, `zoho_desk_ticket_id`, `is_key_account`, `is_seller_in_golden_list`
- P4-P5: `suggested_team_routing`
Expand All @@ -66,117 +77,114 @@ Centralizes all JIRA field preparation for both P1-P3 and P4-P5 workflows.

### Impact → JIRA Sync

**Trigger**: Incident field updates in Impact
**Handler**: `sync_incident_changes_to_jira()`
**Trigger**: Incident field updates in Impact (via `incident_updated` with `updated_fields`), plus admin saves via post_save fallbacks for status/priority.

**Handlers**: `incident_updated_close_ticket_when_mitigated_or_postmortem` (status), `incident_updated_sync_priority_to_jira` (priority), post_save fallbacks for both.

**Syncable Fields**:

- `title` → `summary`
- `description` → `description`
- `priority` → `priority` (with value mapping)
- `status` → `status` (with transitions)
- `priority` → Jira `customfield_11064` (numeric 1–5, or mapped option)
- `status` → Jira status (transitions via workflow)
- `commander` → `assignee`

**Process**:

1. Check if RAID is enabled
2. Validate update_fields parameter
3. Filter for syncable fields only
4. Apply loop prevention cache
5. Call `sync_incident_to_jira()`
2. Validate/update_fields
3. Apply loop prevention
4. Push status (Impact→Jira map)
5. Push priority to Jira `customfield_11064`

### JIRA → Impact Sync

**Trigger**: JIRA webhook updates

**Handler**: `handle_jira_webhook_update()`

**Process**:

1. Parse webhook changelog data
2. Identify changed fields
3. Apply appropriate sync functions:
- `sync_jira_status_to_incident()`
- `sync_jira_priority_to_incident()`
- `sync_jira_fields_to_incident()`
3. For each changelog item, a single helper `_sync_jira_fields_to_incident()` handles:
- Loop-prevention check (skips if mirrored Impact→Jira change)
- Slack alert for the item
- Status updates via `_handle_status_update`
- Priority updates via `_handle_priority_update`

## Field Mapping

### Status Mapping

**JIRA → Impact**:

```python
JIRA_TO_IMPACT_STATUS_MAP = {
"Open": IncidentStatus.INVESTIGATING,
"To Do": IncidentStatus.INVESTIGATING,
"In Progress": IncidentStatus.MITIGATING,
"In Review": IncidentStatus.MITIGATING,
"Resolved": IncidentStatus.MITIGATED,
"Done": IncidentStatus.MITIGATED,
"Closed": IncidentStatus.POST_MORTEM,
"Reopened": IncidentStatus.INVESTIGATING,
"Blocked": IncidentStatus.MITIGATING,
"Waiting": IncidentStatus.MITIGATING,
"Incoming": IncidentStatus.OPEN,
"Pending resolution": IncidentStatus.OPEN,
"in progress": IncidentStatus.MITIGATING,
"Reporter validation": IncidentStatus.MITIGATED,
"Closed": IncidentStatus.CLOSED,
}
```

**Impact → JIRA**:

```python
IMPACT_TO_JIRA_STATUS_MAP = {
IncidentStatus.OPEN: "Open",
IncidentStatus.INVESTIGATING: "In Progress",
IncidentStatus.MITIGATING: "In Progress",
IncidentStatus.MITIGATED: "Resolved",
IncidentStatus.POST_MORTEM: "Closed",
IncidentStatus.OPEN: "Incoming",
IncidentStatus.INVESTIGATING: "in progress",
IncidentStatus.MITIGATING: "in progress",
IncidentStatus.MITIGATED: "Reporter validation",
IncidentStatus.POST_MORTEM: "Reporter validation",
IncidentStatus.CLOSED: "Closed",
}
```

### Priority Mapping

**JIRA → Impact**:
```python
JIRA_TO_IMPACT_PRIORITY_MAP = {
"Highest": 1, # P1 - Critical
"High": 2, # P2 - High
"Medium": 3, # P3 - Medium
"Low": 4, # P4 - Low
"Lowest": 5, # P5 - Lowest
}
```

Uses the numeric Jira priority (1–5) and writes directly to Impact.

**Impact → JIRA**:

Uses the numeric Impact priority (1–5) and writes directly to Jira `customfield_11064`. Admin saves and UI edits both sync via signals/post_save fallback.

## Loop Prevention

### Cache-Based Mechanism

**Function**: `should_skip_sync()`

**Cache Key Format**: `sync:{entity_type}:{entity_id}:{direction}`

**Timeout**: 30 seconds

**Process**:

1. Check if sync recently performed
2. Set cache flag during sync
3. Automatic expiration prevents permanent blocks

### Sync Directions

```python
class SyncDirection(Enum):
IMPACT_TO_JIRA = "impact_to_jira"
JIRA_TO_IMPACT = "jira_to_impact"
IMPACT_TO_SLACK = "impact_to_slack"
SLACK_TO_IMPACT = "slack_to_impact"
```
**Webhook bounce guard**: Impact→Jira writes a short-lived cache key per change (`sync:impact_to_jira:{incident_id}:{field}:{value}`). Jira webhook processing checks and clears that key to skip the mirrored change, preventing loops for status and priority (including `customfield_11064`).

## Error Handling

### Transaction Management

- All sync operations wrapped in `transaction.atomic()`
- Rollback on any failure
- Incident update creation uses `transaction.atomic()` (in `Incident.create_incident_update`) to ensure `IncidentUpdate` persistence and recovered-event handling.
- Jira webhook and Impact→Jira signal handlers are not wrapped in `transaction.atomic()` today (they call out to Jira directly).
- Rollback on any failure where wrapped; external Jira calls are best-effort and may partially succeed.
- Detailed error logging with context

### Graceful Degradation

- Missing JIRA tickets: Log warning, continue
- Field validation errors: Skip invalid fields
- Network failures: Retry mechanism via Celery
- Missing JIRA tickets: Log warning and continue (no rollback).
- Field validation errors: Skip invalid fields (best-effort persist).
- Jira/Slack calls: Best-effort with exception logging; no automatic retry in the sync handlers today.
- Celery retries apply only to the dedicated Celery tasks (not the webhook/signal handlers).

## IncidentUpdate Integration

Expand All @@ -194,6 +202,7 @@ class SyncDirection(Enum):
### Loop Detection for IncidentUpdates

**Pattern**: Updates created by sync have:

- `created_by = None` (system update)
- `message` contains "from Jira"

Expand Down Expand Up @@ -253,23 +262,26 @@ def test_sync_incident_changes(self, mock_sync):

## Jira Post-Mortem Integration

### Overview
### Post-mortem Overview

The Jira post-mortem feature creates dedicated post-mortem issues in Jira when an incident moves to the POST_MORTEM status. This provides a structured place to document root causes, impacts, and mitigation actions.

### Architecture

**Service Layer**: `src/firefighter/jira_app/service_postmortem.py`

- `JiraPostMortemService` - Main service for creating post-mortems
- `create_postmortem_for_incident()` - Creates Jira post-mortem issue
- `_generate_issue_fields()` - Generates content from templates

**Jira Client**: `src/firefighter/jira_app/client.py`

- `create_postmortem_issue()` - Creates the Jira issue
- `_create_issue_link_safe()` - Links post-mortem to incident ticket (robust with fallbacks)
- `assign_issue()` - Assigns to incident commander (graceful failure)

**Signal Handlers**: `src/firefighter/jira_app/signals/`

- `postmortem_created.py` - Triggers post-mortem creation on incident status change
- `incident_key_events_updated.py` - Syncs key events from Slack to Jira timeline

Expand All @@ -278,6 +290,7 @@ The Jira post-mortem feature creates dedicated post-mortem issues in Jira when a
1. **Trigger**: Incident status changes to `POST_MORTEM` (via Slack modal or direct status update)
2. **Signal**: `postmortem_created` signal sent with incident data
3. **Content Generation**: Templates rendered with incident data:

- `incident_summary.txt` - Priority, category, created_at (excludes Status/Created)
- `timeline.txt` - Chronological list of status changes and key events
- `impact.txt` - Business impact description
Expand All @@ -294,13 +307,15 @@ The Jira post-mortem feature creates dedicated post-mortem issues in Jira when a
### Issue Linking Strategy

**Problem**: Jira parent-child relationships have strict hierarchy rules. Setting a parent field can fail with:
```

```text
{"errors":{"parentId":"Given parent work item does not belong to appropriate hierarchy."}}
```

**Solution**: Use flexible issue links instead of parent-child relationships.

**Implementation** (`_create_issue_link_safe()`):

1. Validate both issues exist
2. Try multiple link types in order of preference:
- "Relates" (standard bidirectional link)
Expand All @@ -310,6 +325,7 @@ The Jira post-mortem feature creates dedicated post-mortem issues in Jira when a
4. Post-mortem creation always succeeds even if linking fails

**Benefits**:

- Works across any issue types regardless of hierarchy
- Graceful degradation if link types unavailable
- Main workflow never blocked by linking failures
Expand All @@ -319,13 +335,15 @@ The Jira post-mortem feature creates dedicated post-mortem issues in Jira when a
**Template**: `src/firefighter/jira_app/templates/jira/postmortem/timeline.txt`

**Content**:

- Incident creation event
- All status changes with timestamps
- All key events (detected, started, recovered, etc.) with optional messages
- Sorted chronologically ascending by `event_ts`

**Format** (Jira Wiki Markup):
```

```text
h2. Timeline

|| Time || Event ||
Expand All @@ -338,53 +356,62 @@ h2. Timeline
```

**Key Events Sync**:

- Key events entered in Slack are synced to Jira timeline via `incident_key_events_updated` signal
- Ensures timeline is always up-to-date with the latest incident events

### Graceful Error Handling

**Assignment Failures**:

- `assign_issue()` returns boolean instead of raising exceptions
- Logs WARNING instead of ERROR
- Post-mortem creation succeeds even if commander assignment fails

**Invalid Emojis** (Test Environments):

- Bookmark creation wrapped in try/except `SlackApiError`
- Custom emojis (`:jira_new:`, `:confluence:`) may not exist in test workspaces
- Logs warnings but doesn't fail post-mortem workflow

**Issue Link Failures**:

- Multiple link type fallbacks
- Validates issues before linking
- Post-mortem always created even if linking fails

### Slack Integration

**Notification Message** (`SlackMessageIncidentPostMortemCreated`):

- Posted to incident channel and pinned
- Contains links to all available post-mortems (Confluence + Jira)
- Sent by `postmortem_created_send()` signal handler

**Initial Message Update** (`SlackMessageIncidentDeclaredAnnouncement`):

- The pinned initial incident announcement message is automatically updated
- Shows all post-mortem links alongside incident ticket link
- Uses `SlackMessageStrategy.UPDATE` to update existing message
- Format:
```

```text
:jira_new: <link|Jira ticket>
:confluence: <link|Confluence Post-mortem>
:jira_new: <link|Jira Post-mortem (PM-123)>
```

**Channel Bookmarks**:

- Bookmarks added for quick access to post-mortems
- Confluence: `:confluence:` emoji
- Jira: `:jira_new:` emoji with issue key
- Gracefully handles missing custom emojis in test environments

### Configuration
### Post-mortem Configuration

**Settings** (`settings.py`):

```python
# Post-mortem project and issue type
JIRA_POSTMORTEM_PROJECT_KEY = "POSTMORTEM" # or same as incident project
Expand All @@ -402,13 +429,15 @@ JIRA_POSTMORTEM_FIELDS = {
```

**Environment Variables**:

```bash
ENABLE_JIRA_POSTMORTEM=true # Enable Jira post-mortem feature
```

### Testing

**Test Files**:

- `tests/test_jira_app/test_postmortem_service.py` - Service layer tests (4 tests)
- Incident summary excludes Status and Created fields
- Timeline includes status changes
Expand All @@ -424,6 +453,7 @@ ENABLE_JIRA_POSTMORTEM=true # Enable Jira post-mortem feature
- Handling key events with/without messages

**Test Patterns**:

```python
@pytest.mark.django_db
@patch("firefighter.jira_app.service_postmortem.JiraClient")
Expand All @@ -447,6 +477,7 @@ def test_create_postmortem(mock_jira_client):
### Database Models

**JiraPostMortem** (`src/firefighter/jira_app/models.py`):

- `incident` - OneToOne to Incident (related_name: `jira_postmortem_for`)
- `jira_issue_key` - Jira issue key (e.g., "PM-123")
- `jira_issue_id` - Jira internal ID
Expand Down
Loading