mcp-ticketer 0.3.1__py3-none-any.whl → 0.3.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/adapters/aitrackdown.py +12 -15
- mcp_ticketer/adapters/github.py +7 -4
- mcp_ticketer/adapters/jira.py +23 -22
- mcp_ticketer/adapters/linear/__init__.py +1 -1
- mcp_ticketer/adapters/linear/adapter.py +88 -89
- mcp_ticketer/adapters/linear/client.py +71 -52
- mcp_ticketer/adapters/linear/mappers.py +88 -68
- mcp_ticketer/adapters/linear/queries.py +28 -7
- mcp_ticketer/adapters/linear/types.py +57 -50
- mcp_ticketer/adapters/linear.py +2 -2
- mcp_ticketer/cli/adapter_diagnostics.py +86 -51
- mcp_ticketer/cli/diagnostics.py +165 -72
- mcp_ticketer/cli/linear_commands.py +156 -113
- mcp_ticketer/cli/main.py +153 -82
- mcp_ticketer/cli/simple_health.py +73 -45
- mcp_ticketer/cli/utils.py +15 -10
- mcp_ticketer/core/config.py +23 -19
- mcp_ticketer/core/env_discovery.py +5 -4
- mcp_ticketer/core/env_loader.py +109 -86
- mcp_ticketer/core/exceptions.py +20 -18
- mcp_ticketer/core/models.py +9 -0
- mcp_ticketer/core/project_config.py +1 -1
- mcp_ticketer/mcp/server.py +294 -139
- mcp_ticketer/queue/health_monitor.py +152 -121
- mcp_ticketer/queue/manager.py +11 -4
- mcp_ticketer/queue/queue.py +15 -3
- mcp_ticketer/queue/run_worker.py +1 -1
- mcp_ticketer/queue/ticket_registry.py +190 -132
- mcp_ticketer/queue/worker.py +54 -25
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/METADATA +1 -1
- mcp_ticketer-0.3.2.dist-info/RECORD +59 -0
- mcp_ticketer-0.3.1.dist-info/RECORD +0 -59
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.3.1.dist-info → mcp_ticketer-0.3.2.dist-info}/top_level.txt +0 -0
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import asyncio
|
|
6
5
|
import os
|
|
7
|
-
from datetime import datetime
|
|
8
6
|
from typing import Any, Dict, List, Optional, Union
|
|
9
7
|
|
|
10
8
|
try:
|
|
@@ -15,22 +13,12 @@ except ImportError:
|
|
|
15
13
|
TransportQueryError = Exception
|
|
16
14
|
|
|
17
15
|
from ...core.adapter import BaseAdapter
|
|
18
|
-
from ...core.models import
|
|
19
|
-
Comment,
|
|
20
|
-
Epic,
|
|
21
|
-
Priority,
|
|
22
|
-
SearchQuery,
|
|
23
|
-
Task,
|
|
24
|
-
TicketState,
|
|
25
|
-
TicketType,
|
|
26
|
-
)
|
|
16
|
+
from ...core.models import Comment, Epic, SearchQuery, Task, TicketState
|
|
27
17
|
from ...core.registry import AdapterRegistry
|
|
28
|
-
|
|
29
18
|
from .client import LinearGraphQLClient
|
|
30
19
|
from .mappers import (
|
|
31
20
|
build_linear_issue_input,
|
|
32
21
|
build_linear_issue_update_input,
|
|
33
|
-
extract_child_issue_ids,
|
|
34
22
|
map_linear_comment_to_comment,
|
|
35
23
|
map_linear_issue_to_task,
|
|
36
24
|
map_linear_project_to_epic,
|
|
@@ -38,7 +26,6 @@ from .mappers import (
|
|
|
38
26
|
from .queries import (
|
|
39
27
|
ALL_FRAGMENTS,
|
|
40
28
|
CREATE_ISSUE_MUTATION,
|
|
41
|
-
GET_CURRENT_USER_QUERY,
|
|
42
29
|
LIST_ISSUES_QUERY,
|
|
43
30
|
SEARCH_ISSUES_QUERY,
|
|
44
31
|
UPDATE_ISSUE_MUTATION,
|
|
@@ -49,22 +36,21 @@ from .types import (
|
|
|
49
36
|
build_issue_filter,
|
|
50
37
|
get_linear_priority,
|
|
51
38
|
get_linear_state_type,
|
|
52
|
-
get_universal_state,
|
|
53
39
|
)
|
|
54
40
|
|
|
55
41
|
|
|
56
42
|
class LinearAdapter(BaseAdapter[Task]):
|
|
57
43
|
"""Adapter for Linear issue tracking system using native GraphQL API.
|
|
58
|
-
|
|
44
|
+
|
|
59
45
|
This adapter provides comprehensive integration with Linear's GraphQL API,
|
|
60
46
|
supporting all major ticket management operations including:
|
|
61
|
-
|
|
47
|
+
|
|
62
48
|
- CRUD operations for issues and projects
|
|
63
49
|
- State transitions and workflow management
|
|
64
50
|
- User assignment and search functionality
|
|
65
51
|
- Comment management
|
|
66
52
|
- Epic/Issue/Task hierarchy support
|
|
67
|
-
|
|
53
|
+
|
|
68
54
|
The adapter is organized into multiple modules for better maintainability:
|
|
69
55
|
- client.py: GraphQL client management
|
|
70
56
|
- queries.py: GraphQL queries and fragments
|
|
@@ -85,6 +71,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
85
71
|
|
|
86
72
|
Raises:
|
|
87
73
|
ValueError: If required configuration is missing
|
|
74
|
+
|
|
88
75
|
"""
|
|
89
76
|
# Initialize instance variables before calling super().__init__
|
|
90
77
|
# because parent constructor calls _get_state_mapping()
|
|
@@ -95,79 +82,83 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
95
82
|
self._initialized = False
|
|
96
83
|
|
|
97
84
|
super().__init__(config)
|
|
98
|
-
|
|
85
|
+
|
|
99
86
|
# Extract configuration
|
|
100
87
|
self.api_key = config.get("api_key") or os.getenv("LINEAR_API_KEY")
|
|
101
88
|
if not self.api_key:
|
|
102
|
-
raise ValueError(
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
89
|
+
raise ValueError(
|
|
90
|
+
"Linear API key is required (api_key or LINEAR_API_KEY env var)"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Clean API key - remove Bearer prefix if accidentally included in config
|
|
94
|
+
# (The client will add it back when making requests)
|
|
95
|
+
if self.api_key.startswith("Bearer "):
|
|
96
|
+
self.api_key = self.api_key.replace("Bearer ", "")
|
|
97
|
+
|
|
108
98
|
self.workspace = config.get("workspace", "")
|
|
109
99
|
self.team_key = config.get("team_key")
|
|
110
100
|
self.team_id = config.get("team_id")
|
|
111
101
|
self.api_url = config.get("api_url", "https://api.linear.app/graphql")
|
|
112
|
-
|
|
102
|
+
|
|
113
103
|
# Validate team configuration
|
|
114
104
|
if not self.team_key and not self.team_id:
|
|
115
105
|
raise ValueError("Either team_key or team_id must be provided")
|
|
116
|
-
|
|
117
|
-
# Initialize client
|
|
118
|
-
|
|
119
|
-
self.client = LinearGraphQLClient(api_key_clean)
|
|
106
|
+
|
|
107
|
+
# Initialize client with clean API key
|
|
108
|
+
self.client = LinearGraphQLClient(self.api_key)
|
|
120
109
|
|
|
121
110
|
def validate_credentials(self) -> tuple[bool, str]:
|
|
122
111
|
"""Validate Linear API credentials.
|
|
123
|
-
|
|
112
|
+
|
|
124
113
|
Returns:
|
|
125
114
|
Tuple of (is_valid, error_message)
|
|
115
|
+
|
|
126
116
|
"""
|
|
127
117
|
if not self.api_key:
|
|
128
118
|
return False, "Linear API key is required"
|
|
129
|
-
|
|
119
|
+
|
|
130
120
|
if not self.team_key and not self.team_id:
|
|
131
121
|
return False, "Either team_key or team_id must be provided"
|
|
132
|
-
|
|
122
|
+
|
|
133
123
|
return True, ""
|
|
134
124
|
|
|
135
125
|
async def initialize(self) -> None:
|
|
136
126
|
"""Initialize adapter by preloading team, states, and labels data concurrently."""
|
|
137
127
|
if self._initialized:
|
|
138
128
|
return
|
|
139
|
-
|
|
129
|
+
|
|
140
130
|
try:
|
|
141
131
|
# Test connection first
|
|
142
132
|
if not await self.client.test_connection():
|
|
143
133
|
raise ValueError("Failed to connect to Linear API - check credentials")
|
|
144
|
-
|
|
134
|
+
|
|
145
135
|
# Load team data and workflow states concurrently
|
|
146
136
|
team_id = await self._ensure_team_id()
|
|
147
|
-
|
|
137
|
+
|
|
148
138
|
# Load workflow states for the team
|
|
149
139
|
await self._load_workflow_states(team_id)
|
|
150
|
-
|
|
140
|
+
|
|
151
141
|
self._initialized = True
|
|
152
|
-
|
|
142
|
+
|
|
153
143
|
except Exception as e:
|
|
154
144
|
raise ValueError(f"Failed to initialize Linear adapter: {e}")
|
|
155
145
|
|
|
156
146
|
async def _ensure_team_id(self) -> str:
|
|
157
147
|
"""Ensure we have a team ID, resolving from team_key if needed.
|
|
158
|
-
|
|
148
|
+
|
|
159
149
|
Returns:
|
|
160
150
|
Linear team UUID
|
|
161
|
-
|
|
151
|
+
|
|
162
152
|
Raises:
|
|
163
153
|
ValueError: If team cannot be found or resolved
|
|
154
|
+
|
|
164
155
|
"""
|
|
165
156
|
if self.team_id:
|
|
166
157
|
return self.team_id
|
|
167
|
-
|
|
158
|
+
|
|
168
159
|
if not self.team_key:
|
|
169
160
|
raise ValueError("Either team_id or team_key must be provided")
|
|
170
|
-
|
|
161
|
+
|
|
171
162
|
# Query team by key
|
|
172
163
|
query = """
|
|
173
164
|
query GetTeamByKey($key: String!) {
|
|
@@ -181,35 +172,35 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
181
172
|
}
|
|
182
173
|
}
|
|
183
174
|
"""
|
|
184
|
-
|
|
175
|
+
|
|
185
176
|
try:
|
|
186
177
|
result = await self.client.execute_query(query, {"key": self.team_key})
|
|
187
178
|
teams = result.get("teams", {}).get("nodes", [])
|
|
188
|
-
|
|
179
|
+
|
|
189
180
|
if not teams:
|
|
190
181
|
raise ValueError(f"Team with key '{self.team_key}' not found")
|
|
191
|
-
|
|
182
|
+
|
|
192
183
|
team = teams[0]
|
|
193
184
|
self.team_id = team["id"]
|
|
194
185
|
self._team_data = team
|
|
195
|
-
|
|
186
|
+
|
|
196
187
|
return self.team_id
|
|
197
|
-
|
|
188
|
+
|
|
198
189
|
except Exception as e:
|
|
199
190
|
raise ValueError(f"Failed to resolve team '{self.team_key}': {e}")
|
|
200
191
|
|
|
201
192
|
async def _load_workflow_states(self, team_id: str) -> None:
|
|
202
193
|
"""Load and cache workflow states for the team.
|
|
203
|
-
|
|
194
|
+
|
|
204
195
|
Args:
|
|
205
196
|
team_id: Linear team ID
|
|
197
|
+
|
|
206
198
|
"""
|
|
207
199
|
try:
|
|
208
200
|
result = await self.client.execute_query(
|
|
209
|
-
WORKFLOW_STATES_QUERY,
|
|
210
|
-
{"teamId": team_id}
|
|
201
|
+
WORKFLOW_STATES_QUERY, {"teamId": team_id}
|
|
211
202
|
)
|
|
212
|
-
|
|
203
|
+
|
|
213
204
|
workflow_states = {}
|
|
214
205
|
for state in result["workflowStates"]["nodes"]:
|
|
215
206
|
state_type = state["type"].lower()
|
|
@@ -217,23 +208,24 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
217
208
|
workflow_states[state_type] = state
|
|
218
209
|
elif state["position"] < workflow_states[state_type]["position"]:
|
|
219
210
|
workflow_states[state_type] = state
|
|
220
|
-
|
|
211
|
+
|
|
221
212
|
self._workflow_states = workflow_states
|
|
222
|
-
|
|
213
|
+
|
|
223
214
|
except Exception as e:
|
|
224
215
|
raise ValueError(f"Failed to load workflow states: {e}")
|
|
225
216
|
|
|
226
217
|
def _get_state_mapping(self) -> Dict[TicketState, str]:
|
|
227
218
|
"""Get mapping from universal states to Linear workflow state IDs.
|
|
228
|
-
|
|
219
|
+
|
|
229
220
|
Returns:
|
|
230
221
|
Dictionary mapping TicketState to Linear state ID
|
|
222
|
+
|
|
231
223
|
"""
|
|
232
224
|
if not self._workflow_states:
|
|
233
225
|
# Return type-based mapping if states not loaded
|
|
234
226
|
return {
|
|
235
227
|
TicketState.OPEN: "unstarted",
|
|
236
|
-
TicketState.IN_PROGRESS: "started",
|
|
228
|
+
TicketState.IN_PROGRESS: "started",
|
|
237
229
|
TicketState.READY: "unstarted",
|
|
238
230
|
TicketState.TESTED: "started",
|
|
239
231
|
TicketState.DONE: "completed",
|
|
@@ -241,7 +233,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
241
233
|
TicketState.WAITING: "unstarted",
|
|
242
234
|
TicketState.BLOCKED: "unstarted",
|
|
243
235
|
}
|
|
244
|
-
|
|
236
|
+
|
|
245
237
|
# Return ID-based mapping using cached workflow states
|
|
246
238
|
mapping = {}
|
|
247
239
|
for universal_state, linear_type in LinearStateMapping.TO_LINEAR.items():
|
|
@@ -250,23 +242,24 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
250
242
|
else:
|
|
251
243
|
# Fallback to type name
|
|
252
244
|
mapping[universal_state] = linear_type
|
|
253
|
-
|
|
245
|
+
|
|
254
246
|
return mapping
|
|
255
247
|
|
|
256
248
|
async def _get_user_id(self, user_identifier: str) -> Optional[str]:
|
|
257
249
|
"""Get Linear user ID from email or display name.
|
|
258
|
-
|
|
250
|
+
|
|
259
251
|
Args:
|
|
260
252
|
user_identifier: Email address or display name
|
|
261
|
-
|
|
253
|
+
|
|
262
254
|
Returns:
|
|
263
255
|
Linear user ID or None if not found
|
|
256
|
+
|
|
264
257
|
"""
|
|
265
258
|
# Try to get user by email first
|
|
266
259
|
user = await self.client.get_user_by_email(user_identifier)
|
|
267
260
|
if user:
|
|
268
261
|
return user["id"]
|
|
269
|
-
|
|
262
|
+
|
|
270
263
|
# If not found by email, could implement search by display name
|
|
271
264
|
# For now, assume the identifier is already a user ID
|
|
272
265
|
return user_identifier if user_identifier else None
|
|
@@ -284,6 +277,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
284
277
|
|
|
285
278
|
Raises:
|
|
286
279
|
ValueError: If credentials are invalid or creation fails
|
|
280
|
+
|
|
287
281
|
"""
|
|
288
282
|
# Validate credentials before attempting operation
|
|
289
283
|
is_valid, error_message = self.validate_credentials()
|
|
@@ -308,6 +302,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
308
302
|
|
|
309
303
|
Returns:
|
|
310
304
|
Created task with Linear metadata
|
|
305
|
+
|
|
311
306
|
"""
|
|
312
307
|
team_id = await self._ensure_team_id()
|
|
313
308
|
|
|
@@ -322,8 +317,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
322
317
|
|
|
323
318
|
try:
|
|
324
319
|
result = await self.client.execute_mutation(
|
|
325
|
-
CREATE_ISSUE_MUTATION,
|
|
326
|
-
{"input": issue_input}
|
|
320
|
+
CREATE_ISSUE_MUTATION, {"input": issue_input}
|
|
327
321
|
)
|
|
328
322
|
|
|
329
323
|
if not result["issueCreate"]["success"]:
|
|
@@ -343,6 +337,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
343
337
|
|
|
344
338
|
Returns:
|
|
345
339
|
Created epic with Linear metadata
|
|
340
|
+
|
|
346
341
|
"""
|
|
347
342
|
team_id = await self._ensure_team_id()
|
|
348
343
|
|
|
@@ -387,8 +382,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
387
382
|
|
|
388
383
|
try:
|
|
389
384
|
result = await self.client.execute_mutation(
|
|
390
|
-
create_query,
|
|
391
|
-
{"input": project_input}
|
|
385
|
+
create_query, {"input": project_input}
|
|
392
386
|
)
|
|
393
387
|
|
|
394
388
|
if not result["projectCreate"]["success"]:
|
|
@@ -408,25 +402,26 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
408
402
|
|
|
409
403
|
Returns:
|
|
410
404
|
Task with full details or None if not found
|
|
405
|
+
|
|
411
406
|
"""
|
|
412
407
|
# Validate credentials before attempting operation
|
|
413
408
|
is_valid, error_message = self.validate_credentials()
|
|
414
409
|
if not is_valid:
|
|
415
410
|
raise ValueError(error_message)
|
|
416
411
|
|
|
417
|
-
query =
|
|
412
|
+
query = (
|
|
413
|
+
ALL_FRAGMENTS
|
|
414
|
+
+ """
|
|
418
415
|
query GetIssue($identifier: String!) {
|
|
419
416
|
issue(id: $identifier) {
|
|
420
417
|
...IssueFullFields
|
|
421
418
|
}
|
|
422
419
|
}
|
|
423
420
|
"""
|
|
421
|
+
)
|
|
424
422
|
|
|
425
423
|
try:
|
|
426
|
-
result = await self.client.execute_query(
|
|
427
|
-
query,
|
|
428
|
-
{"identifier": ticket_id}
|
|
429
|
-
)
|
|
424
|
+
result = await self.client.execute_query(query, {"identifier": ticket_id})
|
|
430
425
|
|
|
431
426
|
if result.get("issue"):
|
|
432
427
|
return map_linear_issue_to_task(result["issue"])
|
|
@@ -446,6 +441,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
446
441
|
|
|
447
442
|
Returns:
|
|
448
443
|
Updated task or None if not found
|
|
444
|
+
|
|
449
445
|
"""
|
|
450
446
|
# Validate credentials before attempting operation
|
|
451
447
|
is_valid, error_message = self.validate_credentials()
|
|
@@ -463,8 +459,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
463
459
|
|
|
464
460
|
try:
|
|
465
461
|
result = await self.client.execute_query(
|
|
466
|
-
id_query,
|
|
467
|
-
{"identifier": ticket_id}
|
|
462
|
+
id_query, {"identifier": ticket_id}
|
|
468
463
|
)
|
|
469
464
|
|
|
470
465
|
if not result.get("issue"):
|
|
@@ -477,7 +472,11 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
477
472
|
|
|
478
473
|
# Handle state transitions
|
|
479
474
|
if "state" in updates:
|
|
480
|
-
target_state =
|
|
475
|
+
target_state = (
|
|
476
|
+
TicketState(updates["state"])
|
|
477
|
+
if isinstance(updates["state"], str)
|
|
478
|
+
else updates["state"]
|
|
479
|
+
)
|
|
481
480
|
state_mapping = self._get_state_mapping()
|
|
482
481
|
if target_state in state_mapping:
|
|
483
482
|
update_input["stateId"] = state_mapping[target_state]
|
|
@@ -490,8 +489,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
490
489
|
|
|
491
490
|
# Execute update
|
|
492
491
|
result = await self.client.execute_mutation(
|
|
493
|
-
UPDATE_ISSUE_MUTATION,
|
|
494
|
-
{"id": linear_id, "input": update_input}
|
|
492
|
+
UPDATE_ISSUE_MUTATION, {"id": linear_id, "input": update_input}
|
|
495
493
|
)
|
|
496
494
|
|
|
497
495
|
if not result["issueUpdate"]["success"]:
|
|
@@ -511,6 +509,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
511
509
|
|
|
512
510
|
Returns:
|
|
513
511
|
True if successfully deleted/archived
|
|
512
|
+
|
|
514
513
|
"""
|
|
515
514
|
# Linear doesn't support true deletion, so we archive the issue
|
|
516
515
|
try:
|
|
@@ -520,10 +519,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
520
519
|
return False
|
|
521
520
|
|
|
522
521
|
async def list(
|
|
523
|
-
self,
|
|
524
|
-
limit: int = 10,
|
|
525
|
-
offset: int = 0,
|
|
526
|
-
filters: Optional[Dict[str, Any]] = None
|
|
522
|
+
self, limit: int = 10, offset: int = 0, filters: Optional[Dict[str, Any]] = None
|
|
527
523
|
) -> List[Task]:
|
|
528
524
|
"""List Linear issues with optional filtering.
|
|
529
525
|
|
|
@@ -534,6 +530,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
534
530
|
|
|
535
531
|
Returns:
|
|
536
532
|
List of tasks matching the criteria
|
|
533
|
+
|
|
537
534
|
"""
|
|
538
535
|
# Validate credentials
|
|
539
536
|
is_valid, error_message = self.validate_credentials()
|
|
@@ -548,7 +545,9 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
548
545
|
team_id=team_id,
|
|
549
546
|
state=filters.get("state") if filters else None,
|
|
550
547
|
priority=filters.get("priority") if filters else None,
|
|
551
|
-
include_archived=
|
|
548
|
+
include_archived=(
|
|
549
|
+
filters.get("includeArchived", False) if filters else False
|
|
550
|
+
),
|
|
552
551
|
)
|
|
553
552
|
|
|
554
553
|
# Add additional filters
|
|
@@ -567,8 +566,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
567
566
|
|
|
568
567
|
try:
|
|
569
568
|
result = await self.client.execute_query(
|
|
570
|
-
LIST_ISSUES_QUERY,
|
|
571
|
-
{"filter": issue_filter, "first": limit}
|
|
569
|
+
LIST_ISSUES_QUERY, {"filter": issue_filter, "first": limit}
|
|
572
570
|
)
|
|
573
571
|
|
|
574
572
|
tasks = []
|
|
@@ -588,6 +586,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
588
586
|
|
|
589
587
|
Returns:
|
|
590
588
|
List of tasks matching the search criteria
|
|
589
|
+
|
|
591
590
|
"""
|
|
592
591
|
# Validate credentials
|
|
593
592
|
is_valid, error_message = self.validate_credentials()
|
|
@@ -631,8 +630,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
631
630
|
|
|
632
631
|
try:
|
|
633
632
|
result = await self.client.execute_query(
|
|
634
|
-
SEARCH_ISSUES_QUERY,
|
|
635
|
-
{"filter": issue_filter, "first": query.limit}
|
|
633
|
+
SEARCH_ISSUES_QUERY, {"filter": issue_filter, "first": query.limit}
|
|
636
634
|
)
|
|
637
635
|
|
|
638
636
|
tasks = []
|
|
@@ -655,6 +653,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
655
653
|
|
|
656
654
|
Returns:
|
|
657
655
|
Updated task or None if transition failed
|
|
656
|
+
|
|
658
657
|
"""
|
|
659
658
|
# Validate transition
|
|
660
659
|
if not await self.validate_transition(ticket_id, target_state):
|
|
@@ -674,6 +673,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
674
673
|
|
|
675
674
|
Returns:
|
|
676
675
|
True if transition is valid
|
|
676
|
+
|
|
677
677
|
"""
|
|
678
678
|
# For now, allow all transitions
|
|
679
679
|
# In practice, you might want to implement Linear's workflow rules
|
|
@@ -687,6 +687,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
687
687
|
|
|
688
688
|
Returns:
|
|
689
689
|
Created comment with ID
|
|
690
|
+
|
|
690
691
|
"""
|
|
691
692
|
# First get the Linear internal ID
|
|
692
693
|
id_query = """
|
|
@@ -699,8 +700,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
699
700
|
|
|
700
701
|
try:
|
|
701
702
|
result = await self.client.execute_query(
|
|
702
|
-
id_query,
|
|
703
|
-
{"identifier": comment.ticket_id}
|
|
703
|
+
id_query, {"identifier": comment.ticket_id}
|
|
704
704
|
)
|
|
705
705
|
|
|
706
706
|
if not result.get("issue"):
|
|
@@ -735,8 +735,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
735
735
|
}
|
|
736
736
|
|
|
737
737
|
result = await self.client.execute_mutation(
|
|
738
|
-
create_comment_query,
|
|
739
|
-
{"input": comment_input}
|
|
738
|
+
create_comment_query, {"input": comment_input}
|
|
740
739
|
)
|
|
741
740
|
|
|
742
741
|
if not result["commentCreate"]["success"]:
|
|
@@ -760,6 +759,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
760
759
|
|
|
761
760
|
Returns:
|
|
762
761
|
List of comments for the issue
|
|
762
|
+
|
|
763
763
|
"""
|
|
764
764
|
query = """
|
|
765
765
|
query GetIssueComments($identifier: String!, $first: Int!) {
|
|
@@ -788,8 +788,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
788
788
|
|
|
789
789
|
try:
|
|
790
790
|
result = await self.client.execute_query(
|
|
791
|
-
query,
|
|
792
|
-
{"identifier": ticket_id, "first": limit}
|
|
791
|
+
query, {"identifier": ticket_id, "first": limit}
|
|
793
792
|
)
|
|
794
793
|
|
|
795
794
|
if not result.get("issue"):
|