github-haystack 2.0.0__tar.gz → 3.0.0__tar.gz

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.
Files changed (50) hide show
  1. github_haystack-3.0.0/CHANGELOG.md +102 -0
  2. {github_haystack-2.0.0 → github_haystack-3.0.0}/LICENSE.txt +1 -1
  3. {github_haystack-2.0.0 → github_haystack-3.0.0}/PKG-INFO +5 -4
  4. github_haystack-3.0.0/pydoc/config_docusaurus.yml +18 -0
  5. {github_haystack-2.0.0 → github_haystack-3.0.0}/pyproject.toml +19 -5
  6. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/components/connectors/github/file_editor.py +2 -2
  7. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/components/connectors/github/issue_commenter.py +1 -1
  8. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/components/connectors/github/issue_viewer.py +1 -1
  9. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/components/connectors/github/pr_creator.py +7 -2
  10. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/components/connectors/github/repo_forker.py +1 -1
  11. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/components/connectors/github/repo_viewer.py +1 -1
  12. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/prompts/github/context_prompt.py +0 -5
  13. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/tools/github/file_editor_tool.py +1 -1
  14. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/tools/github/issue_commenter_tool.py +1 -1
  15. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/tools/github/issue_viewer_tool.py +1 -1
  16. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/tools/github/pr_creator_tool.py +1 -1
  17. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/tools/github/repo_forker_tool.py +1 -1
  18. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/tools/github/repo_viewer_tool.py +1 -1
  19. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/tools/github/utils.py +2 -2
  20. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_file_editor.py +91 -0
  21. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_file_editor_tool.py +18 -11
  22. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_pr_creator.py +79 -0
  23. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_repo_forker.py +54 -0
  24. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_repo_viewer.py +49 -0
  25. github_haystack-3.0.0/tests/test_utils.py +63 -0
  26. github_haystack-2.0.0/CHANGELOG.md +0 -52
  27. github_haystack-2.0.0/pydoc/config_docusaurus.yml +0 -33
  28. {github_haystack-2.0.0 → github_haystack-3.0.0}/.gitignore +0 -0
  29. {github_haystack-2.0.0 → github_haystack-3.0.0}/README.md +0 -0
  30. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/components/connectors/github/__init__.py +0 -0
  31. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/components/connectors/py.typed +0 -0
  32. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/prompts/github/__init__.py +0 -0
  33. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/prompts/github/file_editor_prompt.py +0 -0
  34. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/prompts/github/issue_commenter_prompt.py +0 -0
  35. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/prompts/github/issue_viewer_prompt.py +0 -0
  36. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/prompts/github/pr_creator_prompt.py +0 -0
  37. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/prompts/github/repo_forker_prompt.py +0 -0
  38. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/prompts/github/repo_viewer_prompt.py +0 -0
  39. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/prompts/github/system_prompt.py +0 -0
  40. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/prompts/py.typed +0 -0
  41. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/tools/github/__init__.py +0 -0
  42. {github_haystack-2.0.0 → github_haystack-3.0.0}/src/haystack_integrations/tools/py.typed +0 -0
  43. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/__init__.py +0 -0
  44. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_issue_commenter.py +0 -0
  45. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_issue_commenter_tool.py +0 -0
  46. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_issue_viewer.py +0 -0
  47. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_issue_viewer_tool.py +0 -0
  48. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_pr_creator_tool.py +0 -0
  49. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_repo_forker_tool.py +0 -0
  50. {github_haystack-2.0.0 → github_haystack-3.0.0}/tests/test_repo_viewer_tool.py +0 -0
@@ -0,0 +1,102 @@
1
+ # Changelog
2
+
3
+ ## [integrations/github-v2.1.0] - 2026-05-13
4
+
5
+ ### 📚 Documentation
6
+
7
+ - Simplify pydoc configs (#2855)
8
+
9
+ ### 🧪 Testing
10
+
11
+ - GitHub - make tests compatible with new Agent parameters (#2865)
12
+ - Test compatible integrations with python 3.14; update pyproject (#3001)
13
+ - Track test coverage for all integrations (#3065)
14
+ - Add unit tests for github + fix small handler bug (#3280)
15
+
16
+ ### 🧹 Chores
17
+
18
+ - Add ANN type annotations to faiss, fastembed, firecrawl, github, google_ai (#2988)
19
+ - Enforce ruff docstring rules (D102/D103/D205/D209/D213/D417/D419) in integrations 11-20 (#3009)
20
+
21
+ ### 🌀 Miscellaneous
22
+
23
+ - Test: Fix github tests due to changes on Haystack main (#2778)
24
+
25
+ ## [integrations/github-v2.0.0] - 2026-01-12
26
+
27
+ ### 📚 Documentation
28
+
29
+ - Add pydoc configurations for Docusaurus (#2411)
30
+
31
+ ### ⚙️ CI
32
+
33
+ - Change pytest command (#2475)
34
+
35
+ ### 🧹 Chores
36
+
37
+ - Remove Readme API CI workflow and configs (#2573)
38
+ - Fix nightly test (#2580)
39
+ - Make fmt command more forgiving (#2671)
40
+ - [**breaking**] Github - drop Python 3.9 and use X|Y typing (#2705)
41
+
42
+ ### 🌀 Miscellaneous
43
+
44
+ - Adopt PEP 585 type hinting (part 3) (#2510)
45
+ - Chore: Fix tool validation test failures in github integration (#2659)
46
+ - Fix: Fix github integration unit tests (#2703)
47
+
48
+ ## [integrations/github-v1.2.1] - 2025-10-20
49
+
50
+
51
+ ### 🌀 Miscellaneous
52
+
53
+ - Fix: handle empty GitHub tokens in authentication headers (#2400)
54
+
55
+ ## [integrations/github-v1.2.0] - 2025-10-10
56
+
57
+ ### 🚀 Features
58
+
59
+ - Add support for GitHubRepoForkerTool (#1968)
60
+
61
+ ### 🧪 Testing
62
+
63
+ - Fix GitHub test (#2119)
64
+
65
+ ### 🧹 Chores
66
+
67
+ - Standardize readmes - part 2 (#2205)
68
+
69
+ ### 🌀 Miscellaneous
70
+
71
+ - Chore: Fix linting github (#2255)
72
+
73
+ ## [integrations/github-v1.1.0] - 2025-07-01
74
+
75
+ ### 🐛 Bug Fixes
76
+
77
+ - Fix Github types + add py.typed (#2024)
78
+
79
+ ### 🧹 Chores
80
+
81
+ - Fix linting for ruff 0.12.0 (#1969)
82
+ - Remove black (#1985)
83
+
84
+
85
+ ## [integrations/github-v1.0.1] - 2025-06-12
86
+
87
+ ### 🐛 Bug Fixes
88
+
89
+ - Prompts and schemas of GitHubIssueCommenterTool and GitHubIssueViewerTool now properly explain url parameter (#1929)
90
+
91
+
92
+ ### 🧹 Chores
93
+
94
+ - Align core-integrations Hatch scripts (#1898)
95
+
96
+ ## [integrations/github-v1.0.0] - 2025-05-28
97
+
98
+ ### 🚀 Features
99
+
100
+ - Add GitHub integration with components, tools, and prompts (#1637)
101
+
102
+ <!-- generated by git-cliff -->
@@ -58,7 +58,7 @@ APPENDIX: How to apply the Apache License to your work.
58
58
 
59
59
  To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
60
60
 
61
- Copyright [yyyy] [name of copyright owner]
61
+ Copyright 2023-present deepset GmbH
62
62
 
63
63
  Licensed under the Apache License, Version 2.0 (the "License");
64
64
  you may not use this file except in compliance with the License.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: github-haystack
3
- Version: 2.0.0
3
+ Version: 3.0.0
4
4
  Summary: Haystack components for interacting with GitHub repositories
5
5
  Project-URL: Source, https://github.com/deepset-ai/haystack-core-integrations/github
6
6
  Project-URL: Documentation, https://github.com/deepset-ai/haystack-core-integrations/blob/main/integrations/github/README.md
@@ -11,13 +11,14 @@ License-File: LICENSE.txt
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: License :: OSI Approved :: Apache Software License
13
13
  Classifier: Programming Language :: Python
14
- Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
16
15
  Classifier: Programming Language :: Python :: 3.12
17
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3.15
18
19
  Classifier: Programming Language :: Python :: Implementation :: CPython
19
20
  Classifier: Programming Language :: Python :: Implementation :: PyPy
20
- Requires-Python: >=3.10
21
+ Requires-Python: >=3.11
21
22
  Requires-Dist: haystack-ai>=2.22.0
22
23
  Description-Content-Type: text/markdown
23
24
 
@@ -0,0 +1,18 @@
1
+ loaders:
2
+ - modules:
3
+ - haystack_integrations.components.connectors.github.file_editor
4
+ - haystack_integrations.components.connectors.github.issue_commenter
5
+ - haystack_integrations.components.connectors.github.issue_viewer
6
+ - haystack_integrations.components.connectors.github.pr_creator
7
+ - haystack_integrations.components.connectors.github.repo_viewer
8
+ - haystack_integrations.components.connectors.github.repo_forker
9
+ search_path: [../src]
10
+ processors:
11
+ - type: filter
12
+ documented_only: true
13
+ skip_empty_modules: true
14
+ renderer:
15
+ description: GitHub integration for Haystack
16
+ id: integrations-github
17
+ filename: github.md
18
+ title: GitHub
@@ -7,7 +7,7 @@ name = "github-haystack"
7
7
  dynamic = ["version"]
8
8
  description = 'Haystack components for interacting with GitHub repositories'
9
9
  readme = "README.md"
10
- requires-python = ">=3.10"
10
+ requires-python = ">=3.11"
11
11
  license = "Apache-2.0"
12
12
  keywords = []
13
13
  authors = [{ name = "deepset GmbH", email = "info@deepset.ai" }]
@@ -15,10 +15,11 @@ classifiers = [
15
15
  "License :: OSI Approved :: Apache Software License",
16
16
  "Development Status :: 4 - Beta",
17
17
  "Programming Language :: Python",
18
- "Programming Language :: Python :: 3.10",
19
18
  "Programming Language :: Python :: 3.11",
20
19
  "Programming Language :: Python :: 3.12",
21
20
  "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
22
+ "Programming Language :: Python :: 3.15",
22
23
  "Programming Language :: Python :: Implementation :: CPython",
23
24
  "Programming Language :: Python :: Implementation :: PyPy",
24
25
  ]
@@ -45,7 +46,7 @@ installer = "uv"
45
46
  dependencies = ["haystack-pydoc-tools", "ruff"]
46
47
 
47
48
  [tool.hatch.envs.default.scripts]
48
- docs = ["pydoc-markdown pydoc/config_docusaurus.yml"]
49
+ docs = ["haystack-pydoc pydoc/config_docusaurus.yml"]
49
50
  fmt = "ruff check --fix {args}; ruff format {args}"
50
51
  fmt-check = "ruff check {args} && ruff format --check {args}"
51
52
 
@@ -63,7 +64,8 @@ dependencies = [
63
64
  unit = 'pytest -m "not integration" {args:tests}'
64
65
  integration = 'pytest -m "integration" {args:tests}'
65
66
  all = 'pytest {args:tests}'
66
- cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x {args:tests}'
67
+ unit-cov-retry = 'pytest --cov=haystack_integrations --reruns 3 --reruns-delay 30 -x -m "not integration" {args:tests}'
68
+ integration-cov-append-retry = 'pytest --cov=haystack_integrations --cov-append --reruns 3 --reruns-delay 30 -x -m "integration" {args:tests}'
67
69
 
68
70
  types = """mypy -p haystack_integrations.components.connectors.github \
69
71
  -p haystack_integrations.prompts.github \
@@ -81,9 +83,17 @@ line-length = 120
81
83
  [tool.ruff.lint]
82
84
  select = [
83
85
  "A",
86
+ "ANN",
84
87
  "ARG",
85
88
  "B",
86
89
  "C",
90
+ "D102", # Missing docstring in public method
91
+ "D103", # Missing docstring in public function
92
+ "D205", # 1 blank line required between summary line and description
93
+ "D209", # Closing triple quotes go to new line
94
+ "D213", # summary lines must be positioned on the second physical line of the docstring
95
+ "D417", # Missing argument descriptions in the docstring
96
+ "D419", # Docstring is empty
87
97
  "DTZ",
88
98
  "E",
89
99
  "EM",
@@ -118,9 +128,12 @@ ignore = [
118
128
  "PLR0912",
119
129
  "PLR0913",
120
130
  "PLR0915",
131
+ "PLR0917",
121
132
  # Misc
122
133
  "B008",
123
134
  "S101",
135
+ # Allow `Any` - used legitimately for dynamic types and SDK boundaries
136
+ "ANN401",
124
137
  ]
125
138
 
126
139
  [tool.ruff.lint.isort]
@@ -131,13 +144,14 @@ ban-relative-imports = "parents"
131
144
 
132
145
  [tool.ruff.lint.per-file-ignores]
133
146
  # Tests can use magic values, assertions, and relative imports
134
- "tests/**/*" = ["PLR2004", "S101", "TID252"]
147
+ "tests/**/*" = ["D", "PLR2004", "S101", "TID252", "ANN"]
135
148
  # Ignore RUF001 for all files in the prompts directory
136
149
  "src/haystack_integrations/components/prompts/**/*" = ["RUF001"]
137
150
 
138
151
  [tool.coverage.run]
139
152
  source = ["haystack_integrations"]
140
153
  branch = true
154
+ relative_files = true
141
155
  parallel = false
142
156
 
143
157
 
@@ -12,7 +12,7 @@ from haystack.utils import Secret, deserialize_secrets_inplace
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
14
 
15
- class Command(str, Enum):
15
+ class Command(str, Enum): # noqa: UP042 # StrEnum would change str() of this public enum
16
16
  """
17
17
  Available commands for file operations in GitHub.
18
18
 
@@ -82,7 +82,7 @@ class GitHubFileEditor:
82
82
  repo: str | None = None,
83
83
  branch: str = "main",
84
84
  raise_on_failure: bool = True,
85
- ):
85
+ ) -> None:
86
86
  """
87
87
  Initialize the component.
88
88
 
@@ -41,7 +41,7 @@ class GitHubIssueCommenter:
41
41
  github_token: Secret = Secret.from_env_var("GITHUB_TOKEN"),
42
42
  raise_on_failure: bool = True,
43
43
  retry_attempts: int = 2,
44
- ):
44
+ ) -> None:
45
45
  """
46
46
  Initialize the component.
47
47
 
@@ -40,7 +40,7 @@ class GitHubIssueViewer:
40
40
  github_token: Secret | None = None,
41
41
  raise_on_failure: bool = True,
42
42
  retry_attempts: int = 2,
43
- ):
43
+ ) -> None:
44
44
  """
45
45
  Initialize the component.
46
46
 
@@ -38,7 +38,12 @@ class GitHubPRCreator:
38
38
  ```
39
39
  """
40
40
 
41
- def __init__(self, *, github_token: Secret = Secret.from_env_var("GITHUB_TOKEN"), raise_on_failure: bool = True):
41
+ def __init__(
42
+ self,
43
+ *,
44
+ github_token: Secret = Secret.from_env_var("GITHUB_TOKEN"),
45
+ raise_on_failure: bool = True,
46
+ ) -> None:
42
47
  """
43
48
  Initialize the component.
44
49
 
@@ -243,7 +248,7 @@ class GitHubPRCreator:
243
248
  # For cross-repository PRs, head must be in the format username:branch
244
249
  head = f"{fork_owner}:{branch}"
245
250
 
246
- pr_data = {
251
+ pr_data: dict[str, Any] = {
247
252
  "title": title,
248
253
  "body": body,
249
254
  "head": head,
@@ -48,7 +48,7 @@ class GitHubRepoForker:
48
48
  poll_interval: int = 2,
49
49
  auto_sync: bool = True,
50
50
  create_branch: bool = True,
51
- ):
51
+ ) -> None:
52
52
  """
53
53
  Initialize the component.
54
54
 
@@ -76,7 +76,7 @@ class GitHubRepoViewer:
76
76
  max_file_size: int = 1_000_000, # 1MB default limit
77
77
  repo: str | None = None,
78
78
  branch: str = "main",
79
- ):
79
+ ) -> None:
80
80
  """
81
81
  Initialize the component.
82
82
 
@@ -168,9 +168,4 @@ This is a mono-repo maintained by the deepset-Team that contains integrations fo
168
168
  Typically, an integration consists of one or more components. Some integrations only contain document stores.
169
169
  Each integration is a standalone pypi-package but you can find all of them in the core integrations repo.
170
170
 
171
-
172
- 3. "deepset-ai/haystack-experimental"
173
-
174
- Contains experimental features for the Haystack framework.
175
-
176
171
  """
@@ -31,7 +31,7 @@ class GitHubFileEditorTool(ComponentTool):
31
31
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
32
32
  inputs_from_state: dict[str, str] | None = None,
33
33
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
34
- ):
34
+ ) -> None:
35
35
  """
36
36
  Initialize the GitHub file editor tool.
37
37
 
@@ -30,7 +30,7 @@ class GitHubIssueCommenterTool(ComponentTool):
30
30
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
31
31
  inputs_from_state: dict[str, str] | None = None,
32
32
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
33
- ):
33
+ ) -> None:
34
34
  """
35
35
  Initialize the GitHub issue commenter tool.
36
36
 
@@ -30,7 +30,7 @@ class GitHubIssueViewerTool(ComponentTool):
30
30
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
31
31
  inputs_from_state: dict[str, str] | None = None,
32
32
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
33
- ):
33
+ ) -> None:
34
34
  """
35
35
  Initialize the GitHub issue viewer tool.
36
36
 
@@ -29,7 +29,7 @@ class GitHubPRCreatorTool(ComponentTool):
29
29
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
30
30
  inputs_from_state: dict[str, str] | None = None,
31
31
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
32
- ):
32
+ ) -> None:
33
33
  """
34
34
  Initialize the GitHub PR creator tool.
35
35
 
@@ -29,7 +29,7 @@ class GitHubRepoForkerTool(ComponentTool):
29
29
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
30
30
  inputs_from_state: dict[str, str] | None = None,
31
31
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
32
- ):
32
+ ) -> None:
33
33
  """
34
34
  Initialize the GitHub Repo Forker tool.
35
35
 
@@ -32,7 +32,7 @@ class GitHubRepoViewerTool(ComponentTool):
32
32
  outputs_to_string: dict[str, str | Callable[[Any], str]] | None = None,
33
33
  inputs_from_state: dict[str, str] | None = None,
34
34
  outputs_to_state: dict[str, dict[str, str | Callable]] | None = None,
35
- ):
35
+ ) -> None:
36
36
  """
37
37
  Initialize the GitHub repository viewer tool.
38
38
 
@@ -21,7 +21,7 @@ def message_handler(documents: list[Document], max_length: int = 150_000) -> str
21
21
  result_str = ""
22
22
  for document in documents:
23
23
  if document.meta["type"] in ["file", "dir", "error"]:
24
- result_str += document.content or "" + "\n"
24
+ result_str += (document.content or "") + "\n"
25
25
  else:
26
26
  result_str += f"File Content for {document.meta['path']}\n\n"
27
27
  result_str += document.content or ""
@@ -29,7 +29,7 @@ def message_handler(documents: list[Document], max_length: int = 150_000) -> str
29
29
  if len(result_str) > max_length:
30
30
  result_str = result_str[:max_length] + "...(large file can't be fully displayed)"
31
31
 
32
- return result_str
32
+ return result_str.strip()
33
33
 
34
34
 
35
35
  def serialize_handlers(
@@ -280,3 +280,94 @@ class TestGitHubFileEditor:
280
280
  assert "Authorization" not in headers
281
281
  assert headers["Accept"] == "application/vnd.github.v3+json"
282
282
  assert headers["User-Agent"] == "Haystack/GitHubFileEditor"
283
+
284
+ @pytest.mark.parametrize(
285
+ "file_b64,original,expected",
286
+ [
287
+ ("SGVsbG8gV29ybGQ=", "missing", "Error: Original string not found in file"),
288
+ ("SGVsbG8gSGVsbG8=", "Hello", "Error: Original string appears multiple times. Please provide more context"),
289
+ ],
290
+ )
291
+ @patch("requests.get")
292
+ def test_run_edit_string_edge_cases(self, mock_get, file_b64, original, expected, monkeypatch):
293
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
294
+ mock_get.return_value.json.return_value = {"content": file_b64, "sha": "abc123"}
295
+ mock_get.return_value.raise_for_status.return_value = None
296
+
297
+ editor = GitHubFileEditor()
298
+ result = editor.run(
299
+ command=Command.EDIT,
300
+ payload={"path": "f.txt", "original": original, "replacement": "X", "message": "m"},
301
+ repo="owner/repo",
302
+ branch="main",
303
+ )
304
+ assert result["result"] == expected
305
+
306
+ @patch("requests.get")
307
+ def test_run_undo_not_last_user(self, mock_get, monkeypatch):
308
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
309
+
310
+ mock_get.return_value.raise_for_status.return_value = None
311
+ mock_get.return_value.json.side_effect = [
312
+ [{"author": {"login": "different_user"}, "sha": "sha1"}],
313
+ {"login": "another_user"},
314
+ ]
315
+
316
+ editor = GitHubFileEditor()
317
+ result = editor.run(command=Command.UNDO, payload={"message": "m"}, repo="owner/repo", branch="main")
318
+ assert result["result"] == "Error: Last commit was not made by the current user"
319
+
320
+ @pytest.mark.parametrize(
321
+ "kwargs,expected_substring",
322
+ [
323
+ ({"command": Command.EDIT, "payload": {}}, "Error: No repository specified"),
324
+ (
325
+ {"command": "bogus", "payload": {}, "repo": "owner/repo"},
326
+ None,
327
+ ),
328
+ ],
329
+ )
330
+ def test_run_validation_errors(self, kwargs, expected_substring, monkeypatch):
331
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
332
+ editor = GitHubFileEditor()
333
+ if expected_substring is None:
334
+ with pytest.raises(ValueError):
335
+ editor.run(**kwargs)
336
+ else:
337
+ result = editor.run(**kwargs)
338
+ assert expected_substring in result["result"]
339
+
340
+ @patch("requests.get")
341
+ def test_run_command_as_string(self, mock_get, monkeypatch):
342
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
343
+ mock_get.return_value.json.return_value = {"content": "SGVsbG8=", "sha": "abc"}
344
+ mock_get.return_value.raise_for_status.return_value = None
345
+
346
+ editor = GitHubFileEditor(repo="owner/repo")
347
+ with patch.object(editor, "_update_file", return_value=True):
348
+ result = editor.run(
349
+ command="EDIT",
350
+ payload={"path": "f.txt", "original": "Hello", "replacement": "Hi", "message": "m"},
351
+ )
352
+ assert result["result"] == "Edit successful"
353
+
354
+ @pytest.mark.parametrize(
355
+ "command,payload",
356
+ [
357
+ (Command.UNDO, {"message": "m"}),
358
+ (Command.CREATE, {"path": "n.txt", "content": "x", "message": "m"}),
359
+ (Command.DELETE, {"path": "n.txt", "message": "m"}),
360
+ ],
361
+ )
362
+ @patch("requests.get")
363
+ def test_run_command_error_handling_no_raise(self, mock_get, command, payload, monkeypatch):
364
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
365
+ mock_get.side_effect = requests.RequestException("API Error")
366
+
367
+ editor = GitHubFileEditor(raise_on_failure=False)
368
+ with (
369
+ patch("requests.put", side_effect=requests.RequestException("API Error")),
370
+ patch("requests.delete", side_effect=requests.RequestException("API Error")),
371
+ ):
372
+ result = editor.run(command=command, payload=payload, repo="owner/repo", branch="main")
373
+ assert "Error: API Error" in result["result"]
@@ -1,7 +1,7 @@
1
1
  # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
- import unittest
4
+ import unittest.mock
5
5
 
6
6
  from haystack import Pipeline
7
7
  from haystack.components.agents import Agent
@@ -143,16 +143,6 @@ class TestGitHubFileEditorTool:
143
143
 
144
144
  pipeline_dict = pipeline.to_dict()
145
145
 
146
- # Remove parameters introduced after haystack-ai==2.12.0 (the minimum supported version)
147
- # to maintain compatibility
148
- try:
149
- pipeline_dict["components"]["agent"]["init_parameters"]["chat_generator"]["init_parameters"].pop(
150
- "http_client_kwargs", None
151
- )
152
- pipeline_dict["components"]["agent"]["init_parameters"].pop("tool_invoker_kwargs", None)
153
- except KeyError:
154
- pass
155
-
156
146
  expected_dict = {
157
147
  "metadata": {},
158
148
  "max_runs_per_component": 100,
@@ -205,9 +195,26 @@ class TestGitHubFileEditorTool:
205
195
  "connection_type_validation": True,
206
196
  }
207
197
 
198
+ # The Agent and the OpenAIChatGenerator belong to haystack-ai, and their init parameters vary across the
199
+ # haystack-ai versions this integration supports. Accept whatever the installed version adds beyond the
200
+ # expected baseline: this test only pins the serialization of GitHubFileEditorTool.
201
+ actual_agent_params = pipeline_dict["components"]["agent"]["init_parameters"]
202
+ expected_agent_params = expected_dict["components"]["agent"]["init_parameters"]
203
+ for actual_params, expected_params in [
204
+ (actual_agent_params, expected_agent_params),
205
+ (
206
+ actual_agent_params["chat_generator"]["init_parameters"],
207
+ expected_agent_params["chat_generator"]["init_parameters"],
208
+ ),
209
+ ]:
210
+ for key in actual_params.keys() - expected_params.keys():
211
+ expected_params[key] = actual_params[key]
212
+
208
213
  assert pipeline_dict == expected_dict
209
214
 
210
215
  deserialized_pipeline = Pipeline.from_dict(pipeline_dict)
216
+ assert deserialized_pipeline == pipeline
217
+
211
218
  deserialized_components = [instance for _, instance in deserialized_pipeline.graph.nodes(data="instance")]
212
219
  deserialized_agent = deserialized_components[0]
213
220
  assert isinstance(deserialized_agent, Agent)
@@ -143,3 +143,82 @@ class TestGitHubPRCreator:
143
143
  assert "Authorization" not in headers
144
144
  assert headers["Accept"] == "application/vnd.github.v3+json"
145
145
  assert headers["User-Agent"] == "Haystack/GitHubPRCreator"
146
+
147
+ def test_parse_issue_url_invalid(self, monkeypatch):
148
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
149
+ pr_creator = GitHubPRCreator()
150
+ with pytest.raises(ValueError, match="Invalid GitHub issue URL format"):
151
+ pr_creator._parse_issue_url("https://github.com/owner/repo/pull/1")
152
+
153
+ @patch("requests.get")
154
+ def test_run_returns_error_when_fork_not_found(self, mock_get, monkeypatch):
155
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
156
+ mock_get.return_value.json.return_value = {"login": "test_user"}
157
+ mock_get.return_value.raise_for_status.return_value = None
158
+
159
+ pr_creator = GitHubPRCreator()
160
+ with patch.object(pr_creator, "_check_fork_exists", return_value=False):
161
+ result = pr_creator.run(
162
+ issue_url="https://github.com/owner/repo/issues/456",
163
+ title="Test PR",
164
+ branch="feature-branch",
165
+ base="main",
166
+ )
167
+ assert result == {"result": "Error: Fork not found at test_user/repo"}
168
+
169
+ @pytest.mark.parametrize(
170
+ "fork_payload,raises_request,expected",
171
+ [
172
+ ({"fork": True}, False, True),
173
+ ({"fork": False}, False, False),
174
+ ({}, False, False),
175
+ (None, True, False),
176
+ ],
177
+ )
178
+ @patch("requests.get")
179
+ def test_check_fork_exists(self, mock_get, fork_payload, raises_request, expected, monkeypatch):
180
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
181
+ if raises_request:
182
+ mock_get.side_effect = requests.RequestException("boom")
183
+ else:
184
+ mock_get.return_value.json.return_value = fork_payload
185
+ mock_get.return_value.raise_for_status.return_value = None
186
+
187
+ pr_creator = GitHubPRCreator()
188
+ assert pr_creator._check_fork_exists("repo", "test_user") is expected
189
+
190
+ @pytest.mark.parametrize("raise_on_failure", [True, False])
191
+ @patch("requests.post")
192
+ def test_create_fork(self, mock_post, raise_on_failure, monkeypatch):
193
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
194
+ mock_post.return_value.json.return_value = {"owner": {"login": "test_user"}}
195
+ mock_post.return_value.raise_for_status.return_value = None
196
+
197
+ pr_creator = GitHubPRCreator(raise_on_failure=raise_on_failure)
198
+ assert pr_creator._create_fork("owner", "repo") == "test_user"
199
+
200
+ mock_post.side_effect = requests.RequestException("boom")
201
+ if raise_on_failure:
202
+ with pytest.raises(RuntimeError, match="Failed to create fork"):
203
+ pr_creator._create_fork("owner", "repo")
204
+ else:
205
+ assert pr_creator._create_fork("owner", "repo") is None
206
+
207
+ @pytest.mark.parametrize("raise_on_failure", [True, False])
208
+ @patch("requests.post")
209
+ @patch("requests.get")
210
+ def test_create_branch(self, mock_get, mock_post, raise_on_failure, monkeypatch):
211
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
212
+ mock_get.return_value.json.return_value = {"object": {"sha": "abc123"}}
213
+ mock_get.return_value.raise_for_status.return_value = None
214
+ mock_post.return_value.raise_for_status.return_value = None
215
+
216
+ pr_creator = GitHubPRCreator(raise_on_failure=raise_on_failure)
217
+ assert pr_creator._create_branch("owner", "repo", "feature", "main") is True
218
+
219
+ mock_get.side_effect = requests.RequestException("boom")
220
+ if raise_on_failure:
221
+ with pytest.raises(RuntimeError, match="Failed to create branch"):
222
+ pr_creator._create_branch("owner", "repo", "feature", "main")
223
+ else:
224
+ assert pr_creator._create_branch("owner", "repo", "feature", "main") is False
@@ -272,3 +272,57 @@ class TestGitHubRepoForker:
272
272
  assert "Authorization" not in headers
273
273
  assert headers["Accept"] == "application/vnd.github.v3+json"
274
274
  assert headers["User-Agent"] == "Haystack/GitHubRepoForker"
275
+
276
+ @pytest.mark.parametrize(
277
+ "status_code,raises,expected",
278
+ [
279
+ (200, False, True),
280
+ (404, False, False),
281
+ (None, True, False),
282
+ ],
283
+ )
284
+ @patch("requests.get")
285
+ def test_check_fork_status(self, mock_get, status_code, raises, expected, monkeypatch):
286
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
287
+ if raises:
288
+ mock_get.side_effect = requests.RequestException("boom")
289
+ else:
290
+ mock_get.return_value.status_code = status_code
291
+
292
+ forker = GitHubRepoForker()
293
+ assert forker._check_fork_status("test_user/repo") is expected
294
+
295
+ def test_get_existing_repository_returns_none_on_exception(self, monkeypatch):
296
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
297
+
298
+ forker = GitHubRepoForker()
299
+ with (
300
+ patch.object(forker, "_get_authenticated_user", return_value="test_user"),
301
+ patch("requests.get", side_effect=requests.RequestException("boom")),
302
+ ):
303
+ assert forker._get_existing_repository("repo") is None
304
+
305
+ @pytest.mark.parametrize("succeeds_within_timeout", [True, False])
306
+ def test_run_wait_for_completion(self, succeeds_within_timeout, monkeypatch):
307
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
308
+
309
+ forker = GitHubRepoForker(
310
+ wait_for_completion=True,
311
+ max_wait_seconds=5 if succeeds_within_timeout else 0,
312
+ poll_interval=0,
313
+ create_branch=False,
314
+ )
315
+ with (
316
+ patch.object(forker, "_parse_github_url", return_value=("owner", "repo", "123")),
317
+ patch.object(forker, "_get_authenticated_user", return_value="test_user"),
318
+ patch.object(forker, "_get_existing_repository", return_value=None),
319
+ patch.object(forker, "_create_fork", return_value="test_user/repo"),
320
+ patch.object(forker, "_check_fork_status", side_effect=[False, True]),
321
+ patch("time.sleep", lambda _: None),
322
+ ):
323
+ if succeeds_within_timeout:
324
+ result = forker.run(url="https://github.com/owner/repo/issues/123")
325
+ assert result == {"repo": "test_user/repo", "issue_branch": None}
326
+ else:
327
+ with pytest.raises(TimeoutError):
328
+ forker.run(url="https://github.com/owner/repo/issues/123")
@@ -186,3 +186,52 @@ class TestGitHubRepoViewer:
186
186
  assert "Authorization" not in headers
187
187
  assert headers["Accept"] == "application/vnd.github.v3+json"
188
188
  assert headers["User-Agent"] == "Haystack/GitHubRepoViewer"
189
+
190
+ def test_run_no_repo_raises(self, monkeypatch):
191
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
192
+ viewer = GitHubRepoViewer()
193
+ with pytest.raises(ValueError, match="Repository not provided"):
194
+ viewer.run(path="README.md")
195
+
196
+ @pytest.mark.parametrize(
197
+ "file_payload,error_substring",
198
+ [
199
+ (
200
+ {
201
+ "name": "big.bin",
202
+ "path": "big.bin",
203
+ "size": 2_000_000,
204
+ "html_url": "https://x",
205
+ "content": "",
206
+ "encoding": "base64",
207
+ },
208
+ "exceeds limit",
209
+ ),
210
+ (
211
+ {
212
+ "name": "raw.txt",
213
+ "path": "raw.txt",
214
+ "size": 5,
215
+ "html_url": "https://x",
216
+ "content": "Hello",
217
+ "encoding": "utf-8",
218
+ },
219
+ None,
220
+ ),
221
+ ],
222
+ )
223
+ @patch("requests.get")
224
+ def test_run_file_size_and_encoding(self, mock_get, file_payload, error_substring, monkeypatch):
225
+ monkeypatch.setenv("GITHUB_TOKEN", "test-token")
226
+ mock_get.return_value.json.return_value = file_payload
227
+ mock_get.return_value.raise_for_status.return_value = None
228
+
229
+ viewer = GitHubRepoViewer(raise_on_failure=False)
230
+ result = viewer.run(repo="owner/repo", path=file_payload["path"])
231
+ doc = result["documents"][0]
232
+ if error_substring is None:
233
+ assert doc.content == "Hello"
234
+ assert doc.meta["type"] == "file_content"
235
+ else:
236
+ assert doc.meta["type"] == "error"
237
+ assert error_substring in doc.content
@@ -0,0 +1,63 @@
1
+ # SPDX-FileCopyrightText: 2023-present deepset GmbH <info@deepset.ai>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ import pytest
5
+ from haystack import Document
6
+
7
+ from haystack_integrations.tools.github.utils import message_handler, serialize_handlers
8
+
9
+
10
+ class TestMessageHandler:
11
+ @pytest.mark.parametrize(
12
+ "documents,expected",
13
+ [
14
+ (
15
+ [Document(content="error message", meta={"type": "error"})],
16
+ "error message",
17
+ ),
18
+ (
19
+ [
20
+ Document(content="docs", meta={"type": "dir"}),
21
+ Document(content="README.md", meta={"type": "file"}),
22
+ ],
23
+ "docs\nREADME.md",
24
+ ),
25
+ (
26
+ [Document(content="print('hi')", meta={"type": "file_content", "path": "main.py"})],
27
+ "File Content for main.py\n\nprint('hi')",
28
+ ),
29
+ ],
30
+ )
31
+ def test_message_handler_renders_documents(self, documents, expected):
32
+ assert message_handler(documents) == expected
33
+
34
+ def test_message_handler_truncates_to_max_length(self):
35
+ big_content = "x" * 200
36
+ doc = Document(content=big_content, meta={"type": "file_content", "path": "main.py"})
37
+ result = message_handler([doc], max_length=50)
38
+ assert result.endswith("...(large file can't be fully displayed)")
39
+ assert len(result) == 50 + len("...(large file can't be fully displayed)")
40
+
41
+
42
+ class TestSerializeHandlers:
43
+ @pytest.mark.parametrize(
44
+ "outputs_to_state,outputs_to_string,error_match",
45
+ [
46
+ (
47
+ {"documents": {"handler": "not_callable"}},
48
+ None,
49
+ "outputs_to_state\\[documents\\] is not a callable",
50
+ ),
51
+ (
52
+ None,
53
+ {"handler": "not_callable"},
54
+ "outputs_to_string is not a callable",
55
+ ),
56
+ ],
57
+ )
58
+ def test_serialize_handlers_raises_when_handler_not_callable(
59
+ self, outputs_to_state, outputs_to_string, error_match
60
+ ):
61
+ serialized: dict = {}
62
+ with pytest.raises(ValueError, match=error_match):
63
+ serialize_handlers(serialized, outputs_to_state, outputs_to_string)
@@ -1,52 +0,0 @@
1
- # Changelog
2
-
3
- ## [integrations/github-v1.2.1] - 2025-10-20
4
-
5
- ### 🚀 Features
6
-
7
- - Add support for GitHubRepoForkerTool (#1968)
8
-
9
-
10
- ### 🧪 Testing
11
-
12
- - Fix GitHub test (#2119)
13
-
14
- ### 🧹 Chores
15
-
16
- - Standardize readmes - part 2 (#2205)
17
-
18
- ### 🌀 Miscellaneous
19
-
20
- - Chore: Fix linting github (#2255)
21
- - Fix: handle empty GitHub tokens in authentication headers (#2400)
22
-
23
- ## [integrations/github-v1.1.0] - 2025-07-01
24
-
25
- ### 🐛 Bug Fixes
26
-
27
- - Fix Github types + add py.typed (#2024)
28
-
29
- ### 🧹 Chores
30
-
31
- - Fix linting for ruff 0.12.0 (#1969)
32
- - Remove black (#1985)
33
-
34
-
35
- ## [integrations/github-v1.0.1] - 2025-06-12
36
-
37
- ### 🐛 Bug Fixes
38
-
39
- - Prompts and schemas of GitHubIssueCommenterTool and GitHubIssueViewerTool now properly explain url parameter (#1929)
40
-
41
-
42
- ### 🧹 Chores
43
-
44
- - Align core-integrations Hatch scripts (#1898)
45
-
46
- ## [integrations/github-v1.0.0] - 2025-05-28
47
-
48
- ### 🚀 Features
49
-
50
- - Add GitHub integration with components, tools, and prompts (#1637)
51
-
52
- <!-- generated by git-cliff -->
@@ -1,33 +0,0 @@
1
- loaders:
2
- - ignore_when_discovered:
3
- - __init__
4
- modules:
5
- - haystack_integrations.components.connectors.github.file_editor
6
- - haystack_integrations.components.connectors.github.issue_commenter
7
- - haystack_integrations.components.connectors.github.issue_viewer
8
- - haystack_integrations.components.connectors.github.pr_creator
9
- - haystack_integrations.components.connectors.github.repo_viewer
10
- - haystack_integrations.components.connectors.github.repo_forker
11
- search_path:
12
- - ../src
13
- type: haystack_pydoc_tools.loaders.CustomPythonLoader
14
- processors:
15
- - do_not_filter_modules: false
16
- documented_only: true
17
- expression: null
18
- skip_empty_modules: true
19
- type: filter
20
- - type: smart
21
- - type: crossref
22
- renderer:
23
- description: GitHub integration for Haystack
24
- id: integrations-github
25
- markdown:
26
- add_member_class_prefix: false
27
- add_method_class_prefix: true
28
- classdef_code_block: false
29
- descriptive_class_title: false
30
- descriptive_module_title: true
31
- filename: github.md
32
- title: GitHub
33
- type: haystack_pydoc_tools.renderers.DocusaurusRenderer