Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ All notable user-visible changes to CASCADE are documented here. The format is l
- **PM integration plug-and-play (infrastructure).** Introduced `PMProviderManifest` as the canonical per-provider contract — one object declares credentials, webhook route and verifier, router adapter, trigger handlers, platform client, job-id extractor, and optional label-creation hook. Landed `pmProviderRegistry`, a conformance test harness (`tests/unit/integrations/pm-conformance.test.ts`), shared helpers (`_shared/auth-headers.ts`, `_shared/webhook-verifier.ts`, `_shared/label-id-resolver.ts`, `_shared/project-id-extractor.ts`), a new `pm.discovery` tRPC router, and a frontend provider-wizard registry with a generic step renderer. Dormant in this release — Trello, JIRA, and Linear continue to register through the legacy path; they migrate onto the manifest in follow-up PRs. No operator-visible changes. Closes plan 006/1 of spec [006](docs/specs/006-pm-integration-plug-and-play.md).
- **PM integration plug-and-play (Trello migrated).** Trello's webhook signature verifier, router adapter, triggers, platform client, job-id extractor, wizard steps, and label/custom-field creation hooks are now composed via a single `trelloManifest` + `trelloProviderWizard`. Extended the `ProviderWizardDefinition` contract with an optional `useProviderHooks` field so provider-specific React hooks run inside a shell component — `ManifestProviderWizardSection` — rather than at the wizard root; this is how we satisfy the React rules-of-hooks while still keeping Trello's Discovery/LabelCreation/CustomFieldCreation hook composition per-provider. The conformance harness now exercises Trello alongside the test fixture (22 shared tests × provider). Trello's legacy registrations in `bootstrap.ts` stay for now because nine-plus call sites still use `pmRegistry.get('trello')` — plan 006/5 migrates those callers and deletes the legacy lines. No operator-visible changes. Closes plan 006/2 of spec [006](docs/specs/006-pm-integration-plug-and-play.md).
- **PM integration plug-and-play (JIRA migrated).** JIRA joins Trello on the manifest pattern with `jiraManifest` + `jiraProviderWizard`. `verifyWebhookSignature` uses the shared `makeHmacSha256Verifier` factory (Trello's bespoke scheme didn't fit, so this is the first consumer). Wizard steps + discovery / custom-field hooks moved into `jiraProviderWizard.useProviderHooks`; the JIRA-specific branches and hook instantiations are gone from `pm-wizard.tsx`. `worker-env.ts::extractProjectIdFromJob` JIRA branch removed (registry path handles it). Conformance harness now exercises Trello + JIRA + TestProvider (33 shared assertions × provider). Same deferrals as 006/2: `bootstrap.ts` JIRA registration stays until plan 006/5 migrates the `pmRegistry.get('jira')` callers. No operator-visible changes. Closes plan 006/3 of spec [006](docs/specs/006-pm-integration-plug-and-play.md).
- **PM integration plug-and-play (Linear migrated — all PM providers now on manifest).** `linearManifest` + `linearProviderWizard` complete the migration for all three PM providers. Linear uses the shared `makeHmacSha256Verifier({ headerName: 'linear-signature' })` factory. This plan also consolidates three divergent copies of Linear auth/label logic: `src/router/platformClients/linear.ts` and `src/router/bot-identity-resolvers.ts` both switch to the shared `linearAuthHeader` helper, and `src/pm/linear/adapter.ts::resolveLabelId` delegates to the shared `_shared/label-id-resolver`. The divergent copies that shipped the `Bearer`-prefix and silent-label-drop bugs are physically deleted from the codebase. `pm-wizard.tsx` collapses: with all 3 providers on the manifest, the non-manifest fallback path is gone — every PM provider renders via `ManifestProviderWizardSection`. `src/triggers/builtins.ts` is now manifest-only for PM (SCM + alerting still on legacy). Conformance harness runs 44 assertions (11 × TestProvider + Trello + JIRA + Linear). Same deferrals as 006/2 + 006/3: `bootstrap.ts` Linear registration stays until plan 006/5 migrates the ~dozen `pmRegistry.get(...)` callers. No operator-visible changes. Closes plan 006/4 of spec [006](docs/specs/006-pm-integration-plug-and-play.md).

### Added

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ plan: 4
plan_slug: migrate-linear
level: plan
parent_spec: docs/specs/006-pm-integration-plug-and-play.md
depends_on: [1-infrastructure.md]
status: pending
depends_on: [1-infrastructure.md.done, 2-migrate-trello.md.done, 3-migrate-jira.md.done]
status: done
---

# 006/4: Migrate Linear onto the PM provider manifest
Expand Down Expand Up @@ -199,17 +199,19 @@ Automatic. Ensure manifest is imported before harness runs.
## Progress

<!-- /implement updates these as it works. Do not edit manually. -->
- [ ] AC #1 Linear manifest registered
- [ ] AC #2 Conformance harness passes all three providers
- [ ] AC #3 Existing Linear tests green unchanged (modulo shared-helper adoption)
- [ ] AC #4 Wizard Linear branch removed
- [ ] AC #5 Legacy registration branches removed for Linear
- [ ] AC #6 Linear tRPC label endpoints consolidated
- [ ] AC #7 Platform clients + bot resolver use shared auth-header helper
- [ ] AC #8 Adapter delegates to shared label resolver
- [ ] AC #9 Operator-facing Linear behavior unchanged
- [ ] AC #10 All new code has tests
- [ ] AC #11 Build passes
- [ ] AC #12 Tests pass
- [ ] AC #13 Lint passes
- [ ] AC #14 Typecheck passes
- [x] AC #1 Linear manifest registered
- [x] AC #2 Conformance harness passes all three providers + TestProvider (44 tests — 11 × 4)
- [x] AC #3 Existing Linear tests green unchanged
- [x] AC #4 Wizard Linear branch removed — non-manifest fallback path deleted entirely (all 3 providers go through `ManifestProviderWizardSection`)
- [x] AC #5 Legacy registration branches removed — `builtins.ts` now manifest-only for PM; `worker-env.ts` extractor has no PM-specific branches
- [ ] AC #6 Linear tRPC label endpoints consolidated — **deferred to plan 006/5** (same reasoning as 006/2 + 006/3)
- [x] AC #7 Platform clients + bot resolver use `linearAuthHeader` from `_shared/auth-headers`; divergent in-file copies deleted
- [x] AC #8 Adapter delegates to shared `_shared/label-id-resolver.resolveLabelId`; private copy + `UUID_PATTERN` constant deleted
- [x] AC #9 Operator-facing Linear behavior unchanged — 7808/7808 tests pass
- [x] AC #10 All new code has tests (15 new Linear manifest tests)
- [x] AC #11 Build passes (backend + web)
- [x] AC #12 Tests pass (7808/7808)
- [x] AC #13 Lint passes
- [x] AC #14 Typecheck passes

**Partial-state**: `src/integrations/bootstrap.ts` Linear registration retained — same reason as 006/2 + 006/3 (~dozen `pmRegistry.get(...)` callers to migrate). Plan 006/5 handles this.
4 changes: 2 additions & 2 deletions src/integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ CASCADE's PM providers (Trello, JIRA, Linear, and any future Asana/GitLab/ClickU

This document is the canonical guide for adding a new PM provider.

> **Migration status (plan 006/4 in flight):**
> **Trello: ✓ migrated** (plan 006/2). **JIRA: ✓ migrated** (plan 006/3). Linear still on the legacy path — plan 006/4. Trello's and JIRA's `pmRegistry` registrations are kept in `src/integrations/bootstrap.ts` for now because many call sites still look up `pmRegistry.get('trello' | 'jira')`; plan 006/5 removes those callers and the bootstrap lines together.
> **Migration status (plan 006/5 pending — cleanup only):**
> **Trello: ✓ migrated** (006/2). **JIRA: ✓ migrated** (006/3). **Linear: ✓ migrated** (006/4). Every PM provider now registers through the manifest pattern; the shared conformance harness exercises all three alongside `TestProvider`. `src/integrations/bootstrap.ts` still registers all three in `pmRegistry` for backward compatibility with the ~dozen `pmRegistry.get(...)` call sites in webhook handlers, manual runners, and credential scoping. Plan 006/5 migrates those callers to `pmProviderRegistry.get(id)?.pmIntegration` and deletes the legacy registration paths atomically.

---

Expand Down
1 change: 1 addition & 0 deletions src/integrations/pm/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

import './trello/index.js';
import './jira/index.js';
import './linear/index.js';
10 changes: 10 additions & 0 deletions src/integrations/pm/linear/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Linear PM provider — side-effect module that registers the manifest.
*/

import { registerPMProvider } from '../registry.js';
import { linearManifest } from './manifest.js';

registerPMProvider(linearManifest);

export { linearManifest };
64 changes: 64 additions & 0 deletions src/integrations/pm/linear/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* Linear PM provider manifest.
*
* Wires the existing Linear implementation into the PMProviderManifest
* contract. Linear signs webhook bodies with HMAC-SHA256 hex in the
* `linear-signature` header — no prefix — so the shared
* `makeHmacSha256Verifier` factory covers it directly.
*
* This plan (006/4) also migrates Linear's platform client + bot
* identity resolver to the canonical `linearAuthHeader` helper and the
* adapter's `resolveLabelId` to the shared `_shared/label-id-resolver`.
* See the companion src/router/platformClients/linear.ts and
* src/pm/linear/adapter.ts edits.
*/

import { LinearIntegration } from '../../../pm/linear/integration.js';
import { LinearRouterAdapter } from '../../../router/adapters/linear.js';
import { LinearPlatformClient } from '../../../router/platformClients/linear.js';
import { LinearCommentMentionTrigger } from '../../../triggers/linear/comment-mention.js';
import { LinearReadyToProcessLabelTrigger } from '../../../triggers/linear/label-added.js';
import { LinearStatusChangedTrigger } from '../../../triggers/linear/status-changed.js';
import { makeHmacSha256Verifier } from '../_shared/webhook-verifier.js';
import type { PMProviderManifest } from '../manifest.js';

const linearIntegration = new LinearIntegration();

export const linearManifest: PMProviderManifest = {
id: 'linear',
label: 'Linear',
category: 'pm',

credentialRoles: [
{ role: 'api_key', label: 'API Key', envVarKey: 'LINEAR_API_KEY' },
{
role: 'webhook_secret',
label: 'Webhook Secret',
envVarKey: 'LINEAR_WEBHOOK_SECRET',
optional: true,
},
],

webhookRoute: '/linear/webhook',
verifyWebhookSignature: makeHmacSha256Verifier({
headerName: 'linear-signature',
}),

routerAdapter: new LinearRouterAdapter(),

extractProjectIdFromJob: async (jobData) => {
const d = jobData as unknown as { type?: string; projectId?: string };
if (d.type !== 'linear') return null;
return d.projectId ?? null;
},

pmIntegration: linearIntegration,

triggerHandlers: [
new LinearCommentMentionTrigger(),
new LinearStatusChangedTrigger(),
new LinearReadyToProcessLabelTrigger(),
],

platformClientFactory: (projectId) => new LinearPlatformClient(projectId),
};
26 changes: 9 additions & 17 deletions src/pm/linear/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* (sub-issues), following the same pattern used by JiraPMProvider for subtasks.
*/

import { resolveLabelId as sharedResolveLabelId } from '../../integrations/pm/_shared/label-id-resolver.js';
import { linearClient } from '../../linear/client.js';
import { logger } from '../../utils/logging.js';
import type { LinearConfig } from '../config.js';
Expand All @@ -22,8 +23,6 @@ import type {
WorkItemLabel,
} from '../types.js';

const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

export class LinearPMProvider implements PMProvider {
readonly type = 'linear' as const;

Expand All @@ -32,24 +31,17 @@ export class LinearPMProvider implements PMProvider {
/**
* Resolve a label slot name or raw ID to a Linear label UUID.
*
* Linear's GraphQL API requires UUIDs for issueUpdate.labelIds and
* issueLabelCreate lookups. Returning a non-UUID string would silently
* fail server-side, so we short-circuit misconfigurations here with a
* diagnostic. Returns null when the input cannot be resolved to a UUID.
* Delegates to the shared `_shared/label-id-resolver` helper — single
* source of truth for the UUID validation rule. Returns null when the
* input cannot be resolved to a UUID; the caller then short-circuits
* the label operation with a visible warn.
*/
private resolveLabelId(slotOrId: string): string | null {
const mapped = (this.config.labels as Record<string, string> | undefined)?.[slotOrId];
const candidate = mapped ?? slotOrId;
if (UUID_PATTERN.test(candidate)) return candidate;
logger.warn(
'[Linear] Label value is not a UUID — skipping (check PM wizard → Label Mappings)',
{
input: slotOrId,
resolved: mapped ?? '<no mapping>',
teamId: this.config.teamId,
},
return sharedResolveLabelId(
slotOrId,
this.config.labels as Record<string, string> | undefined,
{ providerId: 'linear', extra: { teamId: this.config.teamId } },
);
return null;
}

async getWorkItem(id: string): Promise<WorkItem> {
Expand Down
7 changes: 2 additions & 5 deletions src/router/bot-identity-resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Extracted from `acknowledgments.ts` to keep that module focused on ack CRUD.
*/

import { linearAuthHeader } from '../integrations/pm/_shared/auth-headers.js';
import { BotIdentityCache } from './bot-identity.js';
import {
resolveJiraCredentials,
Expand Down Expand Up @@ -93,11 +94,7 @@ export async function resolveLinearBotUserId(projectId: string): Promise<string

const response = await fetch('https://api.linear.app/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Linear personal API keys are sent bare; `Bearer` is OAuth-only.
Authorization: creds.apiKey,
},
headers: linearAuthHeader(creds.apiKey),
body: JSON.stringify({ query: '{ viewer { id } }' }),
});
if (!response.ok) return null;
Expand Down
8 changes: 2 additions & 6 deletions src/router/platformClients/linear.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Comments are posted using the Linear GraphQL API with markdown body text.
*/

import { linearAuthHeader } from '../../integrations/pm/_shared/auth-headers.js';
import { logger } from '../../utils/logging.js';
import { resolveLinearCredentials } from './credentials.js';
import type { PlatformCommentClient } from './types.js';
Expand All @@ -18,12 +19,7 @@ async function linearGraphQL(
): Promise<Record<string, unknown>> {
const response = await fetch(LINEAR_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Linear personal API keys (lin_api_*) are sent bare; the `Bearer` prefix
// is only valid for OAuth tokens and triggers HTTP 400 with personal keys.
Authorization: apiKey,
},
headers: linearAuthHeader(apiKey),
body: JSON.stringify({ query, variables }),
});

Expand Down
7 changes: 2 additions & 5 deletions src/router/worker-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,8 @@ export async function extractProjectIdFromJob(data: CascadeJob): Promise<string
// Use type assertion since dashboard jobs are cast to CascadeJob
const jobData = data as unknown as { type: string; projectId?: string; repoFullName?: string };

// `trello` (006/2) and `jira` (006/3) are handled by the manifest
// registry. The remaining `linear` branch migrates in plan 006/4.
if (jobData.type === 'linear') {
return jobData.projectId ?? null;
}
// All PM providers (trello / jira / linear) now route through the
// manifest registry above. Non-PM job types remain below.
if (jobData.type === 'github') {
if (!jobData.repoFullName) return null;
const project = await findProjectByRepo(jobData.repoFullName);
Expand Down
7 changes: 3 additions & 4 deletions src/triggers/builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,18 @@

import { listPMProviders } from '../integrations/pm/registry.js';
import { registerGitHubTriggers } from './github/register.js';
import { registerLinearTriggers } from './linear/register.js';
import type { TriggerRegistry } from './registry.js';
import { registerSentryTriggers } from './sentry/register.js';

export function registerBuiltInTriggers(registry: TriggerRegistry): void {
// Manifest-registered PM providers (Trello via 006/2, JIRA via 006/3;
// Linear joins in 006/4) contribute their triggerHandlers here.
// Every PM provider (Trello, JIRA, Linear) contributes triggers via the
// manifest registry. SCM (GitHub) and alerting (Sentry) still use legacy
// registration — spec 006 scoped to PM only.
for (const manifest of listPMProviders()) {
for (const handler of manifest.triggerHandlers) {
registry.register(handler);
}
}
registerLinearTriggers(registry); // migrates in plan 006/4
registerGitHubTriggers(registry);
registerSentryTriggers(registry);
}
3 changes: 2 additions & 1 deletion tests/unit/integrations/pm-conformance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import type { CascadeJob } from '../../../src/router/queue.js';
import { registerTestProvider } from '../../helpers/testPMProvider.js';

// Import every real PM provider so the harness exercises each of them
// alongside the TestProvider fixture. Plan 006/4 will add linear.
// alongside the TestProvider fixture.
import '../../../src/integrations/pm/trello/index.js';
import '../../../src/integrations/pm/jira/index.js';
import '../../../src/integrations/pm/linear/index.js';

// describe.each evaluates at collection time, before beforeAll. Register
// the TestProvider at module load so the iteration sees it.
Expand Down
Loading
Loading