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
a718f32 to
c5c3981
Compare
|
@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?
There was a problem hiding this comment.
@brainstormingdev-super please review this, sounds serious.
There was a problem hiding this comment.
@shiroyasha it is already fixed in updated push.
But I will make it more robust
| { | ||
| "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
| } | ||
|
|
||
| func (c *CreateIssue) Execute(ctx core.ExecutionContext) error { | ||
| if ctx.Integration == nil { |
There was a problem hiding this comment.
No need for this nil check.
|
|
||
| func (h *LinearWebhookHandler) Merge(current, requested any) (any, bool, error) { | ||
| return current, false, nil | ||
| } |
There was a problem hiding this comment.
@brainstormingdev-super do we need this? It seems that it is not doing anything.
You can register integrations without a webhook handler, if you don't need it.
Signed-off-by: Andrew <cool.dev12701@gmail.com>
Signed-off-by: Andrew <cool.dev12701@gmail.com>
| // When this field depends on other fields (e.g. Assignee/Status depend on Team), only fetch when those params are present | ||
| const hasRequiredParameters = | ||
| resourceParameters.length === 0 || | ||
| (additionalQueryParameters !== undefined && Object.keys(additionalQueryParameters).length > 0); |
There was a problem hiding this comment.
Parameter presence check allows partial resolution
Low Severity
The hasRequiredParameters check verifies that at least one parameter from resourceParameters is resolved (Object.keys(...).length > 0), but doesn't verify that all required parameters are resolved. For fields that depend on multiple parameters (e.g., AWS CodeArtifact's repository field depending on both region and domain), resolving only one would allow the fetch to proceed with incomplete parameters. Currently mitigated by VisibilityConditions on existing multi-parameter fields, but the logic doesn't match the comment's stated intent of checking that "those params are present."
|
@brainstormingdev-super the code looks good. You did good given how complex this UX flow is. Contact @AleksandarCole to collect your bounty. On the superplane team's side, when I reviewed it with fresh eyes, @AleksandarCole @ropsii have scheduled a session to solve it on Monday. They will take over and continue with this PR. |
|
@Andrew-127 Hello, I'm from bountyhub, |
Co-authored-by: Cursor <cursoragent@cursor.com>
| if err != nil { | ||
| _ = ctx.Integration.SetSecret(OAuthRefreshToken, []byte("")) | ||
| _ = ctx.Integration.SetSecret(OAuthAccessToken, []byte("")) | ||
| return fmt.Errorf("failed to refresh token: %v", err) |
There was a problem hiding this comment.
Refresh failure clears valid tokens forcing re-authorization
Medium Severity
When refreshToken encounters any error (including transient network blips or temporary Linear API outages), both OAuthAccessToken and OAuthRefreshToken are immediately cleared. The old access token likely remains valid for hours, but destroying it forces the user through a full OAuth re-authorization flow. A brief Linear API hiccup would break the integration for all users until they manually re-authorize.
Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 4 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
|
|
||
| if config.LaunchAgentKey == "" && config.AdminKey == "" { | ||
| return fmt.Errorf("one of the keys is required") | ||
| return nil |
There was a problem hiding this comment.
Cursor sync leaves stale ready state
Medium Severity
Cursor.Sync() now returns nil when both LaunchAgentKey and AdminKey are empty, but it also doesn’t set any “pending/error” state. If credentials are removed after the integration was previously ready, this early return can leave a stale ready status even though the integration can no longer authenticate.
| } | ||
| callbackURL := fmt.Sprintf("%s/api/v1/integrations/%s/callback", baseURL, ctx.Integration.ID()) | ||
| webhookURL := fmt.Sprintf("%s/api/v1/integrations/%s/webhook", baseURL, ctx.Integration.ID()) | ||
|
|
There was a problem hiding this comment.
Linear webhook/callback URLs may be malformed
High Severity
Linear.Sync() builds callbackURL/webhookURL using fmt.Sprintf(... "%s", ctx.Integration.ID()). If ctx.Integration.ID() is not already a string (commonly a UUID type), %s formatting can produce an invalid URL (e.g., %!s(...)), breaking OAuth redirects and webhook delivery.
| ctx.Logger.Errorf("invalid webhook signature") | ||
| ctx.Response.WriteHeader(http.StatusUnauthorized) | ||
| return | ||
| } |
There was a problem hiding this comment.
Linear signature verification likely mismatches header format
Medium Severity
handleWebhookEvent() compares the Linear-Signature header directly to a raw hex digest string. If Linear prefixes/encodes the signature (e.g., sha256= prefix or non-hex encoding), verification will always fail and all webhooks will be rejected; conversely, if the expected format includes a prefix, the current check doesn’t enforce it.
| )} | ||
| </code> | ||
| ); | ||
| } |
There was a problem hiding this comment.
|
Your SuperPlane environment was created but the health check failed (exit code: 7). URL: http://3000-cfd33153-be78-43be-9de2-874b9cae76cf.proxy.daytona.works The sandbox is still running. You can try accessing the URL manually. |
| Label: "Organization", | ||
| Type: configuration.FieldTypeString, | ||
| Description: "Organization to install the app into. If not specified, the app will be installed into the user's account.", | ||
| Required: true, |
There was a problem hiding this comment.
What if I want to install on my personal account? Is that not possible anymore?
|
Your SuperPlane environment was created but the health check failed (exit code: 7). URL: http://3000-8d479e63-105b-4b1c-a953-d6a9cc1625a9.proxy.daytona.works The sandbox is still running. You can try accessing the URL manually. |


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]