Skip to content

feat(linear): add Linear webhook ingestion in router#1099

Merged
aaight merged 2 commits intodevfrom
feature/linear-webhook-ingestion
Apr 14, 2026
Merged

feat(linear): add Linear webhook ingestion in router#1099
aaight merged 2 commits intodevfrom
feature/linear-webhook-ingestion

Conversation

@aaight
Copy link
Copy Markdown
Collaborator

@aaight aaight commented Apr 14, 2026

Summary

  • Adds /linear/webhook POST route with GET verification handler in src/router/index.ts
  • Implements LinearRouterAdapter in src/router/adapters/linear.ts following the same pattern as JiraRouterAdapter
  • Creates LinearPlatformClient in src/router/platformClients/linear.ts for posting/deleting/updating comments on Linear issues via GraphQL API
  • Adds resolveLinearCredentials() to src/router/platformClients/credentials.ts and extends resolveWebhookSecret() to handle 'linear' provider
  • Adds LinearJob interface to src/router/queue.ts and includes it in the CascadeJob union
  • Adds parseLinearPayload() to src/webhook/webhookParsers.ts (re-exported from webhookHandlers.ts)
  • Adds verifyLinearWebhookSignature() and extractLinearTeamId() to src/router/webhookVerification.ts using HMAC-SHA256 on the Linear-Signature header
  • Adds extractLinearContext() to src/router/ackMessageGenerator.ts for contextual ack messages
  • Adds postLinearAck() / deleteLinearAck() to src/router/acknowledgments.ts
  • Exports LinearPlatformClient and resolveLinearCredentials from src/router/platformClients/index.ts

Key decisions

  • Linear signature verification uses raw HMAC-SHA256 hex (no sha256= prefix) matching Linear's documented format
  • extractLinearTeamId() reads from data.teamId in the webhook payload, consistent with the LinearWebhookPayload type
  • LinearRouterAdapter.isSelfAuthored() always returns false (no bot persona in Linear yet); can be enhanced later
  • LinearRouterAdapter.sendReaction() is a no-op (Linear does not support emoji reactions on issues via webhook)
  • LinearPlatformClient uses the Linear GraphQL API with commentCreate/commentDelete/commentUpdate mutations
  • Webhook secret is stored as the PM integration webhook_secret credential (same pattern as JIRA)

Test plan

  • tests/unit/router/adapters/linear.test.ts — 24 tests covering all LinearRouterAdapter methods (parseWebhook, isProcessableEvent, isSelfAuthored, sendReaction, resolveProject, dispatchWithCredentials, postAck, buildJob)
  • tests/unit/router/webhook-signature.test.ts — added 13 tests for extractLinearTeamId and verifyLinearWebhookSignature
  • All 373 test files (7356 tests) pass
  • TypeScript compiles cleanly (zero errors)
  • Lint passes (zero errors in modified files; 2 pre-existing warnings in unrelated files)

Card

https://trello.com/c/ijLbHh21/595-as-a-developer-i-want-linear-webhook-ingestion-in-the-router-so-that-linear-events-are-received-verified-and-enqueued

🤖 Generated with Claude Code

🕵️ claude-code · claude-sonnet-4-6 · run details

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 14, 2026

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Clean, well-structured feature addition that follows established patterns closely (JIRA/Sentry adapters). CI is green. One should-fix item worth discussing before merge.

Architecture & Design

The Linear integration plugs in at the correct layer and follows the existing RouterPlatformAdapter contract faithfully. Pattern consistency with the JIRA adapter is excellent — the file structure, credential resolution, webhook verification, and ack comment flow are all directly analogous.

Code Issues

Should Fix

  • src/router/adapters/linear.ts:98-101isSelfAuthored() always returns false while Comment is in PROCESSABLE_TYPES. When the bot posts an ack comment via LinearPlatformClient.postComment(), Linear fires a create/Comment webhook back to this handler. Since isSelfAuthored never identifies the bot's own comments, the event proceeds through the pipeline. If any trigger matches create/Comment, this creates an infinite loop (bot posts comment → webhook → trigger → agent runs → bot posts comment → …). The JIRA adapter handles this correctly by resolving the bot's account ID and comparing it against the comment author. Even if no triggers currently match comment creation, this is a latent hazard — a single trigger config change could silently activate the loop. Consider either (a) implementing userId comparison against the Linear API key owner, or (b) removing Comment from PROCESSABLE_TYPES until self-authored detection is in place.

Observations (non-blocking)

  • src/linear/types.ts vs runtime payloadsLinearWebhookCommentData doesn't include teamId, but the adapter accesses data.teamId on all event types (including Comment) via a Record<string, unknown> cast. The test fixture includes teamId in Comment payloads. If actual Linear Comment webhook payloads omit teamId from data, comment events would be silently dropped (no project match). This may be intentional ("comments without team context are unprocessable"), but worth confirming against real Linear webhook deliveries.

🕵️ claude-code · claude-opus-4-6 · run details

Add resolveLinearBotUserId() using the Linear GraphQL viewer query,
mirroring the JIRA bot identity pattern. isSelfAuthored() now compares
the comment payload's userId against the bot's user ID so bot-posted
ack comments are filtered before trigger dispatch, preventing an infinite
create/Comment webhook loop when triggers match comment creation events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

LGTM — Clean, well-structured implementation that correctly follows the established JIRA/Trello adapter pattern for adding Linear webhook ingestion to the router.

The implementation touches all the right extension points: webhook parser, signature verification, router adapter, platform client, credential resolution, ack message generation, bot identity resolution, job queue type, and barrel exports. Each piece is consistent with how the other platforms (especially JIRA) are wired up.

Key verifications:

  • Signature verification correctly uses HMAC-SHA256 with raw hex (no prefix), matching Linear's documented format, with timing-safe comparison via the shared verifyHmac helper
  • GraphQL mutations use proper parameterized variables (no string interpolation)
  • All error paths are gracefully handled with logging — no unhandled promise rejections
  • isSelfAuthored properly resolves the bot user via the viewer GraphQL query with caching
  • The createWebhookVerifier factory is reused correctly, maintaining the same null-skip-on-no-secret semantics as other platforms
  • Test coverage is thorough (24 adapter tests + 13 signature tests) with good edge case coverage

One minor observation (not blocking): LinearWebhookCommentData in src/linear/types.ts doesn't include teamId, but the adapter and signature verification both depend on data.teamId for Comment events. This works at runtime because the type union includes Record<string, unknown> as a catch-all, and the real Linear API does send teamId on all event types. Consider adding teamId to LinearWebhookCommentData in a follow-up to make the type more accurate.

🕵️ claude-code · claude-opus-4-6 · run details

@aaight aaight merged commit 04ab3c3 into dev Apr 14, 2026
8 of 9 checks passed
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