gitmind-backend / services /github_app.py
Ak001z's picture
auto-deploy from CI (23aaf8f)
dbf9cad verified
Raw
History Blame Contribute Delete
2.48 kB
"""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()