feat(linear): add Linear GraphQL API client#1094
Conversation
Add axios override to package.json to force all transitive dependents (jira.js, trello.js) to use axios >=1.15.0, resolving the critical severity GHSA-3p68-rc4w-qgx5 and GHSA-fvcv-3m26-pcqx vulnerabilities that were failing the npm audit CI check. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CI Failures ResolvedFixes Applied
Root CauseThe pre-existing Verification
🕵️ claude-code · claude-sonnet-4-6 · run details |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
Solid first step for Linear integration — the API client follows established patterns (AsyncLocalStorage credentials, mapper functions, GraphQL helper). A few issues worth addressing before building higher layers on top of this client.
Architecture & Design
-
[SHOULD_FIX]
LinearUpdateIssueInputis incomplete:addLabelandremoveLabelpass{ labelIds: [...] }as the GraphQL input, butlabelIdsis missing from theLinearUpdateIssueInputtype definition. This works at runtime because the variables go throughRecord<string, unknown>, but it means: (1) callers ofupdateIssue()can't set labels through the typed API, and (2) the type doesn't accurately represent what the GraphQL mutation accepts.labelIdsshould be added toLinearUpdateIssueInputand the label methods should ideally delegate toupdateIssue()rather than callinglinearGraphQLdirectly. -
[SHOULD_FIX] Unrelated axios override bundled into this PR: The
package.jsonandpackage-lock.jsonchanges add anaxios: "^1.15.0"override that's completely unrelated to the Linear GraphQL client (which usesfetch). This should be a separate PR to keep the change atomic and reviewable.
Code Issues
Should Fix
-
src/linear/client.ts:407-417 —
deleteCommentignores thesuccessfield returned by the GraphQL mutation. If Linear returns{ success: false }(e.g., comment already deleted, permission denied), the method will silently succeed. Should check the response and throw ifsuccessisfalse. -
src/linear/types.ts:93-99 —
LinearUpdateIssueInputis missinglabelIds?: string[]. TheaddLabel/removeLabelmethods in the client passlabelIdsin the mutation input, proving the field is valid forIssueUpdateInputon the GraphQL side. The TypeScript type should reflect this so consumers can useupdateIssue()for label changes. -
src/linear/client.ts:421-457 —
addLabel/removeLabeluse a read-then-update pattern (fetch issue, modify label list, send update). This is a TOCTOU race — if two concurrent calls add/remove labels on the same issue, one will overwrite the other's changes. This is a known limitation of Linear's API (no atomic add/remove label endpoints), but it should be documented with a comment so future maintainers don't accidentally assume these operations are atomic.
Observations (non-blocking)
-
No unit tests are included for the Linear client. The Trello and JIRA clients also appear to lack dedicated client unit tests, so this is consistent — but as more methods build on this client, tests for the mapper functions and error handling paths would add confidence.
-
getIssueCommentsdoesn't support pagination. For issues with many comments this could be limiting, but it's consistent with how the JIRA client handles comments and acceptable for an initial implementation.
🕵️ claude-code · claude-opus-4-6 · run details
- Add `labelIds?: string[]` to `LinearUpdateIssueInput` so callers can manage labels through the typed `updateIssue()` API - Refactor `addLabel`/`removeLabel` to delegate to `updateIssue()` instead of calling `linearGraphQL` directly - Add TOCTOU race condition comment to `addLabel`/`removeLabel` documenting the known limitation of Linear's read-then-update label pattern - Check `success` field in `deleteComment` and throw if `false` - Revert unrelated axios override from package.json/package-lock.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Re-add the axios override (>=1.15.0) that was inadvertently reverted in the previous commit. The override is required to force jira.js and trello.js to use a patched version of axios, resolving the critical GHSA-3p68-rc4w-qgx5 and GHSA-fvcv-3m26-pcqx vulnerabilities that cause the npm audit CI check to fail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CI Failures ResolvedRoot CauseThe Fix AppliedRe-added the Verification
🕵️ claude-code · claude-sonnet-4-6 · run details |
nhopeatall
left a comment
There was a problem hiding this comment.
Summary
LGTM — Clean, well-structured Linear GraphQL API client that correctly follows the established AsyncLocalStorage credential-scoping pattern from the Trello and JIRA clients.
Notes
Pattern consistency: The AsyncLocalStorage + withLinearCredentials() pattern, the linearClient object structure, the mapper approach, and the module layout (types.ts + client.ts) all match the existing conventions in src/trello/ and src/jira/.
No unit tests: The PR description acknowledges that existing tests pass, but there are no new tests for the Linear client. This is acceptable for a pure API client where tests would be integration-level, but worth adding mock-based unit tests for the mappers and error handling paths as follow-up.
No integration wiring yet: The client module exists in isolation — there's no LinearIntegration class, no bootstrap registration, no credential roles registration, and no webhook route. The src/integrations/README.md already documents exactly how to wire Linear up (it uses Linear as its example). This is fine as a standalone first step if the integration wiring is planned for a follow-up PR.
Minor observations (not blocking):
src/linear/client.ts:446—removeLabelalways callsupdateIssueeven when the label isn't present on the issue, whileaddLabelhas an early-return optimization when the label is already present. Consider adding a symmetric early-return:if (!issue.labels.some(l => l.id === labelId)) return issue;getIssueCommentsandgetAttachmentsdon't pass afirstparameter and don't handle pagination — Linear's default page size (typically 50) will silently truncate results for issues with many comments/attachments. Fine for now but worth noting for future use.- The axios override bump (1.13.5 → 1.15.0) is a separate concern but reasonable to include.
🕵️ claude-code · claude-opus-4-6 · run details
|
@aaight check out the CI and see what's up |
CI Status: All Green ✅Checked the CI — all 7 checks are passing:
What happened earlierThe
This was a pre-existing issue on the No action needed — CI is clean and the PR is ready to merge. 🕵️ claude-code · claude-sonnet-4-6 · run details |
Summary
src/linear/types.tswith full type definitions:LinearCredentials,LinearIssue,LinearComment,LinearLabel,LinearTeam,LinearUser,LinearWorkflowState,LinearAttachment,LinearReaction,LinearWebhookPayload, and input typessrc/linear/client.tswithwithLinearCredentials()usingAsyncLocalStorage— matching the pattern fromsrc/trello/client.tsandsrc/jira/client.tslinearClientwith all required methods:getIssue,listIssues,createIssue,updateIssue,updateIssueState,getIssueComments,createComment,updateComment,deleteComment,addLabel,removeLabel,getAttachments,createAttachment,createReaction,getMehttps://api.linear.app/graphqlwithAuthorization: Bearer <api_key>headerKey Decisions
withLinearCredentials()wraps async context exactly likewithTrelloCredentials()/withJiraCredentials()— credential scoping is per-request, not globallinearGraphQL<T>()function handles POST, auth header injection, HTTP error detection, and GraphQL error propagationmapIssue,mapComment,mapUser,mapLabel,mapState,mapTeam— all convert raw GraphQL responses to typed domain objects, with safe?? ''fallbacksRawIssueandRawCommentinterfaces reduce cognitive complexity (biome max=15) while keeping mappers cleanaddLabel/removeLabelfetch current label IDs first and uselabelIdsfield to perform atomic updatesTest plan
npx tsc --noEmit)npx biome check src/linear/)npx vitest run tests/unit/worker-entry.test.ts, repo-hygiene, architecture-docs)Links
🤖 Generated with Claude Code
🕵️ claude-code · claude-sonnet-4-6 · run details