Skip to content

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 an installation_id registered via POST /orgs/{org_id}/integrations/github.
  • Custom App — an org registers its own App via the manifest flow: POST /orgs/{org_id}/integrations/github/manifest mints a short-lived, single-use state token (github_manifest_states, same “opaque token + SHA-256 hash + explicit expiry check” pattern as report_links) and a manifest; the admin’s browser does a real top-level form POST to github.com; GitHub redirects back to the public GET /integrations/github/manifest/callback, which resolves state, exchanges the one-time code via GitHub’s own POST /app-manifests/{code}/conversions (libs/github_client.exchange_manifest_code), and stores the resulting App ID/private key/webhook secret on the org’s integrations doc (github_app, encrypted the same way as stored LLM credentials — libs/crypto.py, Fernet). Its installation_id is captured automatically from the installation created webhook 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.