github-haystack 1.0.0__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.
- github_haystack-1.0.0.dist-info/METADATA +45 -0
- github_haystack-1.0.0.dist-info/RECORD +26 -0
- github_haystack-1.0.0.dist-info/WHEEL +4 -0
- github_haystack-1.0.0.dist-info/licenses/LICENSE.txt +73 -0
- haystack_integrations/components/connectors/github/__init__.py +19 -0
- haystack_integrations/components/connectors/github/file_editor.py +303 -0
- haystack_integrations/components/connectors/github/issue_commenter.py +161 -0
- haystack_integrations/components/connectors/github/issue_viewer.py +216 -0
- haystack_integrations/components/connectors/github/pr_creator.py +277 -0
- haystack_integrations/components/connectors/github/repo_forker.py +306 -0
- haystack_integrations/components/connectors/github/repo_viewer.py +272 -0
- haystack_integrations/prompts/github/__init__.py +22 -0
- haystack_integrations/prompts/github/context_prompt.py +176 -0
- haystack_integrations/prompts/github/file_editor_prompt.py +122 -0
- haystack_integrations/prompts/github/issue_commenter_prompt.py +21 -0
- haystack_integrations/prompts/github/issue_viewer_prompt.py +25 -0
- haystack_integrations/prompts/github/pr_creator_prompt.py +83 -0
- haystack_integrations/prompts/github/repo_viewer_prompt.py +81 -0
- haystack_integrations/prompts/github/system_prompt.py +63 -0
- haystack_integrations/tools/github/__init__.py +16 -0
- haystack_integrations/tools/github/file_editor_tool.py +128 -0
- haystack_integrations/tools/github/issue_commenter_tool.py +123 -0
- haystack_integrations/tools/github/issue_viewer_tool.py +123 -0
- haystack_integrations/tools/github/pr_creator_tool.py +117 -0
- haystack_integrations/tools/github/repo_viewer_tool.py +138 -0
- haystack_integrations/tools/github/utils.py +73 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from haystack import Document, component, default_from_dict, default_to_dict, logging
|
|
9
|
+
from haystack.utils import deserialize_secrets_inplace
|
|
10
|
+
from haystack.utils.auth import Secret
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@component
|
|
16
|
+
class GitHubIssueViewer:
|
|
17
|
+
"""
|
|
18
|
+
Fetches and parses GitHub issues into Haystack documents.
|
|
19
|
+
|
|
20
|
+
The component takes a GitHub issue URL and returns a list of documents where:
|
|
21
|
+
- First document contains the main issue content
|
|
22
|
+
- Subsequent documents contain the issue comments
|
|
23
|
+
|
|
24
|
+
### Usage example
|
|
25
|
+
```python
|
|
26
|
+
from haystack_integrations.components.connectors.github import GitHubIssueViewer
|
|
27
|
+
|
|
28
|
+
viewer = GitHubIssueViewer()
|
|
29
|
+
docs = viewer.run(
|
|
30
|
+
url="https://github.com/owner/repo/issues/123"
|
|
31
|
+
)["documents"]
|
|
32
|
+
|
|
33
|
+
print(docs)
|
|
34
|
+
```
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
*,
|
|
40
|
+
github_token: Optional[Secret] = None,
|
|
41
|
+
raise_on_failure: bool = True,
|
|
42
|
+
retry_attempts: int = 2,
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize the component.
|
|
46
|
+
|
|
47
|
+
:param github_token: GitHub personal access token for API authentication as a Secret
|
|
48
|
+
:param raise_on_failure: If True, raises exceptions on API errors
|
|
49
|
+
:param retry_attempts: Number of retry attempts for failed requests
|
|
50
|
+
"""
|
|
51
|
+
self.github_token = github_token
|
|
52
|
+
self.raise_on_failure = raise_on_failure
|
|
53
|
+
self.retry_attempts = retry_attempts
|
|
54
|
+
|
|
55
|
+
# Only set the basic headers during initialization
|
|
56
|
+
self.base_headers = {
|
|
57
|
+
"Accept": "application/vnd.github.v3+json",
|
|
58
|
+
"User-Agent": "Haystack/GitHubIssueViewer",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def _get_request_headers(self) -> dict:
|
|
62
|
+
"""
|
|
63
|
+
Get headers with resolved token for the request.
|
|
64
|
+
|
|
65
|
+
:return: Dictionary of headers including authorization if token is present
|
|
66
|
+
"""
|
|
67
|
+
headers = self.base_headers.copy()
|
|
68
|
+
if self.github_token:
|
|
69
|
+
headers["Authorization"] = f"Bearer {self.github_token.resolve_value()}"
|
|
70
|
+
return headers
|
|
71
|
+
|
|
72
|
+
def _parse_github_url(self, url: str) -> tuple[str, str, int]:
|
|
73
|
+
"""
|
|
74
|
+
Parse GitHub URL into owner, repo and issue number.
|
|
75
|
+
|
|
76
|
+
:param url: GitHub issue URL
|
|
77
|
+
:return: Tuple of (owner, repo, issue_number)
|
|
78
|
+
:raises ValueError: If URL format is invalid
|
|
79
|
+
"""
|
|
80
|
+
pattern = r"https?://github\.com/([^/]+)/([^/]+)/issues/(\d+)"
|
|
81
|
+
match = re.match(pattern, url)
|
|
82
|
+
if not match:
|
|
83
|
+
msg = f"Invalid GitHub issue URL format: {url}"
|
|
84
|
+
raise ValueError(msg)
|
|
85
|
+
|
|
86
|
+
owner, repo, issue_number = match.groups()
|
|
87
|
+
return owner, repo, int(issue_number)
|
|
88
|
+
|
|
89
|
+
def _fetch_issue(self, owner: str, repo: str, issue_number: int) -> Any:
|
|
90
|
+
"""
|
|
91
|
+
Fetch issue data from GitHub API.
|
|
92
|
+
|
|
93
|
+
:param owner: Repository owner
|
|
94
|
+
:param repo: Repository name
|
|
95
|
+
:param issue_number: Issue number
|
|
96
|
+
:return: Issue data dictionary
|
|
97
|
+
"""
|
|
98
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}"
|
|
99
|
+
response = requests.get(url, headers=self._get_request_headers(), timeout=10)
|
|
100
|
+
response.raise_for_status()
|
|
101
|
+
return response.json()
|
|
102
|
+
|
|
103
|
+
def _fetch_comments(self, comments_url: str) -> Any:
|
|
104
|
+
"""
|
|
105
|
+
Fetch issue comments from GitHub API.
|
|
106
|
+
|
|
107
|
+
:param comments_url: URL for issue comments
|
|
108
|
+
:return: List of comment dictionaries
|
|
109
|
+
"""
|
|
110
|
+
response = requests.get(comments_url, headers=self._get_request_headers(), timeout=10)
|
|
111
|
+
response.raise_for_status()
|
|
112
|
+
return response.json()
|
|
113
|
+
|
|
114
|
+
def _create_issue_document(self, issue_data: dict) -> Document:
|
|
115
|
+
"""
|
|
116
|
+
Create a Document from issue data.
|
|
117
|
+
|
|
118
|
+
:param issue_data: Issue data from GitHub API
|
|
119
|
+
:return: Haystack Document
|
|
120
|
+
"""
|
|
121
|
+
return Document( # type: ignore
|
|
122
|
+
content=issue_data["body"],
|
|
123
|
+
meta={
|
|
124
|
+
"type": "issue",
|
|
125
|
+
"title": issue_data["title"],
|
|
126
|
+
"number": issue_data["number"],
|
|
127
|
+
"state": issue_data["state"],
|
|
128
|
+
"created_at": issue_data["created_at"],
|
|
129
|
+
"updated_at": issue_data["updated_at"],
|
|
130
|
+
"author": issue_data["user"]["login"],
|
|
131
|
+
"url": issue_data["html_url"],
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def _create_comment_document(self, comment_data: dict, issue_number: int) -> Document:
|
|
136
|
+
"""
|
|
137
|
+
Create a Document from comment data.
|
|
138
|
+
|
|
139
|
+
:param comment_data: Comment data from GitHub API
|
|
140
|
+
:param issue_number: Parent issue number
|
|
141
|
+
:return: Haystack Document
|
|
142
|
+
"""
|
|
143
|
+
return Document(
|
|
144
|
+
content=comment_data["body"],
|
|
145
|
+
meta={
|
|
146
|
+
"type": "comment",
|
|
147
|
+
"issue_number": issue_number,
|
|
148
|
+
"created_at": comment_data["created_at"],
|
|
149
|
+
"updated_at": comment_data["updated_at"],
|
|
150
|
+
"author": comment_data["user"]["login"],
|
|
151
|
+
"url": comment_data["html_url"],
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
156
|
+
"""
|
|
157
|
+
Serialize the component to a dictionary.
|
|
158
|
+
|
|
159
|
+
:returns: Dictionary with serialized data.
|
|
160
|
+
"""
|
|
161
|
+
return default_to_dict(
|
|
162
|
+
self,
|
|
163
|
+
github_token=self.github_token.to_dict() if self.github_token else None,
|
|
164
|
+
raise_on_failure=self.raise_on_failure,
|
|
165
|
+
retry_attempts=self.retry_attempts,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@classmethod
|
|
169
|
+
def from_dict(cls, data: Dict[str, Any]) -> "GitHubIssueViewer":
|
|
170
|
+
"""
|
|
171
|
+
Deserialize the component from a dictionary.
|
|
172
|
+
|
|
173
|
+
:param data: Dictionary to deserialize from.
|
|
174
|
+
:returns: Deserialized component.
|
|
175
|
+
"""
|
|
176
|
+
init_params = data["init_parameters"]
|
|
177
|
+
deserialize_secrets_inplace(init_params, keys=["github_token"])
|
|
178
|
+
return default_from_dict(cls, data)
|
|
179
|
+
|
|
180
|
+
@component.output_types(documents=List[Document])
|
|
181
|
+
def run(self, url: str) -> dict:
|
|
182
|
+
"""
|
|
183
|
+
Process a GitHub issue URL and return documents.
|
|
184
|
+
|
|
185
|
+
:param url: GitHub issue URL
|
|
186
|
+
:return: Dictionary containing list of documents
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
owner, repo, issue_number = self._parse_github_url(url)
|
|
190
|
+
|
|
191
|
+
# Fetch issue data
|
|
192
|
+
issue_data = self._fetch_issue(owner, repo, issue_number)
|
|
193
|
+
documents = [self._create_issue_document(issue_data)]
|
|
194
|
+
|
|
195
|
+
# Fetch and process comments if they exist
|
|
196
|
+
if issue_data["comments"] > 0:
|
|
197
|
+
comments = self._fetch_comments(issue_data["comments_url"])
|
|
198
|
+
documents.extend(self._create_comment_document(comment, issue_number) for comment in comments)
|
|
199
|
+
|
|
200
|
+
return {"documents": documents}
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
if self.raise_on_failure:
|
|
204
|
+
raise
|
|
205
|
+
|
|
206
|
+
error_message = f"Error processing GitHub issue {url}: {e!s}"
|
|
207
|
+
logger.warning(error_message)
|
|
208
|
+
error_doc = Document(
|
|
209
|
+
content=error_message,
|
|
210
|
+
meta={
|
|
211
|
+
"error": True,
|
|
212
|
+
"type": "error",
|
|
213
|
+
"url": url,
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
return {"documents": [error_doc]}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from haystack import component, default_from_dict, default_to_dict, logging
|
|
9
|
+
from haystack.utils import Secret, deserialize_secrets_inplace
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@component
|
|
15
|
+
class GitHubPRCreator:
|
|
16
|
+
"""
|
|
17
|
+
A Haystack component for creating pull requests from a fork back to the original repository.
|
|
18
|
+
|
|
19
|
+
Uses the authenticated user's fork to create the PR and links it to an existing issue.
|
|
20
|
+
|
|
21
|
+
### Usage example
|
|
22
|
+
```python
|
|
23
|
+
from haystack_integrations.components.connectors.github import GitHubPRCreator
|
|
24
|
+
from haystack.utils import Secret
|
|
25
|
+
|
|
26
|
+
pr_creator = GitHubPRCreator(
|
|
27
|
+
github_token=Secret.from_env_var("GITHUB_TOKEN") # Token from the fork owner
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Create a PR from your fork
|
|
31
|
+
result = pr_creator.run(
|
|
32
|
+
issue_url="https://github.com/owner/repo/issues/123",
|
|
33
|
+
title="Fix issue #123",
|
|
34
|
+
body="This PR addresses issue #123",
|
|
35
|
+
branch="feature-branch", # The branch in your fork with the changes
|
|
36
|
+
base="main" # The branch in the original repo to merge into
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, *, github_token: Secret = Secret.from_env_var("GITHUB_TOKEN"), raise_on_failure: bool = True):
|
|
42
|
+
"""
|
|
43
|
+
Initialize the component.
|
|
44
|
+
|
|
45
|
+
:param github_token: GitHub personal access token for authentication (from the fork owner)
|
|
46
|
+
:param raise_on_failure: If True, raises exceptions on API errors
|
|
47
|
+
"""
|
|
48
|
+
if not isinstance(github_token, Secret):
|
|
49
|
+
msg = "github_token must be a Secret"
|
|
50
|
+
raise TypeError(msg)
|
|
51
|
+
|
|
52
|
+
self.github_token = github_token
|
|
53
|
+
self.raise_on_failure = raise_on_failure
|
|
54
|
+
|
|
55
|
+
self.base_headers = {
|
|
56
|
+
"Accept": "application/vnd.github.v3+json",
|
|
57
|
+
"User-Agent": "Haystack/GitHubPRCreator",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
def _get_request_headers(self) -> dict:
|
|
61
|
+
"""
|
|
62
|
+
Get headers with resolved token for the request.
|
|
63
|
+
|
|
64
|
+
:return: Dictionary of headers including authorization if token is present
|
|
65
|
+
"""
|
|
66
|
+
headers = self.base_headers.copy()
|
|
67
|
+
if self.github_token is not None:
|
|
68
|
+
headers["Authorization"] = f"Bearer {self.github_token.resolve_value()}"
|
|
69
|
+
return headers
|
|
70
|
+
|
|
71
|
+
def _parse_issue_url(self, issue_url: str) -> tuple[str, str, str]:
|
|
72
|
+
"""
|
|
73
|
+
Parse owner, repo name, and issue number from GitHub issue URL.
|
|
74
|
+
|
|
75
|
+
:param issue_url: Full GitHub issue URL
|
|
76
|
+
:return: Tuple of (owner, repo_name, issue_number)
|
|
77
|
+
:raises ValueError: If URL format is invalid
|
|
78
|
+
"""
|
|
79
|
+
pattern = r"https://github\.com/([^/]+)/([^/]+)/issues/(\d+)"
|
|
80
|
+
match = re.match(pattern, issue_url)
|
|
81
|
+
if not match:
|
|
82
|
+
msg = "Invalid GitHub issue URL format"
|
|
83
|
+
raise ValueError(msg)
|
|
84
|
+
return match.group(1), match.group(2), match.group(3)
|
|
85
|
+
|
|
86
|
+
def _get_authenticated_user(self) -> str:
|
|
87
|
+
"""Get the username of the authenticated user (fork owner)."""
|
|
88
|
+
response = requests.get("https://api.github.com/user", headers=self._get_request_headers(), timeout=10)
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
return response.json()["login"]
|
|
91
|
+
|
|
92
|
+
def _check_fork_exists(self, repo: str, fork_owner: str) -> bool:
|
|
93
|
+
"""Check if the fork exists."""
|
|
94
|
+
url = f"https://api.github.com/repos/{fork_owner}/{repo}"
|
|
95
|
+
try:
|
|
96
|
+
response = requests.get(url, headers=self._get_request_headers(), timeout=10)
|
|
97
|
+
response.raise_for_status()
|
|
98
|
+
fork_data = response.json()
|
|
99
|
+
return fork_data.get("fork", False)
|
|
100
|
+
except requests.RequestException:
|
|
101
|
+
return False
|
|
102
|
+
|
|
103
|
+
def _create_fork(self, owner: str, repo: str) -> Optional[str]:
|
|
104
|
+
"""Create a fork of the repository."""
|
|
105
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/forks"
|
|
106
|
+
try:
|
|
107
|
+
response = requests.post(url, headers=self._get_request_headers(), timeout=10)
|
|
108
|
+
response.raise_for_status()
|
|
109
|
+
fork_data = response.json()
|
|
110
|
+
return fork_data["owner"]["login"]
|
|
111
|
+
except requests.RequestException as e:
|
|
112
|
+
if self.raise_on_failure:
|
|
113
|
+
msg = f"Failed to create fork: {e!s}"
|
|
114
|
+
raise RuntimeError(msg) from e
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
def _create_branch(self, owner: str, repo: str, branch_name: str, base_branch: str) -> bool:
|
|
118
|
+
"""Create a new branch in the repository."""
|
|
119
|
+
# Get the SHA of the base branch
|
|
120
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{base_branch}"
|
|
121
|
+
try:
|
|
122
|
+
response = requests.get(url, headers=self._get_request_headers(), timeout=10)
|
|
123
|
+
response.raise_for_status()
|
|
124
|
+
base_sha = response.json()["object"]["sha"]
|
|
125
|
+
|
|
126
|
+
# Create the new branch
|
|
127
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/git/refs"
|
|
128
|
+
data = {"ref": f"refs/heads/{branch_name}", "sha": base_sha}
|
|
129
|
+
response = requests.post(url, headers=self._get_request_headers(), json=data, timeout=10)
|
|
130
|
+
response.raise_for_status()
|
|
131
|
+
return True
|
|
132
|
+
except requests.RequestException as e:
|
|
133
|
+
if self.raise_on_failure:
|
|
134
|
+
msg = f"Failed to create branch: {e!s}"
|
|
135
|
+
raise RuntimeError(msg) from e
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def _create_commit(
|
|
139
|
+
self,
|
|
140
|
+
owner: str,
|
|
141
|
+
repo: str,
|
|
142
|
+
branch_name: str,
|
|
143
|
+
file_path: str,
|
|
144
|
+
content: str,
|
|
145
|
+
message: str,
|
|
146
|
+
) -> bool:
|
|
147
|
+
"""Create a commit with the file changes."""
|
|
148
|
+
# Get the current commit SHA
|
|
149
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch_name}"
|
|
150
|
+
try:
|
|
151
|
+
response = requests.get(url, headers=self._get_request_headers(), timeout=10)
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
current_sha = response.json()["object"]["sha"]
|
|
154
|
+
|
|
155
|
+
# Create a blob with the file content
|
|
156
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/git/blobs"
|
|
157
|
+
data: dict[str, Any] = {"content": content, "encoding": "base64"}
|
|
158
|
+
response = requests.post(url, headers=self._get_request_headers(), json=data, timeout=10)
|
|
159
|
+
response.raise_for_status()
|
|
160
|
+
blob_sha = response.json()["sha"]
|
|
161
|
+
|
|
162
|
+
# Create a tree with the new file
|
|
163
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/git/trees"
|
|
164
|
+
data = {
|
|
165
|
+
"base_tree": current_sha,
|
|
166
|
+
"tree": [{"path": file_path, "mode": "100644", "type": "blob", "sha": blob_sha}],
|
|
167
|
+
}
|
|
168
|
+
response = requests.post(url, headers=self._get_request_headers(), json=data, timeout=10)
|
|
169
|
+
response.raise_for_status()
|
|
170
|
+
tree_sha = response.json()["sha"]
|
|
171
|
+
|
|
172
|
+
# Create the commit
|
|
173
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/git/commits"
|
|
174
|
+
data = {"message": message, "tree": tree_sha, "parents": [current_sha]}
|
|
175
|
+
response = requests.post(url, headers=self._get_request_headers(), json=data, timeout=10)
|
|
176
|
+
response.raise_for_status()
|
|
177
|
+
commit_sha = response.json()["sha"]
|
|
178
|
+
|
|
179
|
+
# Update the branch reference
|
|
180
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/git/refs/heads/{branch_name}"
|
|
181
|
+
data = {"sha": commit_sha}
|
|
182
|
+
response = requests.patch(url, headers=self._get_request_headers(), json=data, timeout=10)
|
|
183
|
+
response.raise_for_status()
|
|
184
|
+
return True
|
|
185
|
+
except requests.RequestException as e:
|
|
186
|
+
if self.raise_on_failure:
|
|
187
|
+
msg = f"Failed to create commit: {e!s}"
|
|
188
|
+
raise RuntimeError(msg) from e
|
|
189
|
+
return False
|
|
190
|
+
|
|
191
|
+
def _create_pull_request(
|
|
192
|
+
self,
|
|
193
|
+
owner: str,
|
|
194
|
+
repo: str,
|
|
195
|
+
branch_name: str,
|
|
196
|
+
base_branch: str,
|
|
197
|
+
title: str,
|
|
198
|
+
body: str,
|
|
199
|
+
) -> bool:
|
|
200
|
+
"""Create a pull request."""
|
|
201
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/pulls"
|
|
202
|
+
data = {"title": title, "body": body, "head": branch_name, "base": base_branch}
|
|
203
|
+
try:
|
|
204
|
+
response = requests.post(url, headers=self._get_request_headers(), json=data, timeout=10)
|
|
205
|
+
response.raise_for_status()
|
|
206
|
+
return True
|
|
207
|
+
except requests.RequestException as e:
|
|
208
|
+
if self.raise_on_failure:
|
|
209
|
+
msg = f"Failed to create pull request: {e!s}"
|
|
210
|
+
raise RuntimeError(msg) from e
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
@component.output_types(result=str)
|
|
214
|
+
def run(
|
|
215
|
+
self, issue_url: str, title: str, branch: str, base: str, body: str = "", draft: bool = False
|
|
216
|
+
) -> Dict[str, str]:
|
|
217
|
+
"""
|
|
218
|
+
Create a new pull request from your fork to the original repository, linked to the specified issue.
|
|
219
|
+
|
|
220
|
+
:param issue_url: URL of the GitHub issue to link the PR to
|
|
221
|
+
:param title: Title of the pull request
|
|
222
|
+
:param branch: Name of the branch in your fork where changes are implemented
|
|
223
|
+
:param base: Name of the branch in the original repo you want to merge into
|
|
224
|
+
:param body: Additional content for the pull request description
|
|
225
|
+
:param draft: Whether to create a draft pull request
|
|
226
|
+
:return: Dictionary containing operation result
|
|
227
|
+
"""
|
|
228
|
+
try:
|
|
229
|
+
# Parse repository information from issue URL
|
|
230
|
+
owner, repo_name, issue_number = self._parse_issue_url(issue_url)
|
|
231
|
+
|
|
232
|
+
# Get the authenticated user (fork owner)
|
|
233
|
+
fork_owner = self._get_authenticated_user()
|
|
234
|
+
|
|
235
|
+
# Check if the fork exists
|
|
236
|
+
if not self._check_fork_exists(repo_name, fork_owner):
|
|
237
|
+
return {"result": f"Error: Fork not found at {fork_owner}/{repo_name}"}
|
|
238
|
+
|
|
239
|
+
url = f"https://api.github.com/repos/{owner}/{repo_name}/pulls"
|
|
240
|
+
|
|
241
|
+
# For cross-repository PRs, head must be in the format username:branch
|
|
242
|
+
head = f"{fork_owner}:{branch}"
|
|
243
|
+
|
|
244
|
+
pr_data = {
|
|
245
|
+
"title": title,
|
|
246
|
+
"body": body,
|
|
247
|
+
"head": head,
|
|
248
|
+
"base": base,
|
|
249
|
+
"draft": draft,
|
|
250
|
+
"maintainer_can_modify": True, # Allow maintainers to modify the PR
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
response = requests.post(url, headers=self._get_request_headers(), json=pr_data, timeout=10)
|
|
254
|
+
response.raise_for_status()
|
|
255
|
+
pr_number = response.json()["number"]
|
|
256
|
+
|
|
257
|
+
return {"result": f"Pull request #{pr_number} created successfully and linked to issue #{issue_number}"}
|
|
258
|
+
|
|
259
|
+
except (requests.RequestException, ValueError) as e:
|
|
260
|
+
if self.raise_on_failure:
|
|
261
|
+
raise
|
|
262
|
+
return {"result": f"Error: {e!s}"}
|
|
263
|
+
|
|
264
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
265
|
+
"""Serialize the component to a dictionary."""
|
|
266
|
+
return default_to_dict(
|
|
267
|
+
self,
|
|
268
|
+
github_token=self.github_token.to_dict() if self.github_token else None,
|
|
269
|
+
raise_on_failure=self.raise_on_failure,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
@classmethod
|
|
273
|
+
def from_dict(cls, data: Dict[str, Any]) -> "GitHubPRCreator":
|
|
274
|
+
"""Deserialize the component from a dictionary."""
|
|
275
|
+
init_params = data["init_parameters"]
|
|
276
|
+
deserialize_secrets_inplace(init_params, keys=["github_token"])
|
|
277
|
+
return default_from_dict(cls, data)
|