mcp-ticketer 0.4.0__py3-none-any.whl → 0.4.2__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.
Potentially problematic release.
This version of mcp-ticketer might be problematic. Click here for more details.
- mcp_ticketer/__version__.py +1 -1
- mcp_ticketer/cli/auggie_configure.py +66 -0
- mcp_ticketer/cli/codex_configure.py +68 -0
- mcp_ticketer/cli/gemini_configure.py +66 -0
- mcp_ticketer/cli/main.py +276 -39
- mcp_ticketer/cli/mcp_configure.py +71 -8
- mcp_ticketer/cli/platform_commands.py +5 -15
- mcp_ticketer/cli/ticket_commands.py +15 -5
- mcp_ticketer/mcp/server_sdk.py +93 -0
- mcp_ticketer/mcp/tools/__init__.py +38 -0
- mcp_ticketer/mcp/tools/attachment_tools.py +180 -0
- mcp_ticketer/mcp/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/tools/comment_tools.py +90 -0
- mcp_ticketer/mcp/tools/hierarchy_tools.py +383 -0
- mcp_ticketer/mcp/tools/pr_tools.py +154 -0
- mcp_ticketer/mcp/tools/search_tools.py +206 -0
- mcp_ticketer/mcp/tools/ticket_tools.py +277 -0
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/METADATA +30 -16
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/RECORD +23 -14
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.4.0.dist-info → mcp_ticketer-0.4.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""Hierarchy management tools for Epic/Issue/Task structure.
|
|
2
|
+
|
|
3
|
+
This module implements tools for managing the three-level ticket hierarchy:
|
|
4
|
+
- Epic: Strategic level containers
|
|
5
|
+
- Issue: Standard work items
|
|
6
|
+
- Task: Sub-work items
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from ...core.models import Epic, Priority, Task, TicketType
|
|
13
|
+
from ..server_sdk import get_adapter, mcp
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@mcp.tool()
|
|
17
|
+
async def epic_create(
|
|
18
|
+
title: str,
|
|
19
|
+
description: str = "",
|
|
20
|
+
target_date: Optional[str] = None,
|
|
21
|
+
lead_id: Optional[str] = None,
|
|
22
|
+
child_issues: Optional[list[str]] = None,
|
|
23
|
+
) -> dict[str, Any]:
|
|
24
|
+
"""Create a new epic (strategic level container).
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
title: Epic title (required)
|
|
28
|
+
description: Detailed description of the epic
|
|
29
|
+
target_date: Target completion date in ISO format (YYYY-MM-DD)
|
|
30
|
+
lead_id: User ID or email of the epic lead
|
|
31
|
+
child_issues: List of existing issue IDs to link to this epic
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Created epic details including ID and metadata, or error information
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
adapter = get_adapter()
|
|
39
|
+
|
|
40
|
+
# Parse target date if provided
|
|
41
|
+
target_datetime = None
|
|
42
|
+
if target_date:
|
|
43
|
+
try:
|
|
44
|
+
target_datetime = datetime.fromisoformat(target_date)
|
|
45
|
+
except ValueError:
|
|
46
|
+
return {
|
|
47
|
+
"status": "error",
|
|
48
|
+
"error": f"Invalid date format '{target_date}'. Use ISO format: YYYY-MM-DD",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Create epic object
|
|
52
|
+
epic = Epic(
|
|
53
|
+
title=title,
|
|
54
|
+
description=description or "",
|
|
55
|
+
due_date=target_datetime,
|
|
56
|
+
assignee=lead_id,
|
|
57
|
+
child_issues=child_issues or [],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Create via adapter
|
|
61
|
+
created = await adapter.create(epic)
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
"status": "completed",
|
|
65
|
+
"epic": created.model_dump(),
|
|
66
|
+
}
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return {
|
|
69
|
+
"status": "error",
|
|
70
|
+
"error": f"Failed to create epic: {str(e)}",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
async def epic_list(
|
|
76
|
+
limit: int = 10,
|
|
77
|
+
offset: int = 0,
|
|
78
|
+
) -> dict[str, Any]:
|
|
79
|
+
"""List all epics with pagination.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
limit: Maximum number of epics to return (default: 10)
|
|
83
|
+
offset: Number of epics to skip for pagination (default: 0)
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
List of epics, or error information
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
try:
|
|
90
|
+
adapter = get_adapter()
|
|
91
|
+
|
|
92
|
+
# List with epic filter
|
|
93
|
+
filters = {"ticket_type": TicketType.EPIC}
|
|
94
|
+
epics = await adapter.list(limit=limit, offset=offset, filters=filters)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"status": "completed",
|
|
98
|
+
"epics": [epic.model_dump() for epic in epics],
|
|
99
|
+
"count": len(epics),
|
|
100
|
+
"limit": limit,
|
|
101
|
+
"offset": offset,
|
|
102
|
+
}
|
|
103
|
+
except Exception as e:
|
|
104
|
+
return {
|
|
105
|
+
"status": "error",
|
|
106
|
+
"error": f"Failed to list epics: {str(e)}",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@mcp.tool()
|
|
111
|
+
async def epic_issues(epic_id: str) -> dict[str, Any]:
|
|
112
|
+
"""Get all issues belonging to an epic.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
epic_id: Unique identifier of the epic
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
List of issues in the epic, or error information
|
|
119
|
+
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
adapter = get_adapter()
|
|
123
|
+
|
|
124
|
+
# Read the epic to get child issue IDs
|
|
125
|
+
epic = await adapter.read(epic_id)
|
|
126
|
+
if epic is None:
|
|
127
|
+
return {
|
|
128
|
+
"status": "error",
|
|
129
|
+
"error": f"Epic {epic_id} not found",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# If epic has no child_issues attribute, use empty list
|
|
133
|
+
child_issue_ids = getattr(epic, "child_issues", [])
|
|
134
|
+
|
|
135
|
+
# Fetch each child issue
|
|
136
|
+
issues = []
|
|
137
|
+
for issue_id in child_issue_ids:
|
|
138
|
+
issue = await adapter.read(issue_id)
|
|
139
|
+
if issue:
|
|
140
|
+
issues.append(issue.model_dump())
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
"status": "completed",
|
|
144
|
+
"epic_id": epic_id,
|
|
145
|
+
"issues": issues,
|
|
146
|
+
"count": len(issues),
|
|
147
|
+
}
|
|
148
|
+
except Exception as e:
|
|
149
|
+
return {
|
|
150
|
+
"status": "error",
|
|
151
|
+
"error": f"Failed to get epic issues: {str(e)}",
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@mcp.tool()
|
|
156
|
+
async def issue_create(
|
|
157
|
+
title: str,
|
|
158
|
+
description: str = "",
|
|
159
|
+
epic_id: Optional[str] = None,
|
|
160
|
+
assignee: Optional[str] = None,
|
|
161
|
+
priority: str = "medium",
|
|
162
|
+
) -> dict[str, Any]:
|
|
163
|
+
"""Create a new issue (standard work item).
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
title: Issue title (required)
|
|
167
|
+
description: Detailed description of the issue
|
|
168
|
+
epic_id: Parent epic ID to link this issue to
|
|
169
|
+
assignee: User ID or email to assign the issue to
|
|
170
|
+
priority: Priority level - must be one of: low, medium, high, critical
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Created issue details including ID and metadata, or error information
|
|
174
|
+
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
adapter = get_adapter()
|
|
178
|
+
|
|
179
|
+
# Validate and convert priority
|
|
180
|
+
try:
|
|
181
|
+
priority_enum = Priority(priority.lower())
|
|
182
|
+
except ValueError:
|
|
183
|
+
return {
|
|
184
|
+
"status": "error",
|
|
185
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# Create issue (Task with ISSUE type)
|
|
189
|
+
issue = Task(
|
|
190
|
+
title=title,
|
|
191
|
+
description=description or "",
|
|
192
|
+
ticket_type=TicketType.ISSUE,
|
|
193
|
+
parent_epic=epic_id,
|
|
194
|
+
assignee=assignee,
|
|
195
|
+
priority=priority_enum,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Create via adapter
|
|
199
|
+
created = await adapter.create(issue)
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
"status": "completed",
|
|
203
|
+
"issue": created.model_dump(),
|
|
204
|
+
}
|
|
205
|
+
except Exception as e:
|
|
206
|
+
return {
|
|
207
|
+
"status": "error",
|
|
208
|
+
"error": f"Failed to create issue: {str(e)}",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@mcp.tool()
|
|
213
|
+
async def issue_tasks(issue_id: str) -> dict[str, Any]:
|
|
214
|
+
"""Get all tasks (sub-items) belonging to an issue.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
issue_id: Unique identifier of the issue
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
List of tasks in the issue, or error information
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
try:
|
|
224
|
+
adapter = get_adapter()
|
|
225
|
+
|
|
226
|
+
# Read the issue to get child task IDs
|
|
227
|
+
issue = await adapter.read(issue_id)
|
|
228
|
+
if issue is None:
|
|
229
|
+
return {
|
|
230
|
+
"status": "error",
|
|
231
|
+
"error": f"Issue {issue_id} not found",
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# Get child task IDs
|
|
235
|
+
child_task_ids = getattr(issue, "children", [])
|
|
236
|
+
|
|
237
|
+
# Fetch each child task
|
|
238
|
+
tasks = []
|
|
239
|
+
for task_id in child_task_ids:
|
|
240
|
+
task = await adapter.read(task_id)
|
|
241
|
+
if task:
|
|
242
|
+
tasks.append(task.model_dump())
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
"status": "completed",
|
|
246
|
+
"issue_id": issue_id,
|
|
247
|
+
"tasks": tasks,
|
|
248
|
+
"count": len(tasks),
|
|
249
|
+
}
|
|
250
|
+
except Exception as e:
|
|
251
|
+
return {
|
|
252
|
+
"status": "error",
|
|
253
|
+
"error": f"Failed to get issue tasks: {str(e)}",
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@mcp.tool()
|
|
258
|
+
async def task_create(
|
|
259
|
+
title: str,
|
|
260
|
+
description: str = "",
|
|
261
|
+
issue_id: Optional[str] = None,
|
|
262
|
+
assignee: Optional[str] = None,
|
|
263
|
+
priority: str = "medium",
|
|
264
|
+
) -> dict[str, Any]:
|
|
265
|
+
"""Create a new task (sub-work item).
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
title: Task title (required)
|
|
269
|
+
description: Detailed description of the task
|
|
270
|
+
issue_id: Parent issue ID to link this task to
|
|
271
|
+
assignee: User ID or email to assign the task to
|
|
272
|
+
priority: Priority level - must be one of: low, medium, high, critical
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Created task details including ID and metadata, or error information
|
|
276
|
+
|
|
277
|
+
"""
|
|
278
|
+
try:
|
|
279
|
+
adapter = get_adapter()
|
|
280
|
+
|
|
281
|
+
# Validate and convert priority
|
|
282
|
+
try:
|
|
283
|
+
priority_enum = Priority(priority.lower())
|
|
284
|
+
except ValueError:
|
|
285
|
+
return {
|
|
286
|
+
"status": "error",
|
|
287
|
+
"error": f"Invalid priority '{priority}'. Must be one of: low, medium, high, critical",
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# Create task (Task with TASK type)
|
|
291
|
+
task = Task(
|
|
292
|
+
title=title,
|
|
293
|
+
description=description or "",
|
|
294
|
+
ticket_type=TicketType.TASK,
|
|
295
|
+
parent_issue=issue_id,
|
|
296
|
+
assignee=assignee,
|
|
297
|
+
priority=priority_enum,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Create via adapter
|
|
301
|
+
created = await adapter.create(task)
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
"status": "completed",
|
|
305
|
+
"task": created.model_dump(),
|
|
306
|
+
}
|
|
307
|
+
except Exception as e:
|
|
308
|
+
return {
|
|
309
|
+
"status": "error",
|
|
310
|
+
"error": f"Failed to create task: {str(e)}",
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@mcp.tool()
|
|
315
|
+
async def hierarchy_tree(
|
|
316
|
+
epic_id: str,
|
|
317
|
+
max_depth: int = 3,
|
|
318
|
+
) -> dict[str, Any]:
|
|
319
|
+
"""Get complete hierarchy tree for an epic.
|
|
320
|
+
|
|
321
|
+
Retrieves the full hierarchy tree starting from an epic, including all
|
|
322
|
+
child issues and their tasks up to the specified depth.
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
epic_id: Unique identifier of the root epic
|
|
326
|
+
max_depth: Maximum depth to traverse (1=epic only, 2=epic+issues, 3=epic+issues+tasks)
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Complete hierarchy tree structure, or error information
|
|
330
|
+
|
|
331
|
+
"""
|
|
332
|
+
try:
|
|
333
|
+
adapter = get_adapter()
|
|
334
|
+
|
|
335
|
+
# Read the epic
|
|
336
|
+
epic = await adapter.read(epic_id)
|
|
337
|
+
if epic is None:
|
|
338
|
+
return {
|
|
339
|
+
"status": "error",
|
|
340
|
+
"error": f"Epic {epic_id} not found",
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
# Build tree structure
|
|
344
|
+
tree = {
|
|
345
|
+
"epic": epic.model_dump(),
|
|
346
|
+
"issues": [],
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if max_depth < 2:
|
|
350
|
+
return {
|
|
351
|
+
"status": "completed",
|
|
352
|
+
"tree": tree,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
# Get child issues
|
|
356
|
+
child_issue_ids = getattr(epic, "child_issues", [])
|
|
357
|
+
for issue_id in child_issue_ids:
|
|
358
|
+
issue = await adapter.read(issue_id)
|
|
359
|
+
if issue:
|
|
360
|
+
issue_data = {
|
|
361
|
+
"issue": issue.model_dump(),
|
|
362
|
+
"tasks": [],
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if max_depth >= 3:
|
|
366
|
+
# Get child tasks
|
|
367
|
+
child_task_ids = getattr(issue, "children", [])
|
|
368
|
+
for task_id in child_task_ids:
|
|
369
|
+
task = await adapter.read(task_id)
|
|
370
|
+
if task:
|
|
371
|
+
issue_data["tasks"].append(task.model_dump())
|
|
372
|
+
|
|
373
|
+
tree["issues"].append(issue_data)
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
"status": "completed",
|
|
377
|
+
"tree": tree,
|
|
378
|
+
}
|
|
379
|
+
except Exception as e:
|
|
380
|
+
return {
|
|
381
|
+
"status": "error",
|
|
382
|
+
"error": f"Failed to build hierarchy tree: {str(e)}",
|
|
383
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""Pull request integration tools for tickets.
|
|
2
|
+
|
|
3
|
+
This module implements tools for linking tickets with pull requests and
|
|
4
|
+
creating PRs from tickets. Note that PR functionality may not be available
|
|
5
|
+
in all adapters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from ..server_sdk import get_adapter, mcp
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@mcp.tool()
|
|
14
|
+
async def ticket_create_pr(
|
|
15
|
+
ticket_id: str,
|
|
16
|
+
title: str,
|
|
17
|
+
description: str = "",
|
|
18
|
+
source_branch: Optional[str] = None,
|
|
19
|
+
target_branch: str = "main",
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
"""Create a pull request linked to a ticket.
|
|
22
|
+
|
|
23
|
+
Creates a new pull request and automatically links it to the specified
|
|
24
|
+
ticket. This functionality may not be available in all adapters.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
ticket_id: Unique identifier of the ticket to link the PR to
|
|
28
|
+
title: Pull request title
|
|
29
|
+
description: Pull request description
|
|
30
|
+
source_branch: Source branch for the PR (if not specified, may use ticket ID)
|
|
31
|
+
target_branch: Target branch for the PR (default: main)
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Created PR details and link information, or error information
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
adapter = get_adapter()
|
|
39
|
+
|
|
40
|
+
# Check if adapter supports PR operations
|
|
41
|
+
if not hasattr(adapter, "create_pull_request"):
|
|
42
|
+
return {
|
|
43
|
+
"status": "error",
|
|
44
|
+
"error": f"Pull request creation not supported by {type(adapter).__name__} adapter",
|
|
45
|
+
"ticket_id": ticket_id,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Read ticket to validate it exists
|
|
49
|
+
ticket = await adapter.read(ticket_id)
|
|
50
|
+
if ticket is None:
|
|
51
|
+
return {
|
|
52
|
+
"status": "error",
|
|
53
|
+
"error": f"Ticket {ticket_id} not found",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Use ticket ID as source branch if not specified
|
|
57
|
+
if source_branch is None:
|
|
58
|
+
source_branch = f"feature/{ticket_id}"
|
|
59
|
+
|
|
60
|
+
# Create PR via adapter
|
|
61
|
+
pr_data = await adapter.create_pull_request( # type: ignore
|
|
62
|
+
ticket_id=ticket_id,
|
|
63
|
+
title=title,
|
|
64
|
+
description=description,
|
|
65
|
+
source_branch=source_branch,
|
|
66
|
+
target_branch=target_branch,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"status": "completed",
|
|
71
|
+
"ticket_id": ticket_id,
|
|
72
|
+
"pull_request": pr_data,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
except AttributeError:
|
|
76
|
+
return {
|
|
77
|
+
"status": "error",
|
|
78
|
+
"error": "Pull request creation not supported by this adapter",
|
|
79
|
+
"ticket_id": ticket_id,
|
|
80
|
+
}
|
|
81
|
+
except Exception as e:
|
|
82
|
+
return {
|
|
83
|
+
"status": "error",
|
|
84
|
+
"error": f"Failed to create pull request: {str(e)}",
|
|
85
|
+
"ticket_id": ticket_id,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@mcp.tool()
|
|
90
|
+
async def ticket_link_pr(
|
|
91
|
+
ticket_id: str,
|
|
92
|
+
pr_url: str,
|
|
93
|
+
) -> dict[str, Any]:
|
|
94
|
+
"""Link an existing pull request to a ticket.
|
|
95
|
+
|
|
96
|
+
Associates an existing pull request (identified by URL) with a ticket.
|
|
97
|
+
This is typically done by adding the PR URL to the ticket's metadata
|
|
98
|
+
or as a comment.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
ticket_id: Unique identifier of the ticket
|
|
102
|
+
pr_url: URL of the pull request to link
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Link confirmation and updated ticket details, or error information
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
adapter = get_adapter()
|
|
110
|
+
|
|
111
|
+
# Read ticket to validate it exists
|
|
112
|
+
ticket = await adapter.read(ticket_id)
|
|
113
|
+
if ticket is None:
|
|
114
|
+
return {
|
|
115
|
+
"status": "error",
|
|
116
|
+
"error": f"Ticket {ticket_id} not found",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# Check if adapter has specialized PR linking
|
|
120
|
+
if hasattr(adapter, "link_pull_request"):
|
|
121
|
+
result = await adapter.link_pull_request( # type: ignore
|
|
122
|
+
ticket_id=ticket_id, pr_url=pr_url
|
|
123
|
+
)
|
|
124
|
+
return {
|
|
125
|
+
"status": "completed",
|
|
126
|
+
"ticket_id": ticket_id,
|
|
127
|
+
"pr_url": pr_url,
|
|
128
|
+
"result": result,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Fallback: Add PR link as comment
|
|
132
|
+
from ...core.models import Comment
|
|
133
|
+
|
|
134
|
+
comment = Comment(
|
|
135
|
+
ticket_id=ticket_id,
|
|
136
|
+
content=f"Pull Request: {pr_url}",
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
created_comment = await adapter.add_comment(comment)
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
"status": "completed",
|
|
143
|
+
"ticket_id": ticket_id,
|
|
144
|
+
"pr_url": pr_url,
|
|
145
|
+
"method": "comment",
|
|
146
|
+
"comment": created_comment.model_dump(),
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
return {
|
|
151
|
+
"status": "error",
|
|
152
|
+
"error": f"Failed to link pull request: {str(e)}",
|
|
153
|
+
"ticket_id": ticket_id,
|
|
154
|
+
}
|