fix(integrations): Linear and GitHub sync need Pro; Linear plugin gets its own connection

Merged
SA
1mo ago
+260-42

What & why

Two support reports about Linear "not connecting". They had different causes, and both left the user without an explanation.

Free orgs looped on reconnect. Since #7947, the hourly suspend cron revokes Linear and GitHub for orgs outside the sync policy. It has revoked 1,334 Linear connections so far. Connecting was still open, though. A free org could connect, get revoked at the next hour, and then see "Linear authorization expired. Reconnect to resume syncing." Since the cron started, 5 of the 8 new Linear connections were revoked within an hour.

  • linear/connect and github/install send an org that organizationSyncsNow rejects to /settings/billing, so it never reaches Linear or GitHub. This is the same predicate the cron uses, so the two can't disagree.
  • New integration.syncAllowed query. Its callers are in #8048, which merges after this is live: the web pages redirect free orgs to billing, desktop's Settings → Integrations opens the Pro dialog, and the CLI checks before connecting.

A paid org's Linear workspace was already linked to a coworker's separate org. The callback returned workspace_already_linked, but the Linear page had no message for it.

  • The Linear callback now passes owner and the page says "…already connected by {owner}. Ask them to disconnect first.", as Slack does.

Two older bugs found while checking the above:

  • No integration callback toast has ever shown. IntegrationErrorHandler publishes in the page's effect. That runs before the root layout's <Toaster> subscribes, and sonner 2.0.7 drops anything published before then. The toast is now deferred one task (not cancelled on cleanup) and has a stable id so StrictMode can't show it twice.
  • The {owner}/{detail} messages on Slack and Teams rendered the name blank, because Lingui filled the placeholder with nothing on the server. The placeholder is now passed through as its own value.

The Linear plugin gets its own personal connection. The plugin and Tasks sync shared one per-person linear row, which uses our Linear OAuth app. Grants from that app turn on webhook deliveries. So #7947's revoke broke the plugin for free orgs, reconnecting through the plugin turned webhooks back on until the next revoke, and uninstalling the plugin disconnected sync.

  • New linear_mcp connector: a dynamic client against mcp.linear.app, like notion_mcp/sentry_mcp. It uses the client-metadata document on https and registration on localhost. It also offers a Personal API key option, since Linear's MCP server accepts keys as bearer tokens. Identity comes from the server's get_user("me"). These tokens belong to Linear's MCP app, so no webhooks follow and the cron never touches them. The plugin stays free.
  • plugins/linear 1.5.3 points at linear_mcp. Existing installs follow through installConnector, and each person signs in once.
  • The suspend cron revokes only OAuth linear rows. An API key starts no webhooks, so revoking it only deleted the person's key.
  • After merge: tag linear@1.5.3 (git tag linear@1.5.3 <merge sha> && git push --tags).

Not in this PR: the pricing page still lists "GitHub integration" under Free. Whether it moves to Pro is an open decision.

How I tested it

Local API and web on this workspace's Neon branch, signed in as the seeded dev admin, with temporary subscriptions and connections rows (deleted afterwards).

  • Free org: both connect URLs return 302 to /settings/billing.

  • Pro org: linear/connect returns 302 to linear.app/oauth/authorize, and github/install returns 302 to the dev GitHub App install page. Unchanged.

  • Toasts in headless Chrome: the Linear conflict toast names the owner, the Slack and Teams owner toasts include the email, and oauth_denied now shows. One toast each.

  • bun test in apps/api integrations (57 pass), typecheck for api/web/trpc/cli, biome, check:i18n.

  • Plugin, end to end against the real mcp.linear.app with a local API: /api/connectors/linear_mcp/connect registered a client, then Linear consent and workspace picker (Superset). The callback saved a linear_mcp row keyed on the Linear user with read/write and a refresh token. Then tools/call get_workspace through /mcp/plugins/superset/linear returned the Superset workspace. After that I disconnected, uninstalled and revoked the key.

  • check:plugins, 174 api integration tests, 84 connector/plugin tests.

Not tested: the Personal API key option on linear_mcp (no key available; Linear's docs say the MCP server accepts keys), the production client-metadata-document path (localhost always registers), a real Linear OAuth round-trip that hits the conflict (the page was checked by loading the callback's redirect URL directly), the CLI change beyond typecheck, and non-English rendering of the new strings.

Checklist

  • PR title follows conventional commits (type(scope): subject)
  • bun run lint and bun run typecheck pass (CI fails on lint warnings too)
  • "Allow edits from maintainers" is checked on fork PRs

Summary by cubic

Linear and GitHub sync now require Pro: on free orgs, the connect routes redirect to billing instead of allowing a connection the hourly suspend cron would revoke. The Linear conflict toast now names the workspace owner, and callback toasts show correctly after being deferred past the root layout's <Toaster> subscription.

The Linear plugin and Tasks sync no longer share one linear connection row. The plugin uses a new linear_mcp connector on mcp.linear.app (OAuth or personal API key) that starts no webhooks, so the suspend cron now revokes only OAuth linear rows, and existing installs pick up the connector automatically. A new integration.syncAllowed query lets clients check the sync policy before connecting.

Summary by CodeRabbit

  • New Features
    • Added support for connecting to Linear through its MCP endpoint using OAuth or a personal API key.
    • Linear connection-conflict messages now identify the existing owner or organization, with translations in supported languages.
  • Bug Fixes
    • GitHub and Linear connections now redirect to billing settings when synchronization isn’t enabled.
    • Linear workspace conflicts now return users to integration settings with a clear message.
    • Linear suspension checks no longer target active connections that don’t use OAuth.
  • Chores
    • Updated the Linear plugin version.