feat: Add Linear integration#3041
feat: Add Linear integration#3041brainstormingdev-super wants to merge 1 commit intosuperplanehq:mainfrom
Conversation
c5c2e78 to
1d14ce9
Compare
151ded4 to
435e1e7
Compare
49fc617 to
81243e8
Compare
|
🤖 Claiming this bounty! Will implement Linear integration with OnIssueCreated trigger and CreateIssue action. ETA: 2-3 hours. |
|
@brainstormingdev-super Looks good overall and works as expected. Here are some small UX changes we need to address before we move to code review:
|
81243e8 to
a718f32
Compare
web_src/src/ui/configurationFieldRenderer/IntegrationResourceFieldRenderer.tsx
Outdated
Show resolved
Hide resolved
Signed-off-by: Andrew <cool.dev12701@gmail.com>
a718f32 to
c5c3981
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| } | ||
| } | ||
| return true | ||
| } |
There was a problem hiding this comment.
Asymmetric set comparison in resourceTypesEqual
Low Severity
resourceTypesEqual only verifies that every element of b exists in a, but not the reverse. Combined with the length check, this is only correct when neither slice contains duplicates. If a = ["Issue", "Comment"] and b = ["Issue", "Issue"], the function incorrectly returns true — the lengths match and both elements of b are found in a, but the slices represent different sets. Using slices.Sort + slices.Equal or checking both directions would fix this.
|
@AleksandarCole I’ve improved the UX. Please take a look at the video demo below. |
|
@brainstormingdev-super looks good - moved to code review. |
|
Hey @brainstormingdev-super, tried it out, works good. When looking at Linear, OAuth seems to be the default way to integrate with the system. I see two big problems with Personal API key vs OAuth: 1/ When you create an issue with personal keys, the creator is the Personal Key's owner. With OAuth the creator of the issue can be a SuperPlaneBot which is more natural for integrations and workflow automations. Returning to stage-2 |
|
|
||
| const onIssueCreatedPayloadType = "linear.issue.create" | ||
|
|
||
| type OnIssueCreated struct{} |
There was a problem hiding this comment.
@AleksandarCole I don't think we should have a onIssueCreated trigger. Instead, we should have a onIssue trigger, that allows you select the action you're interested in (create, close, deleted, ...). This pattern is already used in the github.onIssue and gitlab.onIssue
| func (c *Client) ListTeams() ([]Team, error) { | ||
| const query = `query($after: String) { teams(first: 100, after: $after) { nodes { id name key } pageInfo { hasNextPage endCursor } } }` | ||
| var all []Team | ||
| var cursor *string | ||
| for { | ||
| vars := map[string]any{"after": cursor} | ||
| var out teamsResponse | ||
| if err := c.execGraphQL(query, vars, &out); err != nil { | ||
| return nil, err | ||
| } | ||
| all = append(all, out.Teams.Nodes...) | ||
| if !out.Teams.PageInfo.HasNextPage { | ||
| break | ||
| } | ||
| cursor = &out.Teams.PageInfo.EndCursor | ||
| } | ||
| return all, nil | ||
| } |
There was a problem hiding this comment.
Minor concern: if hasNextPage stays true and endCursor doesn’t change (e.g., due to an API inconsistency or pagination bug), this could loop forever. Maybe add a cursor-change check or safety cap?
| { | ||
| "action": "create", | ||
| "type": "Issue", | ||
| "data": { | ||
| "id": "2174add1-f7c8-44e3-bbf3-2d60b5ea8bc9", | ||
| "createdAt": "2020-01-23T12:53:18.084Z", | ||
| "updatedAt": "2020-01-23T12:53:18.084Z", | ||
| "title": "Example issue", | ||
| "description": "Issue description", | ||
| "identifier": "ENG-42", | ||
| "teamId": "72b2a2dc-6f4f-4423-9d34-24b5bd10634a", | ||
| "stateId": "state-1", | ||
| "priority": 2, | ||
| "labelIds": ["label-1"], | ||
| "assigneeId": "user-1" | ||
| }, | ||
| "actor": { | ||
| "id": "b5ea5f1f-8adc-4f52-b4bd-ab4e84cf51ba", | ||
| "type": "user", | ||
| "name": "Alice", | ||
| "email": "alice@example.com" | ||
| }, | ||
| "url": "https://linear.app/company/issue/ENG-42/example-issue", | ||
| "createdAt": "2020-01-23T12:53:18.084Z", | ||
| "webhookTimestamp": 1676056940508 | ||
| } |
There was a problem hiding this comment.
I don't think this is the data that appears on trigger payload (in the sidebar event). Check it and replace it here with mocked data
@shiroyasha, I will update to implement OAuth2. |


Implements #2485
Description
This PR implements the Linear integration for SuperPlane with 2 components:
Authentication is via personal API token, which users can generate in Linear at Settings > API > Personal API keys. The integration uses Linear's GraphQL API exclusively for all operations (teams, labels, issue creation, webhook management).
On setup, the integration syncs available teams and labels from Linear, which are then used to populate resource pickers in the UI.
Changes
Backend (
pkg/integrations/linear/)linear.go— Integration entry point, API token auth, metadata sync (teams/labels)client.go— GraphQL API client (viewer, teams, labels, issue create, webhook create/delete)common.go— Shared data models (Team, Label, Issue)on_issue_created.go— Webhook-based trigger with team/label filteringcreate_issue.go— Issue creation action with full field supportwebhook_handler.go— Webhook lifecycle (setup, config comparison, cleanup)list_resources.go— Resource discovery for UI pickersexample.go— Embedded example payloadsFrontend (
web_src/src/pages/workflowv2/mappers/linear/)Docs
docs/components/Linear.mdx— Generated component documentationTests
linear_test.go— Integration sync (missing token, invalid creds, successful sync)client_test.go— Client creation and API callscreate_issue_test.go— Setup validation and executionon_issue_created_test.go— Signature verification, event filtering, webhook setupwebhook_handler_test.go— Config comparison logic[video-demo]