mcp-ticketer 0.1.20__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.20.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.20.dist-info/RECORD +0 -42
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/WHEEL +0 -0
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/entry_points.txt +0 -0
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/licenses/LICENSE +0 -0
- {mcp_ticketer-0.1.20.dist-info → mcp_ticketer-0.1.22.dist-info}/top_level.txt +0 -0
mcp_ticketer/adapters/github.py
CHANGED
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
import re
|
|
5
|
-
import asyncio
|
|
6
|
-
from typing import List, Optional, Dict, Any, Union, Tuple
|
|
7
5
|
from datetime import datetime
|
|
8
|
-
from
|
|
9
|
-
import json
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
10
7
|
|
|
11
8
|
import httpx
|
|
12
|
-
from pydantic import BaseModel
|
|
13
9
|
|
|
14
10
|
from ..core.adapter import BaseAdapter
|
|
15
|
-
from ..core.models import
|
|
11
|
+
from ..core.models import Comment, Epic, Priority, SearchQuery, Task, TicketState
|
|
16
12
|
from ..core.registry import AdapterRegistry
|
|
17
13
|
|
|
18
14
|
|
|
@@ -150,13 +146,18 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
150
146
|
- api_url: Optional API URL for GitHub Enterprise (defaults to github.com)
|
|
151
147
|
- use_projects_v2: Enable GitHub Projects v2 integration (default: False)
|
|
152
148
|
- custom_priority_scheme: Custom priority label mapping
|
|
149
|
+
|
|
153
150
|
"""
|
|
154
151
|
super().__init__(config)
|
|
155
152
|
|
|
156
153
|
# Get authentication token - support both 'api_key' and 'token' for compatibility
|
|
157
|
-
self.token =
|
|
154
|
+
self.token = (
|
|
155
|
+
config.get("api_key") or config.get("token") or os.getenv("GITHUB_TOKEN")
|
|
156
|
+
)
|
|
158
157
|
if not self.token:
|
|
159
|
-
raise ValueError(
|
|
158
|
+
raise ValueError(
|
|
159
|
+
"GitHub token required (config.api_key, config.token or GITHUB_TOKEN env var)"
|
|
160
|
+
)
|
|
160
161
|
|
|
161
162
|
# Get repository information
|
|
162
163
|
self.owner = config.get("owner") or os.getenv("GITHUB_OWNER")
|
|
@@ -167,7 +168,11 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
167
168
|
|
|
168
169
|
# API URLs
|
|
169
170
|
self.api_url = config.get("api_url", "https://api.github.com")
|
|
170
|
-
self.graphql_url =
|
|
171
|
+
self.graphql_url = (
|
|
172
|
+
f"{self.api_url}/graphql"
|
|
173
|
+
if "github.com" in self.api_url
|
|
174
|
+
else f"{self.api_url}/api/graphql"
|
|
175
|
+
)
|
|
171
176
|
|
|
172
177
|
# Configuration options
|
|
173
178
|
self.use_projects_v2 = config.get("use_projects_v2", False)
|
|
@@ -196,13 +201,23 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
196
201
|
|
|
197
202
|
Returns:
|
|
198
203
|
(is_valid, error_message) - Tuple of validation result and error message
|
|
204
|
+
|
|
199
205
|
"""
|
|
200
206
|
if not self.token:
|
|
201
|
-
return
|
|
207
|
+
return (
|
|
208
|
+
False,
|
|
209
|
+
"GITHUB_TOKEN is required but not found. Set it in .env.local or environment.",
|
|
210
|
+
)
|
|
202
211
|
if not self.owner:
|
|
203
|
-
return
|
|
212
|
+
return (
|
|
213
|
+
False,
|
|
214
|
+
"GitHub owner is required in configuration. Set GITHUB_OWNER in .env.local or configure with 'mcp-ticketer init --adapter github --github-owner <owner>'",
|
|
215
|
+
)
|
|
204
216
|
if not self.repo:
|
|
205
|
-
return
|
|
217
|
+
return (
|
|
218
|
+
False,
|
|
219
|
+
"GitHub repo is required in configuration. Set GITHUB_REPO in .env.local or configure with 'mcp-ticketer init --adapter github --github-repo <repo>'",
|
|
220
|
+
)
|
|
206
221
|
return True, ""
|
|
207
222
|
|
|
208
223
|
def _get_state_mapping(self) -> Dict[TicketState, str]:
|
|
@@ -251,7 +266,11 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
251
266
|
|
|
252
267
|
# Use default labels
|
|
253
268
|
labels = GitHubStateMapping.PRIORITY_LABELS.get(priority, [])
|
|
254
|
-
return
|
|
269
|
+
return (
|
|
270
|
+
labels[0]
|
|
271
|
+
if labels
|
|
272
|
+
else f"P{['0', '1', '2', '3'][list(Priority).index(priority)]}"
|
|
273
|
+
)
|
|
255
274
|
|
|
256
275
|
def _extract_state_from_issue(self, issue: Dict[str, Any]) -> TicketState:
|
|
257
276
|
"""Extract ticket state from GitHub issue data."""
|
|
@@ -263,8 +282,10 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
263
282
|
labels = []
|
|
264
283
|
if "labels" in issue:
|
|
265
284
|
if isinstance(issue["labels"], list):
|
|
266
|
-
labels = [
|
|
267
|
-
|
|
285
|
+
labels = [
|
|
286
|
+
label.get("name", "") if isinstance(label, dict) else str(label)
|
|
287
|
+
for label in issue["labels"]
|
|
288
|
+
]
|
|
268
289
|
elif isinstance(issue["labels"], dict) and "nodes" in issue["labels"]:
|
|
269
290
|
labels = [label["name"] for label in issue["labels"]["nodes"]]
|
|
270
291
|
|
|
@@ -283,8 +304,10 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
283
304
|
labels = []
|
|
284
305
|
if "labels" in issue:
|
|
285
306
|
if isinstance(issue["labels"], list):
|
|
286
|
-
labels = [
|
|
287
|
-
|
|
307
|
+
labels = [
|
|
308
|
+
label.get("name", "") if isinstance(label, dict) else str(label)
|
|
309
|
+
for label in issue["labels"]
|
|
310
|
+
]
|
|
288
311
|
elif isinstance(issue["labels"], dict) and "nodes" in issue["labels"]:
|
|
289
312
|
labels = [label["name"] for label in issue["labels"]["nodes"]]
|
|
290
313
|
|
|
@@ -314,22 +337,34 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
314
337
|
# Parse dates
|
|
315
338
|
created_at = None
|
|
316
339
|
if issue.get("created_at"):
|
|
317
|
-
created_at = datetime.fromisoformat(
|
|
340
|
+
created_at = datetime.fromisoformat(
|
|
341
|
+
issue["created_at"].replace("Z", "+00:00")
|
|
342
|
+
)
|
|
318
343
|
elif issue.get("createdAt"):
|
|
319
|
-
created_at = datetime.fromisoformat(
|
|
344
|
+
created_at = datetime.fromisoformat(
|
|
345
|
+
issue["createdAt"].replace("Z", "+00:00")
|
|
346
|
+
)
|
|
320
347
|
|
|
321
348
|
updated_at = None
|
|
322
349
|
if issue.get("updated_at"):
|
|
323
|
-
updated_at = datetime.fromisoformat(
|
|
350
|
+
updated_at = datetime.fromisoformat(
|
|
351
|
+
issue["updated_at"].replace("Z", "+00:00")
|
|
352
|
+
)
|
|
324
353
|
elif issue.get("updatedAt"):
|
|
325
|
-
updated_at = datetime.fromisoformat(
|
|
354
|
+
updated_at = datetime.fromisoformat(
|
|
355
|
+
issue["updatedAt"].replace("Z", "+00:00")
|
|
356
|
+
)
|
|
326
357
|
|
|
327
358
|
# Build metadata
|
|
328
359
|
metadata = {
|
|
329
360
|
"github": {
|
|
330
361
|
"number": issue.get("number"),
|
|
331
362
|
"url": issue.get("url") or issue.get("html_url"),
|
|
332
|
-
"author":
|
|
363
|
+
"author": (
|
|
364
|
+
issue.get("user", {}).get("login")
|
|
365
|
+
if "user" in issue
|
|
366
|
+
else issue.get("author", {}).get("login")
|
|
367
|
+
),
|
|
333
368
|
"labels": labels,
|
|
334
369
|
}
|
|
335
370
|
}
|
|
@@ -359,7 +394,9 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
359
394
|
metadata=metadata,
|
|
360
395
|
)
|
|
361
396
|
|
|
362
|
-
async def _ensure_label_exists(
|
|
397
|
+
async def _ensure_label_exists(
|
|
398
|
+
self, label_name: str, color: str = "0366d6"
|
|
399
|
+
) -> None:
|
|
363
400
|
"""Ensure a label exists in the repository."""
|
|
364
401
|
if not self._labels_cache:
|
|
365
402
|
response = await self.client.get(f"/repos/{self.owner}/{self.repo}/labels")
|
|
@@ -372,16 +409,17 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
372
409
|
# Create the label
|
|
373
410
|
response = await self.client.post(
|
|
374
411
|
f"/repos/{self.owner}/{self.repo}/labels",
|
|
375
|
-
json={"name": label_name, "color": color}
|
|
412
|
+
json={"name": label_name, "color": color},
|
|
376
413
|
)
|
|
377
414
|
if response.status_code == 201:
|
|
378
415
|
self._labels_cache.append(response.json())
|
|
379
416
|
|
|
380
|
-
async def _graphql_request(
|
|
417
|
+
async def _graphql_request(
|
|
418
|
+
self, query: str, variables: Dict[str, Any]
|
|
419
|
+
) -> Dict[str, Any]:
|
|
381
420
|
"""Execute a GraphQL query."""
|
|
382
421
|
response = await self.client.post(
|
|
383
|
-
self.graphql_url,
|
|
384
|
-
json={"query": query, "variables": variables}
|
|
422
|
+
self.graphql_url, json={"query": query, "variables": variables}
|
|
385
423
|
)
|
|
386
424
|
response.raise_for_status()
|
|
387
425
|
|
|
@@ -448,8 +486,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
448
486
|
|
|
449
487
|
# Create the issue
|
|
450
488
|
response = await self.client.post(
|
|
451
|
-
f"/repos/{self.owner}/{self.repo}/issues",
|
|
452
|
-
json=issue_data
|
|
489
|
+
f"/repos/{self.owner}/{self.repo}/issues", json=issue_data
|
|
453
490
|
)
|
|
454
491
|
response.raise_for_status()
|
|
455
492
|
|
|
@@ -459,7 +496,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
459
496
|
if ticket.state in [TicketState.DONE, TicketState.CLOSED]:
|
|
460
497
|
await self.client.patch(
|
|
461
498
|
f"/repos/{self.owner}/{self.repo}/issues/{created_issue['number']}",
|
|
462
|
-
json={"state": "closed"}
|
|
499
|
+
json={"state": "closed"},
|
|
463
500
|
)
|
|
464
501
|
created_issue["state"] = "closed"
|
|
465
502
|
|
|
@@ -530,8 +567,10 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
530
567
|
|
|
531
568
|
# Remove old state labels
|
|
532
569
|
labels_to_update = [
|
|
533
|
-
label
|
|
534
|
-
|
|
570
|
+
label
|
|
571
|
+
for label in current_labels
|
|
572
|
+
if label.lower()
|
|
573
|
+
not in [sl.lower() for sl in GitHubStateMapping.STATE_LABELS.values()]
|
|
535
574
|
]
|
|
536
575
|
|
|
537
576
|
# Add new state label if needed
|
|
@@ -561,8 +600,10 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
561
600
|
all_priority_labels.extend([l.lower() for l in labels])
|
|
562
601
|
|
|
563
602
|
labels_to_update = [
|
|
564
|
-
label
|
|
565
|
-
|
|
603
|
+
label
|
|
604
|
+
for label in labels_to_update
|
|
605
|
+
if label.lower() not in all_priority_labels
|
|
606
|
+
and not re.match(r"^P[0-3]$", label, re.IGNORECASE)
|
|
566
607
|
]
|
|
567
608
|
|
|
568
609
|
# Add new priority label
|
|
@@ -584,12 +625,16 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
584
625
|
# Preserve state and priority labels
|
|
585
626
|
preserved_labels = []
|
|
586
627
|
for label in current_labels:
|
|
587
|
-
if label.lower() in [
|
|
628
|
+
if label.lower() in [
|
|
629
|
+
sl.lower() for sl in GitHubStateMapping.STATE_LABELS.values()
|
|
630
|
+
]:
|
|
588
631
|
preserved_labels.append(label)
|
|
589
|
-
elif any(
|
|
590
|
-
|
|
632
|
+
elif any(
|
|
633
|
+
label.lower() in [pl.lower() for pl in labels]
|
|
634
|
+
for labels in GitHubStateMapping.PRIORITY_LABELS.values()
|
|
635
|
+
):
|
|
591
636
|
preserved_labels.append(label)
|
|
592
|
-
elif re.match(r
|
|
637
|
+
elif re.match(r"^P[0-3]$", label, re.IGNORECASE):
|
|
593
638
|
preserved_labels.append(label)
|
|
594
639
|
|
|
595
640
|
# Add new tags
|
|
@@ -602,7 +647,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
602
647
|
if update_data:
|
|
603
648
|
response = await self.client.patch(
|
|
604
649
|
f"/repos/{self.owner}/{self.repo}/issues/{issue_number}",
|
|
605
|
-
json=update_data
|
|
650
|
+
json=update_data,
|
|
606
651
|
)
|
|
607
652
|
response.raise_for_status()
|
|
608
653
|
|
|
@@ -626,7 +671,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
626
671
|
try:
|
|
627
672
|
response = await self.client.patch(
|
|
628
673
|
f"/repos/{self.owner}/{self.repo}/issues/{issue_number}",
|
|
629
|
-
json={"state": "closed", "state_reason": "not_planned"}
|
|
674
|
+
json={"state": "closed", "state_reason": "not_planned"},
|
|
630
675
|
)
|
|
631
676
|
response.raise_for_status()
|
|
632
677
|
return True
|
|
@@ -634,10 +679,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
634
679
|
return False
|
|
635
680
|
|
|
636
681
|
async def list(
|
|
637
|
-
self,
|
|
638
|
-
limit: int = 10,
|
|
639
|
-
offset: int = 0,
|
|
640
|
-
filters: Optional[Dict[str, Any]] = None
|
|
682
|
+
self, limit: int = 10, offset: int = 0, filters: Optional[Dict[str, Any]] = None
|
|
641
683
|
) -> List[Task]:
|
|
642
684
|
"""List GitHub issues with filters."""
|
|
643
685
|
# Build query parameters
|
|
@@ -683,8 +725,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
683
725
|
params["milestone"] = filters["parent_epic"]
|
|
684
726
|
|
|
685
727
|
response = await self.client.get(
|
|
686
|
-
f"/repos/{self.owner}/{self.repo}/issues",
|
|
687
|
-
params=params
|
|
728
|
+
f"/repos/{self.owner}/{self.repo}/issues", params=params
|
|
688
729
|
)
|
|
689
730
|
response.raise_for_status()
|
|
690
731
|
|
|
@@ -742,8 +783,9 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
742
783
|
github_query = " ".join(search_parts)
|
|
743
784
|
|
|
744
785
|
# Use GraphQL for better search capabilities
|
|
745
|
-
full_query = (
|
|
746
|
-
|
|
786
|
+
full_query = (
|
|
787
|
+
GitHubGraphQLQueries.ISSUE_FRAGMENT + GitHubGraphQLQueries.SEARCH_ISSUES
|
|
788
|
+
)
|
|
747
789
|
|
|
748
790
|
variables = {
|
|
749
791
|
"query": github_query,
|
|
@@ -788,9 +830,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
788
830
|
return issues
|
|
789
831
|
|
|
790
832
|
async def transition_state(
|
|
791
|
-
self,
|
|
792
|
-
ticket_id: str,
|
|
793
|
-
target_state: TicketState
|
|
833
|
+
self, ticket_id: str, target_state: TicketState
|
|
794
834
|
) -> Optional[Task]:
|
|
795
835
|
"""Transition GitHub issue to a new state."""
|
|
796
836
|
# Validate transition
|
|
@@ -810,7 +850,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
810
850
|
# Create comment
|
|
811
851
|
response = await self.client.post(
|
|
812
852
|
f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments",
|
|
813
|
-
json={"body": comment.content}
|
|
853
|
+
json={"body": comment.content},
|
|
814
854
|
)
|
|
815
855
|
response.raise_for_status()
|
|
816
856
|
|
|
@@ -821,7 +861,9 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
821
861
|
ticket_id=comment.ticket_id,
|
|
822
862
|
author=created_comment["user"]["login"],
|
|
823
863
|
content=created_comment["body"],
|
|
824
|
-
created_at=datetime.fromisoformat(
|
|
864
|
+
created_at=datetime.fromisoformat(
|
|
865
|
+
created_comment["created_at"].replace("Z", "+00:00")
|
|
866
|
+
),
|
|
825
867
|
metadata={
|
|
826
868
|
"github": {
|
|
827
869
|
"id": created_comment["id"],
|
|
@@ -832,10 +874,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
832
874
|
)
|
|
833
875
|
|
|
834
876
|
async def get_comments(
|
|
835
|
-
self,
|
|
836
|
-
ticket_id: str,
|
|
837
|
-
limit: int = 10,
|
|
838
|
-
offset: int = 0
|
|
877
|
+
self, ticket_id: str, limit: int = 10, offset: int = 0
|
|
839
878
|
) -> List[Comment]:
|
|
840
879
|
"""Get comments for a GitHub issue."""
|
|
841
880
|
try:
|
|
@@ -851,26 +890,30 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
851
890
|
try:
|
|
852
891
|
response = await self.client.get(
|
|
853
892
|
f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments",
|
|
854
|
-
params=params
|
|
893
|
+
params=params,
|
|
855
894
|
)
|
|
856
895
|
response.raise_for_status()
|
|
857
896
|
|
|
858
897
|
comments = []
|
|
859
898
|
for comment_data in response.json():
|
|
860
|
-
comments.append(
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
"
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
899
|
+
comments.append(
|
|
900
|
+
Comment(
|
|
901
|
+
id=str(comment_data["id"]),
|
|
902
|
+
ticket_id=ticket_id,
|
|
903
|
+
author=comment_data["user"]["login"],
|
|
904
|
+
content=comment_data["body"],
|
|
905
|
+
created_at=datetime.fromisoformat(
|
|
906
|
+
comment_data["created_at"].replace("Z", "+00:00")
|
|
907
|
+
),
|
|
908
|
+
metadata={
|
|
909
|
+
"github": {
|
|
910
|
+
"id": comment_data["id"],
|
|
911
|
+
"url": comment_data["html_url"],
|
|
912
|
+
"author_avatar": comment_data["user"]["avatar_url"],
|
|
913
|
+
}
|
|
914
|
+
},
|
|
915
|
+
)
|
|
916
|
+
)
|
|
874
917
|
|
|
875
918
|
return comments
|
|
876
919
|
except httpx.HTTPError:
|
|
@@ -891,8 +934,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
891
934
|
}
|
|
892
935
|
|
|
893
936
|
response = await self.client.post(
|
|
894
|
-
f"/repos/{self.owner}/{self.repo}/milestones",
|
|
895
|
-
json=milestone_data
|
|
937
|
+
f"/repos/{self.owner}/{self.repo}/milestones", json=milestone_data
|
|
896
938
|
)
|
|
897
939
|
response.raise_for_status()
|
|
898
940
|
|
|
@@ -902,9 +944,17 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
902
944
|
id=str(created_milestone["number"]),
|
|
903
945
|
title=created_milestone["title"],
|
|
904
946
|
description=created_milestone["description"],
|
|
905
|
-
state=
|
|
906
|
-
|
|
907
|
-
|
|
947
|
+
state=(
|
|
948
|
+
TicketState.OPEN
|
|
949
|
+
if created_milestone["state"] == "open"
|
|
950
|
+
else TicketState.CLOSED
|
|
951
|
+
),
|
|
952
|
+
created_at=datetime.fromisoformat(
|
|
953
|
+
created_milestone["created_at"].replace("Z", "+00:00")
|
|
954
|
+
),
|
|
955
|
+
updated_at=datetime.fromisoformat(
|
|
956
|
+
created_milestone["updated_at"].replace("Z", "+00:00")
|
|
957
|
+
),
|
|
908
958
|
metadata={
|
|
909
959
|
"github": {
|
|
910
960
|
"number": created_milestone["number"],
|
|
@@ -931,9 +981,17 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
931
981
|
id=str(milestone["number"]),
|
|
932
982
|
title=milestone["title"],
|
|
933
983
|
description=milestone["description"],
|
|
934
|
-
state=
|
|
935
|
-
|
|
936
|
-
|
|
984
|
+
state=(
|
|
985
|
+
TicketState.OPEN
|
|
986
|
+
if milestone["state"] == "open"
|
|
987
|
+
else TicketState.CLOSED
|
|
988
|
+
),
|
|
989
|
+
created_at=datetime.fromisoformat(
|
|
990
|
+
milestone["created_at"].replace("Z", "+00:00")
|
|
991
|
+
),
|
|
992
|
+
updated_at=datetime.fromisoformat(
|
|
993
|
+
milestone["updated_at"].replace("Z", "+00:00")
|
|
994
|
+
),
|
|
937
995
|
metadata={
|
|
938
996
|
"github": {
|
|
939
997
|
"number": milestone["number"],
|
|
@@ -947,10 +1005,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
947
1005
|
return None
|
|
948
1006
|
|
|
949
1007
|
async def list_milestones(
|
|
950
|
-
self,
|
|
951
|
-
state: str = "open",
|
|
952
|
-
limit: int = 10,
|
|
953
|
-
offset: int = 0
|
|
1008
|
+
self, state: str = "open", limit: int = 10, offset: int = 0
|
|
954
1009
|
) -> List[Epic]:
|
|
955
1010
|
"""List GitHub milestones as Epics."""
|
|
956
1011
|
params = {
|
|
@@ -960,29 +1015,38 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
960
1015
|
}
|
|
961
1016
|
|
|
962
1017
|
response = await self.client.get(
|
|
963
|
-
f"/repos/{self.owner}/{self.repo}/milestones",
|
|
964
|
-
params=params
|
|
1018
|
+
f"/repos/{self.owner}/{self.repo}/milestones", params=params
|
|
965
1019
|
)
|
|
966
1020
|
response.raise_for_status()
|
|
967
1021
|
|
|
968
1022
|
epics = []
|
|
969
1023
|
for milestone in response.json():
|
|
970
|
-
epics.append(
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1024
|
+
epics.append(
|
|
1025
|
+
Epic(
|
|
1026
|
+
id=str(milestone["number"]),
|
|
1027
|
+
title=milestone["title"],
|
|
1028
|
+
description=milestone["description"],
|
|
1029
|
+
state=(
|
|
1030
|
+
TicketState.OPEN
|
|
1031
|
+
if milestone["state"] == "open"
|
|
1032
|
+
else TicketState.CLOSED
|
|
1033
|
+
),
|
|
1034
|
+
created_at=datetime.fromisoformat(
|
|
1035
|
+
milestone["created_at"].replace("Z", "+00:00")
|
|
1036
|
+
),
|
|
1037
|
+
updated_at=datetime.fromisoformat(
|
|
1038
|
+
milestone["updated_at"].replace("Z", "+00:00")
|
|
1039
|
+
),
|
|
1040
|
+
metadata={
|
|
1041
|
+
"github": {
|
|
1042
|
+
"number": milestone["number"],
|
|
1043
|
+
"url": milestone["html_url"],
|
|
1044
|
+
"open_issues": milestone["open_issues"],
|
|
1045
|
+
"closed_issues": milestone["closed_issues"],
|
|
1046
|
+
}
|
|
1047
|
+
},
|
|
1048
|
+
)
|
|
1049
|
+
)
|
|
986
1050
|
|
|
987
1051
|
return epics
|
|
988
1052
|
|
|
@@ -994,7 +1058,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
994
1058
|
|
|
995
1059
|
response = await self.client.post(
|
|
996
1060
|
f"/repos/{self.owner}/{self.repo}/issues/{issue_number}/comments",
|
|
997
|
-
json={"body": comment}
|
|
1061
|
+
json={"body": comment},
|
|
998
1062
|
)
|
|
999
1063
|
|
|
1000
1064
|
return response.status_code == 201
|
|
@@ -1020,6 +1084,7 @@ class GitHubAdapter(BaseAdapter[Task]):
|
|
|
1020
1084
|
|
|
1021
1085
|
Returns:
|
|
1022
1086
|
Dictionary with PR details including number, url, and branch
|
|
1087
|
+
|
|
1023
1088
|
"""
|
|
1024
1089
|
try:
|
|
1025
1090
|
issue_number = int(ticket_id)
|
|
@@ -1105,7 +1170,7 @@ Fixes #{issue_number}
|
|
|
1105
1170
|
json={
|
|
1106
1171
|
"ref": f"refs/heads/{head_branch}",
|
|
1107
1172
|
"sha": base_sha,
|
|
1108
|
-
}
|
|
1173
|
+
},
|
|
1109
1174
|
)
|
|
1110
1175
|
|
|
1111
1176
|
if ref_response.status_code != 201:
|
|
@@ -1122,8 +1187,7 @@ Fixes #{issue_number}
|
|
|
1122
1187
|
}
|
|
1123
1188
|
|
|
1124
1189
|
pr_response = await self.client.post(
|
|
1125
|
-
f"/repos/{self.owner}/{self.repo}/pulls",
|
|
1126
|
-
json=pr_data
|
|
1190
|
+
f"/repos/{self.owner}/{self.repo}/pulls", json=pr_data
|
|
1127
1191
|
)
|
|
1128
1192
|
|
|
1129
1193
|
if pr_response.status_code == 422:
|
|
@@ -1134,7 +1198,7 @@ Fixes #{issue_number}
|
|
|
1134
1198
|
"head": f"{self.owner}:{head_branch}",
|
|
1135
1199
|
"base": base_branch,
|
|
1136
1200
|
"state": "open",
|
|
1137
|
-
}
|
|
1201
|
+
},
|
|
1138
1202
|
)
|
|
1139
1203
|
|
|
1140
1204
|
if search_response.status_code == 200:
|
|
@@ -1191,6 +1255,7 @@ Fixes #{issue_number}
|
|
|
1191
1255
|
|
|
1192
1256
|
Returns:
|
|
1193
1257
|
Dictionary with link status and PR details
|
|
1258
|
+
|
|
1194
1259
|
"""
|
|
1195
1260
|
try:
|
|
1196
1261
|
issue_number = int(ticket_id)
|
|
@@ -1200,6 +1265,7 @@ Fixes #{issue_number}
|
|
|
1200
1265
|
# Parse PR URL to extract owner, repo, and PR number
|
|
1201
1266
|
# Expected format: https://github.com/owner/repo/pull/123
|
|
1202
1267
|
import re
|
|
1268
|
+
|
|
1203
1269
|
pr_pattern = r"github\.com/([^/]+)/([^/]+)/pull/(\d+)"
|
|
1204
1270
|
match = re.search(pr_pattern, pr_url)
|
|
1205
1271
|
|
|
@@ -1239,7 +1305,7 @@ Fixes #{issue_number}
|
|
|
1239
1305
|
# Update the PR
|
|
1240
1306
|
update_response = await self.client.patch(
|
|
1241
1307
|
f"/repos/{self.owner}/{self.repo}/pulls/{pr_number}",
|
|
1242
|
-
json={"body": updated_body}
|
|
1308
|
+
json={"body": updated_body},
|
|
1243
1309
|
)
|
|
1244
1310
|
update_response.raise_for_status()
|
|
1245
1311
|
|
|
@@ -1268,4 +1334,4 @@ Fixes #{issue_number}
|
|
|
1268
1334
|
|
|
1269
1335
|
|
|
1270
1336
|
# Register the adapter
|
|
1271
|
-
AdapterRegistry.register("github", GitHubAdapter)
|
|
1337
|
+
AdapterRegistry.register("github", GitHubAdapter)
|