Sealrdocs
Guides

Coverage and the GitHub App

Connect GitHub so a push that bypasses a recorder becomes a dated, attributed coverage gap instead of silence.

A recorder only sees what passes through it. A developer who never installs the hook, a CI job that calls git push directly, an agent running on a laptop nobody enrolled — none of them produce records. Coverage is a deployment property, not a cryptographic one.

The GitHub App closes that loop from the other side. It observes every push that reaches your organization, whatever ran on whichever machine, and the Console reconciles those observations against recorder records. An observed push with no matching record becomes a coverage gap: dated, attributed to a repository, ref and actor, and countable.

This is the honest answer to "what if someone works around the recorder". Sealr cannot prevent it. It can make it visible.

What the app is allowed to do

  • Repository → Contents: Read-only
  • Repository → Metadata: Read-only
  • Subscribed events: Push (add Delete if you also want branch and tag deletions observed)

Nothing else. And no private key is generated or stored: the app never calls the GitHub API, it only receives signed webhooks. That is what keeps "Sealr holds no credential that can read your code" true rather than aspirational.

Two connection models

The Console supports both at once. Choose per customer; there is no global switch.

Model A — shared Sealr appModel B — your own app
Who registers the appSealr, onceYou, on your org
Effort to connectone click: Installregister the app, copy three values
Who holds the webhook secretSealr (server config, never shown)you
Blast radius if the secret leaksevery tenant on that Consolethat one installation
Best forthe defaultsovereign or regulated estates that will not route events through a vendor-controlled app
Console requirementGITHUB_APP_SLUG and GITHUB_APP_WEBHOOK_SECRET setnone

In both models the tenant is taken from the stored installation→tenant binding, never from the webhook payload, and no event is recorded until an HMAC-SHA256 over the raw body verifies in constant time.

Once, by whoever runs the Console. Register a GitHub App under Settings → Developer settings → GitHub Apps → New GitHub App:

  • Webhook → Active: on, URL https://<console-host>/v1/ingest/github
  • Webhook secret: a long random string — this is GITHUB_APP_WEBHOOK_SECRET, and customers never see it
  • Setup URL (Post-installation): https://<console-host>/v1/integrations/github/callback, with Redirect on update ticked — this is what binds an installation to a tenant
  • Permissions: Contents read-only, Metadata read-only
  • Subscribe to events: Push

Then set two environment variables on the Console and restart:

GITHUB_APP_SLUG=sealr                  # github.com/apps/<slug>
GITHUB_APP_WEBHOOK_SECRET=<the secret you generated>

GET /v1/integrations/github/capabilities now reports sharedApp: true and the Console shows the one-click button.

Per customer. In the Console: Coverage → Connect GitHub. That calls POST /v1/integrations/github/connect, which returns a GitHub install URL carrying a signed, 15-minute state naming the tenant. GitHub shows its own Install & choose repositories screen; on confirm it redirects to the Setup URL with installation_id and state. The Console verifies the state's signature and expiry and binds that installation to that tenant.

A forged or expired state creates no binding, and an installation already bound to another tenant is never silently moved.

The test that proves it works

Do not test the plumbing. Test the guarantee. With a repository connected:

# 1. A push THROUGH a recorder — should reconcile cleanly.
sealr run --resource repo:acme/api -- git push origin feature-a

# 2. A push that BYPASSES the recorder entirely — plain git.
git push origin feature-b

Wait for the reconciliation grace window, then open Coverage in the Console. Push #2 appears as a coverage gap naming the repository, ref, actor and time; a coverage.gap webhook fires; and the Exposure Report counts it under unrecorded_pushes. Push #1 does not appear.

That is the product's promise made visible: not "nobody can bypass the recorder", but "bypassing it is seen".

How reconciliation decides

An observed push becomes eligible for reconciliation once it is older than the grace window — 5 minutes by default, set with RECONCILE_GRACE_SECONDS — so a push whose record is still syncing is not flagged.

A recorder record accounts for an observation when all of these hold:

  • the record's integration is git;
  • its target resource equals repo:<full_name> of the observed repository;
  • the record's wall time is within 15 minutes either side of the observation;
  • when the observation names a ref, the record's projected operation contains the short ref name (refs/heads/main → main).

The match is on repository, ref name and a generous time window rather than an exact identifier, because the two sides have independent clocks and the recorder only ever projects the redacted command line. Being generous is deliberate: false gaps train operators to ignore the report, which costs more than an occasional missed match.

For a push to reconcile, the recorder must record it against the matching resource. sealr run --resource repo:acme/api -- git push … does that. A recorded push with no --resource gets a cmd:git target resource and will not match an observation.

Everything reconciled is written back: the observation is marked with its matching record id, or flagged as a gap. Gaps are durable rows — the coverage.gap webhook is best-effort alerting on top of them, not the evidence itself.

Reading the result

  • Coverage — the gap list: repository, ref, actor, whether the push was forced or a deletion, and when it was observed. GET /v1/coverage/gaps?days=…
  • Exposure Report — unrecorded_pushes over the reporting window.
  • Overview — the same figure leads the page, because it describes what you do not have.

The Console runs a reconciliation pass every 60 seconds across all tenants. You can force one with POST /v1/coverage/reconcile rather than waiting.

What this does not give you

  • It observes pushes to GitHub. An agent that writes to a database, applies Terraform, or pushes to a different forge is outside its view.
  • A gap says an observed push had no matching record. It does not say who intended to bypass anything — a mis-set --resource, a recorder that was offline past the match window, or a genuinely unrecorded machine all look the same from here.
  • It is detection, not prevention. Read what Sealr proves before you rely on it.

On this page