GitHub App Integration
The only inbound-triggered flow in the system — every other endpoint is driven by an explicit client call.
sequenceDiagram
participant GH as GitHub
participant WH as POST /integrations/github/webhook
participant Job as background job
participant LLM as LLM provider
GH->>WH: pull_request opened/synchronize (signed)
WH->>WH: look up integration by installation.id (or installation.app_id)
WH->>WH: verify X-Hub-Signature-256 against that integration's secret (or config.github, if none matched)
WH-->>GH: 202 {job_id}
WH->>Job: asyncio.create_task
Job->>GH: mint App JWT (org's own App, or the platform App) -> installation token
Job->>GH: fetch PR diff
Job->>LLM: run_analysis_auto() — auto-route each changed file, see above
Job->>Job: report_service.build_report_data()
Job->>GH: post PR comment + commit status
Two ways an org connects a GitHub App, coexisting on the same deployment:
- Platform App — one identity (
config.github: App ID, RSA private key, webhook secret) shared across every org that doesn’t register its own, set once by the operator. An org “installs” it on its own repos, producing aninstallation_idregistered viaPOST /orgs/{org_id}/integrations/github. - Custom App — an org registers its own App via the manifest flow:
POST /orgs/{org_id}/integrations/github/manifestmints a short-lived, single-usestatetoken (github_manifest_states, same “opaque token + SHA-256 hash + explicit expiry check” pattern asreport_links) and a manifest; the admin’s browser does a real top-level form POST togithub.com; GitHub redirects back to the publicGET /integrations/github/manifest/callback, which resolvesstate, exchanges the one-timecodevia GitHub’s ownPOST /app-manifests/{code}/conversions(libs/github_client.exchange_manifest_code), and stores the resulting App ID/private key/webhook secret on the org’sintegrationsdoc (github_app, encrypted the same way as stored LLM credentials —libs/crypto.py, Fernet). Itsinstallation_idis captured automatically from theinstallationcreatedwebhook event rather than pasted manually.
Every webhook-secret lookup and GitHubClient construction (routers/integrations.py’s _webhook_secret_for/_github_client_for) prefers an org’s own github_app when present, falling back to config.github otherwise — this is what lets the two paths coexist. Identifying which secret to try from the still-unverified payload (via installation.id, or installation.app_id for a brand-new custom App’s first-ever event, before any App has an installation_id on file) is safe precisely because it only selects a candidate to verify against — the HMAC check is still what gates any actual effect.
libs/github_client.py’s GitHubClient is a thin httpx wrapper (no GitHub SDK): it signs an RS256 JWT as whichever App identity applies, exchanges it for a scoped installation token, fetches the PR diff, and posts back a comment + commit status — failure if any open finding is CRITICAL/HIGH, else pending if any finding is needs_review (see Findings lifecycle above), else success.
The PR diff is handed to content_router.run_analysis_auto() (see
Auto-routing
above) rather than a hardcoded analyzer — each changed file is routed to
whichever org-enabled analyzer’s file_globs matches it. terraform-plan
never matches a PR diff (it declares no file_globs/sniff() for diff
input — it needs actual terraform plan output, which nothing in this
codebase produces). The job document’s analyzer field is the literal
"auto", with analyzers_used/detections carrying the real breakdown —
this is also what fixed a real gap: before auto-routing, the webhook always
called terraform-code-change directly, whose own parser silently dropped
every non-.tf file in the diff with no trace in the job record at all.
There is deliberately no IntegrationProvider abstraction yet — with exactly one provider implemented (GitHub; GitLab is on the roadmap), an interface to “support more providers later” would have nothing to validate it against. The platform-vs-custom-App split is a property of the integrations document, not a second provider.