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