appsmith-api / integrations /github_client.py
Kakashiix26's picture
W1: enhance-premium + GitHub publish + encrypted PAT
238042d verified
Raw
History Blame Contribute Delete
3.97 kB
"""
Thin GitHub REST client for token validation + publishing a repo.
ponytail: kept tiny and HTTP-call-shaped (module-level httpx.get/post/put) so tests can
patch `integrations.github_client.httpx.*` without any network. Never logs or returns the PAT.
"""
import base64
import logging
import httpx
logger = logging.getLogger(__name__)
GITHUB_API = "https://api.github.com"
_TIMEOUT = 15.0
class GitHubError(RuntimeError):
"""A GitHub API call failed (network or non-2xx). Message is safe to surface — no token."""
def _headers(token: str) -> dict[str, str]:
return {
"Authorization": f"token {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
}
def validate_token(token: str) -> str:
"""Return the GitHub login for a PAT. Raise ValueError if the token is invalid/unauthorized."""
try:
resp = httpx.get(f"{GITHUB_API}/user", headers=_headers(token), timeout=_TIMEOUT)
except httpx.HTTPError as exc: # network failure — not the user's fault, but token unverifiable
raise ValueError("Could not reach GitHub to validate the token") from exc
if resp.status_code != 200:
raise ValueError("Invalid GitHub token")
login = (resp.json() or {}).get("login")
if not login:
raise ValueError("Invalid GitHub token")
return login
def _ensure_repo(token: str, login: str, name: str) -> dict:
"""Get the repo if it exists, else create it (public). Returns the repo JSON."""
h = _headers(token)
try:
existing = httpx.get(f"{GITHUB_API}/repos/{login}/{name}", headers=h, timeout=_TIMEOUT)
if existing.status_code == 200:
return existing.json()
created = httpx.post(
f"{GITHUB_API}/user/repos",
headers=h,
json={"name": name, "private": False, "auto_init": True,
"description": "Generated by AppSmith / CodyBuddy Studio"},
timeout=_TIMEOUT,
)
except httpx.HTTPError as exc:
raise GitHubError("GitHub request failed while creating the repository") from exc
if created.status_code not in (200, 201):
raise GitHubError(f"GitHub could not create the repository (status {created.status_code})")
return created.json()
def _put_file(token: str, owner: str, repo: str, path: str, content: str, branch: str) -> None:
"""Create or update a single file via the contents API (handles existing-file sha)."""
h = _headers(token)
url = f"{GITHUB_API}/repos/{owner}/{repo}/contents/{path}"
try:
sha = None
head = httpx.get(url, headers=h, params={"ref": branch}, timeout=_TIMEOUT)
if head.status_code == 200:
sha = (head.json() or {}).get("sha")
payload: dict = {
"message": f"chore: add {path}",
"content": base64.b64encode(content.encode()).decode(),
"branch": branch,
}
if sha:
payload["sha"] = sha
resp = httpx.put(url, headers=h, json=payload, timeout=_TIMEOUT)
except httpx.HTTPError as exc:
raise GitHubError(f"GitHub request failed while writing {path}") from exc
if resp.status_code not in (200, 201):
raise GitHubError(f"GitHub rejected writing {path} (status {resp.status_code})")
def publish_files(token: str, repo_name: str, files: dict[str, str]) -> str:
"""Create-or-update `repo_name` under the authed user and push all files. Returns the repo HTML URL.
Raises ValueError if the token is invalid, GitHubError on any API failure.
"""
login = validate_token(token) # also resolves the owner login
repo = _ensure_repo(token, login, repo_name)
branch = repo.get("default_branch") or "main"
repo_url = repo.get("html_url") or f"https://github.com/{login}/{repo_name}"
for path, content in files.items():
_put_file(token, login, repo_name, path.lstrip("/"), content, branch)
return repo_url