Skip to content

Merge dev to main: Linear router multi-project-per-team fix#1334

Merged
zbigniewsobiecki merged 4 commits into
mainfrom
dev
May 11, 2026
Merged

Merge dev to main: Linear router multi-project-per-team fix#1334
zbigniewsobiecki merged 4 commits into
mainfrom
dev

Conversation

@zbigniewsobiecki
Copy link
Copy Markdown
Member

Summary

  • fix(router): route Linear webhooks by team + issue's project, not just team (fix(router): route Linear webhooks by team + issue's project, not just team #1332) — closes the prod regression that surfaced live today (MNG-638 wasn't triggering implementation despite multiple moves to Todo). Two cascade projects (cascade + ucho) share one Linear team after the cascade Trello→Linear migration; the router used `.find()` on teamId which always returned ucho, then the scope filter dropped events for issues in cascade's Linear Project. Replaces single-match selection with team + Linear-Project candidate selection (strong scoped match → unscoped catch-all → drop with diagnostics).

Test plan

  • dev CI green pre-merge
  • Watch main CI + Build and Deploy after merge
  • Post-deploy: user re-moves MNG-638 to Todo → cascade implementation agent fires

🤖 Generated with Claude Code

aaight and others added 4 commits May 11, 2026 11:28
Co-authored-by: Cascade Bot <bot@cascade.dev>
…t team

User reported MNG-638 (Linear) wasn't triggering the implementation agent
despite multiple moves to Todo and `cascade-ready` label applications.
Investigation found:

1. Linear webhooks ARE arriving (`webhooklogs list --source linear`) but
   every one is `processed=False decisionReason="Event unparseable or
   not processable"`.
2. Loki shows the real reason — `LinearRouterAdapter: dropping event
   outside project scope` — fires ~20 times on MNG-638's UUID. The
   webhooklogs UI hides the specific drop reason because parseWebhook()
   returns null and the outer wrapper emits the generic "unparseable"
   message.
3. The actual bug: two cascade projects share one Linear team after
   `cascade` was migrated from Trello → Linear:
     - `cascade` → Linear team 310c41fe-..., scoped to Linear Project 83a0f22b-...
     - `ucho`    → same team, scoped to Linear Project 7108c72e-...
   The router used `config.projects.find((p) => p.linear?.teamId === teamId)`
   which always returned the first match by team. The follow-up
   matchesConfiguredProjectScope() then dropped events whose issue
   belonged to the OTHER cascade project's Linear scope. MNG-638 is in
   cascade's Linear Project, but the .find() picked ucho first → scope
   mismatch → drop. Latent until the second cascade project landed on
   the same team this morning; now a deterministic loss for everything
   in cascade's Linear Project.

Fix: replace "find first by teamId" with "best match by teamId + Linear
Project scope".

- Collect ALL candidates that share the teamId via `.filter()`.
- Extract issue's Linear Project ID BEFORE selecting a candidate
  (`resolveIssueProjectId` helper — pulls from data.projectId /
  data.project.id, or fetches via API for Comment events without inline
  context).
- Pick the candidate with deterministic fallback:
    1. Strong match: candidate whose `linear.projectId` equals issue's project.
    2. Catch-all: candidate with NO `linear.projectId` configured.
    3. Otherwise: drop with the existing info-level log (now augmented
       with the full candidates list for diagnostics — "no candidate
       matches issue project" vs "issue has no project").

Delete `matchesConfiguredProjectScope` — its job is now done inline by
the new selection logic. `fetchIssueProjectId` stays (called by the
helper). When `candidates.length === 1` the new logic degrades to the
old behavior, so single-project-per-team setups continue working.

Tests:
- `tests/unit/router/adapters/linear.test.ts`: updated the existing scope-
  filter assertions to match the new log shape (reason field + candidates
  list), added a `parseWebhook — multi-cascade-project-per-team` describe
  block with 7 scenarios:
    * routes to the project matching the issue's Linear scope (cascade)
    * routes to the other project when the issue belongs to it (ucho)
    * drops with candidates list when no candidate subscribes
    * drops with `'issue has no project'` reason when issue is unscoped
      and all candidates are scoped
    * falls back to unscoped catch-all candidate when no scoped match
    * prefers the scoped match over an unscoped catch-all
    * Comment event uses API fallback via the first candidate's creds
  All 45 LinearRouterAdapter tests pass.

JIRA and Trello router adapters use their own unique discriminator
(project key / board ID) per cascade project so they don't share the
same multi-project-per-discriminator vulnerability.

Verification: full suite (9280 tests) clean. After deploy, re-moving
MNG-638 to Todo should route to `cascade` (issue's projectId `83a0f22b-...`
matches cascade's scope) and fire the implementation agent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Cascade Bot <bot@cascade.dev>
…multi-project-per-team

fix(router): route Linear webhooks by team + issue's project, not just team
@zbigniewsobiecki zbigniewsobiecki merged commit f1e8d78 into main May 11, 2026
14 checks passed
@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

❌ Patch coverage is 96.68874% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/router/worker-timeouts.ts 91.80% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants