mcp-ticketer 0.1.30__py3-none-any.whl → 1.2.11__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/__init__.py +10 -10
- mcp_ticketer/__version__.py +3 -3
- mcp_ticketer/adapters/__init__.py +2 -0
- mcp_ticketer/adapters/aitrackdown.py +796 -46
- mcp_ticketer/adapters/asana/__init__.py +15 -0
- mcp_ticketer/adapters/asana/adapter.py +1416 -0
- mcp_ticketer/adapters/asana/client.py +292 -0
- mcp_ticketer/adapters/asana/mappers.py +348 -0
- mcp_ticketer/adapters/asana/types.py +146 -0
- mcp_ticketer/adapters/github.py +879 -129
- mcp_ticketer/adapters/hybrid.py +11 -11
- mcp_ticketer/adapters/jira.py +973 -73
- mcp_ticketer/adapters/linear/__init__.py +24 -0
- mcp_ticketer/adapters/linear/adapter.py +2732 -0
- mcp_ticketer/adapters/linear/client.py +344 -0
- mcp_ticketer/adapters/linear/mappers.py +420 -0
- mcp_ticketer/adapters/linear/queries.py +479 -0
- mcp_ticketer/adapters/linear/types.py +360 -0
- mcp_ticketer/adapters/linear.py +10 -2315
- mcp_ticketer/analysis/__init__.py +23 -0
- mcp_ticketer/analysis/orphaned.py +218 -0
- mcp_ticketer/analysis/similarity.py +224 -0
- mcp_ticketer/analysis/staleness.py +266 -0
- mcp_ticketer/cache/memory.py +9 -8
- mcp_ticketer/cli/adapter_diagnostics.py +421 -0
- mcp_ticketer/cli/auggie_configure.py +116 -15
- mcp_ticketer/cli/codex_configure.py +274 -82
- mcp_ticketer/cli/configure.py +888 -151
- mcp_ticketer/cli/diagnostics.py +400 -157
- mcp_ticketer/cli/discover.py +297 -26
- mcp_ticketer/cli/gemini_configure.py +119 -26
- mcp_ticketer/cli/init_command.py +880 -0
- mcp_ticketer/cli/instruction_commands.py +435 -0
- mcp_ticketer/cli/linear_commands.py +616 -0
- mcp_ticketer/cli/main.py +203 -1165
- mcp_ticketer/cli/mcp_configure.py +474 -90
- mcp_ticketer/cli/mcp_server_commands.py +415 -0
- mcp_ticketer/cli/migrate_config.py +12 -8
- mcp_ticketer/cli/platform_commands.py +123 -0
- mcp_ticketer/cli/platform_detection.py +418 -0
- mcp_ticketer/cli/platform_installer.py +513 -0
- mcp_ticketer/cli/python_detection.py +126 -0
- mcp_ticketer/cli/queue_commands.py +15 -15
- mcp_ticketer/cli/setup_command.py +639 -0
- mcp_ticketer/cli/simple_health.py +90 -65
- mcp_ticketer/cli/ticket_commands.py +1013 -0
- mcp_ticketer/cli/update_checker.py +313 -0
- mcp_ticketer/cli/utils.py +114 -66
- mcp_ticketer/core/__init__.py +24 -1
- mcp_ticketer/core/adapter.py +250 -16
- mcp_ticketer/core/config.py +145 -37
- mcp_ticketer/core/env_discovery.py +101 -22
- mcp_ticketer/core/env_loader.py +349 -0
- mcp_ticketer/core/exceptions.py +160 -0
- mcp_ticketer/core/http_client.py +26 -26
- mcp_ticketer/core/instructions.py +405 -0
- mcp_ticketer/core/label_manager.py +732 -0
- mcp_ticketer/core/mappers.py +42 -30
- mcp_ticketer/core/models.py +280 -28
- mcp_ticketer/core/onepassword_secrets.py +379 -0
- mcp_ticketer/core/project_config.py +183 -49
- mcp_ticketer/core/registry.py +3 -3
- mcp_ticketer/core/session_state.py +171 -0
- mcp_ticketer/core/state_matcher.py +592 -0
- mcp_ticketer/core/url_parser.py +425 -0
- mcp_ticketer/core/validators.py +69 -0
- mcp_ticketer/defaults/ticket_instructions.md +644 -0
- mcp_ticketer/mcp/__init__.py +29 -1
- mcp_ticketer/mcp/__main__.py +60 -0
- mcp_ticketer/mcp/server/__init__.py +25 -0
- mcp_ticketer/mcp/server/__main__.py +60 -0
- mcp_ticketer/mcp/server/constants.py +58 -0
- mcp_ticketer/mcp/server/diagnostic_helper.py +175 -0
- mcp_ticketer/mcp/server/dto.py +195 -0
- mcp_ticketer/mcp/server/main.py +1343 -0
- mcp_ticketer/mcp/server/response_builder.py +206 -0
- mcp_ticketer/mcp/server/routing.py +655 -0
- mcp_ticketer/mcp/server/server_sdk.py +151 -0
- mcp_ticketer/mcp/server/tools/__init__.py +56 -0
- mcp_ticketer/mcp/server/tools/analysis_tools.py +495 -0
- mcp_ticketer/mcp/server/tools/attachment_tools.py +226 -0
- mcp_ticketer/mcp/server/tools/bulk_tools.py +273 -0
- mcp_ticketer/mcp/server/tools/comment_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/config_tools.py +1439 -0
- mcp_ticketer/mcp/server/tools/diagnostic_tools.py +211 -0
- mcp_ticketer/mcp/server/tools/hierarchy_tools.py +921 -0
- mcp_ticketer/mcp/server/tools/instruction_tools.py +300 -0
- mcp_ticketer/mcp/server/tools/label_tools.py +948 -0
- mcp_ticketer/mcp/server/tools/pr_tools.py +152 -0
- mcp_ticketer/mcp/server/tools/search_tools.py +215 -0
- mcp_ticketer/mcp/server/tools/session_tools.py +170 -0
- mcp_ticketer/mcp/server/tools/ticket_tools.py +1268 -0
- mcp_ticketer/mcp/server/tools/user_ticket_tools.py +547 -0
- mcp_ticketer/queue/__init__.py +1 -0
- mcp_ticketer/queue/health_monitor.py +168 -136
- mcp_ticketer/queue/manager.py +95 -25
- mcp_ticketer/queue/queue.py +40 -21
- mcp_ticketer/queue/run_worker.py +6 -1
- mcp_ticketer/queue/ticket_registry.py +213 -155
- mcp_ticketer/queue/worker.py +109 -49
- mcp_ticketer-1.2.11.dist-info/METADATA +792 -0
- mcp_ticketer-1.2.11.dist-info/RECORD +110 -0
- mcp_ticketer/mcp/server.py +0 -1895
- mcp_ticketer-0.1.30.dist-info/METADATA +0 -413
- mcp_ticketer-0.1.30.dist-info/RECORD +0 -49
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.30.dist-info → mcp_ticketer-1.2.11.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Linear-specific types and enums."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from mcp_ticketer.core.models import Priority, TicketState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class LinearPriorityMapping:
|
|
12
|
+
"""Mapping between universal Priority and Linear priority values."""
|
|
13
|
+
|
|
14
|
+
# Linear uses numeric priorities: 0=No priority, 1=Urgent, 2=High, 3=Medium, 4=Low
|
|
15
|
+
TO_LINEAR: dict[Priority, int] = {
|
|
16
|
+
Priority.CRITICAL: 1, # Urgent
|
|
17
|
+
Priority.HIGH: 2, # High
|
|
18
|
+
Priority.MEDIUM: 3, # Medium
|
|
19
|
+
Priority.LOW: 4, # Low
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
FROM_LINEAR: dict[int, Priority] = {
|
|
23
|
+
0: Priority.LOW, # No priority -> Low
|
|
24
|
+
1: Priority.CRITICAL, # Urgent -> Critical
|
|
25
|
+
2: Priority.HIGH, # High -> High
|
|
26
|
+
3: Priority.MEDIUM, # Medium -> Medium
|
|
27
|
+
4: Priority.LOW, # Low -> Low
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LinearStateMapping:
|
|
32
|
+
"""Mapping between universal TicketState and Linear workflow state types."""
|
|
33
|
+
|
|
34
|
+
# Linear workflow state types
|
|
35
|
+
TO_LINEAR: dict[TicketState, str] = {
|
|
36
|
+
TicketState.OPEN: "unstarted",
|
|
37
|
+
TicketState.IN_PROGRESS: "started",
|
|
38
|
+
TicketState.READY: "unstarted", # No direct equivalent, use unstarted
|
|
39
|
+
TicketState.TESTED: "started", # No direct equivalent, use started
|
|
40
|
+
TicketState.DONE: "completed",
|
|
41
|
+
TicketState.CLOSED: "canceled",
|
|
42
|
+
TicketState.WAITING: "unstarted",
|
|
43
|
+
TicketState.BLOCKED: "unstarted",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
FROM_LINEAR: dict[str, TicketState] = {
|
|
47
|
+
"backlog": TicketState.OPEN,
|
|
48
|
+
"unstarted": TicketState.OPEN,
|
|
49
|
+
"started": TicketState.IN_PROGRESS,
|
|
50
|
+
"completed": TicketState.DONE,
|
|
51
|
+
"canceled": TicketState.CLOSED,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class LinearWorkflowStateType(Enum):
|
|
56
|
+
"""Linear workflow state types."""
|
|
57
|
+
|
|
58
|
+
BACKLOG = "backlog"
|
|
59
|
+
UNSTARTED = "unstarted"
|
|
60
|
+
STARTED = "started"
|
|
61
|
+
COMPLETED = "completed"
|
|
62
|
+
CANCELED = "canceled"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class LinearProjectState(Enum):
|
|
66
|
+
"""Linear project states."""
|
|
67
|
+
|
|
68
|
+
PLANNED = "planned"
|
|
69
|
+
STARTED = "started"
|
|
70
|
+
COMPLETED = "completed"
|
|
71
|
+
CANCELED = "canceled"
|
|
72
|
+
PAUSED = "paused"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class LinearIssueRelationType(Enum):
|
|
76
|
+
"""Linear issue relation types."""
|
|
77
|
+
|
|
78
|
+
BLOCKS = "blocks"
|
|
79
|
+
BLOCKED_BY = "blockedBy"
|
|
80
|
+
DUPLICATE = "duplicate"
|
|
81
|
+
DUPLICATED_BY = "duplicatedBy"
|
|
82
|
+
RELATES = "relates"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class LinearCommentType(Enum):
|
|
86
|
+
"""Linear comment types."""
|
|
87
|
+
|
|
88
|
+
COMMENT = "comment"
|
|
89
|
+
SYSTEM = "system"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def get_linear_priority(priority: Priority) -> int:
|
|
93
|
+
"""Convert universal Priority to Linear priority value.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
priority: Universal priority enum
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Linear priority integer (0-4)
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
return LinearPriorityMapping.TO_LINEAR.get(priority, 3) # Default to Medium
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def get_universal_priority(linear_priority: int) -> Priority:
|
|
106
|
+
"""Convert Linear priority value to universal Priority.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
linear_priority: Linear priority integer (0-4)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Universal priority enum
|
|
113
|
+
|
|
114
|
+
"""
|
|
115
|
+
return LinearPriorityMapping.FROM_LINEAR.get(linear_priority, Priority.MEDIUM)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_linear_state_type(state: TicketState) -> str:
|
|
119
|
+
"""Convert universal TicketState to Linear workflow state type.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
state: Universal ticket state enum
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Linear workflow state type string
|
|
126
|
+
|
|
127
|
+
"""
|
|
128
|
+
return LinearStateMapping.TO_LINEAR.get(state, "unstarted")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_universal_state(
|
|
132
|
+
linear_state_type: str, state_name: str | None = None
|
|
133
|
+
) -> TicketState:
|
|
134
|
+
"""Convert Linear workflow state type to universal TicketState with synonym matching.
|
|
135
|
+
|
|
136
|
+
This function implements intelligent state mapping with fallback strategies:
|
|
137
|
+
1. Try exact match on state type (backlog, unstarted, started, completed, canceled)
|
|
138
|
+
2. Try synonym matching on state name (ToDo, In Review, Testing, etc.)
|
|
139
|
+
3. Default to OPEN for unknown states
|
|
140
|
+
|
|
141
|
+
Synonym Matching Rules (ticket 1M-164):
|
|
142
|
+
- "Done", "Closed", "Cancelled", "Completed", "Won't Do" → CLOSED
|
|
143
|
+
- Everything else → OPEN
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
linear_state_type: Linear workflow state type string (from state.type field)
|
|
147
|
+
state_name: Linear workflow state name (from state.name field, optional)
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Universal ticket state enum
|
|
151
|
+
|
|
152
|
+
"""
|
|
153
|
+
# First try exact type match
|
|
154
|
+
if linear_state_type in LinearStateMapping.FROM_LINEAR:
|
|
155
|
+
return LinearStateMapping.FROM_LINEAR[linear_state_type]
|
|
156
|
+
|
|
157
|
+
# If no exact match and state_name provided, try synonym matching
|
|
158
|
+
if state_name:
|
|
159
|
+
state_name_lower = state_name.lower().strip()
|
|
160
|
+
|
|
161
|
+
# Check for "done/closed" synonyms - these become CLOSED
|
|
162
|
+
closed_synonyms = [
|
|
163
|
+
"done",
|
|
164
|
+
"closed",
|
|
165
|
+
"cancelled",
|
|
166
|
+
"canceled",
|
|
167
|
+
"completed",
|
|
168
|
+
"won't do",
|
|
169
|
+
"wont do",
|
|
170
|
+
"rejected",
|
|
171
|
+
"resolved",
|
|
172
|
+
"finished",
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
if any(synonym in state_name_lower for synonym in closed_synonyms):
|
|
176
|
+
return (
|
|
177
|
+
TicketState.DONE if state_name_lower == "done" else TicketState.CLOSED
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Check for "in progress" synonyms
|
|
181
|
+
in_progress_synonyms = [
|
|
182
|
+
"in progress",
|
|
183
|
+
"in-progress",
|
|
184
|
+
"working",
|
|
185
|
+
"active",
|
|
186
|
+
"started",
|
|
187
|
+
"doing",
|
|
188
|
+
"in development",
|
|
189
|
+
"in dev",
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
if any(synonym in state_name_lower for synonym in in_progress_synonyms):
|
|
193
|
+
return TicketState.IN_PROGRESS
|
|
194
|
+
|
|
195
|
+
# Check for "review/testing" synonyms
|
|
196
|
+
review_synonyms = [
|
|
197
|
+
"review",
|
|
198
|
+
"in review",
|
|
199
|
+
"in-review",
|
|
200
|
+
"testing",
|
|
201
|
+
"in test",
|
|
202
|
+
"in-test",
|
|
203
|
+
"qa",
|
|
204
|
+
"ready for review",
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
if any(synonym in state_name_lower for synonym in review_synonyms):
|
|
208
|
+
return TicketState.READY
|
|
209
|
+
|
|
210
|
+
# Default: everything else is OPEN (including "ToDo", "Backlog", "To Do", etc.)
|
|
211
|
+
return TicketState.OPEN
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def build_issue_filter(
|
|
215
|
+
state: TicketState | None = None,
|
|
216
|
+
assignee_id: str | None = None,
|
|
217
|
+
priority: Priority | None = None,
|
|
218
|
+
team_id: str | None = None,
|
|
219
|
+
project_id: str | None = None,
|
|
220
|
+
parent_id: str | None = None,
|
|
221
|
+
labels: list[str] | None = None,
|
|
222
|
+
created_after: str | None = None,
|
|
223
|
+
updated_after: str | None = None,
|
|
224
|
+
due_before: str | None = None,
|
|
225
|
+
include_archived: bool = False,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""Build a Linear issue filter from parameters.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
state: Filter by ticket state
|
|
231
|
+
assignee_id: Filter by assignee Linear user ID
|
|
232
|
+
priority: Filter by priority
|
|
233
|
+
team_id: Filter by team ID
|
|
234
|
+
project_id: Filter by project ID
|
|
235
|
+
parent_id: Filter by parent issue ID (for listing sub-issues)
|
|
236
|
+
labels: Filter by label names
|
|
237
|
+
created_after: Filter by creation date (ISO string)
|
|
238
|
+
updated_after: Filter by update date (ISO string)
|
|
239
|
+
due_before: Filter by due date (ISO string)
|
|
240
|
+
include_archived: Whether to include archived issues
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
Linear GraphQL filter object
|
|
244
|
+
|
|
245
|
+
"""
|
|
246
|
+
issue_filter: dict[str, Any] = {}
|
|
247
|
+
|
|
248
|
+
# Team filter (required for most operations)
|
|
249
|
+
if team_id:
|
|
250
|
+
issue_filter["team"] = {"id": {"eq": team_id}}
|
|
251
|
+
|
|
252
|
+
# State filter
|
|
253
|
+
if state:
|
|
254
|
+
state_type = get_linear_state_type(state)
|
|
255
|
+
issue_filter["state"] = {"type": {"eq": state_type}}
|
|
256
|
+
|
|
257
|
+
# Assignee filter
|
|
258
|
+
if assignee_id:
|
|
259
|
+
issue_filter["assignee"] = {"id": {"eq": assignee_id}}
|
|
260
|
+
|
|
261
|
+
# Priority filter
|
|
262
|
+
if priority:
|
|
263
|
+
linear_priority = get_linear_priority(priority)
|
|
264
|
+
issue_filter["priority"] = {"eq": linear_priority}
|
|
265
|
+
|
|
266
|
+
# Project filter
|
|
267
|
+
if project_id:
|
|
268
|
+
issue_filter["project"] = {"id": {"eq": project_id}}
|
|
269
|
+
|
|
270
|
+
# Parent filter (for listing children/sub-issues)
|
|
271
|
+
if parent_id:
|
|
272
|
+
issue_filter["parent"] = {"id": {"eq": parent_id}}
|
|
273
|
+
|
|
274
|
+
# Labels filter
|
|
275
|
+
if labels:
|
|
276
|
+
issue_filter["labels"] = {"some": {"name": {"in": labels}}}
|
|
277
|
+
|
|
278
|
+
# Date filters
|
|
279
|
+
if created_after:
|
|
280
|
+
issue_filter["createdAt"] = {"gte": created_after}
|
|
281
|
+
if updated_after:
|
|
282
|
+
issue_filter["updatedAt"] = {"gte": updated_after}
|
|
283
|
+
if due_before:
|
|
284
|
+
issue_filter["dueDate"] = {"lte": due_before}
|
|
285
|
+
|
|
286
|
+
# Archived filter
|
|
287
|
+
if not include_archived:
|
|
288
|
+
issue_filter["archivedAt"] = {"null": True}
|
|
289
|
+
|
|
290
|
+
return issue_filter
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def build_project_filter(
|
|
294
|
+
state: str | None = None,
|
|
295
|
+
team_id: str | None = None,
|
|
296
|
+
include_completed: bool = True,
|
|
297
|
+
) -> dict[str, Any]:
|
|
298
|
+
"""Build a Linear project filter from parameters.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
state: Filter by project state
|
|
302
|
+
team_id: Filter by team ID
|
|
303
|
+
include_completed: Whether to include completed projects
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Linear GraphQL filter object
|
|
307
|
+
|
|
308
|
+
"""
|
|
309
|
+
project_filter: dict[str, Any] = {}
|
|
310
|
+
|
|
311
|
+
# Team filter
|
|
312
|
+
if team_id:
|
|
313
|
+
project_filter["teams"] = {"some": {"id": {"eq": team_id}}}
|
|
314
|
+
|
|
315
|
+
# State filter
|
|
316
|
+
if state:
|
|
317
|
+
project_filter["state"] = {"eq": state}
|
|
318
|
+
elif not include_completed:
|
|
319
|
+
# Exclude completed projects by default
|
|
320
|
+
project_filter["state"] = {"neq": "completed"}
|
|
321
|
+
|
|
322
|
+
return project_filter
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def extract_linear_metadata(issue_data: dict[str, Any]) -> dict[str, Any]:
|
|
326
|
+
"""Extract Linear-specific metadata from issue data.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
issue_data: Raw Linear issue data from GraphQL
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
Dictionary of Linear-specific metadata
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
metadata = {}
|
|
336
|
+
|
|
337
|
+
# Extract Linear-specific fields
|
|
338
|
+
if "dueDate" in issue_data and issue_data["dueDate"]:
|
|
339
|
+
metadata["due_date"] = issue_data["dueDate"]
|
|
340
|
+
|
|
341
|
+
if "cycle" in issue_data and issue_data["cycle"]:
|
|
342
|
+
metadata["cycle_id"] = issue_data["cycle"]["id"]
|
|
343
|
+
metadata["cycle_name"] = issue_data["cycle"]["name"]
|
|
344
|
+
|
|
345
|
+
if "estimate" in issue_data and issue_data["estimate"]:
|
|
346
|
+
metadata["estimate"] = issue_data["estimate"]
|
|
347
|
+
|
|
348
|
+
if "branchName" in issue_data and issue_data["branchName"]:
|
|
349
|
+
metadata["branch_name"] = issue_data["branchName"]
|
|
350
|
+
|
|
351
|
+
if "url" in issue_data:
|
|
352
|
+
metadata["linear_url"] = issue_data["url"]
|
|
353
|
+
|
|
354
|
+
if "slaBreachesAt" in issue_data and issue_data["slaBreachesAt"]:
|
|
355
|
+
metadata["sla_breaches_at"] = issue_data["slaBreachesAt"]
|
|
356
|
+
|
|
357
|
+
if "customerTicketCount" in issue_data:
|
|
358
|
+
metadata["customer_ticket_count"] = issue_data["customerTicketCount"]
|
|
359
|
+
|
|
360
|
+
return metadata
|