Git Integration (GitHub / GitLab)
Connect GitHub or GitLab repositories to a LumiBase site to track pull requests
and CI, view/store CI logs, post a content-validation status back, reconcile
declarative content intents (GitOps), and run opt-in ephemeral preview
environments per PR. Spec: .kiro/specs/git-integration/.
1. Goal & model
- Per-tenant: a site connects one or more repos; every row carries
site_id. - Provider-agnostic: business logic depends on the
GitProviderinterface;GitHubProvider/GitLabProvideradapters use onlyfetch(REST), so they run on both Cloudflare Workers and Docker via the shared runtime. - Earned autonomy: a
git-syncagent role is seeded at conservative L1 (PROPOSE) on first connect; goal creation stays planner-only.
2. Auth
- App install (
authMethod: 'app') — GitHub App / GitLab App; installation tokens are minted on demand (GitHub App JWT is RS256 over a PKCS#8 key). - OAuth / PAT (
authMethod: 'pat') — user pastes a token, or authorizes via the OAuth flow (GET /:id/oauth/authorizereturns a URL; the public callback is bound to a single-use cachestate).
3. Token security
- Access tokens and webhook secrets are encrypted at rest via
CryptoService(AES-GCM), AAD-bound to{ siteId, integrationId, field }— a ciphertext cannot be replayed across sites or fields. RequiresENCRYPTION_KEY. - Tokens are never returned by the API (
hasTokenboolean only) and never logged (maskTokenfingerprint).
4. Webhooks & CI
- Public receiver
POST /api/v1/integrations/git/webhook/:provider/:siteId/:integrationId. GitHub is verified by HMAC-SHA256 (X-Hub-Signature-256), GitLab by shared token (X-Gitlab-Token), compared in constant time. Unverified requests get401 INVALID_SIGNATUREbefore any state is touched. Events are logged idempotently by(provider, delivery_id)and processed async into the PR/CI cache. A failed CI run records anagent_incident+git_ci_failedaudit. - CI logs are fetched through the provider and cached in runtime blob storage so they remain viewable after the provider expires the originals.
5. GitOps
POST /:id/gitops/sync reads lumibase/intents.json from the repo, upserts each
definition into content_intents (via IntentService), then runs a drift scan +
reconcile so out-of-policy items become agent goals; provenance is recorded per
intent. Schema (collections/fields) apply through the HITL harness is a
follow-up.
6. Preview environments (opt-in)
When sync_config.preview = true, an opened PR provisions a derived ephemeral
site (${siteId}__pr-${number}) seeded with a copy of the base site's
collections/fields/items/pages, served at
/api/v1/deliver/page/${ephemeralSiteId}/…. It refreshes on push and is torn
down (cascade-delete) on close/merge, with a TTL cleanup.
7. Multi-tenancy
Shared across the deployment (server identity / config, not tenant data):
ENCRYPTION_KEY(KEK for token encryption),GITHUB_*/GITLAB_*OAuth & App credentials,LUMIBASE_PUBLIC_URL. These identify the server/app, not a tenant, and never carry tenant data.
Isolated per tenant (site_id):
- All six tables (
lumibase_git_integrations,_git_pull_requests,_git_ci_runs,_git_webhook_events,_git_preview_envs,_git_provenance) carrysite_id; everyGitIntegrationService/ query is scopedwhere(eq(table.siteId, siteId)); all six are registered for RLSsite_isolationinpackages/database/migrations/rls-policies.sql. - Webhook routing: the public receiver embeds
siteId+integrationIdin the URL and sets the RLS session var to thatsiteIdbefore reading — it can only ever touch that one site's rows, and the per-integration secret signature is the authenticity gate. - Preview: the ephemeral site id is derived from the base
siteId(${siteId}__pr-${n}); preview content lives in its ownsitesrow, isolated from the base site's production content. Provisioning is a deliberate cross-site system write (reads base under the base RLS context, writes the ephemeral site under the ephemeral context). - Token crypto: AAD binds ciphertext to
{ siteId, integrationId }, so a token row copied to another site fails to decrypt.
How to verify two-site isolation (staging, with Postgres):
- Connect repo R on site A and (separately) on site B.
GET /api/v1/integrations/gitwithX-Lumi-Site: Areturns only A's integrations; never B's. Same for/pull-requests,/ci,/provenance.- Each integration's
webhookUrlembeds its ownsiteId; deliver a signed webhook to A's URL and confirm only A's PR/CI cache changes. - With
sync_config.preview, open a PR on A → preview siteA__pr-Nis created; confirm it is invisible underX-Lumi-Site: Band its content does not touch A's production items.
Automated coverage: crypto.test.ts proves cross-site AAD isolation;
git-service-tenant.test.ts proves the site-scoped webhook URL / resource
mapping. The full DB-backed two-site list/query isolation is verified on staging
per the steps above (the CI environment has no Postgres).
8. Setup notes
- No setup-wizard step / seed / capability flag. Integrations are created after setup in Studio → Settings → Integrations → Git repositories.
- Migration
0009_git_integrationis additive (CREATE TABLE IF NOT EXISTS,lumibase_git_*prefix per ADR-010) — no backfill. See Setup Impact Registry row for git-integration. - The
git-syncrole is seeded lazily; its L1 autonomy grant is seeded on the first repo connect (idempotent, never overrides operator-set levels).