From 4f30fc258e0d4cc75f2fa6eb5223d40054d924a5 Mon Sep 17 00:00:00 2001 From: Zbigniew Sobiecki Date: Thu, 23 Apr 2026 09:33:14 +0000 Subject: [PATCH] fix(linear): correct webhook URL origin and path in wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Linear wizard was constructing the webhook URL from window.location.origin (the dashboard) instead of API_URL (the router), and using the path /webhooks/{projectId}/linear instead of the actual route /linear/webhook. Since Linear's API forbids programmatic webhook registration the user copies this URL manually — it has to be right. Fixed to match the pattern used by JIRA and Trello: derive base from API_URL with a dev-port fallback, then append /linear/webhook. Co-Authored-By: Claude Sonnet 4.6 --- .../unit/web/linear-wizard-generator.test.ts | 37 +++++++++++++++++++ .../projects/pm-providers/linear/wizard.ts | 6 ++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/unit/web/linear-wizard-generator.test.ts b/tests/unit/web/linear-wizard-generator.test.ts index 91845c96..d1d11b7c 100644 --- a/tests/unit/web/linear-wizard-generator.test.ts +++ b/tests/unit/web/linear-wizard-generator.test.ts @@ -7,7 +7,19 @@ * focuses on the Linear-specific wizard wiring. */ +import { readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { describe, expect, it } from 'vitest'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const REPO_ROOT = resolve(__dirname, '..', '..', '..'); +const LINEAR_WIZARD_PATH = resolve( + REPO_ROOT, + 'web/src/components/projects/pm-providers/linear/wizard.ts', +); + import { linearManifest } from '../../../src/integrations/pm/linear/manifest.js'; import { renderStandardStep, @@ -77,3 +89,28 @@ describe('linearProviderWizard (post plan 011/4)', () => { } }); }); + +describe('linearProviderWizard — webhook URL construction guard', () => { + // Linear's API forbids programmatic webhook registration so the user copies + // the URL manually into linear.app. The URL must point at the router server + // (API_URL / VITE_API_URL), not the dashboard origin, and must use the + // correct path /linear/webhook — not the project-scoped /webhooks/{id}/linear + // pattern that was briefly wrong. + + it('uses API_URL (router origin) not window.location.origin for webhookUrl', () => { + const source = readFileSync(LINEAR_WIZARD_PATH, 'utf8'); + // Must import API_URL + expect(source, 'API_URL must be imported').toContain("import { API_URL } from '@/lib/api.js'"); + // Must use it to build the base + expect(source, 'routerOrigin must be derived from API_URL').toContain('API_URL ||'); + }); + + it('uses /linear/webhook path not /webhooks/{projectId}/linear', () => { + const source = readFileSync(LINEAR_WIZARD_PATH, 'utf8'); + expect(source, '/linear/webhook must be the path').toContain('/linear/webhook'); + expect( + source, + '/webhooks/{projectId}/linear must not appear — wrong path and wrong origin', + ).not.toContain('/webhooks/'); + }); +}); diff --git a/web/src/components/projects/pm-providers/linear/wizard.ts b/web/src/components/projects/pm-providers/linear/wizard.ts index 36fca642..2d42755c 100644 --- a/web/src/components/projects/pm-providers/linear/wizard.ts +++ b/web/src/components/projects/pm-providers/linear/wizard.ts @@ -21,6 +21,7 @@ import { useQuery } from '@tanstack/react-query'; import { type ReactElement, useState } from 'react'; +import { API_URL } from '@/lib/api.js'; import { trpc } from '@/lib/trpc.js'; import { useLinearDiscovery, useLinearLabelCreation } from '../../pm-wizard-hooks.js'; import { buildLinearIntegrationConfig } from '../../pm-wizard-state.js'; @@ -293,7 +294,10 @@ export const linearProviderWizard: ProviderWizardDefinition = { labels.createMissingLabelsMutation.mutate(resolved); }; - const webhookUrl = projectId ? `${window.location.origin}/webhooks/${projectId}/linear` : ''; + const routerOrigin = + API_URL || + (typeof window !== 'undefined' ? window.location.origin.replace(':5173', ':3000') : ''); + const webhookUrl = routerOrigin ? `${routerOrigin}/linear/webhook` : ''; const details = state.linearTeamDetails;