Integrations
Repo admins manage integrations at /<owner>/<repo>/settings/integrations. Org owners manage them at
/orgs/<org>/integrations, and org integrations receive events from every repo in the org.
How delivery works
- Forge events (
emitWebhook) are fanned out on thegitfor-integrationsqueue. Failed deliveries retry with exponential backoff (15 s doubling, capped at 1 h). After 7 attempts (6 retries) a delivery moves togitfor-integrations-dlqand is markeddeadin the delivery log. - Permanent failures (most 4xx, missing credentials, rejected URLs) are logged as
failedand not retried. - Every outbound URL goes through
assertPublicHttpsUrl(hostname rules) and thenassertResolvesPublic(src/server/net/resolve-guard.ts), which does DNS-over-HTTPS A/AAAA lookups (following CNAMEs) and rejects private, loopback, link-local, CGNAT, metadata, multicast and reserved addresses, including IPv4-mapped, NAT64 and 6to4 forms. The check runs before every request; redirects are never followed. DoH outages fail closed with a retryable error. - TOCTOU: Workers'
fetch()cannot pin a connection to the checked address, so a DNS-rebinding window remains between the lookup and Cloudflare's own resolution. Answers are cached for 60 s per isolate so repeated checks agree, and Workers egress cannot reach RFC 1918, loopback or metadata addresses anyway. - Deliveries are idempotent per (source event, integration): retried events reuse the same delivery row,
and a short lease stops two consumers sending the same delivery. Slack bot posts track each channel, so
a retry only resends to channels that failed. Relayed GitHub webhooks are keyed by
X-GitHub-Delivery, so GitHub's own "Redeliver" of an already-processed delivery is ignored. - Each delivery is metered once when it reaches its final outcome (
ops.integration_deliveries, per owning account, idempotency keyintegration:<deliveryId>) intointegration_usage_dailyuntil the usage-metering service lands. - Git imports and ref syncs (
src/server/git/relay.ts) use the same DNS check on every hop, with no loopback escape. a 256 KB request cap and a 64 KB response cap. - Each payload is signed with the integration's secret in
X-Gitfor-Signature-256: sha256=<hex HMAC>. - Credentials (webhook URLs, OAuth tokens, signing secrets) are stored AES-GCM encrypted with
SECRETS_KEY.
Keeping noise down
Integrations are quiet by default:
- Only pull requests and releases are on.
- CI only reports failures.
- Pushes and CI only report the default branch.
- Drafts and bots are skipped.
Each integration has per-event toggles, branch globs, and a digest mode that batches events into one message every N minutes. The digest is flushed by the 5-minute cron.
Providers and secrets
| Provider | Secrets | Notes |
|---|---|---|
| Slack | SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_SIGNING_SECRET |
OAuth install (chat:write,commands,incoming-webhook) or a pasted incoming webhook. Each channel gets its own event filter. The slash command /gitfor status owner/repo is served at /integrations/hooks/slack/commands. |
| Google Chat | none | Paste a space webhook (chat.googleapis.com). Messages are sent as cardsV2. |
| Linear | LINEAR_CLIENT_ID, LINEAR_CLIENT_SECRET |
Issue keys (ENG-123) in PR titles, branches and commits link PRs and commits to issues. On PR open, merge or close, the issue moves to the configured state, and is never moved backwards. A signed Linear webhook is registered automatically at /integrations/hooks/linear/<id>. |
| GitHub | GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_PRIVATE_KEY, GITHUB_APP_CLIENT_ID, GITHUB_APP_CLIENT_SECRET, GITHUB_APP_WEBHOOK_SECRET |
Authenticates with GitHub App installation tokens only. Enable "Request user authorization during installation" so ownership of the installation can be verified. Supports a one-way mirror in either direction, a one-shot import of issues and PRs, and webhook relay. The app webhook URL is /integrations/hooks/github. |
| Webhook | none | Signed JSON with { event, summary, payload }. |
Notes on the GitHub integration (for a token-based, bidirectional mirror with any forge, see mirroring.md):
- The GitHub → gitfor mirror runs at most every 15 minutes, and also runs on each relayed GitHub push.
- Mirrors cover branches and tags only.
- Mirrors update and create refs but never delete them.
- The GitHub → gitfor mirror goes through the same gates as a git push, acting as a write-permission pusher (the installing admin): hidden refs are never written, branch protection applies (so a branch that requires pull requests is never updated by the mirror, and a protected branch only accepts a non-fast-forward when its rule allows force pushes), and push protection scans for secrets. Applied refs fire normal push events, so CI, webhooks and integrations run.
- By default the mirror is fast-forward only. Force mirror is an explicit per-integration admin opt-in that lets diverged unprotected branches be overwritten (it never relaxes push protection); enabling or disabling it (and changing the mirror direction) is recorded in the integration's audit log and shown in the UI. Refused refs are listed in the delivery log.
- Only repositories that the installing GitHub user could access through the installation can be chosen.
- PRs are imported as issues labelled
pull-request, since their branches are not copied. - Re-running an import skips items that were already imported.
Local development and e2e
Add INTEGRATIONS_ALLOW_LOOPBACK=true to .dev.vars to allow http://localhost and 127.0.0.1 sinks.
Never set it in production. Then run:
bun test src
E2E_BASE_URL=http://localhost:3000 bun run test:e2e:integrations
The e2e flow:
- Signs up and creates an org.
- Checks that a private-network URL is rejected.
- Adds a webhook integration that points at a local sink.
- Sends a test event and verifies its HMAC signature.
- Waits for the delivery log to show success, then redelivers.