github-agent 0.29.0__tar.gz → 0.31.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.
- {github_agent-0.29.0/github_agent.egg-info → github_agent-0.31.0}/PKG-INFO +18 -3
- github_agent-0.29.0/PKG-INFO → github_agent-0.31.0/README.md +16 -22
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/agent_server.py +1 -1
- github_agent-0.31.0/github_agent/api/api_client_orgs.py +293 -0
- github_agent-0.31.0/github_agent/api/api_client_pages.py +234 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api_client.py +10 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/auth.py +10 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/github_input_models.py +134 -1
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/github_response_models.py +117 -0
- github_agent-0.31.0/github_agent/mcp/mcp_org.py +270 -0
- github_agent-0.31.0/github_agent/mcp/mcp_repo.py +287 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp_server.py +375 -6
- github_agent-0.29.0/README.md → github_agent-0.31.0/github_agent.egg-info/PKG-INFO +37 -1
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent.egg-info/SOURCES.txt +3 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent.egg-info/requires.txt +1 -1
- {github_agent-0.29.0 → github_agent-0.31.0}/pyproject.toml +2 -2
- {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_init_dynamics.py +7 -1
- github_agent-0.31.0/tests/test_org_crud.py +587 -0
- github_agent-0.31.0/tests/test_pages.py +616 -0
- github_agent-0.29.0/github_agent/api/api_client_orgs.py +0 -40
- github_agent-0.29.0/github_agent/mcp/mcp_org.py +0 -75
- github_agent-0.29.0/github_agent/mcp/mcp_repo.py +0 -113
- {github_agent-0.29.0 → github_agent-0.31.0}/LICENSE +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/__init__.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/__main__.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/__init__.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_base.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_branches.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_commits.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_contents.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_issues.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_pulls.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_releases.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_repos.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_search.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/api/api_client_workflows.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/__init__.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_action.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_branch.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_collaborator.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_commit.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_content.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_issue.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_pull.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_release.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp/mcp_search.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent/mcp_config.json +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent.egg-info/dependency_links.txt +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent.egg-info/entry_points.txt +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/github_agent.egg-info/top_level.txt +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/scripts/security_sanitizer.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/scripts/validate_a2a_agent.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/scripts/verify_api_integration.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/setup.cfg +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/tests/conftest.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_api_client_endpoints.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_api_validation.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_auth_client_edge_cases.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_concept_parity.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_github_agent_api_brute_force_coverage.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_github_agent_brute_force_coverage.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_mcp_coverage.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_model_fields.py +0 -0
- {github_agent-0.29.0 → github_agent-0.31.0}/tests/test_startup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-agent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.31.0
|
|
4
4
|
Summary: GitHub Agent for MCP
|
|
5
5
|
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -12,7 +12,7 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Requires-Python: <3.15,>=3.11
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: agent-utilities[agent,logfire]>=0.
|
|
15
|
+
Requires-Dist: agent-utilities[agent,logfire]>=0.49.0
|
|
16
16
|
Provides-Extra: test
|
|
17
17
|
Requires-Dist: pytest-xdist>=3.6.0; extra == "test"
|
|
18
18
|
Requires-Dist: pytest; extra == "test"
|
|
@@ -41,7 +41,7 @@ Dynamic: license-file
|
|
|
41
41
|

|
|
42
42
|

|
|
43
43
|
|
|
44
|
-
*Version: 0.
|
|
44
|
+
*Version: 0.31.0*
|
|
45
45
|
|
|
46
46
|
> **Documentation** — Installation, deployment, and usage across the MCP, API, and
|
|
47
47
|
> CLI interfaces, including the integrated A2A agent server, are maintained in the
|
|
@@ -199,6 +199,21 @@ docker run -d \
|
|
|
199
199
|
|
|
200
200
|
---
|
|
201
201
|
|
|
202
|
+
<!-- BEGIN GENERATED: additional-deployment-options -->
|
|
203
|
+
### Additional Deployment Options
|
|
204
|
+
|
|
205
|
+
`github-agent` can also run as a **local container** (Docker / Podman / `uv`) or be
|
|
206
|
+
consumed from a **remote deployment**. The
|
|
207
|
+
[Deployment guide](https://knuckles-team.github.io/github-agent/deployment/) has full, copy-paste
|
|
208
|
+
`mcp_config.json` for all four transports — **stdio**, **streamable-http**,
|
|
209
|
+
**local container / uv**, and **remote URL**:
|
|
210
|
+
|
|
211
|
+
- **Local container / uv** — launch the server from `mcp_config.json` via `uvx`,
|
|
212
|
+
`docker run`, or `podman run`, or point at a local streamable-http container by `url`.
|
|
213
|
+
- **Remote URL** — connect to a server deployed behind Caddy at
|
|
214
|
+
`http://github-mcp.arpa/mcp` using the `"url"` key.
|
|
215
|
+
<!-- END GENERATED: additional-deployment-options -->
|
|
216
|
+
|
|
202
217
|
## Agent
|
|
203
218
|
|
|
204
219
|
This repository features a fully integrated Pydantic AI Graph Agent. It communicates over the **Agent Control Protocol (ACP)** and interacts seamlessly with the **Agent Web UI (AG-UI)** and Terminal interface.
|
|
@@ -1,24 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: github-agent
|
|
3
|
-
Version: 0.29.0
|
|
4
|
-
Summary: GitHub Agent for MCP
|
|
5
|
-
Author-email: Audel Rouhi <knucklessg1@gmail.com>
|
|
6
|
-
License: MIT
|
|
7
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
-
Classifier: License :: Public Domain
|
|
9
|
-
Classifier: Environment :: Console
|
|
10
|
-
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Requires-Python: <3.15,>=3.11
|
|
13
|
-
Description-Content-Type: text/markdown
|
|
14
|
-
License-File: LICENSE
|
|
15
|
-
Requires-Dist: agent-utilities[agent,logfire]>=0.47.0
|
|
16
|
-
Provides-Extra: test
|
|
17
|
-
Requires-Dist: pytest-xdist>=3.6.0; extra == "test"
|
|
18
|
-
Requires-Dist: pytest; extra == "test"
|
|
19
|
-
Requires-Dist: pytest-asyncio; extra == "test"
|
|
20
|
-
Dynamic: license-file
|
|
21
|
-
|
|
22
1
|
# Github Agent
|
|
23
2
|
## CLI or API | MCP | Agent
|
|
24
3
|
|
|
@@ -41,7 +20,7 @@ Dynamic: license-file
|
|
|
41
20
|

|
|
42
21
|

|
|
43
22
|
|
|
44
|
-
*Version: 0.
|
|
23
|
+
*Version: 0.31.0*
|
|
45
24
|
|
|
46
25
|
> **Documentation** — Installation, deployment, and usage across the MCP, API, and
|
|
47
26
|
> CLI interfaces, including the integrated A2A agent server, are maintained in the
|
|
@@ -199,6 +178,21 @@ docker run -d \
|
|
|
199
178
|
|
|
200
179
|
---
|
|
201
180
|
|
|
181
|
+
<!-- BEGIN GENERATED: additional-deployment-options -->
|
|
182
|
+
### Additional Deployment Options
|
|
183
|
+
|
|
184
|
+
`github-agent` can also run as a **local container** (Docker / Podman / `uv`) or be
|
|
185
|
+
consumed from a **remote deployment**. The
|
|
186
|
+
[Deployment guide](https://knuckles-team.github.io/github-agent/deployment/) has full, copy-paste
|
|
187
|
+
`mcp_config.json` for all four transports — **stdio**, **streamable-http**,
|
|
188
|
+
**local container / uv**, and **remote URL**:
|
|
189
|
+
|
|
190
|
+
- **Local container / uv** — launch the server from `mcp_config.json` via `uvx`,
|
|
191
|
+
`docker run`, or `podman run`, or point at a local streamable-http container by `url`.
|
|
192
|
+
- **Remote URL** — connect to a server deployed behind Caddy at
|
|
193
|
+
`http://github-mcp.arpa/mcp` using the `"url"` key.
|
|
194
|
+
<!-- END GENERATED: additional-deployment-options -->
|
|
195
|
+
|
|
202
196
|
## Agent
|
|
203
197
|
|
|
204
198
|
This repository features a fully integrated Pydantic AI Graph Agent. It communicates over the **Agent Control Protocol (ACP)** and interacts seamlessly with the **Agent Web UI (AG-UI)** and Terminal interface.
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
from agent_utilities.decorators import require_auth
|
|
5
|
+
from agent_utilities.exceptions import (
|
|
6
|
+
ParameterError,
|
|
7
|
+
)
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from github_agent.api.api_client_base import BaseApiClient
|
|
11
|
+
from github_agent.github_input_models import (
|
|
12
|
+
OrgListModel,
|
|
13
|
+
OrgMemberModel,
|
|
14
|
+
OrgUpdateModel,
|
|
15
|
+
)
|
|
16
|
+
from github_agent.github_response_models import (
|
|
17
|
+
Organization,
|
|
18
|
+
OrganizationMembership,
|
|
19
|
+
OrganizationSummary,
|
|
20
|
+
Repository,
|
|
21
|
+
Response,
|
|
22
|
+
User,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OrganizationCreationNotSupportedError(Exception):
|
|
27
|
+
"""Raised when organization creation is requested against github.com.
|
|
28
|
+
|
|
29
|
+
github.com does not expose an API to create organizations — they can only
|
|
30
|
+
be created through the web UI. The POST /admin/organizations endpoint
|
|
31
|
+
exists only on GitHub Enterprise Server.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Api(BaseApiClient):
|
|
36
|
+
@require_auth
|
|
37
|
+
def get_organization(self, org: str) -> Response:
|
|
38
|
+
"""Get an organization's full profile.
|
|
39
|
+
|
|
40
|
+
GET /orgs/{org}
|
|
41
|
+
https://docs.github.com/en/rest/orgs/orgs#get-an-organization
|
|
42
|
+
"""
|
|
43
|
+
response = self._session.get(
|
|
44
|
+
url=f"{self.url}/orgs/{org}",
|
|
45
|
+
headers=self.headers,
|
|
46
|
+
verify=self.verify,
|
|
47
|
+
proxies=self.proxies,
|
|
48
|
+
)
|
|
49
|
+
response.raise_for_status()
|
|
50
|
+
try:
|
|
51
|
+
parsed_data = Organization(**response.json())
|
|
52
|
+
return Response(response=response, data=parsed_data)
|
|
53
|
+
except ValidationError as e:
|
|
54
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
55
|
+
|
|
56
|
+
@require_auth
|
|
57
|
+
def list_organizations(self, **kwargs) -> Response:
|
|
58
|
+
"""List organizations.
|
|
59
|
+
|
|
60
|
+
With scope='member' (default): organizations the authenticated user
|
|
61
|
+
belongs to.
|
|
62
|
+
GET /user/orgs
|
|
63
|
+
https://docs.github.com/en/rest/orgs/orgs#list-organizations-for-the-authenticated-user
|
|
64
|
+
|
|
65
|
+
With scope='all': every organization, in ascending ID order. GitHub
|
|
66
|
+
paginates this endpoint with a 'since' ID cursor — pass the highest
|
|
67
|
+
organization ID from the previous page to fetch the next one.
|
|
68
|
+
GET /organizations
|
|
69
|
+
https://docs.github.com/en/rest/orgs/orgs#list-organizations
|
|
70
|
+
"""
|
|
71
|
+
model = OrgListModel(**kwargs)
|
|
72
|
+
endpoint = "/user/orgs" if model.scope == "member" else "/organizations"
|
|
73
|
+
response, data = self._fetch_all_pages(endpoint, model)
|
|
74
|
+
try:
|
|
75
|
+
parsed_data = [OrganizationSummary(**item) for item in data]
|
|
76
|
+
return Response(response=response, data=parsed_data)
|
|
77
|
+
except ValidationError as e:
|
|
78
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
79
|
+
|
|
80
|
+
@require_auth
|
|
81
|
+
def update_organization(self, org: str, **kwargs) -> Response:
|
|
82
|
+
"""Update an organization's profile and member settings.
|
|
83
|
+
|
|
84
|
+
Accepts the documented mutable fields (validated by OrgUpdateModel):
|
|
85
|
+
billing_email, company, email, twitter_username, location, name,
|
|
86
|
+
description, blog, has_organization_projects, has_repository_projects,
|
|
87
|
+
default_repository_permission, members_can_create_repositories,
|
|
88
|
+
members_can_create_public/private/internal_repositories,
|
|
89
|
+
members_can_create_pages, members_can_fork_private_repositories,
|
|
90
|
+
web_commit_signoff_required.
|
|
91
|
+
|
|
92
|
+
PATCH /orgs/{org}
|
|
93
|
+
https://docs.github.com/en/rest/orgs/orgs#update-an-organization
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
model = OrgUpdateModel(**kwargs)
|
|
97
|
+
except ValidationError as e:
|
|
98
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
99
|
+
response = self._session.patch(
|
|
100
|
+
url=f"{self.url}/orgs/{org}",
|
|
101
|
+
json=model.model_dump(exclude_none=True),
|
|
102
|
+
headers=self.headers,
|
|
103
|
+
verify=self.verify,
|
|
104
|
+
proxies=self.proxies,
|
|
105
|
+
)
|
|
106
|
+
response.raise_for_status()
|
|
107
|
+
try:
|
|
108
|
+
parsed_data = Organization(**response.json())
|
|
109
|
+
return Response(response=response, data=parsed_data)
|
|
110
|
+
except ValidationError as e:
|
|
111
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
112
|
+
|
|
113
|
+
@require_auth
|
|
114
|
+
def delete_organization(self, org: str) -> Response:
|
|
115
|
+
"""Schedule an organization for deletion. IRREVERSIBLE.
|
|
116
|
+
|
|
117
|
+
GitHub responds 202 Accepted and deletes the organization, all of its
|
|
118
|
+
repositories, and all of its data asynchronously. The MCP layer gates
|
|
119
|
+
this action behind allow_destructive / GITHUB_ALLOW_DESTRUCTIVE.
|
|
120
|
+
|
|
121
|
+
DELETE /orgs/{org}
|
|
122
|
+
https://docs.github.com/en/rest/orgs/orgs#delete-an-organization
|
|
123
|
+
"""
|
|
124
|
+
response = self._session.delete(
|
|
125
|
+
url=f"{self.url}/orgs/{org}",
|
|
126
|
+
headers=self.headers,
|
|
127
|
+
verify=self.verify,
|
|
128
|
+
proxies=self.proxies,
|
|
129
|
+
)
|
|
130
|
+
response.raise_for_status()
|
|
131
|
+
return Response(response=response, data={"status": "deletion_scheduled"})
|
|
132
|
+
|
|
133
|
+
@require_auth
|
|
134
|
+
def create_organization(
|
|
135
|
+
self, login: str, admin: str, profile_name: str | None = None
|
|
136
|
+
) -> Response:
|
|
137
|
+
"""Create an organization — GitHub Enterprise Server ONLY.
|
|
138
|
+
|
|
139
|
+
POST /admin/organizations (site-admin API)
|
|
140
|
+
https://docs.github.com/en/enterprise-server@latest/rest/enterprise-admin/orgs#create-an-organization
|
|
141
|
+
|
|
142
|
+
github.com does NOT expose an API to create organizations; on
|
|
143
|
+
github.com they can only be created through the web UI
|
|
144
|
+
(https://github.com/account/organizations/new). When this client
|
|
145
|
+
points at api.github.com this method raises
|
|
146
|
+
OrganizationCreationNotSupportedError instead of calling the API.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
login: The organization's username.
|
|
150
|
+
admin: The login of the user who will manage this organization.
|
|
151
|
+
profile_name: The organization's display name.
|
|
152
|
+
"""
|
|
153
|
+
host = urlparse(self.url).hostname or ""
|
|
154
|
+
if host == "api.github.com":
|
|
155
|
+
raise OrganizationCreationNotSupportedError(
|
|
156
|
+
"github.com organizations cannot be created via the API — "
|
|
157
|
+
"create them in the web UI at "
|
|
158
|
+
"https://github.com/account/organizations/new. "
|
|
159
|
+
"POST /admin/organizations is only available on GitHub "
|
|
160
|
+
"Enterprise Server (point GITHUB_URL at your enterprise "
|
|
161
|
+
"instance's API, e.g. https://ghe.example.com/api/v3)."
|
|
162
|
+
)
|
|
163
|
+
payload: dict = {"login": login, "admin": admin}
|
|
164
|
+
if profile_name is not None:
|
|
165
|
+
payload["profile_name"] = profile_name
|
|
166
|
+
response = self._session.post(
|
|
167
|
+
url=f"{self.url}/admin/organizations",
|
|
168
|
+
json=payload,
|
|
169
|
+
headers=self.headers,
|
|
170
|
+
verify=self.verify,
|
|
171
|
+
proxies=self.proxies,
|
|
172
|
+
)
|
|
173
|
+
response.raise_for_status()
|
|
174
|
+
try:
|
|
175
|
+
parsed_data = OrganizationSummary(**response.json())
|
|
176
|
+
return Response(response=response, data=parsed_data)
|
|
177
|
+
except ValidationError as e:
|
|
178
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
179
|
+
|
|
180
|
+
@require_auth
|
|
181
|
+
def create_organization_repository(self, org: str, name: str, **kwargs) -> Response:
|
|
182
|
+
"""Create a repository in an organization.
|
|
183
|
+
|
|
184
|
+
Accepts the same payload fields as create_repository (which posts to
|
|
185
|
+
/user/repos): description, homepage, private, visibility, has_issues,
|
|
186
|
+
has_projects, has_wiki, auto_init, gitignore_template,
|
|
187
|
+
license_template, etc.
|
|
188
|
+
|
|
189
|
+
POST /orgs/{org}/repos
|
|
190
|
+
https://docs.github.com/en/rest/repos/repos#create-an-organization-repository
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
payload = {"name": name, **kwargs}
|
|
194
|
+
response = self._session.post(
|
|
195
|
+
url=f"{self.url}/orgs/{org}/repos",
|
|
196
|
+
json=payload,
|
|
197
|
+
headers=self.headers,
|
|
198
|
+
verify=self.verify,
|
|
199
|
+
proxies=self.proxies,
|
|
200
|
+
)
|
|
201
|
+
response.raise_for_status()
|
|
202
|
+
parsed_data = Repository(**response.json())
|
|
203
|
+
return Response(response=response, data=parsed_data)
|
|
204
|
+
except ValidationError as e:
|
|
205
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
206
|
+
|
|
207
|
+
@require_auth
|
|
208
|
+
def get_organization_membership(self, org: str, username: str) -> Response:
|
|
209
|
+
"""Get a user's organization membership (state and role).
|
|
210
|
+
|
|
211
|
+
GET /orgs/{org}/memberships/{username}
|
|
212
|
+
https://docs.github.com/en/rest/orgs/members#get-organization-membership-for-a-user
|
|
213
|
+
"""
|
|
214
|
+
response = self._session.get(
|
|
215
|
+
url=f"{self.url}/orgs/{org}/memberships/{username}",
|
|
216
|
+
headers=self.headers,
|
|
217
|
+
verify=self.verify,
|
|
218
|
+
proxies=self.proxies,
|
|
219
|
+
)
|
|
220
|
+
response.raise_for_status()
|
|
221
|
+
try:
|
|
222
|
+
parsed_data = OrganizationMembership(**response.json())
|
|
223
|
+
return Response(response=response, data=parsed_data)
|
|
224
|
+
except ValidationError as e:
|
|
225
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
226
|
+
|
|
227
|
+
@require_auth
|
|
228
|
+
def set_organization_membership(
|
|
229
|
+
self, org: str, username: str, role: str = "member"
|
|
230
|
+
) -> Response:
|
|
231
|
+
"""Add a user to an organization or update their role.
|
|
232
|
+
|
|
233
|
+
Invites the user when they are not yet a member (membership state
|
|
234
|
+
'pending' until accepted). Role is 'member' or 'admin'.
|
|
235
|
+
|
|
236
|
+
PUT /orgs/{org}/memberships/{username}
|
|
237
|
+
https://docs.github.com/en/rest/orgs/members#set-organization-membership-for-a-user
|
|
238
|
+
"""
|
|
239
|
+
response = self._session.put(
|
|
240
|
+
url=f"{self.url}/orgs/{org}/memberships/{username}",
|
|
241
|
+
json={"role": role},
|
|
242
|
+
headers=self.headers,
|
|
243
|
+
verify=self.verify,
|
|
244
|
+
proxies=self.proxies,
|
|
245
|
+
)
|
|
246
|
+
response.raise_for_status()
|
|
247
|
+
try:
|
|
248
|
+
parsed_data = OrganizationMembership(**response.json())
|
|
249
|
+
return Response(response=response, data=parsed_data)
|
|
250
|
+
except ValidationError as e:
|
|
251
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
252
|
+
|
|
253
|
+
@require_auth
|
|
254
|
+
def remove_organization_member(self, org: str, username: str) -> Response:
|
|
255
|
+
"""Remove a user from an organization (repositories access included).
|
|
256
|
+
|
|
257
|
+
Destructive: the MCP layer gates this action behind
|
|
258
|
+
allow_destructive / GITHUB_ALLOW_DESTRUCTIVE. GitHub responds 204.
|
|
259
|
+
|
|
260
|
+
DELETE /orgs/{org}/members/{username}
|
|
261
|
+
https://docs.github.com/en/rest/orgs/members#remove-an-organization-member
|
|
262
|
+
"""
|
|
263
|
+
response = self._session.delete(
|
|
264
|
+
url=f"{self.url}/orgs/{org}/members/{username}",
|
|
265
|
+
headers=self.headers,
|
|
266
|
+
verify=self.verify,
|
|
267
|
+
proxies=self.proxies,
|
|
268
|
+
)
|
|
269
|
+
response.raise_for_status()
|
|
270
|
+
return Response(response=response, data={"status": "removed"})
|
|
271
|
+
|
|
272
|
+
@require_auth
|
|
273
|
+
def get_org_members(self, **kwargs) -> Response:
|
|
274
|
+
"""List members for an organization."""
|
|
275
|
+
model = OrgMemberModel(**kwargs)
|
|
276
|
+
response, data = self._fetch_all_pages(f"/orgs/{model.org}/members", model)
|
|
277
|
+
try:
|
|
278
|
+
parsed_data = [User(**item) for item in data]
|
|
279
|
+
return Response(response=response, data=parsed_data)
|
|
280
|
+
except ValidationError as e:
|
|
281
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
282
|
+
|
|
283
|
+
@require_auth
|
|
284
|
+
def get_org_teams(self, org: str) -> Response:
|
|
285
|
+
"""List teams for an organization."""
|
|
286
|
+
response = self._session.get(
|
|
287
|
+
url=f"{self.url}/orgs/{org}/teams",
|
|
288
|
+
headers=self.headers,
|
|
289
|
+
verify=self.verify,
|
|
290
|
+
proxies=self.proxies,
|
|
291
|
+
)
|
|
292
|
+
response.raise_for_status()
|
|
293
|
+
return Response(response=response, data=response.json())
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
import requests
|
|
3
|
+
from agent_utilities.decorators import require_auth
|
|
4
|
+
from agent_utilities.exceptions import (
|
|
5
|
+
ParameterError,
|
|
6
|
+
)
|
|
7
|
+
from pydantic import ValidationError
|
|
8
|
+
|
|
9
|
+
from github_agent.api.api_client_base import BaseApiClient
|
|
10
|
+
from github_agent.github_input_models import (
|
|
11
|
+
PagesBuildsModel,
|
|
12
|
+
PagesUpdateModel,
|
|
13
|
+
)
|
|
14
|
+
from github_agent.github_response_models import (
|
|
15
|
+
PagesAlreadyEnabled,
|
|
16
|
+
PagesBuild,
|
|
17
|
+
PagesBuildRequest,
|
|
18
|
+
PagesNotEnabled,
|
|
19
|
+
PagesSite,
|
|
20
|
+
Response,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalize_pages_source(source: str | dict) -> dict:
|
|
25
|
+
"""Coerce a Pages publishing source into the API's {'branch', 'path'} shape.
|
|
26
|
+
|
|
27
|
+
Accepts a branch name string or a {'branch': ..., 'path': ...} mapping;
|
|
28
|
+
path defaults to '/'.
|
|
29
|
+
"""
|
|
30
|
+
if isinstance(source, str):
|
|
31
|
+
return {"branch": source, "path": "/"}
|
|
32
|
+
if isinstance(source, dict) and "branch" in source:
|
|
33
|
+
return {"branch": source["branch"], "path": source.get("path", "/")}
|
|
34
|
+
raise ParameterError(
|
|
35
|
+
"Invalid 'source': expected a branch name string or a mapping with "
|
|
36
|
+
"'branch' (and an optional 'path' of '/' or '/docs')."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Api(BaseApiClient):
|
|
41
|
+
@require_auth
|
|
42
|
+
def get_pages(self, owner: str, repo: str) -> Response:
|
|
43
|
+
"""Get the GitHub Pages site configuration for a repository.
|
|
44
|
+
|
|
45
|
+
Returns PagesSite data when Pages is enabled. When the repository has
|
|
46
|
+
no Pages site GitHub responds 404 — instead of raising, this returns
|
|
47
|
+
a typed PagesNotEnabled result so callers can branch on it.
|
|
48
|
+
|
|
49
|
+
GET /repos/{owner}/{repo}/pages
|
|
50
|
+
https://docs.github.com/en/rest/pages/pages#get-a-apiname-pages-site
|
|
51
|
+
"""
|
|
52
|
+
response = self._session.get(
|
|
53
|
+
url=f"{self.url}/repos/{owner}/{repo}/pages",
|
|
54
|
+
headers=self.headers,
|
|
55
|
+
verify=self.verify,
|
|
56
|
+
proxies=self.proxies,
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
response.raise_for_status()
|
|
60
|
+
except requests.exceptions.HTTPError as e:
|
|
61
|
+
if e.response is not None and e.response.status_code == 404:
|
|
62
|
+
return Response(response=response, data=PagesNotEnabled())
|
|
63
|
+
raise
|
|
64
|
+
try:
|
|
65
|
+
parsed_data = PagesSite(**response.json())
|
|
66
|
+
return Response(response=response, data=parsed_data)
|
|
67
|
+
except ValidationError as e:
|
|
68
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
69
|
+
|
|
70
|
+
@require_auth
|
|
71
|
+
def create_pages(
|
|
72
|
+
self,
|
|
73
|
+
owner: str,
|
|
74
|
+
repo: str,
|
|
75
|
+
build_type: str = "workflow",
|
|
76
|
+
source: str | dict | None = None,
|
|
77
|
+
) -> Response:
|
|
78
|
+
"""Enable GitHub Pages for a repository (HTTP 201).
|
|
79
|
+
|
|
80
|
+
build_type 'workflow' (default) publishes via GitHub Actions and
|
|
81
|
+
needs no source; 'legacy' publishes from a branch and requires
|
|
82
|
+
source (a branch name string or {'branch', 'path'} mapping).
|
|
83
|
+
|
|
84
|
+
When Pages is already enabled GitHub responds 409 Conflict — instead
|
|
85
|
+
of raising, this returns a typed PagesAlreadyEnabled result.
|
|
86
|
+
|
|
87
|
+
POST /repos/{owner}/{repo}/pages
|
|
88
|
+
https://docs.github.com/en/rest/pages/pages#create-a-apiname-pages-site
|
|
89
|
+
"""
|
|
90
|
+
if build_type not in ("legacy", "workflow"):
|
|
91
|
+
raise ParameterError(
|
|
92
|
+
"Invalid 'build_type': must be 'legacy' or 'workflow'."
|
|
93
|
+
)
|
|
94
|
+
payload: dict = {"build_type": build_type}
|
|
95
|
+
if source is not None:
|
|
96
|
+
payload["source"] = _normalize_pages_source(source)
|
|
97
|
+
elif build_type == "legacy":
|
|
98
|
+
raise ParameterError(
|
|
99
|
+
"build_type 'legacy' requires 'source' (a branch name string "
|
|
100
|
+
"or a {'branch', 'path'} mapping)."
|
|
101
|
+
)
|
|
102
|
+
response = self._session.post(
|
|
103
|
+
url=f"{self.url}/repos/{owner}/{repo}/pages",
|
|
104
|
+
json=payload,
|
|
105
|
+
headers=self.headers,
|
|
106
|
+
verify=self.verify,
|
|
107
|
+
proxies=self.proxies,
|
|
108
|
+
)
|
|
109
|
+
try:
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
except requests.exceptions.HTTPError as e:
|
|
112
|
+
if e.response is not None and e.response.status_code == 409:
|
|
113
|
+
return Response(response=response, data=PagesAlreadyEnabled())
|
|
114
|
+
raise
|
|
115
|
+
try:
|
|
116
|
+
parsed_data = PagesSite(**response.json())
|
|
117
|
+
return Response(response=response, data=parsed_data)
|
|
118
|
+
except ValidationError as e:
|
|
119
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
120
|
+
|
|
121
|
+
@require_auth
|
|
122
|
+
def update_pages(self, owner: str, repo: str, **kwargs) -> Response:
|
|
123
|
+
"""Update the GitHub Pages configuration for a repository.
|
|
124
|
+
|
|
125
|
+
Accepts the documented mutable fields (validated by PagesUpdateModel):
|
|
126
|
+
build_type ('legacy' or 'workflow'), source (a branch name string or
|
|
127
|
+
{'branch', 'path'} mapping, for legacy builds), cname (custom
|
|
128
|
+
domain), https_enforced. GitHub responds 204 No Content.
|
|
129
|
+
|
|
130
|
+
PUT /repos/{owner}/{repo}/pages
|
|
131
|
+
https://docs.github.com/en/rest/pages/pages#update-information-about-a-apiname-pages-site
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
model = PagesUpdateModel(**kwargs)
|
|
135
|
+
except ValidationError as e:
|
|
136
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
137
|
+
payload = model.model_dump(exclude_none=True)
|
|
138
|
+
if not payload:
|
|
139
|
+
raise ParameterError(
|
|
140
|
+
"No fields to update: pass at least one of build_type, "
|
|
141
|
+
"source, cname, https_enforced."
|
|
142
|
+
)
|
|
143
|
+
if "source" in payload:
|
|
144
|
+
payload["source"] = _normalize_pages_source(payload["source"])
|
|
145
|
+
response = self._session.put(
|
|
146
|
+
url=f"{self.url}/repos/{owner}/{repo}/pages",
|
|
147
|
+
json=payload,
|
|
148
|
+
headers=self.headers,
|
|
149
|
+
verify=self.verify,
|
|
150
|
+
proxies=self.proxies,
|
|
151
|
+
)
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
return Response(response=response, data={"status": "pages_updated"})
|
|
154
|
+
|
|
155
|
+
@require_auth
|
|
156
|
+
def delete_pages(self, owner: str, repo: str) -> Response:
|
|
157
|
+
"""Disable GitHub Pages for a repository and delete the site.
|
|
158
|
+
|
|
159
|
+
Destructive: the MCP layer gates this action behind
|
|
160
|
+
allow_destructive / GITHUB_ALLOW_DESTRUCTIVE. GitHub responds 204.
|
|
161
|
+
|
|
162
|
+
DELETE /repos/{owner}/{repo}/pages
|
|
163
|
+
https://docs.github.com/en/rest/pages/pages#delete-a-apiname-pages-site
|
|
164
|
+
"""
|
|
165
|
+
response = self._session.delete(
|
|
166
|
+
url=f"{self.url}/repos/{owner}/{repo}/pages",
|
|
167
|
+
headers=self.headers,
|
|
168
|
+
verify=self.verify,
|
|
169
|
+
proxies=self.proxies,
|
|
170
|
+
)
|
|
171
|
+
response.raise_for_status()
|
|
172
|
+
return Response(response=response, data={"status": "pages_deleted"})
|
|
173
|
+
|
|
174
|
+
@require_auth
|
|
175
|
+
def get_pages_build_latest(self, owner: str, repo: str) -> Response:
|
|
176
|
+
"""Get the latest GitHub Pages build for a repository.
|
|
177
|
+
|
|
178
|
+
GET /repos/{owner}/{repo}/pages/builds/latest
|
|
179
|
+
https://docs.github.com/en/rest/pages/pages#get-latest-pages-build
|
|
180
|
+
"""
|
|
181
|
+
response = self._session.get(
|
|
182
|
+
url=f"{self.url}/repos/{owner}/{repo}/pages/builds/latest",
|
|
183
|
+
headers=self.headers,
|
|
184
|
+
verify=self.verify,
|
|
185
|
+
proxies=self.proxies,
|
|
186
|
+
)
|
|
187
|
+
response.raise_for_status()
|
|
188
|
+
try:
|
|
189
|
+
parsed_data = PagesBuild(**response.json())
|
|
190
|
+
return Response(response=response, data=parsed_data)
|
|
191
|
+
except ValidationError as e:
|
|
192
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
193
|
+
|
|
194
|
+
@require_auth
|
|
195
|
+
def list_pages_builds(self, **kwargs) -> Response:
|
|
196
|
+
"""List GitHub Pages builds for a repository (newest first).
|
|
197
|
+
|
|
198
|
+
GET /repos/{owner}/{repo}/pages/builds
|
|
199
|
+
https://docs.github.com/en/rest/pages/pages#list-apiname-pages-builds
|
|
200
|
+
"""
|
|
201
|
+
model = PagesBuildsModel(**kwargs)
|
|
202
|
+
response, data = self._fetch_all_pages(
|
|
203
|
+
f"/repos/{model.owner}/{model.repo}/pages/builds", model
|
|
204
|
+
)
|
|
205
|
+
try:
|
|
206
|
+
parsed_data = [PagesBuild(**item) for item in data]
|
|
207
|
+
return Response(response=response, data=parsed_data)
|
|
208
|
+
except ValidationError as e:
|
|
209
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
210
|
+
|
|
211
|
+
@require_auth
|
|
212
|
+
def request_pages_build(self, owner: str, repo: str) -> Response:
|
|
213
|
+
"""Request a fresh GitHub Pages build without pushing a commit.
|
|
214
|
+
|
|
215
|
+
The programmatic fix for the first-deploy race where the initial
|
|
216
|
+
Pages build never runs (e.g. Pages enabled after the deploy workflow
|
|
217
|
+
already finished): trigger a build of the latest revision on demand.
|
|
218
|
+
GitHub responds 201 with the queued build request.
|
|
219
|
+
|
|
220
|
+
POST /repos/{owner}/{repo}/pages/builds
|
|
221
|
+
https://docs.github.com/en/rest/pages/pages#request-a-apiname-pages-build
|
|
222
|
+
"""
|
|
223
|
+
response = self._session.post(
|
|
224
|
+
url=f"{self.url}/repos/{owner}/{repo}/pages/builds",
|
|
225
|
+
headers=self.headers,
|
|
226
|
+
verify=self.verify,
|
|
227
|
+
proxies=self.proxies,
|
|
228
|
+
)
|
|
229
|
+
response.raise_for_status()
|
|
230
|
+
try:
|
|
231
|
+
parsed_data = PagesBuildRequest(**response.json())
|
|
232
|
+
return Response(response=response, data=parsed_data)
|
|
233
|
+
except ValidationError as e:
|
|
234
|
+
raise ParameterError(f"Invalid parameters: {e.errors()}") from e
|
|
@@ -5,6 +5,7 @@ from github_agent.api.api_client_commits import Api as CommitsApi
|
|
|
5
5
|
from github_agent.api.api_client_contents import Api as ContentsApi
|
|
6
6
|
from github_agent.api.api_client_issues import Api as IssuesApi
|
|
7
7
|
from github_agent.api.api_client_orgs import Api as OrgsApi
|
|
8
|
+
from github_agent.api.api_client_pages import Api as PagesApi
|
|
8
9
|
from github_agent.api.api_client_pulls import Api as PullsApi
|
|
9
10
|
from github_agent.api.api_client_releases import Api as ReleasesApi
|
|
10
11
|
from github_agent.api.api_client_repos import Api as ReposApi
|
|
@@ -16,6 +17,14 @@ from github_agent.github_response_models import ( # noqa: F401
|
|
|
16
17
|
Commit,
|
|
17
18
|
Content,
|
|
18
19
|
Issue,
|
|
20
|
+
Organization,
|
|
21
|
+
OrganizationMembership,
|
|
22
|
+
OrganizationSummary,
|
|
23
|
+
PagesAlreadyEnabled,
|
|
24
|
+
PagesBuild,
|
|
25
|
+
PagesBuildRequest,
|
|
26
|
+
PagesNotEnabled,
|
|
27
|
+
PagesSite,
|
|
19
28
|
PullRequest,
|
|
20
29
|
Release,
|
|
21
30
|
Repository,
|
|
@@ -32,6 +41,7 @@ class Api(
|
|
|
32
41
|
ContentsApi,
|
|
33
42
|
IssuesApi,
|
|
34
43
|
OrgsApi,
|
|
44
|
+
PagesApi,
|
|
35
45
|
PullsApi,
|
|
36
46
|
ReleasesApi,
|
|
37
47
|
ReposApi,
|
|
@@ -13,6 +13,16 @@ from github_agent.api_client import Api
|
|
|
13
13
|
logger = get_logger(__name__)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
+
def allow_destructive_default() -> bool:
|
|
17
|
+
"""Fleet-wide default for the destructive-action gate.
|
|
18
|
+
|
|
19
|
+
Destructive MCP actions (e.g. organization delete, member removal) are
|
|
20
|
+
blocked unless the caller passes allow_destructive=true or the
|
|
21
|
+
GITHUB_ALLOW_DESTRUCTIVE environment variable is set truthy.
|
|
22
|
+
"""
|
|
23
|
+
return to_boolean(string=os.getenv("GITHUB_ALLOW_DESTRUCTIVE", "False"))
|
|
24
|
+
|
|
25
|
+
|
|
16
26
|
def get_client(config: dict | None = None) -> Api:
|
|
17
27
|
"""
|
|
18
28
|
Factory function to create the GitHub Api client.
|