Spaces:
Sleeping
Sleeping
| """GitHub App authentication helpers — JWT generation and installation token exchange.""" | |
| import os | |
| import time | |
| import httpx | |
| import jwt | |
| import structlog | |
| logger = structlog.get_logger(__name__) | |
| def _get_app_config() -> tuple[str, str]: | |
| """Return (app_id, private_key_pem) from env vars.""" | |
| app_id = os.getenv("GITHUB_APP_ID", "") | |
| private_key = os.getenv("GITHUB_APP_PRIVATE_KEY", "").replace("\\n", "\n") | |
| return app_id, private_key | |
| def generate_app_jwt() -> str: | |
| """Generate a short-lived JWT for authenticating as the GitHub App.""" | |
| app_id, private_key = _get_app_config() | |
| if not app_id or not private_key: | |
| raise RuntimeError("GITHUB_APP_ID and GITHUB_APP_PRIVATE_KEY must be set") | |
| now = int(time.time()) | |
| payload = { | |
| "iat": now - 60, # backdate 60s to avoid clock skew | |
| "exp": now + 600, # 10 min expiry | |
| "iss": app_id, | |
| } | |
| return jwt.encode(payload, private_key, algorithm="RS256") | |
| async def get_installation_token(installation_id: int | None = None) -> str: | |
| """Exchange the app JWT for an installation access token.""" | |
| inst_id = installation_id or int(os.getenv("GITHUB_APP_INSTALLATION_ID", "0")) | |
| if not inst_id: | |
| raise RuntimeError("GITHUB_APP_INSTALLATION_ID must be set") | |
| app_jwt = generate_app_jwt() | |
| async with httpx.AsyncClient(timeout=10) as client: | |
| resp = await client.post( | |
| f"https://api.github.com/app/installations/{inst_id}/access_tokens", | |
| headers={ | |
| "Authorization": f"Bearer {app_jwt}", | |
| "Accept": "application/vnd.github.v3+json", | |
| }, | |
| ) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| token = data["token"] | |
| logger.info("GitHub App installation token acquired", installation_id=inst_id) | |
| return token | |
| async def post_comment(owner: str, repo: str, issue_number: int, body: str) -> dict: | |
| """Post a comment on a PR/issue using the installation token.""" | |
| token = await get_installation_token() | |
| async with httpx.AsyncClient(timeout=15) as client: | |
| resp = await client.post( | |
| f"https://api.github.com/repos/{owner}/{repo}/issues/{issue_number}/comments", | |
| headers={ | |
| "Authorization": f"Bearer {token}", | |
| "Accept": "application/vnd.github.v3+json", | |
| }, | |
| json={"body": body}, | |
| ) | |
| resp.raise_for_status() | |
| return resp.json() | |