feat(Linear Node): Add options to add a link and a comment to an issue (#13464)

This commit is contained in:
Jon
2025-06-12 16:40:58 +01:00
committed by GitHub
parent ecfb6674ef
commit a417ed3ac8
9 changed files with 1187 additions and 13 deletions

View File

@@ -0,0 +1,525 @@
{
"name": "Linear Test Workflow",
"nodes": [
{
"parameters": {},
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [-80, 260],
"id": "4dbe018c-edec-46d3-b18d-f4517435e1f8",
"name": "When clicking Execute workflow"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, -400],
"id": "512f2fec-df9f-46ac-8df3-e84fd352f7ff",
"name": "Create Comment - Output"
},
{
"parameters": {
"resource": "comment",
"issueId": "test-17",
"comment": "test",
"additionalFields": {}
},
"type": "n8n-nodes-base.linear",
"typeVersion": 1,
"position": [200, -400],
"id": "be60984b-28a5-4c5a-8926-87824300e63e",
"name": "Add Comment",
"credentials": {
"linearApi": {
"id": "6bJTI5tzXOk9m6cv",
"name": "86-88"
}
}
},
{
"parameters": {
"resource": "comment",
"issueId": "test-17",
"comment": "Add to parent",
"additionalFields": {
"parentId": "ff12069e-fac8-4b18-8455-cc6b29fa1e77"
}
},
"type": "n8n-nodes-base.linear",
"typeVersion": 1,
"position": [200, -220],
"id": "328939ed-41ac-442e-819c-f470ce0986b4",
"name": "Add Comment - with parent",
"credentials": {
"linearApi": {
"id": "6bJTI5tzXOk9m6cv",
"name": "86-88"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, -220],
"id": "d28e9fe2-4a7b-4714-aa15-a6c6732c4e0d",
"name": "Create Comment with Parent"
},
{
"parameters": {
"operation": "addLink",
"issueId": "test-17",
"link": "https://n8n.io"
},
"type": "n8n-nodes-base.linear",
"typeVersion": 1,
"position": [200, -20],
"id": "6ec9fdbc-eed8-4e9a-850b-973f72f6f3a5",
"name": "Add link to issue",
"credentials": {
"linearApi": {
"id": "6bJTI5tzXOk9m6cv",
"name": "86-88"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, -20],
"id": "229e5b09-8867-44f5-a71c-da31d97ab1b5",
"name": "Add Link output"
},
{
"parameters": {
"teamId": "0a2994c1-5d99-48aa-ab22-8b5ba4711ebc",
"title": "This is a test issue",
"additionalFields": {
"assigneeId": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
"description": "test description",
"priorityId": 3,
"stateId": "65a87a3a-5729-4d82-96bf-badccbeb49af"
}
},
"type": "n8n-nodes-base.linear",
"typeVersion": 1,
"position": [200, 160],
"id": "3262089e-902a-4922-b5e9-1bd2dae4915f",
"name": "Create Issue",
"credentials": {
"linearApi": {
"id": "6bJTI5tzXOk9m6cv",
"name": "86-88"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 160],
"id": "3515db04-1fb9-4c15-904b-1a34c7bd63fb",
"name": "Create Issue Response"
},
{
"parameters": {
"operation": "getAll",
"limit": 1
},
"type": "n8n-nodes-base.linear",
"typeVersion": 1,
"position": [200, 520],
"id": "912a5680-2d83-4d24-921b-f622cc8be0be",
"name": "Issue Get Many",
"credentials": {
"linearApi": {
"id": "6bJTI5tzXOk9m6cv",
"name": "86-88"
}
}
},
{
"parameters": {
"operation": "get",
"issueId": "test-18"
},
"type": "n8n-nodes-base.linear",
"typeVersion": 1,
"position": [200, 340],
"id": "8604ce38-89aa-4793-bd2d-13c7d4fce215",
"name": "Get Issue",
"credentials": {
"linearApi": {
"id": "6bJTI5tzXOk9m6cv",
"name": "86-88"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 520],
"id": "d01f72ce-4f5f-4a3f-9805-7975e67d041d",
"name": "Get Many Issues"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 340],
"id": "a2ad027f-8507-4488-84f5-f339ad826120",
"name": "Get Issue Output"
},
{
"parameters": {
"operation": "update",
"issueId": "test-18",
"updateFields": {
"assigneeId": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
"description": "New Description",
"priorityId": 3,
"stateId": "622493c0-f4ee-456d-af65-49a7611ede7a",
"teamId": "0a2994c1-5d99-48aa-ab22-8b5ba4711ebc",
"title": "New Title"
}
},
"type": "n8n-nodes-base.linear",
"typeVersion": 1,
"position": [200, 700],
"id": "141d4f10-6812-40b4-a146-fa9cf929f86a",
"name": "Update Issue",
"credentials": {
"linearApi": {
"id": "6bJTI5tzXOk9m6cv",
"name": "86-88"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 700],
"id": "b08bba99-9d96-43be-8875-c72c7b51f734",
"name": "Update Issue Response"
},
{
"parameters": {
"operation": "delete",
"issueId": "test-18"
},
"type": "n8n-nodes-base.linear",
"typeVersion": 1,
"position": [200, 880],
"id": "d30a2ff2-f8cd-4277-b5c1-6f2df7985872",
"name": "Delete Issue",
"credentials": {
"linearApi": {
"id": "6bJTI5tzXOk9m6cv",
"name": "86-88"
}
}
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [400, 880],
"id": "8769421e-01ea-41ee-9e35-432408879869",
"name": "Delete Issue Response"
}
],
"pinData": {
"Create Comment - Output": [
{
"json": {
"success": true,
"comment": {
"id": "ff12069e-fac8-4b18-8455-cc6b29fa1e77"
}
}
}
],
"Create Comment with Parent": [
{
"json": {
"success": true,
"comment": {
"id": "bd0e4d70-7964-4877-aa30-d81534027f44"
}
}
}
],
"Add Link output": [
{
"json": {
"success": true
}
}
],
"Create Issue Response": [
{
"json": {
"id": "3c7316e3-4224-424d-8cc8-1dd3b96764b8",
"identifier": "TEST-18",
"title": "This is a test issue",
"priority": 3,
"archivedAt": null,
"assignee": {
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
"displayName": "nathan"
},
"state": {
"id": "65a87a3a-5729-4d82-96bf-badccbeb49af",
"name": "Backlog"
},
"createdAt": "2025-06-12T10:38:35.296Z",
"creator": {
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
"displayName": "nathan"
},
"description": "test description",
"dueDate": null,
"cycle": null
}
}
],
"Get Issue Output": [
{
"json": {
"id": "3c7316e3-4224-424d-8cc8-1dd3b96764b8",
"identifier": "TEST-18",
"title": "This is a test issue",
"priority": 3,
"archivedAt": null,
"assignee": {
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
"displayName": "nathan"
},
"state": {
"id": "65a87a3a-5729-4d82-96bf-badccbeb49af",
"name": "Backlog"
},
"createdAt": "2025-06-12T10:38:35.296Z",
"creator": {
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
"displayName": "nathan"
},
"description": "test description",
"dueDate": null,
"cycle": null
}
}
],
"Get Many Issues": [
{
"json": {
"id": "3c7316e3-4224-424d-8cc8-1dd3b96764b8",
"identifier": "TEST-18",
"title": "This is a test issue",
"priority": 3,
"archivedAt": null,
"assignee": {
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
"displayName": "nathan"
},
"state": {
"id": "65a87a3a-5729-4d82-96bf-badccbeb49af",
"name": "Backlog"
},
"createdAt": "2025-06-12T10:38:35.296Z",
"creator": {
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
"displayName": "nathan"
},
"description": "test description",
"dueDate": null,
"cycle": null
}
}
],
"Update Issue Response": [
{
"json": {
"id": "3c7316e3-4224-424d-8cc8-1dd3b96764b8",
"identifier": "TEST-18",
"title": "New Title",
"priority": 3,
"archivedAt": null,
"assignee": {
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
"displayName": "nathan"
},
"state": {
"id": "622493c0-f4ee-456d-af65-49a7611ede7a",
"name": "Canceled"
},
"createdAt": "2025-06-12T10:38:35.296Z",
"creator": {
"id": "1c51f0c4-c552-4614-a534-8de1752ba7d7",
"displayName": "nathan"
},
"description": "New Description",
"dueDate": null,
"cycle": null
}
}
],
"Delete Issue Response": [
{
"json": {
"success": true
}
}
]
},
"connections": {
"When clicking Execute workflow": {
"main": [
[
{
"node": "Add Comment",
"type": "main",
"index": 0
},
{
"node": "Add Comment - with parent",
"type": "main",
"index": 0
},
{
"node": "Add link to issue",
"type": "main",
"index": 0
},
{
"node": "Create Issue",
"type": "main",
"index": 0
},
{
"node": "Get Issue",
"type": "main",
"index": 0
},
{
"node": "Issue Get Many",
"type": "main",
"index": 0
},
{
"node": "Update Issue",
"type": "main",
"index": 0
},
{
"node": "Delete Issue",
"type": "main",
"index": 0
}
]
]
},
"Add Comment": {
"main": [
[
{
"node": "Create Comment - Output",
"type": "main",
"index": 0
}
]
]
},
"Add Comment - with parent": {
"main": [
[
{
"node": "Create Comment with Parent",
"type": "main",
"index": 0
}
]
]
},
"Add link to issue": {
"main": [
[
{
"node": "Add Link output",
"type": "main",
"index": 0
}
]
]
},
"Create Issue": {
"main": [
[
{
"node": "Create Issue Response",
"type": "main",
"index": 0
}
]
]
},
"Get Issue": {
"main": [
[
{
"node": "Get Issue Output",
"type": "main",
"index": 0
}
]
]
},
"Issue Get Many": {
"main": [
[
{
"node": "Get Many Issues",
"type": "main",
"index": 0
}
]
]
},
"Update Issue": {
"main": [
[
{
"node": "Update Issue Response",
"type": "main",
"index": 0
}
]
]
},
"Delete Issue": {
"main": [
[
{
"node": "Delete Issue Response",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "2cc012ac-e157-4f3e-97d8-15fb2da556ef",
"meta": {
"templateCredsSetupCompleted": true,
"instanceId": "0fa937d34dcabeff4bd6480d3b42cc95edf3bc20e6810819086ef1ce2623639d"
},
"id": "6FHfNEY4tcmoXVeg",
"tags": []
}

View File

@@ -0,0 +1,43 @@
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
import nock from 'nock';
import {
addCommentRequest,
addCommentWithParentRequest,
addCommentLink,
issueCreateRequest,
getIssueRequest,
getManyIssuesRequest,
updateIssueRequest,
deleteIssueRequest,
} from './apiRequest';
import {
commentCreateResponse,
commentCreateWithParentResponse,
attachmentLinkURLResponse,
issueCreateResponse,
getIssueResponse,
getManyIssueResponse,
issueUpdateResponse,
deleteIssueResponse,
} from './apiResponses';
describe('Linear', () => {
describe('Run Test Workflow', () => {
beforeAll(() => {
const mock = nock('https://api.linear.app');
mock.post('/graphql', addCommentRequest).reply(200, commentCreateResponse);
mock.post('/graphql', addCommentLink).reply(200, attachmentLinkURLResponse);
mock
.post('/graphql', addCommentWithParentRequest)
.reply(200, commentCreateWithParentResponse);
mock.post('/graphql', issueCreateRequest).reply(200, issueCreateResponse);
mock.post('/graphql', getIssueRequest).reply(200, getIssueResponse);
mock.post('/graphql', getManyIssuesRequest).reply(200, getManyIssueResponse);
mock.post('/graphql', updateIssueRequest).reply(200, issueUpdateResponse);
mock.post('/graphql', deleteIssueRequest).reply(200, deleteIssueResponse);
});
new NodeTestHarness().setupTests();
});
});

View File

@@ -0,0 +1,245 @@
export const addCommentRequest = {
query: `mutation CommentCreate ($issueId: String!, $body: String!, $parentId: String) {
commentCreate(input: {issueId: $issueId, body: $body, parentId: $parentId}) {
success
comment {
id
}
}
}`,
variables: {
issueId: 'test-17',
body: 'test',
},
};
export const addCommentWithParentRequest = {
query: `mutation CommentCreate ($issueId: String!, $body: String!, $parentId: String) {
commentCreate(input: {issueId: $issueId, body: $body, parentId: $parentId}) {
success
comment {
id
}
}
}`,
variables: {
issueId: 'test-17',
body: 'Add to parent',
parentId: 'ff12069e-fac8-4b18-8455-cc6b29fa1e77',
},
};
export const addCommentLink = {
query: `mutation AttachmentLinkURL($url: String!, $issueId: String!) {
attachmentLinkURL(url: $url, issueId: $issueId) {
success
}
}`,
variables: {
issueId: 'test-17',
url: 'https://n8n.io',
},
};
export const issueCreateRequest = {
query: `mutation IssueCreate (
$title: String!,
$teamId: String!,
$description: String,
$assigneeId: String,
$priorityId: Int,
$stateId: String){
issueCreate(
input: {
title: $title
description: $description
teamId: $teamId
assigneeId: $assigneeId
priority: $priorityId
stateId: $stateId
}
) {
success
issue {
id,
identifier,
title,
priority
archivedAt
assignee {
id
displayName
}
state {
id
name
}
createdAt
creator {
id
displayName
}
description
dueDate
cycle {
id
name
}
}
}
}`,
variables: {
teamId: '0a2994c1-5d99-48aa-ab22-8b5ba4711ebc',
title: 'This is a test issue',
assigneeId: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
description: 'test description',
priorityId: 3,
stateId: '65a87a3a-5729-4d82-96bf-badccbeb49af',
},
};
export const getIssueRequest = {
query: `query Issue($issueId: String!) {
issue(id: $issueId) {
id,
identifier,
title,
priority,
archivedAt,
assignee {
id,
displayName
}
state {
id
name
}
createdAt
creator {
id
displayName
}
description
dueDate
cycle {
id
name
}
}
}`,
variables: {
issueId: 'test-18',
},
};
export const getManyIssuesRequest = {
query: `query Issue ($first: Int, $after: String){
issues (first: $first, after: $after){
nodes {
id,
identifier,
title,
priority
archivedAt
assignee {
id
displayName
}
state {
id
name
}
createdAt
creator {
id
displayName
}
description
dueDate
cycle {
id
name
}
}
pageInfo {
hasNextPage
endCursor
}
}
}`,
variables: {
first: 1,
after: null,
},
};
export const updateIssueRequest = {
query: `mutation IssueUpdate (
$issueId: String!,
$title: String,
$teamId: String,
$description: String,
$assigneeId: String,
$priorityId: Int,
$stateId: String){
issueUpdate(
id: $issueId,
input: {
title: $title
description: $description
teamId: $teamId
assigneeId: $assigneeId
priority: $priorityId
stateId: $stateId
}
) {
success
issue {
id,
identifier,
title,
priority
archivedAt
assignee {
id
displayName
}
state {
id
name
}
createdAt
creator {
id
displayName
}
description
dueDate
cycle {
id
name
}
}
}
}`,
variables: {
issueId: 'test-18',
assigneeId: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
description: 'New Description',
priorityId: 3,
stateId: '622493c0-f4ee-456d-af65-49a7611ede7a',
teamId: '0a2994c1-5d99-48aa-ab22-8b5ba4711ebc',
title: 'New Title',
},
};
export const deleteIssueRequest = {
query: `mutation IssueDelete ($issueId: String!) {
issueDelete(id: $issueId) {
success
}
}`,
variables: {
issueId: 'test-18',
},
};

View File

@@ -0,0 +1,163 @@
export const commentCreateResponse = {
data: {
commentCreate: {
success: true,
comment: {
id: 'ff12069e-fac8-4b18-8455-cc6b29fa1e77',
},
},
},
};
export const commentCreateWithParentResponse = {
data: {
commentCreate: {
success: true,
comment: {
id: 'bd0e4d70-7964-4877-aa30-d81534027f44',
},
},
},
};
export const attachmentLinkURLResponse = {
data: {
attachmentLinkURL: {
success: true,
},
},
};
export const issueCreateResponse = {
data: {
issueCreate: {
success: true,
issue: {
id: '3c7316e3-4224-424d-8cc8-1dd3b96764b8',
identifier: 'TEST-18',
title: 'This is a test issue',
priority: 3,
archivedAt: null,
assignee: {
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
displayName: 'nathan',
},
state: {
id: '65a87a3a-5729-4d82-96bf-badccbeb49af',
name: 'Backlog',
},
createdAt: '2025-06-12T10:38:35.296Z',
creator: {
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
displayName: 'nathan',
},
description: 'test description',
dueDate: null,
cycle: null,
},
},
},
};
export const getIssueResponse = {
data: {
issue: {
id: '3c7316e3-4224-424d-8cc8-1dd3b96764b8',
identifier: 'TEST-18',
title: 'This is a test issue',
priority: 3,
archivedAt: null,
assignee: {
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
displayName: 'nathan',
},
state: {
id: '65a87a3a-5729-4d82-96bf-badccbeb49af',
name: 'Backlog',
},
createdAt: '2025-06-12T10:38:35.296Z',
creator: {
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
displayName: 'nathan',
},
description: 'test description',
dueDate: null,
cycle: null,
},
},
};
export const getManyIssueResponse = {
data: {
issues: {
nodes: [
{
id: '3c7316e3-4224-424d-8cc8-1dd3b96764b8',
identifier: 'TEST-18',
title: 'This is a test issue',
priority: 3,
archivedAt: null,
assignee: {
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
displayName: 'nathan',
},
state: {
id: '65a87a3a-5729-4d82-96bf-badccbeb49af',
name: 'Backlog',
},
createdAt: '2025-06-12T10:38:35.296Z',
creator: {
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
displayName: 'nathan',
},
description: 'test description',
dueDate: null,
cycle: null,
},
],
pageInfo: {
hasNextPage: true,
endCursor: '3c7316e3-4224-424d-8cc8-1dd3b96764b8',
},
},
},
};
export const issueUpdateResponse = {
data: {
issueUpdate: {
success: true,
issue: {
id: '3c7316e3-4224-424d-8cc8-1dd3b96764b8',
identifier: 'TEST-18',
title: 'New Title',
priority: 3,
archivedAt: null,
assignee: {
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
displayName: 'nathan',
},
state: {
id: '622493c0-f4ee-456d-af65-49a7611ede7a',
name: 'Canceled',
},
createdAt: '2025-06-12T10:38:35.296Z',
creator: {
id: '1c51f0c4-c552-4614-a534-8de1752ba7d7',
displayName: 'nathan',
},
description: 'New Description',
dueDate: null,
cycle: null,
},
},
},
};
export const deleteIssueResponse = {
data: {
issueDelete: {
success: true,
},
},
};