mcp-ticketer 0.1.21__py3-none-any.whl → 0.1.23__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 +7 -7
- mcp_ticketer/__version__.py +4 -2
- mcp_ticketer/adapters/__init__.py +4 -4
- mcp_ticketer/adapters/aitrackdown.py +66 -49
- mcp_ticketer/adapters/github.py +192 -125
- mcp_ticketer/adapters/hybrid.py +99 -53
- mcp_ticketer/adapters/jira.py +161 -151
- mcp_ticketer/adapters/linear.py +396 -246
- mcp_ticketer/cache/__init__.py +1 -1
- mcp_ticketer/cache/memory.py +15 -16
- mcp_ticketer/cli/__init__.py +1 -1
- mcp_ticketer/cli/configure.py +69 -93
- mcp_ticketer/cli/discover.py +43 -35
- mcp_ticketer/cli/main.py +283 -298
- mcp_ticketer/cli/mcp_configure.py +39 -15
- mcp_ticketer/cli/migrate_config.py +11 -13
- mcp_ticketer/cli/queue_commands.py +21 -58
- mcp_ticketer/cli/utils.py +121 -66
- mcp_ticketer/core/__init__.py +2 -2
- mcp_ticketer/core/adapter.py +46 -39
- mcp_ticketer/core/config.py +128 -92
- mcp_ticketer/core/env_discovery.py +69 -37
- mcp_ticketer/core/http_client.py +57 -40
- mcp_ticketer/core/mappers.py +98 -54
- mcp_ticketer/core/models.py +38 -24
- mcp_ticketer/core/project_config.py +145 -80
- mcp_ticketer/core/registry.py +16 -16
- mcp_ticketer/mcp/__init__.py +1 -1
- mcp_ticketer/mcp/server.py +199 -145
- mcp_ticketer/queue/__init__.py +2 -2
- mcp_ticketer/queue/__main__.py +1 -1
- mcp_ticketer/queue/manager.py +30 -26
- mcp_ticketer/queue/queue.py +147 -85
- mcp_ticketer/queue/run_worker.py +2 -3
- mcp_ticketer/queue/worker.py +55 -40
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/METADATA +1 -1
- mcp_ticketer-0.1.23.dist-info/RECORD +42 -0
- mcp_ticketer-0.1.21.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.21.dist-info → mcp_ticketer-0.1.23.dist-info}/top_level.txt +0 -0
mcp_ticketer/adapters/linear.py
CHANGED
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
"""Linear adapter implementation using native GraphQL API with full feature support."""
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
import asyncio
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import builtins
|
|
5
|
+
import os
|
|
6
|
+
from datetime import date, datetime
|
|
7
7
|
from enum import Enum
|
|
8
|
+
from typing import Any, Optional, Union
|
|
8
9
|
|
|
9
|
-
from gql import
|
|
10
|
-
from gql.transport.httpx import HTTPXAsyncTransport
|
|
10
|
+
from gql import Client, gql
|
|
11
11
|
from gql.transport.exceptions import TransportQueryError
|
|
12
|
-
import
|
|
12
|
+
from gql.transport.httpx import HTTPXAsyncTransport
|
|
13
13
|
|
|
14
14
|
from ..core.adapter import BaseAdapter
|
|
15
|
-
from ..core.models import
|
|
15
|
+
from ..core.models import (
|
|
16
|
+
Comment,
|
|
17
|
+
Epic,
|
|
18
|
+
Priority,
|
|
19
|
+
SearchQuery,
|
|
20
|
+
Task,
|
|
21
|
+
TicketState,
|
|
22
|
+
TicketType,
|
|
23
|
+
)
|
|
16
24
|
from ..core.registry import AdapterRegistry
|
|
17
25
|
|
|
18
26
|
|
|
19
27
|
class LinearStateType(str, Enum):
|
|
20
28
|
"""Linear workflow state types."""
|
|
29
|
+
|
|
21
30
|
BACKLOG = "backlog"
|
|
22
31
|
UNSTARTED = "unstarted"
|
|
23
32
|
STARTED = "started"
|
|
@@ -38,9 +47,9 @@ class LinearPriorityMapping:
|
|
|
38
47
|
FROM_LINEAR = {
|
|
39
48
|
0: Priority.CRITICAL, # Urgent
|
|
40
49
|
1: Priority.CRITICAL, # High
|
|
41
|
-
2: Priority.HIGH,
|
|
42
|
-
3: Priority.MEDIUM,
|
|
43
|
-
4: Priority.LOW,
|
|
50
|
+
2: Priority.HIGH, # Medium
|
|
51
|
+
3: Priority.MEDIUM, # Low
|
|
52
|
+
4: Priority.LOW, # No priority
|
|
44
53
|
}
|
|
45
54
|
|
|
46
55
|
|
|
@@ -242,35 +251,35 @@ ISSUE_FULL_FRAGMENT = """
|
|
|
242
251
|
|
|
243
252
|
# Combine all fragments
|
|
244
253
|
ALL_FRAGMENTS = (
|
|
245
|
-
USER_FRAGMENT
|
|
246
|
-
WORKFLOW_STATE_FRAGMENT
|
|
247
|
-
TEAM_FRAGMENT
|
|
248
|
-
CYCLE_FRAGMENT
|
|
249
|
-
PROJECT_FRAGMENT
|
|
250
|
-
LABEL_FRAGMENT
|
|
251
|
-
ATTACHMENT_FRAGMENT
|
|
252
|
-
COMMENT_FRAGMENT
|
|
253
|
-
ISSUE_COMPACT_FRAGMENT
|
|
254
|
-
ISSUE_FULL_FRAGMENT
|
|
254
|
+
USER_FRAGMENT
|
|
255
|
+
+ WORKFLOW_STATE_FRAGMENT
|
|
256
|
+
+ TEAM_FRAGMENT
|
|
257
|
+
+ CYCLE_FRAGMENT
|
|
258
|
+
+ PROJECT_FRAGMENT
|
|
259
|
+
+ LABEL_FRAGMENT
|
|
260
|
+
+ ATTACHMENT_FRAGMENT
|
|
261
|
+
+ COMMENT_FRAGMENT
|
|
262
|
+
+ ISSUE_COMPACT_FRAGMENT
|
|
263
|
+
+ ISSUE_FULL_FRAGMENT
|
|
255
264
|
)
|
|
256
265
|
|
|
257
266
|
# Fragments needed for issue list/search (without comments)
|
|
258
267
|
ISSUE_LIST_FRAGMENTS = (
|
|
259
|
-
USER_FRAGMENT
|
|
260
|
-
WORKFLOW_STATE_FRAGMENT
|
|
261
|
-
TEAM_FRAGMENT
|
|
262
|
-
CYCLE_FRAGMENT
|
|
263
|
-
PROJECT_FRAGMENT
|
|
264
|
-
LABEL_FRAGMENT
|
|
265
|
-
ATTACHMENT_FRAGMENT
|
|
266
|
-
ISSUE_COMPACT_FRAGMENT
|
|
268
|
+
USER_FRAGMENT
|
|
269
|
+
+ WORKFLOW_STATE_FRAGMENT
|
|
270
|
+
+ TEAM_FRAGMENT
|
|
271
|
+
+ CYCLE_FRAGMENT
|
|
272
|
+
+ PROJECT_FRAGMENT
|
|
273
|
+
+ LABEL_FRAGMENT
|
|
274
|
+
+ ATTACHMENT_FRAGMENT
|
|
275
|
+
+ ISSUE_COMPACT_FRAGMENT
|
|
267
276
|
)
|
|
268
277
|
|
|
269
278
|
|
|
270
279
|
class LinearAdapter(BaseAdapter[Task]):
|
|
271
280
|
"""Adapter for Linear issue tracking system using native GraphQL API."""
|
|
272
281
|
|
|
273
|
-
def __init__(self, config:
|
|
282
|
+
def __init__(self, config: dict[str, Any]):
|
|
274
283
|
"""Initialize Linear adapter.
|
|
275
284
|
|
|
276
285
|
Args:
|
|
@@ -282,13 +291,16 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
282
291
|
- api_url: Optional Linear API URL
|
|
283
292
|
|
|
284
293
|
Note: Either team_key or team_id is required. If both are provided, team_id takes precedence.
|
|
294
|
+
|
|
285
295
|
"""
|
|
286
296
|
super().__init__(config)
|
|
287
297
|
|
|
288
298
|
# Get API key from config or environment
|
|
289
299
|
self.api_key = config.get("api_key") or os.getenv("LINEAR_API_KEY")
|
|
290
300
|
if not self.api_key:
|
|
291
|
-
raise ValueError(
|
|
301
|
+
raise ValueError(
|
|
302
|
+
"Linear API key required (config.api_key or LINEAR_API_KEY env var)"
|
|
303
|
+
)
|
|
292
304
|
|
|
293
305
|
self.workspace = config.get("workspace") # Optional, for documentation
|
|
294
306
|
|
|
@@ -304,9 +316,9 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
304
316
|
|
|
305
317
|
# Caches for frequently used data
|
|
306
318
|
self._team_id: Optional[str] = None
|
|
307
|
-
self._workflow_states: Optional[
|
|
308
|
-
self._labels: Optional[
|
|
309
|
-
self._users: Optional[
|
|
319
|
+
self._workflow_states: Optional[dict[str, dict[str, Any]]] = None
|
|
320
|
+
self._labels: Optional[dict[str, str]] = None # name -> id
|
|
321
|
+
self._users: Optional[dict[str, str]] = None # email -> id
|
|
310
322
|
|
|
311
323
|
# Initialize state mapping
|
|
312
324
|
self._state_mapping = self._get_state_mapping()
|
|
@@ -323,6 +335,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
323
335
|
|
|
324
336
|
Returns:
|
|
325
337
|
Client: Fresh GraphQL client instance
|
|
338
|
+
|
|
326
339
|
"""
|
|
327
340
|
transport = HTTPXAsyncTransport(
|
|
328
341
|
url=self.api_url,
|
|
@@ -372,7 +385,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
372
385
|
# If team_id (UUID) is provided, use it directly (preferred)
|
|
373
386
|
if self.team_id_config:
|
|
374
387
|
# Validate that this team ID exists
|
|
375
|
-
query = gql(
|
|
388
|
+
query = gql(
|
|
389
|
+
"""
|
|
376
390
|
query GetTeamById($id: String!) {
|
|
377
391
|
team(id: $id) {
|
|
378
392
|
id
|
|
@@ -380,11 +394,14 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
380
394
|
key
|
|
381
395
|
}
|
|
382
396
|
}
|
|
383
|
-
"""
|
|
397
|
+
"""
|
|
398
|
+
)
|
|
384
399
|
|
|
385
400
|
client = self._create_client()
|
|
386
401
|
async with client as session:
|
|
387
|
-
result = await session.execute(
|
|
402
|
+
result = await session.execute(
|
|
403
|
+
query, variable_values={"id": self.team_id_config}
|
|
404
|
+
)
|
|
388
405
|
|
|
389
406
|
if not result.get("team"):
|
|
390
407
|
raise ValueError(f"Team with ID '{self.team_id_config}' not found")
|
|
@@ -392,7 +409,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
392
409
|
return result["team"]["id"]
|
|
393
410
|
|
|
394
411
|
# Otherwise, fetch team ID by key
|
|
395
|
-
query = gql(
|
|
412
|
+
query = gql(
|
|
413
|
+
"""
|
|
396
414
|
query GetTeamByKey($key: String!) {
|
|
397
415
|
teams(filter: { key: { eq: $key } }) {
|
|
398
416
|
nodes {
|
|
@@ -402,20 +420,26 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
402
420
|
}
|
|
403
421
|
}
|
|
404
422
|
}
|
|
405
|
-
"""
|
|
423
|
+
"""
|
|
424
|
+
)
|
|
406
425
|
|
|
407
426
|
client = self._create_client()
|
|
408
427
|
async with client as session:
|
|
409
|
-
result = await session.execute(
|
|
428
|
+
result = await session.execute(
|
|
429
|
+
query, variable_values={"key": self.team_key}
|
|
430
|
+
)
|
|
410
431
|
|
|
411
432
|
if not result["teams"]["nodes"]:
|
|
412
433
|
raise ValueError(f"Team with key '{self.team_key}' not found")
|
|
413
434
|
|
|
414
435
|
return result["teams"]["nodes"][0]["id"]
|
|
415
436
|
|
|
416
|
-
async def _fetch_workflow_states_data(
|
|
437
|
+
async def _fetch_workflow_states_data(
|
|
438
|
+
self, team_id: str
|
|
439
|
+
) -> dict[str, dict[str, Any]]:
|
|
417
440
|
"""Fetch workflow states data."""
|
|
418
|
-
query = gql(
|
|
441
|
+
query = gql(
|
|
442
|
+
"""
|
|
419
443
|
query WorkflowStates($teamId: ID!) {
|
|
420
444
|
workflowStates(filter: { team: { id: { eq: $teamId } } }) {
|
|
421
445
|
nodes {
|
|
@@ -427,7 +451,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
427
451
|
}
|
|
428
452
|
}
|
|
429
453
|
}
|
|
430
|
-
"""
|
|
454
|
+
"""
|
|
455
|
+
)
|
|
431
456
|
|
|
432
457
|
client = self._create_client()
|
|
433
458
|
async with client as session:
|
|
@@ -443,9 +468,10 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
443
468
|
|
|
444
469
|
return workflow_states
|
|
445
470
|
|
|
446
|
-
async def _fetch_labels_data(self, team_id: str) ->
|
|
471
|
+
async def _fetch_labels_data(self, team_id: str) -> dict[str, str]:
|
|
447
472
|
"""Fetch labels data."""
|
|
448
|
-
query = gql(
|
|
473
|
+
query = gql(
|
|
474
|
+
"""
|
|
449
475
|
query GetLabels($teamId: ID!) {
|
|
450
476
|
issueLabels(filter: { team: { id: { eq: $teamId } } }) {
|
|
451
477
|
nodes {
|
|
@@ -454,7 +480,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
454
480
|
}
|
|
455
481
|
}
|
|
456
482
|
}
|
|
457
|
-
"""
|
|
483
|
+
"""
|
|
484
|
+
)
|
|
458
485
|
|
|
459
486
|
client = self._create_client()
|
|
460
487
|
async with client as session:
|
|
@@ -472,7 +499,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
472
499
|
await self._ensure_initialized()
|
|
473
500
|
return self._team_id
|
|
474
501
|
|
|
475
|
-
async def _get_workflow_states(self) ->
|
|
502
|
+
async def _get_workflow_states(self) -> dict[str, dict[str, Any]]:
|
|
476
503
|
"""Get cached workflow states from Linear."""
|
|
477
504
|
await self._ensure_initialized()
|
|
478
505
|
return self._workflow_states
|
|
@@ -487,7 +514,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
487
514
|
|
|
488
515
|
# Try to find existing label (may have been added since initialization)
|
|
489
516
|
team_id = self._team_id
|
|
490
|
-
search_query = gql(
|
|
517
|
+
search_query = gql(
|
|
518
|
+
"""
|
|
491
519
|
query GetLabel($name: String!, $teamId: ID!) {
|
|
492
520
|
issueLabels(filter: { name: { eq: $name }, team: { id: { eq: $teamId } } }) {
|
|
493
521
|
nodes {
|
|
@@ -496,13 +524,13 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
496
524
|
}
|
|
497
525
|
}
|
|
498
526
|
}
|
|
499
|
-
"""
|
|
527
|
+
"""
|
|
528
|
+
)
|
|
500
529
|
|
|
501
530
|
client = self._create_client()
|
|
502
531
|
async with client as session:
|
|
503
532
|
result = await session.execute(
|
|
504
|
-
search_query,
|
|
505
|
-
variable_values={"name": name, "teamId": team_id}
|
|
533
|
+
search_query, variable_values={"name": name, "teamId": team_id}
|
|
506
534
|
)
|
|
507
535
|
|
|
508
536
|
if result["issueLabels"]["nodes"]:
|
|
@@ -511,7 +539,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
511
539
|
return label_id
|
|
512
540
|
|
|
513
541
|
# Create new label
|
|
514
|
-
create_query = gql(
|
|
542
|
+
create_query = gql(
|
|
543
|
+
"""
|
|
515
544
|
mutation CreateLabel($input: IssueLabelCreateInput!) {
|
|
516
545
|
issueLabelCreate(input: $input) {
|
|
517
546
|
issueLabel {
|
|
@@ -520,7 +549,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
520
549
|
}
|
|
521
550
|
}
|
|
522
551
|
}
|
|
523
|
-
"""
|
|
552
|
+
"""
|
|
553
|
+
)
|
|
524
554
|
|
|
525
555
|
label_input = {
|
|
526
556
|
"name": name,
|
|
@@ -532,8 +562,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
532
562
|
client = self._create_client()
|
|
533
563
|
async with client as session:
|
|
534
564
|
result = await session.execute(
|
|
535
|
-
create_query,
|
|
536
|
-
variable_values={"input": label_input}
|
|
565
|
+
create_query, variable_values={"input": label_input}
|
|
537
566
|
)
|
|
538
567
|
|
|
539
568
|
label_id = result["issueLabelCreate"]["issueLabel"]["id"]
|
|
@@ -548,7 +577,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
548
577
|
if email in self._users:
|
|
549
578
|
return self._users[email]
|
|
550
579
|
|
|
551
|
-
query = gql(
|
|
580
|
+
query = gql(
|
|
581
|
+
"""
|
|
552
582
|
query GetUser($email: String!) {
|
|
553
583
|
users(filter: { email: { eq: $email } }) {
|
|
554
584
|
nodes {
|
|
@@ -557,7 +587,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
557
587
|
}
|
|
558
588
|
}
|
|
559
589
|
}
|
|
560
|
-
"""
|
|
590
|
+
"""
|
|
591
|
+
)
|
|
561
592
|
|
|
562
593
|
client = self._create_client()
|
|
563
594
|
async with client as session:
|
|
@@ -575,14 +606,21 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
575
606
|
|
|
576
607
|
Returns:
|
|
577
608
|
(is_valid, error_message) - Tuple of validation result and error message
|
|
609
|
+
|
|
578
610
|
"""
|
|
579
611
|
if not self.api_key:
|
|
580
|
-
return
|
|
612
|
+
return (
|
|
613
|
+
False,
|
|
614
|
+
"LINEAR_API_KEY is required but not found. Set it in .env.local or environment.",
|
|
615
|
+
)
|
|
581
616
|
if not self.team_key and not self.team_id_config:
|
|
582
|
-
return
|
|
617
|
+
return (
|
|
618
|
+
False,
|
|
619
|
+
"Either Linear team_key or team_id is required in configuration. Set it in .mcp-ticketer/config.json",
|
|
620
|
+
)
|
|
583
621
|
return True, ""
|
|
584
622
|
|
|
585
|
-
def _get_state_mapping(self) ->
|
|
623
|
+
def _get_state_mapping(self) -> dict[TicketState, str]:
|
|
586
624
|
"""Get mapping from universal states to Linear state types.
|
|
587
625
|
|
|
588
626
|
Required by BaseAdapter abstract method.
|
|
@@ -605,7 +643,9 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
605
643
|
state = TicketState(state)
|
|
606
644
|
return self._state_mapping.get(state, LinearStateType.BACKLOG)
|
|
607
645
|
|
|
608
|
-
def _map_linear_state(
|
|
646
|
+
def _map_linear_state(
|
|
647
|
+
self, state_data: dict[str, Any], labels: list[str]
|
|
648
|
+
) -> TicketState:
|
|
609
649
|
"""Map Linear state and labels to universal state."""
|
|
610
650
|
state_type = state_data.get("type", "").lower()
|
|
611
651
|
|
|
@@ -630,7 +670,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
630
670
|
}
|
|
631
671
|
return state_mapping.get(state_type, TicketState.OPEN)
|
|
632
672
|
|
|
633
|
-
def _task_from_linear_issue(self, issue:
|
|
673
|
+
def _task_from_linear_issue(self, issue: dict[str, Any]) -> Task:
|
|
634
674
|
"""Convert Linear issue to universal Task."""
|
|
635
675
|
# Extract labels
|
|
636
676
|
tags = []
|
|
@@ -639,7 +679,9 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
639
679
|
|
|
640
680
|
# Map priority
|
|
641
681
|
linear_priority = issue.get("priority", 4)
|
|
642
|
-
priority = LinearPriorityMapping.FROM_LINEAR.get(
|
|
682
|
+
priority = LinearPriorityMapping.FROM_LINEAR.get(
|
|
683
|
+
linear_priority, Priority.MEDIUM
|
|
684
|
+
)
|
|
643
685
|
|
|
644
686
|
# Map state
|
|
645
687
|
state = self._map_linear_state(issue.get("state", {}), tags)
|
|
@@ -654,10 +696,20 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
654
696
|
"state_name": issue.get("state", {}).get("name"),
|
|
655
697
|
"team_id": issue.get("team", {}).get("id"),
|
|
656
698
|
"team_name": issue.get("team", {}).get("name"),
|
|
657
|
-
"cycle_id":
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
"
|
|
699
|
+
"cycle_id": (
|
|
700
|
+
issue.get("cycle", {}).get("id") if issue.get("cycle") else None
|
|
701
|
+
),
|
|
702
|
+
"cycle_name": (
|
|
703
|
+
issue.get("cycle", {}).get("name") if issue.get("cycle") else None
|
|
704
|
+
),
|
|
705
|
+
"project_id": (
|
|
706
|
+
issue.get("project", {}).get("id") if issue.get("project") else None
|
|
707
|
+
),
|
|
708
|
+
"project_name": (
|
|
709
|
+
issue.get("project", {}).get("name")
|
|
710
|
+
if issue.get("project")
|
|
711
|
+
else None
|
|
712
|
+
),
|
|
661
713
|
"priority_label": issue.get("priorityLabel"),
|
|
662
714
|
"estimate": issue.get("estimate"),
|
|
663
715
|
"due_date": issue.get("dueDate"),
|
|
@@ -715,17 +767,27 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
715
767
|
ticket_type=ticket_type,
|
|
716
768
|
parent_issue=parent_issue_id,
|
|
717
769
|
parent_epic=parent_epic_id,
|
|
718
|
-
assignee=
|
|
770
|
+
assignee=(
|
|
771
|
+
issue.get("assignee", {}).get("email")
|
|
772
|
+
if issue.get("assignee")
|
|
773
|
+
else None
|
|
774
|
+
),
|
|
719
775
|
children=child_ids,
|
|
720
776
|
estimated_hours=issue.get("estimate"),
|
|
721
|
-
created_at=
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
777
|
+
created_at=(
|
|
778
|
+
datetime.fromisoformat(issue["createdAt"].replace("Z", "+00:00"))
|
|
779
|
+
if issue.get("createdAt")
|
|
780
|
+
else None
|
|
781
|
+
),
|
|
782
|
+
updated_at=(
|
|
783
|
+
datetime.fromisoformat(issue["updatedAt"].replace("Z", "+00:00"))
|
|
784
|
+
if issue.get("updatedAt")
|
|
785
|
+
else None
|
|
786
|
+
),
|
|
725
787
|
metadata=metadata,
|
|
726
788
|
)
|
|
727
789
|
|
|
728
|
-
def _epic_from_linear_project(self, project:
|
|
790
|
+
def _epic_from_linear_project(self, project: dict[str, Any]) -> Epic:
|
|
729
791
|
"""Convert Linear project to universal Epic."""
|
|
730
792
|
# Map project state to ticket state
|
|
731
793
|
project_state = project.get("state", "planned").lower()
|
|
@@ -764,10 +826,16 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
764
826
|
state=state,
|
|
765
827
|
ticket_type=TicketType.EPIC,
|
|
766
828
|
tags=[f"team:{team}" for team in teams],
|
|
767
|
-
created_at=
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
829
|
+
created_at=(
|
|
830
|
+
datetime.fromisoformat(project["createdAt"].replace("Z", "+00:00"))
|
|
831
|
+
if project.get("createdAt")
|
|
832
|
+
else None
|
|
833
|
+
),
|
|
834
|
+
updated_at=(
|
|
835
|
+
datetime.fromisoformat(project["updatedAt"].replace("Z", "+00:00"))
|
|
836
|
+
if project.get("updatedAt")
|
|
837
|
+
else None
|
|
838
|
+
),
|
|
771
839
|
metadata=metadata,
|
|
772
840
|
)
|
|
773
841
|
|
|
@@ -803,19 +871,33 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
803
871
|
|
|
804
872
|
# Set priority
|
|
805
873
|
if ticket.priority:
|
|
806
|
-
issue_input["priority"] = LinearPriorityMapping.TO_LINEAR.get(
|
|
874
|
+
issue_input["priority"] = LinearPriorityMapping.TO_LINEAR.get(
|
|
875
|
+
ticket.priority, 3
|
|
876
|
+
)
|
|
807
877
|
|
|
808
878
|
# Handle labels/tags
|
|
809
879
|
if ticket.tags:
|
|
810
880
|
label_ids = []
|
|
811
881
|
for tag in ticket.tags:
|
|
812
882
|
# Add special state labels if needed
|
|
813
|
-
if ticket.state == TicketState.BLOCKED and "blocked" not in [
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
label_ids.append(
|
|
817
|
-
|
|
818
|
-
|
|
883
|
+
if ticket.state == TicketState.BLOCKED and "blocked" not in [
|
|
884
|
+
t.lower() for t in ticket.tags
|
|
885
|
+
]:
|
|
886
|
+
label_ids.append(
|
|
887
|
+
await self._get_or_create_label("blocked", "#FF0000")
|
|
888
|
+
)
|
|
889
|
+
elif ticket.state == TicketState.WAITING and "waiting" not in [
|
|
890
|
+
t.lower() for t in ticket.tags
|
|
891
|
+
]:
|
|
892
|
+
label_ids.append(
|
|
893
|
+
await self._get_or_create_label("waiting", "#FFA500")
|
|
894
|
+
)
|
|
895
|
+
elif ticket.state == TicketState.READY and "ready" not in [
|
|
896
|
+
t.lower() for t in ticket.tags
|
|
897
|
+
]:
|
|
898
|
+
label_ids.append(
|
|
899
|
+
await self._get_or_create_label("ready", "#00FF00")
|
|
900
|
+
)
|
|
819
901
|
|
|
820
902
|
label_id = await self._get_or_create_label(tag)
|
|
821
903
|
label_ids.append(label_id)
|
|
@@ -835,18 +917,19 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
835
917
|
# Handle parent issue
|
|
836
918
|
if ticket.parent_issue:
|
|
837
919
|
# Get parent issue's Linear ID
|
|
838
|
-
parent_query = gql(
|
|
920
|
+
parent_query = gql(
|
|
921
|
+
"""
|
|
839
922
|
query GetIssue($identifier: String!) {
|
|
840
923
|
issue(id: $identifier) {
|
|
841
924
|
id
|
|
842
925
|
}
|
|
843
926
|
}
|
|
844
|
-
"""
|
|
927
|
+
"""
|
|
928
|
+
)
|
|
845
929
|
client = self._create_client()
|
|
846
930
|
async with client as session:
|
|
847
931
|
parent_result = await session.execute(
|
|
848
|
-
parent_query,
|
|
849
|
-
variable_values={"identifier": ticket.parent_issue}
|
|
932
|
+
parent_query, variable_values={"identifier": ticket.parent_issue}
|
|
850
933
|
)
|
|
851
934
|
if parent_result.get("issue"):
|
|
852
935
|
issue_input["parentId"] = parent_result["issue"]["id"]
|
|
@@ -864,7 +947,9 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
864
947
|
issue_input["cycleId"] = linear_meta["cycle_id"]
|
|
865
948
|
|
|
866
949
|
# Create issue mutation with full fields
|
|
867
|
-
create_query = gql(
|
|
950
|
+
create_query = gql(
|
|
951
|
+
ALL_FRAGMENTS
|
|
952
|
+
+ """
|
|
868
953
|
mutation CreateIssue($input: IssueCreateInput!) {
|
|
869
954
|
issueCreate(input: $input) {
|
|
870
955
|
success
|
|
@@ -873,13 +958,13 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
873
958
|
}
|
|
874
959
|
}
|
|
875
960
|
}
|
|
876
|
-
"""
|
|
961
|
+
"""
|
|
962
|
+
)
|
|
877
963
|
|
|
878
964
|
client = self._create_client()
|
|
879
965
|
async with client as session:
|
|
880
966
|
result = await session.execute(
|
|
881
|
-
create_query,
|
|
882
|
-
variable_values={"input": issue_input}
|
|
967
|
+
create_query, variable_values={"input": issue_input}
|
|
883
968
|
)
|
|
884
969
|
|
|
885
970
|
if not result["issueCreate"]["success"]:
|
|
@@ -895,20 +980,22 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
895
980
|
if not is_valid:
|
|
896
981
|
raise ValueError(error_message)
|
|
897
982
|
|
|
898
|
-
query = gql(
|
|
983
|
+
query = gql(
|
|
984
|
+
ALL_FRAGMENTS
|
|
985
|
+
+ """
|
|
899
986
|
query GetIssue($identifier: String!) {
|
|
900
987
|
issue(id: $identifier) {
|
|
901
988
|
...IssueFullFields
|
|
902
989
|
}
|
|
903
990
|
}
|
|
904
|
-
"""
|
|
991
|
+
"""
|
|
992
|
+
)
|
|
905
993
|
|
|
906
994
|
try:
|
|
907
995
|
client = self._create_client()
|
|
908
996
|
async with client as session:
|
|
909
997
|
result = await session.execute(
|
|
910
|
-
query,
|
|
911
|
-
variable_values={"identifier": ticket_id}
|
|
998
|
+
query, variable_values={"identifier": ticket_id}
|
|
912
999
|
)
|
|
913
1000
|
|
|
914
1001
|
if result.get("issue"):
|
|
@@ -919,7 +1006,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
919
1006
|
|
|
920
1007
|
return None
|
|
921
1008
|
|
|
922
|
-
async def update(self, ticket_id: str, updates:
|
|
1009
|
+
async def update(self, ticket_id: str, updates: dict[str, Any]) -> Optional[Task]:
|
|
923
1010
|
"""Update a Linear issue with comprehensive field support."""
|
|
924
1011
|
# Validate credentials before attempting operation
|
|
925
1012
|
is_valid, error_message = self.validate_credentials()
|
|
@@ -927,19 +1014,20 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
927
1014
|
raise ValueError(error_message)
|
|
928
1015
|
|
|
929
1016
|
# First get the Linear internal ID
|
|
930
|
-
query = gql(
|
|
1017
|
+
query = gql(
|
|
1018
|
+
"""
|
|
931
1019
|
query GetIssueId($identifier: String!) {
|
|
932
1020
|
issue(id: $identifier) {
|
|
933
1021
|
id
|
|
934
1022
|
}
|
|
935
1023
|
}
|
|
936
|
-
"""
|
|
1024
|
+
"""
|
|
1025
|
+
)
|
|
937
1026
|
|
|
938
1027
|
client = self._create_client()
|
|
939
1028
|
async with client as session:
|
|
940
1029
|
result = await session.execute(
|
|
941
|
-
query,
|
|
942
|
-
variable_values={"identifier": ticket_id}
|
|
1030
|
+
query, variable_values={"identifier": ticket_id}
|
|
943
1031
|
)
|
|
944
1032
|
|
|
945
1033
|
if not result.get("issue"):
|
|
@@ -1001,7 +1089,9 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1001
1089
|
update_input["projectId"] = linear_meta["project_id"]
|
|
1002
1090
|
|
|
1003
1091
|
# Update mutation
|
|
1004
|
-
update_query = gql(
|
|
1092
|
+
update_query = gql(
|
|
1093
|
+
ALL_FRAGMENTS
|
|
1094
|
+
+ """
|
|
1005
1095
|
mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
|
|
1006
1096
|
issueUpdate(id: $id, input: $input) {
|
|
1007
1097
|
success
|
|
@@ -1010,13 +1100,13 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1010
1100
|
}
|
|
1011
1101
|
}
|
|
1012
1102
|
}
|
|
1013
|
-
"""
|
|
1103
|
+
"""
|
|
1104
|
+
)
|
|
1014
1105
|
|
|
1015
1106
|
client = self._create_client()
|
|
1016
1107
|
async with client as session:
|
|
1017
1108
|
result = await session.execute(
|
|
1018
|
-
update_query,
|
|
1019
|
-
variable_values={"id": linear_id, "input": update_input}
|
|
1109
|
+
update_query, variable_values={"id": linear_id, "input": update_input}
|
|
1020
1110
|
)
|
|
1021
1111
|
|
|
1022
1112
|
if result["issueUpdate"]["success"]:
|
|
@@ -1032,19 +1122,20 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1032
1122
|
raise ValueError(error_message)
|
|
1033
1123
|
|
|
1034
1124
|
# Get Linear ID
|
|
1035
|
-
query = gql(
|
|
1125
|
+
query = gql(
|
|
1126
|
+
"""
|
|
1036
1127
|
query GetIssueId($identifier: String!) {
|
|
1037
1128
|
issue(id: $identifier) {
|
|
1038
1129
|
id
|
|
1039
1130
|
}
|
|
1040
1131
|
}
|
|
1041
|
-
"""
|
|
1132
|
+
"""
|
|
1133
|
+
)
|
|
1042
1134
|
|
|
1043
1135
|
client = self._create_client()
|
|
1044
1136
|
async with client as session:
|
|
1045
1137
|
result = await session.execute(
|
|
1046
|
-
query,
|
|
1047
|
-
variable_values={"identifier": ticket_id}
|
|
1138
|
+
query, variable_values={"identifier": ticket_id}
|
|
1048
1139
|
)
|
|
1049
1140
|
|
|
1050
1141
|
if not result.get("issue"):
|
|
@@ -1053,29 +1144,27 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1053
1144
|
linear_id = result["issue"]["id"]
|
|
1054
1145
|
|
|
1055
1146
|
# Archive mutation
|
|
1056
|
-
archive_query = gql(
|
|
1147
|
+
archive_query = gql(
|
|
1148
|
+
"""
|
|
1057
1149
|
mutation ArchiveIssue($id: String!) {
|
|
1058
1150
|
issueArchive(id: $id) {
|
|
1059
1151
|
success
|
|
1060
1152
|
}
|
|
1061
1153
|
}
|
|
1062
|
-
"""
|
|
1154
|
+
"""
|
|
1155
|
+
)
|
|
1063
1156
|
|
|
1064
1157
|
client = self._create_client()
|
|
1065
1158
|
async with client as session:
|
|
1066
1159
|
result = await session.execute(
|
|
1067
|
-
archive_query,
|
|
1068
|
-
variable_values={"id": linear_id}
|
|
1160
|
+
archive_query, variable_values={"id": linear_id}
|
|
1069
1161
|
)
|
|
1070
1162
|
|
|
1071
1163
|
return result.get("issueArchive", {}).get("success", False)
|
|
1072
1164
|
|
|
1073
1165
|
async def list(
|
|
1074
|
-
self,
|
|
1075
|
-
|
|
1076
|
-
offset: int = 0,
|
|
1077
|
-
filters: Optional[Dict[str, Any]] = None
|
|
1078
|
-
) -> List[Task]:
|
|
1166
|
+
self, limit: int = 10, offset: int = 0, filters: Optional[dict[str, Any]] = None
|
|
1167
|
+
) -> list[Task]:
|
|
1079
1168
|
"""List Linear issues with comprehensive filtering."""
|
|
1080
1169
|
team_id = await self._ensure_team_id()
|
|
1081
1170
|
|
|
@@ -1137,10 +1226,16 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1137
1226
|
issue_filter["dueDate"] = {"lte": filters["due_before"]}
|
|
1138
1227
|
|
|
1139
1228
|
# Exclude archived issues by default
|
|
1140
|
-
if
|
|
1229
|
+
if (
|
|
1230
|
+
not filters
|
|
1231
|
+
or "includeArchived" not in filters
|
|
1232
|
+
or not filters["includeArchived"]
|
|
1233
|
+
):
|
|
1141
1234
|
issue_filter["archivedAt"] = {"null": True}
|
|
1142
1235
|
|
|
1143
|
-
query = gql(
|
|
1236
|
+
query = gql(
|
|
1237
|
+
ISSUE_LIST_FRAGMENTS
|
|
1238
|
+
+ """
|
|
1144
1239
|
query ListIssues($filter: IssueFilter, $first: Int!) {
|
|
1145
1240
|
issues(
|
|
1146
1241
|
filter: $filter
|
|
@@ -1156,7 +1251,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1156
1251
|
}
|
|
1157
1252
|
}
|
|
1158
1253
|
}
|
|
1159
|
-
"""
|
|
1254
|
+
"""
|
|
1255
|
+
)
|
|
1160
1256
|
|
|
1161
1257
|
client = self._create_client()
|
|
1162
1258
|
async with client as session:
|
|
@@ -1167,7 +1263,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1167
1263
|
"first": limit,
|
|
1168
1264
|
# Note: Linear uses cursor-based pagination, not offset
|
|
1169
1265
|
# For simplicity, we ignore offset here
|
|
1170
|
-
}
|
|
1266
|
+
},
|
|
1171
1267
|
)
|
|
1172
1268
|
|
|
1173
1269
|
tasks = []
|
|
@@ -1176,7 +1272,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1176
1272
|
|
|
1177
1273
|
return tasks
|
|
1178
1274
|
|
|
1179
|
-
async def search(self, query: SearchQuery) ->
|
|
1275
|
+
async def search(self, query: SearchQuery) -> builtins.list[Task]:
|
|
1180
1276
|
"""Search Linear issues with advanced filtering and text search."""
|
|
1181
1277
|
team_id = await self._ensure_team_id()
|
|
1182
1278
|
|
|
@@ -1219,7 +1315,9 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1219
1315
|
# Exclude archived
|
|
1220
1316
|
issue_filter["archivedAt"] = {"null": True}
|
|
1221
1317
|
|
|
1222
|
-
search_query = gql(
|
|
1318
|
+
search_query = gql(
|
|
1319
|
+
ISSUE_LIST_FRAGMENTS
|
|
1320
|
+
+ """
|
|
1223
1321
|
query SearchIssues($filter: IssueFilter, $first: Int!) {
|
|
1224
1322
|
issues(
|
|
1225
1323
|
filter: $filter
|
|
@@ -1231,7 +1329,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1231
1329
|
}
|
|
1232
1330
|
}
|
|
1233
1331
|
}
|
|
1234
|
-
"""
|
|
1332
|
+
"""
|
|
1333
|
+
)
|
|
1235
1334
|
|
|
1236
1335
|
client = self._create_client()
|
|
1237
1336
|
async with client as session:
|
|
@@ -1241,7 +1340,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1241
1340
|
"filter": issue_filter,
|
|
1242
1341
|
"first": query.limit,
|
|
1243
1342
|
# Note: Linear uses cursor-based pagination, not offset
|
|
1244
|
-
}
|
|
1343
|
+
},
|
|
1245
1344
|
)
|
|
1246
1345
|
|
|
1247
1346
|
tasks = []
|
|
@@ -1251,9 +1350,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1251
1350
|
return tasks
|
|
1252
1351
|
|
|
1253
1352
|
async def transition_state(
|
|
1254
|
-
self,
|
|
1255
|
-
ticket_id: str,
|
|
1256
|
-
target_state: TicketState
|
|
1353
|
+
self, ticket_id: str, target_state: TicketState
|
|
1257
1354
|
) -> Optional[Task]:
|
|
1258
1355
|
"""Transition Linear issue to new state with workflow validation."""
|
|
1259
1356
|
# Validate transition
|
|
@@ -1266,19 +1363,20 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1266
1363
|
async def add_comment(self, comment: Comment) -> Comment:
|
|
1267
1364
|
"""Add comment to a Linear issue."""
|
|
1268
1365
|
# Get Linear issue ID
|
|
1269
|
-
query = gql(
|
|
1366
|
+
query = gql(
|
|
1367
|
+
"""
|
|
1270
1368
|
query GetIssueId($identifier: String!) {
|
|
1271
1369
|
issue(id: $identifier) {
|
|
1272
1370
|
id
|
|
1273
1371
|
}
|
|
1274
1372
|
}
|
|
1275
|
-
"""
|
|
1373
|
+
"""
|
|
1374
|
+
)
|
|
1276
1375
|
|
|
1277
1376
|
client = self._create_client()
|
|
1278
1377
|
async with client as session:
|
|
1279
1378
|
result = await session.execute(
|
|
1280
|
-
query,
|
|
1281
|
-
variable_values={"identifier": comment.ticket_id}
|
|
1379
|
+
query, variable_values={"identifier": comment.ticket_id}
|
|
1282
1380
|
)
|
|
1283
1381
|
|
|
1284
1382
|
if not result.get("issue"):
|
|
@@ -1287,7 +1385,10 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1287
1385
|
linear_id = result["issue"]["id"]
|
|
1288
1386
|
|
|
1289
1387
|
# Create comment mutation (only include needed fragments)
|
|
1290
|
-
create_comment_query = gql(
|
|
1388
|
+
create_comment_query = gql(
|
|
1389
|
+
USER_FRAGMENT
|
|
1390
|
+
+ COMMENT_FRAGMENT
|
|
1391
|
+
+ """
|
|
1291
1392
|
mutation CreateComment($input: CommentCreateInput!) {
|
|
1292
1393
|
commentCreate(input: $input) {
|
|
1293
1394
|
success
|
|
@@ -1296,7 +1397,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1296
1397
|
}
|
|
1297
1398
|
}
|
|
1298
1399
|
}
|
|
1299
|
-
"""
|
|
1400
|
+
"""
|
|
1401
|
+
)
|
|
1300
1402
|
|
|
1301
1403
|
comment_input = {
|
|
1302
1404
|
"issueId": linear_id,
|
|
@@ -1310,8 +1412,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1310
1412
|
client = self._create_client()
|
|
1311
1413
|
async with client as session:
|
|
1312
1414
|
result = await session.execute(
|
|
1313
|
-
create_comment_query,
|
|
1314
|
-
variable_values={"input": comment_input}
|
|
1415
|
+
create_comment_query, variable_values={"input": comment_input}
|
|
1315
1416
|
)
|
|
1316
1417
|
|
|
1317
1418
|
if not result["commentCreate"]["success"]:
|
|
@@ -1322,25 +1423,35 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1322
1423
|
return Comment(
|
|
1323
1424
|
id=created_comment["id"],
|
|
1324
1425
|
ticket_id=comment.ticket_id,
|
|
1325
|
-
author=
|
|
1426
|
+
author=(
|
|
1427
|
+
created_comment["user"]["email"]
|
|
1428
|
+
if created_comment.get("user")
|
|
1429
|
+
else None
|
|
1430
|
+
),
|
|
1326
1431
|
content=created_comment["body"],
|
|
1327
|
-
created_at=datetime.fromisoformat(
|
|
1432
|
+
created_at=datetime.fromisoformat(
|
|
1433
|
+
created_comment["createdAt"].replace("Z", "+00:00")
|
|
1434
|
+
),
|
|
1328
1435
|
metadata={
|
|
1329
1436
|
"linear": {
|
|
1330
1437
|
"id": created_comment["id"],
|
|
1331
|
-
"parent_id":
|
|
1438
|
+
"parent_id": (
|
|
1439
|
+
created_comment.get("parent", {}).get("id")
|
|
1440
|
+
if created_comment.get("parent")
|
|
1441
|
+
else None
|
|
1442
|
+
),
|
|
1332
1443
|
}
|
|
1333
1444
|
},
|
|
1334
1445
|
)
|
|
1335
1446
|
|
|
1336
1447
|
async def get_comments(
|
|
1337
|
-
self,
|
|
1338
|
-
|
|
1339
|
-
limit: int = 10,
|
|
1340
|
-
offset: int = 0
|
|
1341
|
-
) -> List[Comment]:
|
|
1448
|
+
self, ticket_id: str, limit: int = 10, offset: int = 0
|
|
1449
|
+
) -> builtins.list[Comment]:
|
|
1342
1450
|
"""Get comments for a Linear issue with pagination."""
|
|
1343
|
-
query = gql(
|
|
1451
|
+
query = gql(
|
|
1452
|
+
USER_FRAGMENT
|
|
1453
|
+
+ COMMENT_FRAGMENT
|
|
1454
|
+
+ """
|
|
1344
1455
|
query GetIssueComments($identifier: String!, $first: Int!) {
|
|
1345
1456
|
issue(id: $identifier) {
|
|
1346
1457
|
comments(first: $first, orderBy: createdAt) {
|
|
@@ -1350,7 +1461,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1350
1461
|
}
|
|
1351
1462
|
}
|
|
1352
1463
|
}
|
|
1353
|
-
"""
|
|
1464
|
+
"""
|
|
1465
|
+
)
|
|
1354
1466
|
|
|
1355
1467
|
try:
|
|
1356
1468
|
client = self._create_client()
|
|
@@ -1361,7 +1473,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1361
1473
|
"identifier": ticket_id,
|
|
1362
1474
|
"first": limit,
|
|
1363
1475
|
# Note: Linear uses cursor-based pagination
|
|
1364
|
-
}
|
|
1476
|
+
},
|
|
1365
1477
|
)
|
|
1366
1478
|
|
|
1367
1479
|
if not result.get("issue"):
|
|
@@ -1369,19 +1481,31 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1369
1481
|
|
|
1370
1482
|
comments = []
|
|
1371
1483
|
for comment_data in result["issue"]["comments"]["nodes"]:
|
|
1372
|
-
comments.append(
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1484
|
+
comments.append(
|
|
1485
|
+
Comment(
|
|
1486
|
+
id=comment_data["id"],
|
|
1487
|
+
ticket_id=ticket_id,
|
|
1488
|
+
author=(
|
|
1489
|
+
comment_data["user"]["email"]
|
|
1490
|
+
if comment_data.get("user")
|
|
1491
|
+
else None
|
|
1492
|
+
),
|
|
1493
|
+
content=comment_data["body"],
|
|
1494
|
+
created_at=datetime.fromisoformat(
|
|
1495
|
+
comment_data["createdAt"].replace("Z", "+00:00")
|
|
1496
|
+
),
|
|
1497
|
+
metadata={
|
|
1498
|
+
"linear": {
|
|
1499
|
+
"id": comment_data["id"],
|
|
1500
|
+
"parent_id": (
|
|
1501
|
+
comment_data.get("parent", {}).get("id")
|
|
1502
|
+
if comment_data.get("parent")
|
|
1503
|
+
else None
|
|
1504
|
+
),
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1507
|
+
)
|
|
1508
|
+
)
|
|
1385
1509
|
|
|
1386
1510
|
return comments
|
|
1387
1511
|
except TransportQueryError:
|
|
@@ -1391,7 +1515,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1391
1515
|
"""Create a Linear project."""
|
|
1392
1516
|
team_id = await self._ensure_team_id()
|
|
1393
1517
|
|
|
1394
|
-
create_query = gql(
|
|
1518
|
+
create_query = gql(
|
|
1519
|
+
"""
|
|
1395
1520
|
mutation CreateProject($input: ProjectCreateInput!) {
|
|
1396
1521
|
projectCreate(input: $input) {
|
|
1397
1522
|
success
|
|
@@ -1401,7 +1526,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1401
1526
|
}
|
|
1402
1527
|
}
|
|
1403
1528
|
}
|
|
1404
|
-
"""
|
|
1529
|
+
"""
|
|
1530
|
+
)
|
|
1405
1531
|
|
|
1406
1532
|
project_input = {
|
|
1407
1533
|
"name": name,
|
|
@@ -1413,8 +1539,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1413
1539
|
client = self._create_client()
|
|
1414
1540
|
async with client as session:
|
|
1415
1541
|
result = await session.execute(
|
|
1416
|
-
create_query,
|
|
1417
|
-
variable_values={"input": project_input}
|
|
1542
|
+
create_query, variable_values={"input": project_input}
|
|
1418
1543
|
)
|
|
1419
1544
|
|
|
1420
1545
|
if not result["projectCreate"]["success"]:
|
|
@@ -1422,7 +1547,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1422
1547
|
|
|
1423
1548
|
return result["projectCreate"]["project"]["id"]
|
|
1424
1549
|
|
|
1425
|
-
async def get_cycles(self, active_only: bool = True) ->
|
|
1550
|
+
async def get_cycles(self, active_only: bool = True) -> builtins.list[dict[str, Any]]:
|
|
1426
1551
|
"""Get Linear cycles (sprints) for the team."""
|
|
1427
1552
|
team_id = await self._ensure_team_id()
|
|
1428
1553
|
|
|
@@ -1430,7 +1555,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1430
1555
|
if active_only:
|
|
1431
1556
|
cycle_filter["isActive"] = {"eq": True}
|
|
1432
1557
|
|
|
1433
|
-
query = gql(
|
|
1558
|
+
query = gql(
|
|
1559
|
+
"""
|
|
1434
1560
|
query GetCycles($filter: CycleFilter) {
|
|
1435
1561
|
cycles(filter: $filter, orderBy: createdAt) {
|
|
1436
1562
|
nodes {
|
|
@@ -1450,43 +1576,49 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1450
1576
|
}
|
|
1451
1577
|
}
|
|
1452
1578
|
}
|
|
1453
|
-
"""
|
|
1579
|
+
"""
|
|
1580
|
+
)
|
|
1454
1581
|
|
|
1455
1582
|
client = self._create_client()
|
|
1456
1583
|
async with client as session:
|
|
1457
1584
|
result = await session.execute(
|
|
1458
|
-
query,
|
|
1459
|
-
variable_values={"filter": cycle_filter}
|
|
1585
|
+
query, variable_values={"filter": cycle_filter}
|
|
1460
1586
|
)
|
|
1461
1587
|
|
|
1462
1588
|
return result["cycles"]["nodes"]
|
|
1463
1589
|
|
|
1464
1590
|
async def add_to_cycle(self, ticket_id: str, cycle_id: str) -> bool:
|
|
1465
1591
|
"""Add an issue to a cycle."""
|
|
1466
|
-
return
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1592
|
+
return (
|
|
1593
|
+
await self.update(
|
|
1594
|
+
ticket_id, {"metadata": {"linear": {"cycle_id": cycle_id}}}
|
|
1595
|
+
)
|
|
1596
|
+
is not None
|
|
1597
|
+
)
|
|
1470
1598
|
|
|
1471
1599
|
async def set_due_date(self, ticket_id: str, due_date: Union[str, date]) -> bool:
|
|
1472
1600
|
"""Set due date for an issue."""
|
|
1473
1601
|
if isinstance(due_date, date):
|
|
1474
1602
|
due_date = due_date.isoformat()
|
|
1475
1603
|
|
|
1476
|
-
return
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1604
|
+
return (
|
|
1605
|
+
await self.update(
|
|
1606
|
+
ticket_id, {"metadata": {"linear": {"due_date": due_date}}}
|
|
1607
|
+
)
|
|
1608
|
+
is not None
|
|
1609
|
+
)
|
|
1480
1610
|
|
|
1481
1611
|
async def add_reaction(self, comment_id: str, emoji: str) -> bool:
|
|
1482
1612
|
"""Add reaction to a comment."""
|
|
1483
|
-
create_query = gql(
|
|
1613
|
+
create_query = gql(
|
|
1614
|
+
"""
|
|
1484
1615
|
mutation CreateReaction($input: ReactionCreateInput!) {
|
|
1485
1616
|
reactionCreate(input: $input) {
|
|
1486
1617
|
success
|
|
1487
1618
|
}
|
|
1488
1619
|
}
|
|
1489
|
-
"""
|
|
1620
|
+
"""
|
|
1621
|
+
)
|
|
1490
1622
|
|
|
1491
1623
|
client = self._create_client()
|
|
1492
1624
|
async with client as session:
|
|
@@ -1497,7 +1629,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1497
1629
|
"commentId": comment_id,
|
|
1498
1630
|
"emoji": emoji,
|
|
1499
1631
|
}
|
|
1500
|
-
}
|
|
1632
|
+
},
|
|
1501
1633
|
)
|
|
1502
1634
|
|
|
1503
1635
|
return result.get("reactionCreate", {}).get("success", False)
|
|
@@ -1507,7 +1639,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1507
1639
|
ticket_id: str,
|
|
1508
1640
|
pr_url: str,
|
|
1509
1641
|
pr_number: Optional[int] = None,
|
|
1510
|
-
) ->
|
|
1642
|
+
) -> dict[str, Any]:
|
|
1511
1643
|
"""Link a Linear issue to a GitHub pull request.
|
|
1512
1644
|
|
|
1513
1645
|
Args:
|
|
@@ -1517,9 +1649,11 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1517
1649
|
|
|
1518
1650
|
Returns:
|
|
1519
1651
|
Dictionary with link status and details
|
|
1652
|
+
|
|
1520
1653
|
"""
|
|
1521
1654
|
# Parse PR URL to extract details
|
|
1522
1655
|
import re
|
|
1656
|
+
|
|
1523
1657
|
pr_pattern = r"github\.com/([^/]+)/([^/]+)/pull/(\d+)"
|
|
1524
1658
|
match = re.search(pr_pattern, pr_url)
|
|
1525
1659
|
|
|
@@ -1531,7 +1665,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1531
1665
|
pr_number = int(extracted_pr_number)
|
|
1532
1666
|
|
|
1533
1667
|
# Create an attachment to link the PR
|
|
1534
|
-
create_query = gql(
|
|
1668
|
+
create_query = gql(
|
|
1669
|
+
"""
|
|
1535
1670
|
mutation CreateAttachment($input: AttachmentCreateInput!) {
|
|
1536
1671
|
attachmentCreate(input: $input) {
|
|
1537
1672
|
attachment {
|
|
@@ -1544,7 +1679,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1544
1679
|
success
|
|
1545
1680
|
}
|
|
1546
1681
|
}
|
|
1547
|
-
"""
|
|
1682
|
+
"""
|
|
1683
|
+
)
|
|
1548
1684
|
|
|
1549
1685
|
# Get the issue ID from the identifier
|
|
1550
1686
|
issue = await self.read(ticket_id)
|
|
@@ -1563,15 +1699,14 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1563
1699
|
"number": pr_number,
|
|
1564
1700
|
"owner": owner,
|
|
1565
1701
|
"repo": repo,
|
|
1566
|
-
}
|
|
1702
|
+
},
|
|
1567
1703
|
},
|
|
1568
1704
|
}
|
|
1569
1705
|
|
|
1570
1706
|
client = self._create_client()
|
|
1571
1707
|
async with client as session:
|
|
1572
1708
|
result = await session.execute(
|
|
1573
|
-
create_query,
|
|
1574
|
-
variable_values={"input": attachment_input}
|
|
1709
|
+
create_query, variable_values={"input": attachment_input}
|
|
1575
1710
|
)
|
|
1576
1711
|
|
|
1577
1712
|
if result.get("attachmentCreate", {}).get("success"):
|
|
@@ -1607,8 +1742,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1607
1742
|
async def create_pull_request_for_issue(
|
|
1608
1743
|
self,
|
|
1609
1744
|
ticket_id: str,
|
|
1610
|
-
github_config:
|
|
1611
|
-
) ->
|
|
1745
|
+
github_config: dict[str, Any],
|
|
1746
|
+
) -> dict[str, Any]:
|
|
1612
1747
|
"""Create a GitHub PR for a Linear issue using GitHub integration.
|
|
1613
1748
|
|
|
1614
1749
|
This requires GitHub integration to be configured in Linear.
|
|
@@ -1623,6 +1758,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1623
1758
|
|
|
1624
1759
|
Returns:
|
|
1625
1760
|
Dictionary with PR creation status
|
|
1761
|
+
|
|
1626
1762
|
"""
|
|
1627
1763
|
# Get the issue details
|
|
1628
1764
|
issue = await self.read(ticket_id)
|
|
@@ -1646,7 +1782,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1646
1782
|
head_branch = f"{ticket_id.lower()}-{safe_title}"
|
|
1647
1783
|
|
|
1648
1784
|
# Update the issue with the branch name
|
|
1649
|
-
update_query = gql(
|
|
1785
|
+
update_query = gql(
|
|
1786
|
+
"""
|
|
1650
1787
|
mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {
|
|
1651
1788
|
issueUpdate(id: $id, input: $input) {
|
|
1652
1789
|
issue {
|
|
@@ -1657,7 +1794,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1657
1794
|
success
|
|
1658
1795
|
}
|
|
1659
1796
|
}
|
|
1660
|
-
"""
|
|
1797
|
+
"""
|
|
1798
|
+
)
|
|
1661
1799
|
|
|
1662
1800
|
linear_id = issue.metadata.get("linear", {}).get("id")
|
|
1663
1801
|
if not linear_id:
|
|
@@ -1671,10 +1809,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1671
1809
|
async with client as session:
|
|
1672
1810
|
result = await session.execute(
|
|
1673
1811
|
update_query,
|
|
1674
|
-
variable_values={
|
|
1675
|
-
"id": linear_id,
|
|
1676
|
-
"input": {"branchName": head_branch}
|
|
1677
|
-
}
|
|
1812
|
+
variable_values={"id": linear_id, "input": {"branchName": head_branch}},
|
|
1678
1813
|
)
|
|
1679
1814
|
|
|
1680
1815
|
if result.get("issueUpdate", {}).get("success"):
|
|
@@ -1703,23 +1838,24 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1703
1838
|
else:
|
|
1704
1839
|
raise ValueError(f"Failed to update issue {ticket_id} with branch name")
|
|
1705
1840
|
|
|
1706
|
-
async def _search_by_identifier(self, identifier: str) -> Optional[
|
|
1841
|
+
async def _search_by_identifier(self, identifier: str) -> Optional[dict[str, Any]]:
|
|
1707
1842
|
"""Search for an issue by its identifier."""
|
|
1708
|
-
search_query = gql(
|
|
1843
|
+
search_query = gql(
|
|
1844
|
+
"""
|
|
1709
1845
|
query SearchIssue($identifier: String!) {
|
|
1710
1846
|
issue(id: $identifier) {
|
|
1711
1847
|
id
|
|
1712
1848
|
identifier
|
|
1713
1849
|
}
|
|
1714
1850
|
}
|
|
1715
|
-
"""
|
|
1851
|
+
"""
|
|
1852
|
+
)
|
|
1716
1853
|
|
|
1717
1854
|
try:
|
|
1718
1855
|
client = self._create_client()
|
|
1719
1856
|
async with client as session:
|
|
1720
1857
|
result = await session.execute(
|
|
1721
|
-
search_query,
|
|
1722
|
-
variable_values={"identifier": identifier}
|
|
1858
|
+
search_query, variable_values={"identifier": identifier}
|
|
1723
1859
|
)
|
|
1724
1860
|
return result.get("issue")
|
|
1725
1861
|
except:
|
|
@@ -1728,10 +1864,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1728
1864
|
# Epic/Issue/Task Hierarchy Methods (Linear: Project = Epic, Issue = Issue, Sub-issue = Task)
|
|
1729
1865
|
|
|
1730
1866
|
async def create_epic(
|
|
1731
|
-
self,
|
|
1732
|
-
title: str,
|
|
1733
|
-
description: Optional[str] = None,
|
|
1734
|
-
**kwargs
|
|
1867
|
+
self, title: str, description: Optional[str] = None, **kwargs
|
|
1735
1868
|
) -> Optional[Epic]:
|
|
1736
1869
|
"""Create epic (Linear Project).
|
|
1737
1870
|
|
|
@@ -1742,10 +1875,13 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1742
1875
|
|
|
1743
1876
|
Returns:
|
|
1744
1877
|
Created epic or None if failed
|
|
1878
|
+
|
|
1745
1879
|
"""
|
|
1746
1880
|
team_id = await self._ensure_team_id()
|
|
1747
1881
|
|
|
1748
|
-
create_query = gql(
|
|
1882
|
+
create_query = gql(
|
|
1883
|
+
PROJECT_FRAGMENT
|
|
1884
|
+
+ """
|
|
1749
1885
|
mutation CreateProject($input: ProjectCreateInput!) {
|
|
1750
1886
|
projectCreate(input: $input) {
|
|
1751
1887
|
success
|
|
@@ -1754,7 +1890,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1754
1890
|
}
|
|
1755
1891
|
}
|
|
1756
1892
|
}
|
|
1757
|
-
"""
|
|
1893
|
+
"""
|
|
1894
|
+
)
|
|
1758
1895
|
|
|
1759
1896
|
project_input = {
|
|
1760
1897
|
"name": title,
|
|
@@ -1772,8 +1909,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1772
1909
|
client = self._create_client()
|
|
1773
1910
|
async with client as session:
|
|
1774
1911
|
result = await session.execute(
|
|
1775
|
-
create_query,
|
|
1776
|
-
variable_values={"input": project_input}
|
|
1912
|
+
create_query, variable_values={"input": project_input}
|
|
1777
1913
|
)
|
|
1778
1914
|
|
|
1779
1915
|
if not result["projectCreate"]["success"]:
|
|
@@ -1790,22 +1926,23 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1790
1926
|
|
|
1791
1927
|
Returns:
|
|
1792
1928
|
Epic if found, None otherwise
|
|
1929
|
+
|
|
1793
1930
|
"""
|
|
1794
|
-
query = gql(
|
|
1931
|
+
query = gql(
|
|
1932
|
+
PROJECT_FRAGMENT
|
|
1933
|
+
+ """
|
|
1795
1934
|
query GetProject($id: String!) {
|
|
1796
1935
|
project(id: $id) {
|
|
1797
1936
|
...ProjectFields
|
|
1798
1937
|
}
|
|
1799
1938
|
}
|
|
1800
|
-
"""
|
|
1939
|
+
"""
|
|
1940
|
+
)
|
|
1801
1941
|
|
|
1802
1942
|
try:
|
|
1803
1943
|
client = self._create_client()
|
|
1804
1944
|
async with client as session:
|
|
1805
|
-
result = await session.execute(
|
|
1806
|
-
query,
|
|
1807
|
-
variable_values={"id": epic_id}
|
|
1808
|
-
)
|
|
1945
|
+
result = await session.execute(query, variable_values={"id": epic_id})
|
|
1809
1946
|
|
|
1810
1947
|
if result.get("project"):
|
|
1811
1948
|
return self._epic_from_linear_project(result["project"])
|
|
@@ -1814,7 +1951,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1814
1951
|
|
|
1815
1952
|
return None
|
|
1816
1953
|
|
|
1817
|
-
async def list_epics(self, **kwargs) ->
|
|
1954
|
+
async def list_epics(self, **kwargs) -> builtins.list[Epic]:
|
|
1818
1955
|
"""List all Linear Projects (Epics).
|
|
1819
1956
|
|
|
1820
1957
|
Args:
|
|
@@ -1822,6 +1959,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1822
1959
|
|
|
1823
1960
|
Returns:
|
|
1824
1961
|
List of epics
|
|
1962
|
+
|
|
1825
1963
|
"""
|
|
1826
1964
|
team_id = await self._ensure_team_id()
|
|
1827
1965
|
|
|
@@ -1840,7 +1978,9 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1840
1978
|
linear_state = state_mapping.get(kwargs["state"], "planned")
|
|
1841
1979
|
project_filter["state"] = {"eq": linear_state}
|
|
1842
1980
|
|
|
1843
|
-
query = gql(
|
|
1981
|
+
query = gql(
|
|
1982
|
+
PROJECT_FRAGMENT
|
|
1983
|
+
+ """
|
|
1844
1984
|
query ListProjects($filter: ProjectFilter, $first: Int!) {
|
|
1845
1985
|
projects(filter: $filter, first: $first, orderBy: updatedAt) {
|
|
1846
1986
|
nodes {
|
|
@@ -1848,7 +1988,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1848
1988
|
}
|
|
1849
1989
|
}
|
|
1850
1990
|
}
|
|
1851
|
-
"""
|
|
1991
|
+
"""
|
|
1992
|
+
)
|
|
1852
1993
|
|
|
1853
1994
|
client = self._create_client()
|
|
1854
1995
|
async with client as session:
|
|
@@ -1856,8 +1997,8 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1856
1997
|
query,
|
|
1857
1998
|
variable_values={
|
|
1858
1999
|
"filter": project_filter,
|
|
1859
|
-
"first": kwargs.get("limit", 50)
|
|
1860
|
-
}
|
|
2000
|
+
"first": kwargs.get("limit", 50),
|
|
2001
|
+
},
|
|
1861
2002
|
)
|
|
1862
2003
|
|
|
1863
2004
|
epics = []
|
|
@@ -1871,7 +2012,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1871
2012
|
title: str,
|
|
1872
2013
|
description: Optional[str] = None,
|
|
1873
2014
|
epic_id: Optional[str] = None,
|
|
1874
|
-
**kwargs
|
|
2015
|
+
**kwargs,
|
|
1875
2016
|
) -> Optional[Task]:
|
|
1876
2017
|
"""Create issue and optionally associate with project (epic).
|
|
1877
2018
|
|
|
@@ -1883,6 +2024,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1883
2024
|
|
|
1884
2025
|
Returns:
|
|
1885
2026
|
Created issue or None if failed
|
|
2027
|
+
|
|
1886
2028
|
"""
|
|
1887
2029
|
# Use existing create method but ensure it's created as an ISSUE type
|
|
1888
2030
|
task = Task(
|
|
@@ -1890,13 +2032,13 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1890
2032
|
description=description,
|
|
1891
2033
|
ticket_type=TicketType.ISSUE,
|
|
1892
2034
|
parent_epic=epic_id,
|
|
1893
|
-
**{k: v for k, v in kwargs.items() if k in Task.__fields__}
|
|
2035
|
+
**{k: v for k, v in kwargs.items() if k in Task.__fields__},
|
|
1894
2036
|
)
|
|
1895
2037
|
|
|
1896
2038
|
# The existing create method handles project association via parent_epic field
|
|
1897
2039
|
return await self.create(task)
|
|
1898
2040
|
|
|
1899
|
-
async def list_issues_by_epic(self, epic_id: str) ->
|
|
2041
|
+
async def list_issues_by_epic(self, epic_id: str) -> builtins.list[Task]:
|
|
1900
2042
|
"""List all issues in a Linear project (epic).
|
|
1901
2043
|
|
|
1902
2044
|
Args:
|
|
@@ -1904,8 +2046,11 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1904
2046
|
|
|
1905
2047
|
Returns:
|
|
1906
2048
|
List of issues belonging to project
|
|
2049
|
+
|
|
1907
2050
|
"""
|
|
1908
|
-
query = gql(
|
|
2051
|
+
query = gql(
|
|
2052
|
+
ISSUE_LIST_FRAGMENTS
|
|
2053
|
+
+ """
|
|
1909
2054
|
query GetProjectIssues($projectId: String!, $first: Int!) {
|
|
1910
2055
|
project(id: $projectId) {
|
|
1911
2056
|
issues(first: $first) {
|
|
@@ -1915,14 +2060,14 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1915
2060
|
}
|
|
1916
2061
|
}
|
|
1917
2062
|
}
|
|
1918
|
-
"""
|
|
2063
|
+
"""
|
|
2064
|
+
)
|
|
1919
2065
|
|
|
1920
2066
|
try:
|
|
1921
2067
|
client = self._create_client()
|
|
1922
2068
|
async with client as session:
|
|
1923
2069
|
result = await session.execute(
|
|
1924
|
-
query,
|
|
1925
|
-
variable_values={"projectId": epic_id, "first": 100}
|
|
2070
|
+
query, variable_values={"projectId": epic_id, "first": 100}
|
|
1926
2071
|
)
|
|
1927
2072
|
|
|
1928
2073
|
if not result.get("project"):
|
|
@@ -1940,11 +2085,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1940
2085
|
return []
|
|
1941
2086
|
|
|
1942
2087
|
async def create_task(
|
|
1943
|
-
self,
|
|
1944
|
-
title: str,
|
|
1945
|
-
parent_id: str,
|
|
1946
|
-
description: Optional[str] = None,
|
|
1947
|
-
**kwargs
|
|
2088
|
+
self, title: str, parent_id: str, description: Optional[str] = None, **kwargs
|
|
1948
2089
|
) -> Optional[Task]:
|
|
1949
2090
|
"""Create task as sub-issue of parent.
|
|
1950
2091
|
|
|
@@ -1959,24 +2100,26 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1959
2100
|
|
|
1960
2101
|
Raises:
|
|
1961
2102
|
ValueError: If parent_id is not provided
|
|
2103
|
+
|
|
1962
2104
|
"""
|
|
1963
2105
|
if not parent_id:
|
|
1964
2106
|
raise ValueError("Tasks must have a parent_id (issue identifier)")
|
|
1965
2107
|
|
|
1966
2108
|
# Get parent issue's Linear ID
|
|
1967
|
-
parent_query = gql(
|
|
2109
|
+
parent_query = gql(
|
|
2110
|
+
"""
|
|
1968
2111
|
query GetIssueId($identifier: String!) {
|
|
1969
2112
|
issue(id: $identifier) {
|
|
1970
2113
|
id
|
|
1971
2114
|
}
|
|
1972
2115
|
}
|
|
1973
|
-
"""
|
|
2116
|
+
"""
|
|
2117
|
+
)
|
|
1974
2118
|
|
|
1975
2119
|
client = self._create_client()
|
|
1976
2120
|
async with client as session:
|
|
1977
2121
|
parent_result = await session.execute(
|
|
1978
|
-
parent_query,
|
|
1979
|
-
variable_values={"identifier": parent_id}
|
|
2122
|
+
parent_query, variable_values={"identifier": parent_id}
|
|
1980
2123
|
)
|
|
1981
2124
|
|
|
1982
2125
|
if not parent_result.get("issue"):
|
|
@@ -1990,7 +2133,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
1990
2133
|
description=description,
|
|
1991
2134
|
ticket_type=TicketType.TASK,
|
|
1992
2135
|
parent_issue=parent_id,
|
|
1993
|
-
**{k: v for k, v in kwargs.items() if k in Task.__fields__}
|
|
2136
|
+
**{k: v for k, v in kwargs.items() if k in Task.__fields__},
|
|
1994
2137
|
)
|
|
1995
2138
|
|
|
1996
2139
|
# Validate hierarchy
|
|
@@ -2024,10 +2167,14 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
2024
2167
|
|
|
2025
2168
|
# Set priority
|
|
2026
2169
|
if task.priority:
|
|
2027
|
-
issue_input["priority"] = LinearPriorityMapping.TO_LINEAR.get(
|
|
2170
|
+
issue_input["priority"] = LinearPriorityMapping.TO_LINEAR.get(
|
|
2171
|
+
task.priority, 3
|
|
2172
|
+
)
|
|
2028
2173
|
|
|
2029
2174
|
# Create sub-issue mutation
|
|
2030
|
-
create_query = gql(
|
|
2175
|
+
create_query = gql(
|
|
2176
|
+
ALL_FRAGMENTS
|
|
2177
|
+
+ """
|
|
2031
2178
|
mutation CreateSubIssue($input: IssueCreateInput!) {
|
|
2032
2179
|
issueCreate(input: $input) {
|
|
2033
2180
|
success
|
|
@@ -2036,13 +2183,13 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
2036
2183
|
}
|
|
2037
2184
|
}
|
|
2038
2185
|
}
|
|
2039
|
-
"""
|
|
2186
|
+
"""
|
|
2187
|
+
)
|
|
2040
2188
|
|
|
2041
2189
|
client = self._create_client()
|
|
2042
2190
|
async with client as session:
|
|
2043
2191
|
result = await session.execute(
|
|
2044
|
-
create_query,
|
|
2045
|
-
variable_values={"input": issue_input}
|
|
2192
|
+
create_query, variable_values={"input": issue_input}
|
|
2046
2193
|
)
|
|
2047
2194
|
|
|
2048
2195
|
if not result["issueCreate"]["success"]:
|
|
@@ -2051,7 +2198,7 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
2051
2198
|
created_issue = result["issueCreate"]["issue"]
|
|
2052
2199
|
return self._task_from_linear_issue(created_issue)
|
|
2053
2200
|
|
|
2054
|
-
async def list_tasks_by_issue(self, issue_id: str) ->
|
|
2201
|
+
async def list_tasks_by_issue(self, issue_id: str) -> builtins.list[Task]:
|
|
2055
2202
|
"""List all tasks (sub-issues) under an issue.
|
|
2056
2203
|
|
|
2057
2204
|
Args:
|
|
@@ -2059,8 +2206,11 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
2059
2206
|
|
|
2060
2207
|
Returns:
|
|
2061
2208
|
List of tasks belonging to issue
|
|
2209
|
+
|
|
2062
2210
|
"""
|
|
2063
|
-
query = gql(
|
|
2211
|
+
query = gql(
|
|
2212
|
+
ISSUE_LIST_FRAGMENTS
|
|
2213
|
+
+ """
|
|
2064
2214
|
query GetIssueSubtasks($identifier: String!) {
|
|
2065
2215
|
issue(id: $identifier) {
|
|
2066
2216
|
children {
|
|
@@ -2070,14 +2220,14 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
2070
2220
|
}
|
|
2071
2221
|
}
|
|
2072
2222
|
}
|
|
2073
|
-
"""
|
|
2223
|
+
"""
|
|
2224
|
+
)
|
|
2074
2225
|
|
|
2075
2226
|
try:
|
|
2076
2227
|
client = self._create_client()
|
|
2077
2228
|
async with client as session:
|
|
2078
2229
|
result = await session.execute(
|
|
2079
|
-
query,
|
|
2080
|
-
variable_values={"identifier": issue_id}
|
|
2230
|
+
query, variable_values={"identifier": issue_id}
|
|
2081
2231
|
)
|
|
2082
2232
|
|
|
2083
2233
|
if not result.get("issue"):
|
|
@@ -2105,4 +2255,4 @@ class LinearAdapter(BaseAdapter[Task]):
|
|
|
2105
2255
|
|
|
2106
2256
|
|
|
2107
2257
|
# Register the adapter
|
|
2108
|
-
AdapterRegistry.register("linear", LinearAdapter)
|
|
2258
|
+
AdapterRegistry.register("linear", LinearAdapter)
|