-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwebhook_handler.py
More file actions
193 lines (161 loc) · 9.24 KB
/
webhook_handler.py
File metadata and controls
193 lines (161 loc) · 9.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
#!/usr/bin/env python3
"""
Webhook handler module for GitLab-Jira Webhook Bot
This module contains the core webhook processing logic and business rules
for handling GitLab pipeline webhooks and managing Jira tickets.
"""
import asyncio
import json
import logging
from typing import Dict, Any, Optional
from fastapi import HTTPException
from fastapi.responses import JSONResponse
from config import Config
from models import GitLabWebhookPayload, WebhookResponse
from gitlab_service import GitLabService
from jira_service import JiraService
logger = logging.getLogger(__name__)
# Global locks for ticket creation to prevent race conditions
TICKET_CREATION_LOCKS = {}
class WebhookHandler:
"""Handler class for processing GitLab webhooks and managing Jira tickets"""
def __init__(self):
self.gitlab_service = GitLabService()
self.jira_service = JiraService()
async def handle_pipeline_webhook(self, payload: Dict[str, Any], signature: Optional[str] = None) -> JSONResponse:
"""Handle GitLab pipeline webhook and manage Jira tickets accordingly"""
try:
# Validate payload structure
if payload.get('object_kind') != 'pipeline':
logger.info(f"Ignoring non-pipeline webhook: {payload.get('object_kind')}")
return JSONResponse(content={"message": "Ignored non-pipeline event"})
# Extract pipeline information
pipeline_info = self.gitlab_service.extract_pipeline_info(payload)
logger.info(f"Processing pipeline {pipeline_info['pipeline_id']} (branch: {pipeline_info['branch_name']}, repo: {pipeline_info['repo_name']}) with status {pipeline_info['pipeline_status']}")
# Check if this is a status change we care about
if pipeline_info['pipeline_status'] not in ['running', 'success', 'failed', 'canceled']:
logger.info(f"Ignoring pipeline status: {pipeline_info['pipeline_status']}")
return JSONResponse(content={"message": f"Ignored status: {pipeline_info['pipeline_status']}"})
# Handle different pipeline statuses
if pipeline_info['pipeline_status'] == 'failed':
return await self._handle_failed_pipeline(pipeline_info, payload)
elif pipeline_info['pipeline_status'] == 'success':
return await self._handle_success_pipeline(pipeline_info, payload)
elif pipeline_info['pipeline_status'] in ['running', 'canceled']:
return await self._handle_other_pipeline_status(pipeline_info, payload)
else:
return JSONResponse(content={
"message": f"Unhandled pipeline status: {pipeline_info['pipeline_status']}",
"status": pipeline_info['pipeline_status'],
"branch": pipeline_info['branch_name'],
"repo": pipeline_info['repo_name']
})
except HTTPException:
raise
except Exception as e:
logger.error(f"Unexpected error processing webhook: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
async def _handle_failed_pipeline(self, pipeline_info: Dict[str, Any], payload: Dict[str, Any]) -> JSONResponse:
"""Handle failed pipeline - create or update ticket"""
lock_key = f"{pipeline_info['repo_name']}/{pipeline_info['branch_name']}"
# Set up locking for this branch/repo combination
if lock_key not in TICKET_CREATION_LOCKS:
TICKET_CREATION_LOCKS[lock_key] = asyncio.Lock()
lock = TICKET_CREATION_LOCKS[lock_key]
async with lock: # Forces only one request to run this block at a time
# Search for an existing OPEN ticket
open_ticket_key = self.jira_service.find_open_ticket_by_branch(
pipeline_info['branch_name'],
pipeline_info['repo_name']
)
if open_ticket_key:
# Ticket already exists; update it
self.jira_service.update_ticket(open_ticket_key, pipeline_info)
message = f"Added failure comment to existing ticket {open_ticket_key}"
else:
# No ticket found; create it
# Fetch pipeline logs if available
pipeline_logs = None
if pipeline_info['project_id']:
pipeline_logs = self.gitlab_service.get_failed_pipeline_logs(
pipeline_info['project_id'],
pipeline_info['pipeline_id']
)
ticket_key = self.jira_service.create_ticket(pipeline_info, pipeline_logs)
if ticket_key:
message = f"Created new Jira ticket {ticket_key}"
# Add watcher to the ticket
if pipeline_info['commit_author_email']:
self.jira_service.add_watcher(ticket_key, pipeline_info['commit_author_email'])
# Wait for Jira's index to refresh BEFORE releasing the lock
logger.info(f"Ticket {ticket_key} created. Waiting 2 seconds for Jira index...")
await asyncio.sleep(2)
else:
raise HTTPException(status_code=500, detail="Failed to create Jira ticket")
return JSONResponse(content={
"message": message,
"status": pipeline_info['pipeline_status'],
"branch": pipeline_info['branch_name'],
"repo": pipeline_info['repo_name']
})
async def _handle_success_pipeline(self, pipeline_info: Dict[str, Any], payload: Dict[str, Any]) -> JSONResponse:
"""Handle successful pipeline - close existing ticket if found"""
# Get open ticket for this branch/repo
open_ticket_key = self.jira_service.find_open_ticket_by_branch(
pipeline_info['branch_name'],
pipeline_info['repo_name']
)
if open_ticket_key:
# Found an open ticket for this branch/repo: close it!
self.jira_service.update_ticket(open_ticket_key, pipeline_info) # Add 'success' comment
transition_success = self.jira_service.transition_ticket(open_ticket_key, self.jira_service.close_transition_id)
if transition_success:
message = f"Successfully closed Jira ticket {open_ticket_key}"
else:
message = f"Updated Jira ticket {open_ticket_key} but failed to close it"
return JSONResponse(content={
"message": message,
"ticket_key": open_ticket_key,
"status": pipeline_info['pipeline_status'],
"branch": pipeline_info['branch_name'],
"repo": pipeline_info['repo_name']
})
else:
logger.info(f"Success pipeline for {pipeline_info['repo_name']}/{pipeline_info['branch_name']} but no open ticket found. Ignoring.")
return JSONResponse(content={
"message": "Success received, no open ticket found to close.",
"status": pipeline_info['pipeline_status'],
"branch": pipeline_info['branch_name'],
"repo": pipeline_info['repo_name']
})
async def _handle_other_pipeline_status(self, pipeline_info: Dict[str, Any], payload: Dict[str, Any]) -> JSONResponse:
"""Handle running/canceled pipeline status - update existing ticket if found"""
# Get open ticket for this branch/repo
open_ticket_key = self.jira_service.find_open_ticket_by_branch(
pipeline_info['branch_name'],
pipeline_info['repo_name']
)
if open_ticket_key:
# Update existing ticket with current status
success = self.jira_service.update_ticket(open_ticket_key, pipeline_info)
if success:
return JSONResponse(content={
"message": f"Updated existing Jira ticket for {pipeline_info['pipeline_status']} pipeline",
"ticket_key": open_ticket_key,
"status": pipeline_info['pipeline_status'],
"branch": pipeline_info['branch_name'],
"repo": pipeline_info['repo_name']
})
else:
raise HTTPException(status_code=500, detail="Failed to update Jira ticket")
else:
logger.info(f"Pipeline {pipeline_info['pipeline_id']} is {pipeline_info['pipeline_status']} but no failure ticket exists for {pipeline_info['repo_name']}/{pipeline_info['branch_name']}")
return JSONResponse(content={
"message": f"Pipeline is {pipeline_info['pipeline_status']} but no failure ticket exists",
"status": pipeline_info['pipeline_status'],
"branch": pipeline_info['branch_name'],
"repo": pipeline_info['repo_name']
})
def verify_webhook_signature(self, payload: bytes, signature: str) -> bool:
"""Verify GitLab webhook signature"""
return self.gitlab_service.verify_webhook_signature(payload, signature)