4.3 KiB
4.3 KiB
Adding a new third-party integration
Jira and Linear are the model: per-workspace credentials, a 90s auth-health poller, a settings page with status banner + reconnect CTA, link/import buttons that gate on availability. New integrations should reuse the shared shapes in this folder rather than copying either.
This file covers both halves of the playbook (backend + frontend) so the pattern is readable in one place.
Backend (apps/backend/internal/<name>/)
- Mirror the package layout:
service.go,store.go,client.go,provider.go,handlers.go,models.go,poller.go. ExposeProvide(writer, reader *sqlx.DB, secrets SecretStore, eventBus bus.EventBus, log *logger.Logger) (*Service, func() error, error). PassnilforeventBuswhen the integration doesn't publish events; both Jira and Linear take and use it for issue-watch publishing. - Use
internal/integrations/secretadapterinstead of writing your own upsert wrapper aroundsecrets.SecretStore. The adapter satisfies any per-integrationSecretStoreinterface shaped as{Reveal, Set, Delete, Exists}. - Use
internal/integrations/healthpollfor the auth-health loop. Implement theProberinterface (ListConfiguredWorkspaces+RecordAuthHealth) on a small adapter and lethealthpoll.New("name", prober, log)own Start/Stop/ticker. Keep integration-specific loops (JQL polling, webhook reconciliation, etc.) separate, like jira's issue-watch loop. - Wire the service via a per-domain
init<Name>Service(...)helper incmd/kandev/services.go, not inline inprovideServices. - Ship a
mock_client.go+mock_controller.gonext to the real client.Providebranches onKANDEV_MOCK_<NAME>=trueand returns the in-memory client;RegisterMockRoutes(router, svc, log)mounts/api/v1/<name>/mock/*only when the service was built with the mock. The e2e backend fixture sets the env var so Playwright tests drive the mock viaapiClient.mock<Name>*()helpers — see jira/linear for the layout. - If the watcher has a per-watch
MaxInflightTaskscap, implementWatchMetadataKey()on the integration'sWatcherSource(internal/orchestrator/source_<name>.go) to return the task-metadata watch-id key — the same constant the source writes intoBuildTaskRequest'sMetadatamap (e.g.sentry_issue_watch_id). The throttle gate (acquireWatcherSlot) passes that key toCountOpenWatcherCreatedTasks(metadataKey, watchID), which counts open tasks for the watch. The task repository (internal/task/repository/sqlite/task.go) is intentionally agnostic of integrations — it keys purely on the metadata key, so no repository change is needed per integration. IfWatchMetadataKey()returns""the gate treats the watch as uncapped and the cap silently never applies (Sentry originally shipped with this gap — the cap was stored and validated but never enforced because the repository's old integration switch had nosentrycase).
Frontend (apps/web/)
- Hooks live under
hooks/domains/<name>/, notcomponents/<name>/. - Use
hooks/domains/integrations/use-integration-availability.tsanduse-integration-enabled.ts— each integration'suseXAvailable/useXEnabledshould be a one-line wrapper passing the storage key + sync event + config-fetch function. - Settings page reuses
<IntegrationAuthStatusBanner>(components/integrations/auth-status-banner.tsx). - "Auth required / reconnect" UI reuses
<IntegrationAuthErrorMessage>(components/integrations/auth-error-message.tsx) — supply the integration's display name, regex check, and reconnect href. - Link / import popovers reuse
<ValidatedPopover>(components/integrations/validated-popover.tsx) — supply the icon, label, key regex, fetch function, and success callback.
Where Jira and Linear deliberately diverge
- Issue model: Jira uses transitions + JQL; Linear uses state IDs + structured filters. Don't merge these schemas — the upstream APIs are genuinely different.
- Watch filter persistence: Jira stores the JQL string verbatim; Linear stores the structured
SearchFilteras JSON infilter_json(Linear has no JQL equivalent). The orchestrator emitsNewJiraIssueEvent/NewLinearIssueEventrespectively and dedups by issue key (Jira) vs identifier (Linear). - Health column extras: Linear's
linear_configsrow carries anorg_slugcaptured from successful probes; Jira's row does not.