diff --git a/packages/frontend/editor-ui/src/constants.ts b/packages/frontend/editor-ui/src/constants.ts
index 72b899e665..2fe6936689 100644
--- a/packages/frontend/editor-ui/src/constants.ts
+++ b/packages/frontend/editor-ui/src/constants.ts
@@ -202,6 +202,7 @@ export const SIMULATE_NODE_TYPE = 'n8n-nodes-base.simulate';
export const SIMULATE_TRIGGER_NODE_TYPE = 'n8n-nodes-base.simulateTrigger';
export const AI_TRANSFORM_NODE_TYPE = 'n8n-nodes-base.aiTransform';
export const FORM_NODE_TYPE = 'n8n-nodes-base.form';
+export const GITHUB_NODE_TYPE = 'n8n-nodes-base.github';
export const SLACK_TRIGGER_NODE_TYPE = 'n8n-nodes-base.slackTrigger';
export const TELEGRAM_TRIGGER_NODE_TYPE = 'n8n-nodes-base.telegramTrigger';
export const FACEBOOK_LEAD_ADS_TRIGGER_NODE_TYPE = 'n8n-nodes-base.facebookLeadAdsTrigger';
diff --git a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
index 7097a3a765..a9b9bc625c 100644
--- a/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
+++ b/packages/frontend/editor-ui/src/plugins/i18n/locales/en.json
@@ -1106,6 +1106,7 @@
"ndv.output.runNodeHint": "Execute this node to view data",
"ndv.output.runNodeHintSubNode": "Output will appear here once the parent node is run",
"ndv.output.waitNodeWaitingForWebhook": "Execution will continue when webhook is received on ",
+ "ndv.output.githubNodeWaitingForWebhook": "Execution will continue when the following webhook URL is called: ",
"ndv.output.sendAndWaitWaitingApproval": "Execution will continue after the user's response",
"ndv.output.waitNodeWaitingForFormSubmission": "Execution will continue when form is submitted on ",
"ndv.output.waitNodeWaiting": "Execution will continue when wait time is over",
diff --git a/packages/frontend/editor-ui/src/utils/executionUtils.test.ts b/packages/frontend/editor-ui/src/utils/executionUtils.test.ts
index b14b60e3a0..1ed50d98cb 100644
--- a/packages/frontend/editor-ui/src/utils/executionUtils.test.ts
+++ b/packages/frontend/editor-ui/src/utils/executionUtils.test.ts
@@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { displayForm, executionFilterToQueryFilter, waitingNodeTooltip } from './executionUtils';
import type { INode, IRunData, IPinData } from 'n8n-workflow';
import { type INodeUi } from '../Interface';
-import { CHAT_TRIGGER_NODE_TYPE, FORM_TRIGGER_NODE_TYPE } from '@/constants';
+import { CHAT_TRIGGER_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, GITHUB_NODE_TYPE } from '@/constants';
import { createTestNode } from '@/__tests__/mocks';
const WAIT_NODE_TYPE = 'waitNode';
@@ -29,6 +29,7 @@ vi.mock('@/plugins/i18n', () => ({
'ndv.output.waitNodeWaiting': 'Waiting for execution to resume...',
'ndv.output.waitNodeWaitingForFormSubmission': 'Waiting for form submission: ',
'ndv.output.waitNodeWaitingForWebhook': 'Waiting for webhook call: ',
+ 'ndv.output.githubNodeWaitingForWebhook': 'Waiting for webhook call: ',
'ndv.output.sendAndWaitWaitingApproval': 'Waiting for approval...',
};
return texts[key] || key;
@@ -283,6 +284,24 @@ describe('waitingNodeTooltip', () => {
expect(waitingNodeTooltip(node)).toBe('Waiting for approval...');
});
+ it('should handle GitHub dispatchAndWait operation', () => {
+ const node: INodeUi = {
+ id: '1',
+ name: 'GitHub',
+ type: GITHUB_NODE_TYPE,
+ typeVersion: 1,
+ position: [0, 0],
+ parameters: {
+ operation: 'dispatchAndWait',
+ },
+ };
+
+ const expectedUrl = 'http://localhost:5678/webhook-waiting/123';
+ expect(waitingNodeTooltip(node)).toBe(
+ `Waiting for webhook call: ${expectedUrl}`,
+ );
+ });
+
it('should ignore object-type webhook suffix', () => {
const node: INodeUi = {
id: '1',
diff --git a/packages/frontend/editor-ui/src/utils/executionUtils.ts b/packages/frontend/editor-ui/src/utils/executionUtils.ts
index 77181c7b03..fae38c4fea 100644
--- a/packages/frontend/editor-ui/src/utils/executionUtils.ts
+++ b/packages/frontend/editor-ui/src/utils/executionUtils.ts
@@ -9,7 +9,7 @@ import type {
} from 'n8n-workflow';
import type { ExecutionFilterType, ExecutionsQueryFilter, INodeUi } from '@/Interface';
import { isEmpty } from '@/utils/typesUtils';
-import { FORM_NODE_TYPE, FORM_TRIGGER_NODE_TYPE } from '../constants';
+import { FORM_NODE_TYPE, FORM_TRIGGER_NODE_TYPE, GITHUB_NODE_TYPE } from '../constants';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useRootStore } from '@/stores/root.store';
import { i18n } from '@/plugins/i18n';
@@ -147,6 +147,11 @@ export const waitingNodeTooltip = (node: INodeUi | null | undefined) => {
try {
const resume = node?.parameters?.resume;
+ if (node?.type === GITHUB_NODE_TYPE && node.parameters?.operation === 'dispatchAndWait') {
+ const resumeUrl = `${useRootStore().webhookWaitingUrl}/${useWorkflowsStore().activeExecutionId}`;
+ const message = i18n.baseText('ndv.output.githubNodeWaitingForWebhook');
+ return `${message}${resumeUrl}`;
+ }
if (resume) {
if (!['webhook', 'form'].includes(resume as string)) {
return i18n.baseText('ndv.output.waitNodeWaiting');
diff --git a/packages/nodes-base/nodes/Github/Github.node.ts b/packages/nodes-base/nodes/Github/Github.node.ts
index 291726c850..699d157394 100644
--- a/packages/nodes-base/nodes/Github/Github.node.ts
+++ b/packages/nodes-base/nodes/Github/Github.node.ts
@@ -6,8 +6,16 @@ import type {
INodeExecutionData,
INodeType,
INodeTypeDescription,
+ IWebhookFunctions,
+ IWebhookResponseData,
+ JsonObject,
+} from 'n8n-workflow';
+import {
+ NodeApiError,
+ NodeConnectionTypes,
+ NodeOperationError,
+ WAIT_INDEFINITELY,
} from 'n8n-workflow';
-import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import {
getFileSha,
@@ -16,15 +24,19 @@ import {
isBase64,
validateJSON,
} from './GenericFunctions';
-import { getRepositories, getUsers, getWorkflows } from './SearchFunctions';
+import { getRefs, getRepositories, getUsers, getWorkflows } from './SearchFunctions';
+import { defaultWebhookDescription } from '../Webhook/description';
export class Github implements INodeType {
description: INodeTypeDescription = {
displayName: 'GitHub',
name: 'github',
- icon: { light: 'file:github.svg', dark: 'file:github.dark.svg' },
+ icon: {
+ light: 'file:github.svg',
+ dark: 'file:github.dark.svg',
+ },
group: ['input'],
- version: 1,
+ version: [1, 1.1],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume GitHub API',
defaults: {
@@ -33,6 +45,15 @@ export class Github implements INodeType {
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
+ webhooks: [
+ {
+ ...defaultWebhookDescription,
+ path: '',
+ restartWebhook: true,
+ httpMethod: 'POST',
+ responseMode: 'onReceived',
+ },
+ ],
credentials: [
{
name: 'githubApi',
@@ -220,7 +241,7 @@ export class Github implements INodeType {
name: 'List',
value: 'list',
description: 'List contents of a folder',
- action: 'List a file',
+ action: 'List files',
},
],
default: 'create',
@@ -396,9 +417,6 @@ export class Github implements INodeType {
default: 'create',
},
- // ----------------------------------
- // workflow
- // ----------------------------------
{
displayName: 'Operation',
name: 'operation',
@@ -422,6 +440,13 @@ export class Github implements INodeType {
description: 'Dispatch a workflow event',
action: 'Dispatch a workflow event',
},
+ {
+ name: 'Dispatch and Wait for Completion',
+ value: 'dispatchAndWait',
+ description:
+ 'Dispatch a workflow event and wait for a webhook to be called before proceeding',
+ action: 'Dispatch a workflow event and wait for completion',
+ },
{
name: 'Enable',
value: 'enable',
@@ -450,86 +475,17 @@ export class Github implements INodeType {
default: 'dispatch',
},
{
- displayName: 'Workflow',
- name: 'workflowId',
- type: 'resourceLocator',
- default: { mode: 'list', value: '' },
- required: true,
- modes: [
- {
- displayName: 'From List',
- name: 'list',
- type: 'list',
- placeholder: 'Select a workflow...',
- typeOptions: {
- searchListMethod: 'getWorkflows',
- },
- },
- {
- displayName: 'By ID',
- name: 'name',
- type: 'string',
- placeholder: 'e.g. 12345678',
- validation: [
- {
- type: 'regex',
- properties: {
- regex: '\\d+',
- errorMessage: 'Not a valid Github Workflow ID',
- },
- },
- ],
- },
- {
- displayName: 'By File Name',
- name: 'filename',
- type: 'string',
- placeholder: 'e.g. main.yaml or main.yml',
- validation: [
- {
- type: 'regex',
- properties: {
- regex: '[a-zA-Z0-9_-]+.(yaml|yml)',
- errorMessage: 'Not a valid Github Workflow File Name',
- },
- },
- ],
- },
- ],
+ displayName:
+ 'Your execution will pause until a webhook is called. This URL will be generated at runtime and passed to your Github workflow as a resumeUrl input.',
+ name: 'webhookNotice',
+ type: 'notice',
displayOptions: {
show: {
resource: ['workflow'],
- operation: ['disable', 'dispatch', 'get', 'getUsage', 'enable'],
+ operation: ['dispatchAndWait'],
},
},
- description: 'The workflow to dispatch',
- },
- {
- displayName: 'Ref',
- name: 'ref',
- type: 'string',
- default: 'main',
- required: true,
- displayOptions: {
- show: {
- resource: ['workflow'],
- operation: ['dispatch'],
- },
- },
- description: 'The git reference for the workflow dispatch (branch, tag, or commit SHA)',
- },
- {
- displayName: 'Inputs',
- name: 'inputs',
- type: 'json',
- default: '{}',
- displayOptions: {
- show: {
- resource: ['workflow'],
- operation: ['dispatch'],
- },
- },
- description: 'JSON object with input parameters for the workflow',
+ default: '',
},
// ----------------------------------
@@ -599,7 +555,10 @@ export class Github implements INodeType {
displayName: 'Repository Name',
name: 'repository',
type: 'resourceLocator',
- default: { mode: 'list', value: '' },
+ default: {
+ mode: 'list',
+ value: '',
+ },
required: true,
modes: [
{
@@ -656,6 +615,142 @@ export class Github implements INodeType {
},
},
+ // ----------------------------------
+ // workflow
+ // ----------------------------------
+ {
+ displayName: 'Workflow',
+ name: 'workflowId',
+ type: 'resourceLocator',
+ default: {
+ mode: 'list',
+ value: '',
+ },
+ required: true,
+ modes: [
+ {
+ displayName: 'Workflow',
+ name: 'list',
+ type: 'list',
+ placeholder: 'Select a workflow...',
+ typeOptions: {
+ searchListMethod: 'getWorkflows',
+ searchable: true,
+ },
+ },
+ {
+ displayName: 'By File Name',
+ name: 'filename',
+ type: 'string',
+ placeholder: 'e.g. main.yaml or main.yml',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: '[a-zA-Z0-9_-]+.(yaml|yml)',
+ errorMessage: 'Not a valid Github Workflow File Name',
+ },
+ },
+ ],
+ },
+ {
+ displayName: 'By ID',
+ name: 'name',
+ type: 'string',
+ placeholder: 'e.g. 12345678',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: '\\d+',
+ errorMessage: 'Not a valid Github Workflow ID',
+ },
+ },
+ ],
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['workflow'],
+ operation: ['disable', 'dispatch', 'dispatchAndWait', 'get', 'getUsage', 'enable'],
+ },
+ },
+ description: 'The workflow to dispatch',
+ },
+ {
+ displayName: 'Ref',
+ name: 'ref',
+ type: 'string',
+ default: 'main',
+ required: true,
+ displayOptions: {
+ show: {
+ resource: ['workflow'],
+ operation: ['dispatch', 'dispatchAndWait'],
+ '@version': [{ _cnd: { lte: 1 } }],
+ },
+ },
+ description: 'The git reference for the workflow dispatch (branch or tag name)',
+ },
+ {
+ displayName: 'Ref',
+ name: 'ref',
+ type: 'resourceLocator',
+ default: {
+ mode: 'list',
+ value: '',
+ },
+ required: true,
+ modes: [
+ {
+ displayName: 'From List',
+ name: 'list',
+ type: 'list',
+ placeholder: 'Select a branch, tag, or commit...',
+ typeOptions: {
+ searchListMethod: 'getRefs',
+ searchable: true,
+ },
+ },
+ {
+ displayName: 'By Name',
+ name: 'name',
+ type: 'string',
+ placeholder: 'e.g. main',
+ validation: [
+ {
+ type: 'regex',
+ properties: {
+ regex: '^[a-zA-Z0-9/._-]+$',
+ errorMessage: 'Not a valid branch, tag',
+ },
+ },
+ ],
+ },
+ ],
+ displayOptions: {
+ show: {
+ resource: ['workflow'],
+ operation: ['dispatch', 'dispatchAndWait'],
+ '@version': [{ _cnd: { gte: 1.1 } }],
+ },
+ },
+ description: 'The git reference for the workflow dispatch (branch, tag, or commit SHA)',
+ },
+ {
+ displayName: 'Inputs',
+ name: 'inputs',
+ type: 'json',
+ default: '{}',
+ displayOptions: {
+ show: {
+ resource: ['workflow'],
+ operation: ['dispatch', 'dispatchAndWait'],
+ },
+ },
+ description: 'JSON object with input parameters for the workflow',
+ },
+
// ----------------------------------
// file
// ----------------------------------
@@ -867,7 +962,6 @@ export class Github implements INodeType {
placeholder: '',
hint: 'The name of the output binary field to put the file in',
},
-
{
displayName: 'Additional Parameters',
name: 'additionalParameters',
@@ -1148,6 +1242,7 @@ export class Github implements INodeType {
},
],
},
+
// ----------------------------------
// issue:get
// ----------------------------------
@@ -1163,7 +1258,7 @@ export class Github implements INodeType {
resource: ['issue'],
},
},
- description: 'The number of the issue get data of',
+ description: 'The issue number to get data for',
},
// ----------------------------------
@@ -1181,7 +1276,7 @@ export class Github implements INodeType {
resource: ['issue'],
},
},
- description: 'The number of the issue to lock',
+ description: 'The issue number to lock',
},
{
displayName: 'Lock Reason',
@@ -1216,7 +1311,7 @@ export class Github implements INodeType {
},
],
default: 'resolved',
- description: 'The reason to lock the issue',
+ description: 'The reason for locking the issue',
},
// ----------------------------------
@@ -1382,6 +1477,7 @@ export class Github implements INodeType {
},
],
},
+
// ----------------------------------
// release:getAll
// ----------------------------------
@@ -1700,9 +1796,11 @@ export class Github implements INodeType {
},
],
},
+
// ----------------------------------
// rerview
// ----------------------------------
+
// ----------------------------------
// review:getAll
// ----------------------------------
@@ -1783,6 +1881,7 @@ export class Github implements INodeType {
default: 50,
description: 'Max number of results to return',
},
+
// ----------------------------------
// review:create
// ----------------------------------
@@ -1871,6 +1970,7 @@ export class Github implements INodeType {
},
],
},
+
// ----------------------------------
// review:update
// ----------------------------------
@@ -1887,6 +1987,7 @@ export class Github implements INodeType {
default: '',
description: 'The body of the review',
},
+
// ----------------------------------
// user:getRepositories
// ----------------------------------
@@ -1954,6 +2055,7 @@ export class Github implements INodeType {
},
description: 'The email address of the invited user',
},
+
// ----------------------------------
// organization:getRepositories
// ----------------------------------
@@ -1993,12 +2095,21 @@ export class Github implements INodeType {
methods = {
listSearch: {
- getUsers,
+ getRefs,
getRepositories,
+ getUsers,
getWorkflows,
},
};
+ async webhook(this: IWebhookFunctions): Promise {
+ const requestObject = this.getRequestObject();
+
+ return {
+ workflowData: [this.helpers.returnJsonArray(requestObject.body)],
+ };
+ }
+
async execute(this: IExecuteFunctions): Promise {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
@@ -2061,6 +2172,53 @@ export class Github implements INodeType {
const resource = this.getNodeParameter('resource', 0);
const fullOperation = `${resource}:${operation}`;
+ if (resource === 'workflow' && operation === 'dispatchAndWait') {
+ const owner = this.getNodeParameter('owner', 0, '', { extractValue: true }) as string;
+ const repository = this.getNodeParameter('repository', 0, '', {
+ extractValue: true,
+ }) as string;
+ const workflowId = this.getNodeParameter('workflowId', 0, '', {
+ extractValue: true,
+ }) as string;
+ const ref = this.getNodeParameter('ref', 0, '', { extractValue: true }) as string;
+
+ const inputs = validateJSON(this.getNodeParameter('inputs', 0) as string) as IDataObject;
+ if (!inputs) {
+ throw new NodeOperationError(this.getNode(), 'Inputs: Invalid JSON');
+ }
+
+ endpoint = `/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`;
+
+ body = {
+ ref,
+ inputs,
+ };
+
+ // Generate a webhook URL for the GitHub workflow to call when done
+ const resumeUrl = this.getWorkflowDataProxy(0).$execution.resumeUrl;
+
+ body.inputs = {
+ ...inputs,
+ resumeUrl,
+ };
+
+ try {
+ responseData = await githubApiRequest.call(this, 'POST', endpoint, body);
+ } catch (error) {
+ if (error.httpCode === '404' || error.statusCode === 404) {
+ throw new NodeOperationError(
+ this.getNode(),
+ 'The workflow to dispatch could not be found. Adjust the "workflow" parameter setting to dispatch the workflow correctly.',
+ { itemIndex: 0 },
+ );
+ }
+ throw new NodeApiError(this.getNode(), error as JsonObject);
+ }
+
+ await this.putExecutionToWait(WAIT_INDEFINITELY);
+ return [this.getInputData()];
+ }
+
for (let i = 0; i < items.length; i++) {
try {
// Reset all values
@@ -2504,22 +2662,27 @@ export class Github implements INodeType {
requestMethod = 'PUT';
- const workflowId = this.getNodeParameter('workflowId', i) as string;
+ const workflowId = this.getNodeParameter('workflowId', i, '', {
+ extractValue: true,
+ }) as string;
endpoint = `/repos/${owner}/${repository}/actions/workflows/${workflowId}/disable`;
- } else if (operation === 'dispatch') {
+ }
+ if (operation === 'dispatch') {
// ----------------------------------
// dispatch
// ----------------------------------
requestMethod = 'POST';
- const workflowId = this.getNodeParameter('workflowId', i, undefined, {
+ const workflowId = this.getNodeParameter('workflowId', i, '', {
extractValue: true,
}) as string;
endpoint = `/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`;
- body.ref = this.getNodeParameter('ref', i) as string;
+
+ const ref = this.getNodeParameter('ref', i, '', { extractValue: true }) as string;
+ body.ref = ref;
const inputs = validateJSON(
this.getNodeParameter('inputs', i) as string,
@@ -2537,7 +2700,9 @@ export class Github implements INodeType {
requestMethod = 'PUT';
- const workflowId = this.getNodeParameter('workflowId', i) as string;
+ const workflowId = this.getNodeParameter('workflowId', i, '', {
+ extractValue: true,
+ }) as string;
endpoint = `/repos/${owner}/${repository}/actions/workflows/${workflowId}/enable`;
} else if (operation === 'get') {
@@ -2547,7 +2712,9 @@ export class Github implements INodeType {
requestMethod = 'GET';
- const workflowId = this.getNodeParameter('workflowId', i) as string;
+ const workflowId = this.getNodeParameter('workflowId', i, '', {
+ extractValue: true,
+ }) as string;
endpoint = `/repos/${owner}/${repository}/actions/workflows/${workflowId}`;
} else if (operation === 'getUsage') {
@@ -2557,7 +2724,9 @@ export class Github implements INodeType {
requestMethod = 'GET';
- const workflowId = this.getNodeParameter('workflowId', i) as string;
+ const workflowId = this.getNodeParameter('workflowId', i, '', {
+ extractValue: true,
+ }) as string;
endpoint = `/repos/${owner}/${repository}/actions/workflows/${workflowId}/timing`;
} else if (operation === 'list') {
diff --git a/packages/nodes-base/nodes/Github/SearchFunctions.ts b/packages/nodes-base/nodes/Github/SearchFunctions.ts
index ff77121464..648d82e628 100644
--- a/packages/nodes-base/nodes/Github/SearchFunctions.ts
+++ b/packages/nodes-base/nodes/Github/SearchFunctions.ts
@@ -26,6 +26,10 @@ type RepositorySearchResponse = {
total_count: number;
};
+type RefItem = {
+ ref: string;
+};
+
export async function getUsers(
this: ILoadOptionsFunctions,
filter?: string,
@@ -115,3 +119,54 @@ export async function getWorkflows(
const nextPaginationToken = page * per_page < responseData.total_count ? page + 1 : undefined;
return { results, paginationToken: nextPaginationToken };
}
+
+export async function getRefs(
+ this: ILoadOptionsFunctions,
+ filter?: string,
+ paginationToken?: string,
+): Promise {
+ const owner = this.getCurrentNodeParameter('owner', { extractValue: true });
+ const repository = this.getCurrentNodeParameter('repository', { extractValue: true });
+ const page = paginationToken ? +paginationToken : 1;
+ const per_page = 100;
+
+ const responseData: RefItem[] = await githubApiRequest.call(
+ this,
+ 'GET',
+ `/repos/${owner}/${repository}/git/refs`,
+ {},
+ { page, per_page },
+ );
+
+ const refs: INodeListSearchItems[] = [];
+
+ for (const ref of responseData) {
+ const refPath = ref.ref.split('/');
+ const refType = refPath[1];
+ const refName = refPath.slice(2).join('/');
+
+ let description = '';
+ if (refType === 'heads') {
+ description = `Branch: ${refName}`;
+ } else if (refType === 'tags') {
+ description = `Tag: ${refName}`;
+ } else {
+ description = `${refType}: ${refName}`;
+ }
+
+ refs.push({
+ name: refName,
+ value: refName,
+ description,
+ });
+ }
+
+ if (filter) {
+ const filteredRefs = refs.filter((ref) =>
+ ref.name.toLowerCase().includes(filter.toLowerCase()),
+ );
+ return { results: filteredRefs };
+ }
+ const nextPaginationToken = responseData.length === per_page ? page + 1 : undefined;
+ return { results: refs, paginationToken: nextPaginationToken };
+}
diff --git a/packages/nodes-base/nodes/Github/__tests__/GenericFunctions.test.ts b/packages/nodes-base/nodes/Github/__tests__/GenericFunctions.test.ts
new file mode 100644
index 0000000000..20a3d9238f
--- /dev/null
+++ b/packages/nodes-base/nodes/Github/__tests__/GenericFunctions.test.ts
@@ -0,0 +1,184 @@
+import type { IExecuteFunctions, IHookFunctions } from 'n8n-workflow';
+import { NodeApiError, NodeOperationError } from 'n8n-workflow';
+
+import {
+ githubApiRequest,
+ getFileSha,
+ githubApiRequestAllItems,
+ isBase64,
+ validateJSON,
+} from '../GenericFunctions';
+
+const mockExecuteHookFunctions = {
+ getNodeParameter: jest.fn().mockImplementation((param: string) => {
+ if (param === 'authentication') return 'accessToken';
+ return undefined;
+ }),
+ getCredentials: jest.fn().mockResolvedValue({
+ server: 'https://api.github.com',
+ }),
+ helpers: {
+ requestWithAuthentication: jest.fn(),
+ },
+ getCurrentNodeParameter: jest.fn(),
+ getWebhookName: jest.fn(),
+ getWebhookDescription: jest.fn(),
+ getNodeWebhookUrl: jest.fn(),
+ getNode: jest.fn().mockReturnValue({
+ id: 'test-node-id',
+ name: 'test-node',
+ }),
+} as unknown as IExecuteFunctions | IHookFunctions;
+
+describe('GenericFunctions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('githubApiRequest', () => {
+ it('should make a successful API request', async () => {
+ const method = 'GET';
+ const endpoint = '/repos/test-owner/test-repo';
+ const body = {};
+ const responseData = { id: 123, name: 'test-repo' };
+
+ (mockExecuteHookFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await githubApiRequest.call(mockExecuteHookFunctions, method, endpoint, body);
+
+ expect(result).toEqual(responseData);
+ expect(mockExecuteHookFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ 'githubApi',
+ {
+ method: 'GET',
+ headers: { 'User-Agent': 'n8n' },
+ body: {},
+ qs: undefined,
+ uri: 'https://api.github.com/repos/test-owner/test-repo',
+ json: true,
+ },
+ );
+ });
+
+ it('should throw a NodeApiError on API failure', async () => {
+ const method = 'GET';
+ const endpoint = '/repos/test-owner/test-repo';
+ const body = {};
+ const error = new Error('API Error');
+
+ (mockExecuteHookFunctions.helpers.requestWithAuthentication as jest.Mock).mockRejectedValue(
+ error,
+ );
+
+ await expect(
+ githubApiRequest.call(mockExecuteHookFunctions, method, endpoint, body),
+ ).rejects.toThrow(NodeApiError);
+ });
+ });
+
+ describe('getFileSha', () => {
+ it('should return the SHA of a file', async () => {
+ const owner = 'test-owner';
+ const repository = 'test-repo';
+ const filePath = 'README.md';
+ const branch = 'main';
+ const responseData = { sha: 'abc123' };
+
+ (mockExecuteHookFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getFileSha.call(
+ mockExecuteHookFunctions,
+ owner,
+ repository,
+ filePath,
+ branch,
+ );
+
+ expect(result).toBe('abc123');
+ expect(mockExecuteHookFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ 'githubApi',
+ {
+ method: 'GET',
+ headers: { 'User-Agent': 'n8n' },
+ body: {},
+ qs: { ref: 'main' },
+ uri: 'https://api.github.com/repos/test-owner/test-repo/contents/README.md',
+ json: true,
+ },
+ );
+ });
+
+ it('should throw a NodeOperationError if SHA is missing', async () => {
+ const owner = 'test-owner';
+ const repository = 'test-repo';
+ const filePath = 'README.md';
+ const responseData = {};
+
+ (mockExecuteHookFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ await expect(
+ getFileSha.call(mockExecuteHookFunctions, owner, repository, filePath),
+ ).rejects.toThrow(NodeOperationError);
+ });
+ });
+
+ describe('githubApiRequestAllItems', () => {
+ it('should fetch all items with pagination', async () => {
+ const method = 'GET';
+ const endpoint = '/repos/test-owner/test-repo/issues';
+ const body = {};
+ const query = { state: 'open' };
+ const responseData1 = [{ id: 1, title: 'Issue 1' }];
+ const responseData2 = [{ id: 2, title: 'Issue 2' }];
+
+ (mockExecuteHookFunctions.helpers.requestWithAuthentication as jest.Mock)
+ .mockResolvedValueOnce({ headers: { link: 'next' }, body: responseData1 })
+ .mockResolvedValueOnce({ headers: {}, body: responseData2 });
+
+ const result = await githubApiRequestAllItems.call(
+ mockExecuteHookFunctions,
+ method,
+ endpoint,
+ body,
+ query,
+ );
+
+ expect(result).toEqual([...responseData1, ...responseData2]);
+ expect(mockExecuteHookFunctions.helpers.requestWithAuthentication).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('isBase64', () => {
+ it('should return true for valid Base64 strings', () => {
+ expect(isBase64('aGVsbG8gd29ybGQ=')).toBe(true);
+ expect(isBase64('Zm9vYmFy')).toBe(true);
+ });
+
+ it('should return false for invalid Base64 strings', () => {
+ expect(isBase64('not base64')).toBe(false);
+ expect(isBase64('123!@#')).toBe(false);
+ });
+ });
+
+ describe('validateJSON', () => {
+ it('should return parsed JSON for valid JSON strings', () => {
+ const jsonString = '{"key": "value"}';
+ const result = validateJSON(jsonString);
+
+ expect(result).toEqual({ key: 'value' });
+ });
+
+ it('should return undefined for invalid JSON strings', () => {
+ const invalidJsonString = 'not json';
+ const result = validateJSON(invalidJsonString);
+
+ expect(result).toBeUndefined();
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Github/__tests__/Github.node.test.ts b/packages/nodes-base/nodes/Github/__tests__/Github.node.test.ts
deleted file mode 100644
index 5700a424aa..0000000000
--- a/packages/nodes-base/nodes/Github/__tests__/Github.node.test.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import nock from 'nock';
-
-import { getWorkflowFilenames, initBinaryDataService, testWorkflows } from '@test/nodes/Helpers';
-
-const workflows = getWorkflowFilenames(__dirname);
-
-describe('Test Github Node', () => {
- describe('Workflow Dispatch', () => {
- const now = 1683028800000;
- const owner = 'testOwner';
- const repository = 'testRepository';
- const workflowId = 147025216;
- const usersResponse = {
- total_count: 12,
- items: [
- {
- login: 'testOwner',
- id: 1,
- },
- ],
- };
- const repositoriesResponse = {
- total_count: 40,
- items: [
- {
- id: 3081286,
- name: 'testRepository',
- },
- ],
- };
- const workflowsResponse = {
- total_count: 2,
- workflows: [
- {
- id: workflowId,
- node_id: 'MDg6V29ya2Zsb3cxNjEzMzU=',
- name: 'CI',
- path: '.github/workflows/blank.yaml',
- state: 'active',
- created_at: '2020-01-08T23:48:37.000-08:00',
- updated_at: '2020-01-08T23:50:21.000-08:00',
- url: 'https://api.github.com/repos/octo-org/octo-repo/actions/workflows/161335',
- html_url: 'https://github.com/octo-org/octo-repo/blob/master/.github/workflows/161335',
- badge_url: 'https://github.com/octo-org/octo-repo/workflows/CI/badge.svg',
- },
- {
- id: 269289,
- node_id: 'MDE4OldvcmtmbG93IFNlY29uZGFyeTI2OTI4OQ==',
- name: 'Linter',
- path: '.github/workflows/linter.yaml',
- state: 'active',
- created_at: '2020-01-08T23:48:37.000-08:00',
- updated_at: '2020-01-08T23:50:21.000-08:00',
- url: 'https://api.github.com/repos/octo-org/octo-repo/actions/workflows/269289',
- html_url: 'https://github.com/octo-org/octo-repo/blob/master/.github/workflows/269289',
- badge_url: 'https://github.com/octo-org/octo-repo/workflows/Linter/badge.svg',
- },
- ],
- };
-
- beforeAll(async () => {
- jest.useFakeTimers({ doNotFake: ['nextTick'], now });
- await initBinaryDataService();
- });
-
- beforeEach(async () => {
- const baseUrl = 'https://api.github.com';
- nock(baseUrl)
- .persist()
- .defaultReplyHeaders({ 'Content-Type': 'application/json' })
- .get('/search/users')
- .query(true)
- .reply(200, usersResponse)
- .get('/search/repositories')
- .query(true)
- .reply(200, repositoriesResponse)
- .get(`/repos/${owner}/${repository}/actions/workflows`)
- .reply(200, workflowsResponse)
- .post(`/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`, {
- ref: 'main',
- inputs: {},
- })
- .reply(200, {});
- });
-
- testWorkflows(workflows);
- });
-});
diff --git a/packages/nodes-base/nodes/Github/__tests__/SearchFunctions.test.ts b/packages/nodes-base/nodes/Github/__tests__/SearchFunctions.test.ts
new file mode 100644
index 0000000000..9e218f8445
--- /dev/null
+++ b/packages/nodes-base/nodes/Github/__tests__/SearchFunctions.test.ts
@@ -0,0 +1,499 @@
+import type { ILoadOptionsFunctions } from 'n8n-workflow';
+
+import { getUsers, getRepositories, getWorkflows, getRefs } from '../SearchFunctions';
+
+const mockLoadOptionsFunctions = {
+ getNodeParameter: jest.fn(),
+ getCredentials: jest.fn().mockResolvedValue({
+ server: 'https://api.github.com',
+ }),
+ helpers: {
+ requestWithAuthentication: jest.fn(),
+ },
+ getCurrentNodeParameter: jest.fn(),
+} as unknown as ILoadOptionsFunctions;
+
+describe('Search Functions', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('getUsers', () => {
+ it('should fetch users', async () => {
+ const filter = 'test-user';
+ const responseData = {
+ items: [
+ { login: 'test-user-1', html_url: 'https://github.com/test-user-1' },
+ { login: 'test-user-2', html_url: 'https://github.com/test-user-2' },
+ ],
+ total_count: 2,
+ };
+
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getUsers.call(mockLoadOptionsFunctions, filter);
+
+ expect(result).toEqual({
+ results: [
+ { name: 'test-user-1', value: 'test-user-1', url: 'https://github.com/test-user-1' },
+ { name: 'test-user-2', value: 'test-user-2', url: 'https://github.com/test-user-2' },
+ ],
+ paginationToken: undefined,
+ });
+
+ expect(mockLoadOptionsFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ 'githubOAuth2Api',
+ expect.objectContaining({
+ method: 'GET',
+ qs: expect.objectContaining({ page: 1 }),
+ }),
+ );
+ });
+
+ it('should handle pagination', async () => {
+ const filter = 'test-user';
+ const responseData = {
+ items: [
+ { login: 'test-user-1', html_url: 'https://github.com/test-user-1' },
+ { login: 'test-user-2', html_url: 'https://github.com/test-user-2' },
+ ],
+ total_count: 200,
+ };
+
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getUsers.call(mockLoadOptionsFunctions, filter);
+
+ expect(result).toEqual({
+ results: [
+ { name: 'test-user-1', value: 'test-user-1', url: 'https://github.com/test-user-1' },
+ { name: 'test-user-2', value: 'test-user-2', url: 'https://github.com/test-user-2' },
+ ],
+ paginationToken: 2,
+ });
+ });
+
+ it('should use paginationToken when provided', async () => {
+ const filter = 'test-user';
+ const paginationToken = '3';
+ const responseData = {
+ items: [
+ { login: 'test-user-5', html_url: 'https://github.com/test-user-5' },
+ { login: 'test-user-6', html_url: 'https://github.com/test-user-6' },
+ ],
+ total_count: 200,
+ };
+
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getUsers.call(mockLoadOptionsFunctions, filter, paginationToken);
+
+ expect(result).toEqual({
+ results: [
+ { name: 'test-user-5', value: 'test-user-5', url: 'https://github.com/test-user-5' },
+ { name: 'test-user-6', value: 'test-user-6', url: 'https://github.com/test-user-6' },
+ ],
+ paginationToken: undefined,
+ });
+
+ expect(mockLoadOptionsFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ 'githubOAuth2Api',
+ expect.objectContaining({
+ method: 'GET',
+ qs: expect.objectContaining({ page: 3 }),
+ }),
+ );
+ });
+ });
+
+ describe('getRepositories', () => {
+ it('should fetch repositories', async () => {
+ const filter = 'test-repo';
+ const owner = 'test-owner';
+ const responseData = {
+ items: [
+ { name: 'test-repo-1', html_url: 'https://github.com/test-owner/test-repo-1' },
+ { name: 'test-repo-2', html_url: 'https://github.com/test-owner/test-repo-2' },
+ ],
+ total_count: 2,
+ };
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockReturnValue(owner);
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getRepositories.call(mockLoadOptionsFunctions, filter);
+
+ expect(result).toEqual({
+ results: [
+ {
+ name: 'test-repo-1',
+ value: 'test-repo-1',
+ url: 'https://github.com/test-owner/test-repo-1',
+ },
+ {
+ name: 'test-repo-2',
+ value: 'test-repo-2',
+ url: 'https://github.com/test-owner/test-repo-2',
+ },
+ ],
+ paginationToken: undefined,
+ });
+ });
+
+ it('should fetch repositories without filter', async () => {
+ const owner = 'test-owner';
+ const responseData = {
+ items: [
+ { name: 'test-repo-1', html_url: 'https://github.com/test-owner/test-repo-1' },
+ { name: 'test-repo-2', html_url: 'https://github.com/test-owner/test-repo-2' },
+ ],
+ total_count: 2,
+ };
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockReturnValue(owner);
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getRepositories.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual({
+ results: [
+ {
+ name: 'test-repo-1',
+ value: 'test-repo-1',
+ url: 'https://github.com/test-owner/test-repo-1',
+ },
+ {
+ name: 'test-repo-2',
+ value: 'test-repo-2',
+ url: 'https://github.com/test-owner/test-repo-2',
+ },
+ ],
+ paginationToken: undefined,
+ });
+ });
+
+ it('should use paginationToken when provided', async () => {
+ const filter = 'test-repo';
+ const paginationToken = '3';
+ const owner = 'test-owner';
+ const responseData = {
+ items: [
+ { name: 'test-repo-5', html_url: 'https://github.com/test-owner/test-repo-5' },
+ { name: 'test-repo-6', html_url: 'https://github.com/test-owner/test-repo-6' },
+ ],
+ total_count: 200,
+ };
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockReturnValue(owner);
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getRepositories.call(mockLoadOptionsFunctions, filter, paginationToken);
+
+ expect(result).toEqual({
+ results: [
+ {
+ name: 'test-repo-5',
+ value: 'test-repo-5',
+ url: 'https://github.com/test-owner/test-repo-5',
+ },
+ {
+ name: 'test-repo-6',
+ value: 'test-repo-6',
+ url: 'https://github.com/test-owner/test-repo-6',
+ },
+ ],
+ paginationToken: undefined,
+ });
+
+ expect(mockLoadOptionsFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ 'githubOAuth2Api',
+ expect.objectContaining({
+ method: 'GET',
+ qs: expect.objectContaining({ page: 3 }),
+ }),
+ );
+ });
+
+ it('should handle empty repositories', async () => {
+ const filter = 'test-repo';
+ const owner = 'test-owner';
+ const responseData = {
+ items: [],
+ total_count: 0,
+ };
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockReturnValue(owner);
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getRepositories.call(mockLoadOptionsFunctions, filter);
+
+ expect(result).toEqual({
+ results: [],
+ paginationToken: undefined,
+ });
+ });
+ });
+
+ describe('getWorkflows', () => {
+ it('should fetch workflows', async () => {
+ const owner = 'test-owner';
+ const repository = 'test-repo';
+ const responseData = {
+ workflows: [
+ { id: '1', name: 'workflow-1' },
+ { id: '2', name: 'workflow-2' },
+ ],
+ total_count: 2,
+ };
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock)
+ .mockReturnValueOnce(owner)
+ .mockReturnValueOnce(repository);
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getWorkflows.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual({
+ results: [
+ { name: 'workflow-1', value: '1' },
+ { name: 'workflow-2', value: '2' },
+ ],
+ paginationToken: undefined,
+ });
+ });
+
+ it('should handle pagination', async () => {
+ const owner = 'test-owner';
+ const repository = 'test-repo';
+ const responseData = {
+ workflows: [
+ { id: '1', name: 'workflow-1' },
+ { id: '2', name: 'workflow-2' },
+ ],
+ total_count: 200,
+ };
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock)
+ .mockReturnValueOnce(owner)
+ .mockReturnValueOnce(repository);
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getWorkflows.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual({
+ results: [
+ { name: 'workflow-1', value: '1' },
+ { name: 'workflow-2', value: '2' },
+ ],
+ paginationToken: 2,
+ });
+ });
+
+ it('should use paginationToken when provided and return next page token', async () => {
+ const paginationToken = '1';
+ const owner = 'test-owner';
+ const repository = 'test-repo';
+ const responseData = {
+ workflows: [
+ { id: '3', name: 'workflow-3' },
+ { id: '4', name: 'workflow-4' },
+ ],
+ total_count: 300,
+ };
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock)
+ .mockReturnValueOnce(owner)
+ .mockReturnValueOnce(repository);
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getWorkflows.call(mockLoadOptionsFunctions, paginationToken);
+
+ expect(result).toEqual({
+ results: [
+ { name: 'workflow-3', value: '3' },
+ { name: 'workflow-4', value: '4' },
+ ],
+ paginationToken: 2,
+ });
+
+ expect(mockLoadOptionsFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ 'githubOAuth2Api',
+ expect.objectContaining({
+ method: 'GET',
+ qs: expect.objectContaining({ page: 1 }),
+ }),
+ );
+ });
+
+ it('should handle empty workflows', async () => {
+ const owner = 'test-owner';
+ const repository = 'test-repo';
+ const responseData = {
+ workflows: [],
+ total_count: 0,
+ };
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock)
+ .mockReturnValueOnce(owner)
+ .mockReturnValueOnce(repository);
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ responseData,
+ );
+
+ const result = await getWorkflows.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual({
+ results: [],
+ paginationToken: undefined,
+ });
+ });
+ });
+
+ describe('getRefs', () => {
+ it('should fetch branches and tags using git/refs endpoint', async () => {
+ const owner = 'test-owner';
+ const repository = 'test-repo';
+ const refsResponse = [
+ { ref: 'refs/heads/Main' },
+ { ref: 'refs/heads/Dev' },
+ { ref: 'refs/tags/v1.0.0' },
+ { ref: 'refs/tags/v2.0.0' },
+ { ref: 'refs/Pull/123/head' },
+ ];
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockImplementation(
+ (param: string) => {
+ if (param === 'owner') return owner;
+ if (param === 'repository') return repository;
+ },
+ );
+
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ refsResponse,
+ );
+
+ const result = await getRefs.call(mockLoadOptionsFunctions);
+
+ expect(result).toEqual({
+ results: [
+ { name: 'Main', value: 'Main', description: 'Branch: Main' },
+ { name: 'Dev', value: 'Dev', description: 'Branch: Dev' },
+ { name: 'v1.0.0', value: 'v1.0.0', description: 'Tag: v1.0.0' },
+ { name: 'v2.0.0', value: 'v2.0.0', description: 'Tag: v2.0.0' },
+ { name: '123/head', value: '123/head', description: 'Pull: 123/head' },
+ ],
+ paginationToken: undefined,
+ });
+ });
+
+ it('should use paginationToken when provided', async () => {
+ const paginationToken = '3';
+ const owner = 'test-owner';
+ const repository = 'test-repo';
+ const refsResponse = [{ ref: 'refs/heads/branch-5' }, { ref: 'refs/heads/branch-6' }];
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockImplementation(
+ (param: string) => {
+ if (param === 'owner') return owner;
+ if (param === 'repository') return repository;
+ },
+ );
+
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ refsResponse,
+ );
+
+ const result = await getRefs.call(mockLoadOptionsFunctions, undefined, paginationToken);
+
+ expect(result).toEqual({
+ results: [
+ { name: 'branch-5', value: 'branch-5', description: 'Branch: branch-5' },
+ { name: 'branch-6', value: 'branch-6', description: 'Branch: branch-6' },
+ ],
+ paginationToken: undefined,
+ });
+
+ expect(mockLoadOptionsFunctions.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ 'githubOAuth2Api',
+ expect.objectContaining({
+ method: 'GET',
+ qs: expect.objectContaining({ page: 3 }),
+ }),
+ );
+ });
+
+ it('should filter refs based on the provided filter', async () => {
+ const owner = 'test-owner';
+ const repository = 'test-repo';
+ const refsResponse = [
+ { ref: 'refs/heads/main' },
+ { ref: 'refs/heads/dev' },
+ { ref: 'refs/tags/v1.0.0' },
+ { ref: 'refs/tags/v2.0.0' },
+ ];
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockImplementation(
+ (param: string) => {
+ if (param === 'owner') return owner;
+ if (param === 'repository') return repository;
+ },
+ );
+
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ refsResponse,
+ );
+
+ const result = await getRefs.call(mockLoadOptionsFunctions, 'v1');
+
+ expect(result).toEqual({
+ results: [{ name: 'v1.0.0', value: 'v1.0.0', description: 'Tag: v1.0.0' }],
+ });
+ });
+
+ it('should handle pagination correctly', async () => {
+ const owner = 'test-owner';
+ const repository = 'test-repo';
+ const refsResponse = Array(100)
+ .fill(0)
+ .map((_, i) => ({
+ ref: i % 2 === 0 ? `refs/heads/branch-${i}` : `refs/tags/tag-${i}`,
+ }));
+
+ (mockLoadOptionsFunctions.getCurrentNodeParameter as jest.Mock).mockImplementation(
+ (param: string) => {
+ if (param === 'owner') return owner;
+ if (param === 'repository') return repository;
+ },
+ );
+
+ (mockLoadOptionsFunctions.helpers.requestWithAuthentication as jest.Mock).mockResolvedValue(
+ refsResponse,
+ );
+
+ const result = await getRefs.call(mockLoadOptionsFunctions);
+
+ expect(result.paginationToken).toBe(2);
+ expect(result.results.length).toBe(100);
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Github/__tests__/node/Github.dispatchAndWait.node.test.ts b/packages/nodes-base/nodes/Github/__tests__/node/Github.dispatchAndWait.node.test.ts
new file mode 100644
index 0000000000..5db2a3336a
--- /dev/null
+++ b/packages/nodes-base/nodes/Github/__tests__/node/Github.dispatchAndWait.node.test.ts
@@ -0,0 +1,93 @@
+import nock from 'nock';
+
+import { getWorkflowFilenames, initBinaryDataService, testWorkflows } from '@test/nodes/Helpers';
+
+const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
+ filename.includes('GithubDispatchAndWaitWorkflow.json'),
+);
+
+describe('Test Github Node - Dispatch and Wait', () => {
+ describe('Workflow Dispatch and Wait', () => {
+ const now = 1683028800000;
+ const owner = 'Owner';
+ const repository = 'test-github-actions';
+ const workflowId = 145370278;
+ const ref = 'test-branch';
+
+ const usersResponse = {
+ total_count: 1,
+ items: [
+ {
+ login: owner,
+ id: 1,
+ },
+ ],
+ };
+
+ const repositoriesResponse = {
+ total_count: 1,
+ items: [
+ {
+ id: 3081286,
+ name: repository,
+ },
+ ],
+ };
+
+ const workflowsResponse = {
+ total_count: 1,
+ workflows: [
+ {
+ id: workflowId,
+ node_id: 'MDg6V29ya2Zsb3cxNjEzMzU=',
+ name: 'New Test Workflow',
+ path: '.github/workflows/test.yaml',
+ state: 'active',
+ created_at: '2020-01-08T23:48:37.000-08:00',
+ updated_at: '2020-01-08T23:50:21.000-08:00',
+ url: `https://api.github.com/repos/${owner}/${repository}/actions/workflows/${workflowId}`,
+ html_url: `https://github.com/${owner}/${repository}/blob/master/.github/workflows/test.yaml`,
+ badge_url: `https://github.com/${owner}/${repository}/workflows/New%20Test%20Workflow/badge.svg`,
+ },
+ ],
+ };
+
+ const refsResponse = [{ ref: `refs/heads/${ref}` }];
+
+ beforeAll(async () => {
+ jest.useFakeTimers({ doNotFake: ['nextTick'], now });
+ await initBinaryDataService();
+ });
+
+ beforeEach(async () => {
+ const baseUrl = 'https://api.github.com';
+ nock.cleanAll();
+ nock(baseUrl)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/json' })
+ .get('/search/users')
+ .query(true)
+ .reply(200, usersResponse)
+ .get('/search/repositories')
+ .query(true)
+ .reply(200, repositoriesResponse)
+ .get(`/repos/${owner}/${repository}/actions/workflows`)
+ .reply(200, workflowsResponse)
+ .get(`/repos/${owner}/${repository}/git/refs`)
+ .reply(200, refsResponse)
+ .post(
+ `/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`,
+ (body) => {
+ return body.ref === ref && body.inputs && body.inputs.resumeUrl;
+ },
+ )
+ .reply(200, {});
+ });
+
+ afterEach(() => {
+ nock.cleanAll();
+ });
+
+ testWorkflows(workflows);
+ });
+});
diff --git a/packages/nodes-base/nodes/Github/__tests__/node/Github.node.test.ts b/packages/nodes-base/nodes/Github/__tests__/node/Github.node.test.ts
new file mode 100644
index 0000000000..bbb5471495
--- /dev/null
+++ b/packages/nodes-base/nodes/Github/__tests__/node/Github.node.test.ts
@@ -0,0 +1,533 @@
+import { NodeApiError, NodeOperationError } from 'n8n-workflow';
+import nock from 'nock';
+
+import { getWorkflowFilenames, initBinaryDataService, testWorkflows } from '@test/nodes/Helpers';
+
+import { Github } from '../../Github.node';
+
+const workflows = getWorkflowFilenames(__dirname).filter((filename) =>
+ filename.includes('GithubTestWorkflow.json'),
+);
+
+describe('Test Github Node', () => {
+ describe('Workflow Dispatch', () => {
+ const now = 1683028800000;
+ const owner = 'testOwner';
+ const repository = 'testRepository';
+ const workflowId = 147025216;
+ const usersResponse = {
+ total_count: 12,
+ items: [
+ {
+ login: 'testOwner',
+ id: 1,
+ },
+ ],
+ };
+ const repositoriesResponse = {
+ total_count: 40,
+ items: [
+ {
+ id: 3081286,
+ name: 'testRepository',
+ },
+ ],
+ };
+ const workflowsResponse = {
+ total_count: 2,
+ workflows: [
+ {
+ id: workflowId,
+ node_id: 'MDg6V29ya2Zsb3cxNjEzMzU=',
+ name: 'CI',
+ path: '.github/workflows/blank.yaml',
+ state: 'active',
+ created_at: '2020-01-08T23:48:37.000-08:00',
+ updated_at: '2020-01-08T23:50:21.000-08:00',
+ url: 'https://api.github.com/repos/octo-org/octo-repo/actions/workflows/161335',
+ html_url: 'https://github.com/octo-org/octo-repo/blob/master/.github/workflows/161335',
+ badge_url: 'https://github.com/octo-org/octo-repo/workflows/CI/badge.svg',
+ },
+ {
+ id: 269289,
+ node_id: 'MDE4OldvcmtmbG93IFNlY29uZGFyeTI2OTI4OQ==',
+ name: 'Linter',
+ path: '.github/workflows/linter.yaml',
+ state: 'active',
+ created_at: '2020-01-08T23:48:37.000-08:00',
+ updated_at: '2020-01-08T23:50:21.000-08:00',
+ url: 'https://api.github.com/repos/octo-org/octo-repo/actions/workflows/269289',
+ html_url: 'https://github.com/octo-org/octo-repo/blob/master/.github/workflows/269289',
+ badge_url: 'https://github.com/octo-org/octo-repo/workflows/Linter/badge.svg',
+ },
+ ],
+ };
+
+ beforeAll(async () => {
+ jest.useFakeTimers({ doNotFake: ['nextTick'], now });
+ await initBinaryDataService();
+ });
+
+ beforeEach(async () => {
+ const baseUrl = 'https://api.github.com';
+ nock(baseUrl)
+ .persist()
+ .defaultReplyHeaders({ 'Content-Type': 'application/json' })
+ .get('/search/users')
+ .query(true)
+ .reply(200, usersResponse)
+ .get('/search/repositories')
+ .query(true)
+ .reply(200, repositoriesResponse)
+ .get(`/repos/${owner}/${repository}/actions/workflows`)
+ .reply(200, workflowsResponse)
+ .post(`/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`, {
+ ref: 'main',
+ inputs: {},
+ })
+ .reply(200, {});
+ });
+
+ testWorkflows(workflows);
+ });
+
+ describe('Error Handling', () => {
+ let githubNode: Github;
+ let mockExecutionContext: any;
+
+ beforeEach(() => {
+ githubNode = new Github();
+ mockExecutionContext = {
+ getNode: jest.fn().mockReturnValue({ name: 'Github' }),
+ getNodeParameter: jest.fn(),
+ getInputData: jest.fn().mockReturnValue([{ json: {} }]),
+ continueOnFail: jest.fn().mockReturnValue(false),
+ putExecutionToWait: jest.fn(),
+ getCredentials: jest.fn().mockResolvedValue({
+ server: 'https://api.github.com',
+ user: 'test',
+ accessToken: 'test',
+ }),
+ helpers: {
+ returnJsonArray: jest.fn().mockReturnValue([{ json: {} }]),
+ httpRequest: jest.fn(),
+ httpRequestWithAuthentication: jest.fn(),
+ requestWithAuthentication: jest
+ .fn()
+ .mockImplementation(async (_credentialType, options) => {
+ if (options.uri.includes('dispatches') && options.method === 'POST') {
+ const error: any = new Error('Not Found');
+ error.statusCode = 404;
+ error.message = 'Not Found';
+ throw error;
+ }
+ return {};
+ }),
+ request: jest.fn(),
+ constructExecutionMetaData: jest.fn().mockReturnValue([{ json: {} }]),
+ assertBinaryData: jest.fn(),
+ prepareBinaryData: jest.fn(),
+ },
+ getWorkflowDataProxy: jest.fn().mockReturnValue({
+ $execution: {
+ resumeUrl: 'https://example.com/webhook',
+ },
+ }),
+ };
+ });
+
+ it('should throw NodeOperationError for invalid JSON inputs', async () => {
+ mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
+ if (parameterName === 'inputs') {
+ return 'invalid json';
+ }
+ if (parameterName === 'resource') {
+ return 'workflow';
+ }
+ if (parameterName === 'operation') {
+ return 'dispatchAndWait';
+ }
+ if (parameterName === 'authentication') {
+ return 'accessToken';
+ }
+ return '';
+ });
+
+ await expect(async () => {
+ await githubNode.execute.call(mockExecutionContext);
+ }).rejects.toThrow(NodeOperationError);
+ });
+
+ it('should throw NodeOperationError for 404 errors when dispatching a workflow', async () => {
+ const owner = 'testOwner';
+ const repository = 'testRepository';
+ const workflowId = 147025216;
+
+ mockExecutionContext.helpers.requestWithAuthentication.mockRejectedValueOnce({
+ statusCode: 404,
+ message: 'Not Found',
+ });
+
+ mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
+ if (parameterName === 'owner') {
+ return owner;
+ }
+ if (parameterName === 'repository') {
+ return repository;
+ }
+ if (parameterName === 'workflowId') {
+ return workflowId;
+ }
+ if (parameterName === 'inputs') {
+ return '{}';
+ }
+ if (parameterName === 'ref') {
+ return 'main';
+ }
+ if (parameterName === 'resource') {
+ return 'workflow';
+ }
+ if (parameterName === 'operation') {
+ return 'dispatchAndWait';
+ }
+ if (parameterName === 'authentication') {
+ return 'accessToken';
+ }
+ return '';
+ });
+
+ await expect(async () => {
+ await githubNode.execute.call(mockExecutionContext);
+ }).rejects.toThrow(/The workflow to dispatch could not be found/);
+ });
+
+ it('should throw NodeApiError for general API errors', async () => {
+ const owner = 'testOwner';
+ const repository = 'testRepository';
+ const workflowId = 147025216;
+
+ mockExecutionContext.helpers.requestWithAuthentication.mockRejectedValueOnce({
+ statusCode: 500,
+ message: 'Internal Server Error',
+ });
+
+ mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
+ if (parameterName === 'owner') {
+ return owner;
+ }
+ if (parameterName === 'repository') {
+ return repository;
+ }
+ if (parameterName === 'workflowId') {
+ return workflowId;
+ }
+ if (parameterName === 'inputs') {
+ return '{}';
+ }
+ if (parameterName === 'ref') {
+ return 'main';
+ }
+ if (parameterName === 'resource') {
+ return 'workflow';
+ }
+ if (parameterName === 'operation') {
+ return 'dispatch';
+ }
+ if (parameterName === 'authentication') {
+ return 'accessToken';
+ }
+ return '';
+ });
+
+ await expect(async () => {
+ await githubNode.execute.call(mockExecutionContext);
+ }).rejects.toThrow();
+ });
+
+ it('should throw NodeApiError for general API errors in dispatchAndWait operation', async () => {
+ const owner = 'testOwner';
+ const repository = 'testRepository';
+ const workflowId = 147025216;
+
+ mockExecutionContext.getWorkflowDataProxy = jest.fn().mockReturnValue({
+ $execution: {
+ resumeUrl: 'https://example.com/webhook',
+ },
+ });
+
+ mockExecutionContext.helpers.requestWithAuthentication.mockRejectedValueOnce({
+ statusCode: 500,
+ message: 'Internal Server Error',
+ });
+
+ mockExecutionContext.getNodeParameter.mockImplementation((parameterName: string) => {
+ if (parameterName === 'owner') {
+ return owner;
+ }
+ if (parameterName === 'repository') {
+ return repository;
+ }
+ if (parameterName === 'workflowId') {
+ return workflowId;
+ }
+ if (parameterName === 'inputs') {
+ return '{}';
+ }
+ if (parameterName === 'ref') {
+ return 'main';
+ }
+ if (parameterName === 'resource') {
+ return 'workflow';
+ }
+ if (parameterName === 'operation') {
+ return 'dispatchAndWait';
+ }
+ if (parameterName === 'authentication') {
+ return 'accessToken';
+ }
+ return '';
+ });
+
+ await expect(async () => {
+ await githubNode.execute.call(mockExecutionContext);
+ }).rejects.toThrow(NodeApiError);
+
+ expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ method: 'POST',
+ uri: expect.stringContaining(
+ `/repos/${owner}/${repository}/actions/workflows/${workflowId}/dispatches`,
+ ),
+ body: expect.objectContaining({
+ ref: 'main',
+ inputs: expect.objectContaining({
+ resumeUrl: 'https://example.com/webhook',
+ }),
+ }),
+ }),
+ );
+ });
+ });
+
+ describe('Workflow Operations', () => {
+ let githubNode: Github;
+ let mockExecutionContext: any;
+
+ beforeEach(() => {
+ githubNode = new Github();
+ mockExecutionContext = {
+ getNode: jest.fn().mockReturnValue({ name: 'Github' }),
+ getNodeParameter: jest.fn(),
+ getInputData: jest.fn().mockReturnValue([{ json: {} }]),
+ continueOnFail: jest.fn().mockReturnValue(false),
+ getCredentials: jest.fn().mockResolvedValue({
+ server: 'https://api.github.com',
+ user: 'test',
+ accessToken: 'test',
+ }),
+ helpers: {
+ returnJsonArray: jest.fn().mockReturnValue([{ json: {} }]),
+ requestWithAuthentication: jest.fn().mockResolvedValue({}),
+ constructExecutionMetaData: jest.fn().mockReturnValue([{ json: {} }]),
+ },
+ };
+ });
+
+ it('should use extractValue for workflowId in disable operation', async () => {
+ const owner = 'testOwner';
+ const repository = 'testRepository';
+ const workflowId = 147025216;
+
+ mockExecutionContext.getNodeParameter.mockImplementation(
+ (parameterName: string, _itemIndex: number, defaultValue: string, options?: any) => {
+ if (parameterName === 'owner') {
+ return owner;
+ }
+ if (parameterName === 'repository') {
+ return repository;
+ }
+ if (parameterName === 'workflowId') {
+ expect(options).toBeDefined();
+ expect(options.extractValue).toBe(true);
+ return workflowId;
+ }
+ if (parameterName === 'resource') {
+ return 'workflow';
+ }
+ if (parameterName === 'operation') {
+ return 'disable';
+ }
+ if (parameterName === 'authentication') {
+ return 'accessToken';
+ }
+ return defaultValue;
+ },
+ );
+
+ await githubNode.execute.call(mockExecutionContext);
+
+ expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ method: 'PUT',
+ uri: `https://api.github.com/repos/${owner}/${repository}/actions/workflows/${workflowId}/disable`,
+ }),
+ );
+ });
+
+ it('should use extractValue for workflowId in enable operation', async () => {
+ const owner = 'testOwner';
+ const repository = 'testRepository';
+ const workflowId = 147025216;
+
+ mockExecutionContext.getNodeParameter.mockImplementation(
+ (parameterName: string, _itemIndex: number, defaultValue: string, options?: any) => {
+ if (parameterName === 'owner') {
+ return owner;
+ }
+ if (parameterName === 'repository') {
+ return repository;
+ }
+ if (parameterName === 'workflowId') {
+ expect(options).toBeDefined();
+ expect(options.extractValue).toBe(true);
+ return workflowId;
+ }
+ if (parameterName === 'resource') {
+ return 'workflow';
+ }
+ if (parameterName === 'operation') {
+ return 'enable';
+ }
+ if (parameterName === 'authentication') {
+ return 'accessToken';
+ }
+ return defaultValue;
+ },
+ );
+
+ await githubNode.execute.call(mockExecutionContext);
+
+ expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ method: 'PUT',
+ uri: `https://api.github.com/repos/${owner}/${repository}/actions/workflows/${workflowId}/enable`,
+ }),
+ );
+ });
+
+ it('should use extractValue for workflowId in get operation', async () => {
+ const owner = 'testOwner';
+ const repository = 'testRepository';
+ const workflowId = 147025216;
+
+ mockExecutionContext.getNodeParameter.mockImplementation(
+ (parameterName: string, _itemIndex: number, defaultValue: string, options?: any) => {
+ if (parameterName === 'owner') {
+ return owner;
+ }
+ if (parameterName === 'repository') {
+ return repository;
+ }
+ if (parameterName === 'workflowId') {
+ expect(options).toBeDefined();
+ expect(options.extractValue).toBe(true);
+ return workflowId;
+ }
+ if (parameterName === 'resource') {
+ return 'workflow';
+ }
+ if (parameterName === 'operation') {
+ return 'get';
+ }
+ if (parameterName === 'authentication') {
+ return 'accessToken';
+ }
+ return defaultValue;
+ },
+ );
+
+ await githubNode.execute.call(mockExecutionContext);
+
+ expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ method: 'GET',
+ uri: `https://api.github.com/repos/${owner}/${repository}/actions/workflows/${workflowId}`,
+ }),
+ );
+ });
+
+ it('should use extractValue for workflowId in getUsage operation', async () => {
+ const owner = 'testOwner';
+ const repository = 'testRepository';
+ const workflowId = 147025216;
+
+ mockExecutionContext.getNodeParameter.mockImplementation(
+ (parameterName: string, _itemIndex: number, defaultValue: string, options?: any) => {
+ if (parameterName === 'owner') {
+ return owner;
+ }
+ if (parameterName === 'repository') {
+ return repository;
+ }
+ if (parameterName === 'workflowId') {
+ expect(options).toBeDefined();
+ expect(options.extractValue).toBe(true);
+ return workflowId;
+ }
+ if (parameterName === 'resource') {
+ return 'workflow';
+ }
+ if (parameterName === 'operation') {
+ return 'getUsage';
+ }
+ if (parameterName === 'authentication') {
+ return 'accessToken';
+ }
+ return defaultValue;
+ },
+ );
+
+ await githubNode.execute.call(mockExecutionContext);
+
+ expect(mockExecutionContext.helpers.requestWithAuthentication).toHaveBeenCalledWith(
+ expect.any(String),
+ expect.objectContaining({
+ method: 'GET',
+ uri: `https://api.github.com/repos/${owner}/${repository}/actions/workflows/${workflowId}/timing`,
+ }),
+ );
+ });
+ });
+
+ describe('Parameter Extraction', () => {
+ it('should use extractValue for workflowId parameter', () => {
+ const githubNode = new Github();
+ const description = githubNode.description;
+
+ const workflowIdParam = description.properties.find((prop) => prop.name === 'workflowId');
+
+ expect(workflowIdParam).toBeDefined();
+ expect(workflowIdParam?.type).toBe('resourceLocator');
+
+ const workflowOperations = description.properties.find(
+ (prop) =>
+ prop.name === 'operation' && prop.displayOptions?.show?.resource?.includes('workflow'),
+ );
+
+ expect(workflowOperations).toBeDefined();
+ expect(workflowOperations?.options).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ value: 'disable' }),
+ expect.objectContaining({ value: 'dispatch' }),
+ expect.objectContaining({ value: 'enable' }),
+ expect.objectContaining({ value: 'get' }),
+ expect.objectContaining({ value: 'getUsage' }),
+ ]),
+ );
+ });
+ });
+});
diff --git a/packages/nodes-base/nodes/Github/__tests__/node/Github.webhook.test.ts b/packages/nodes-base/nodes/Github/__tests__/node/Github.webhook.test.ts
new file mode 100644
index 0000000000..406ab5c38f
--- /dev/null
+++ b/packages/nodes-base/nodes/Github/__tests__/node/Github.webhook.test.ts
@@ -0,0 +1,64 @@
+import type { IWebhookFunctions } from 'n8n-workflow';
+
+import { Github } from '../../Github.node';
+
+describe('Github Node - Webhook Method', () => {
+ let githubNode: Github;
+ let mockWebhookFunctions: IWebhookFunctions;
+
+ beforeEach(() => {
+ githubNode = new Github();
+
+ mockWebhookFunctions = {
+ getRequestObject: jest.fn(),
+ getResponseObject: jest.fn(),
+ getNodeParameter: jest.fn(),
+ getNode: jest.fn(),
+ helpers: {
+ returnJsonArray: jest.fn(),
+ },
+ } as unknown as IWebhookFunctions;
+ });
+
+ it('should process webhook request and return workflowData', async () => {
+ const sampleWebhookBody = {
+ action: 'opened',
+ issue: {
+ number: 123,
+ title: 'Test Issue',
+ body: 'This is a test issue',
+ user: {
+ login: 'testuser',
+ },
+ },
+ repository: {
+ name: 'test-repo',
+ owner: {
+ login: 'test-owner',
+ },
+ },
+ };
+
+ const mockRequestObject = {
+ body: sampleWebhookBody,
+ headers: {
+ 'x-github-event': 'issues',
+ 'x-github-delivery': '72d3162e-cc78-11e3-81ab-4c9367dc0958',
+ },
+ };
+
+ (mockWebhookFunctions.getRequestObject as jest.Mock).mockReturnValue(mockRequestObject);
+ (mockWebhookFunctions.helpers.returnJsonArray as jest.Mock).mockReturnValue([
+ sampleWebhookBody,
+ ]);
+
+ const result = await githubNode.webhook.call(mockWebhookFunctions);
+
+ expect(result).toEqual({
+ workflowData: [[sampleWebhookBody]],
+ });
+
+ expect(mockWebhookFunctions.getRequestObject).toHaveBeenCalled();
+ expect(mockWebhookFunctions.helpers.returnJsonArray).toHaveBeenCalledWith(sampleWebhookBody);
+ });
+});
diff --git a/packages/nodes-base/nodes/Github/__tests__/node/GithubDispatchAndWaitWorkflow.json b/packages/nodes-base/nodes/Github/__tests__/node/GithubDispatchAndWaitWorkflow.json
new file mode 100644
index 0000000000..abfd3daacd
--- /dev/null
+++ b/packages/nodes-base/nodes/Github/__tests__/node/GithubDispatchAndWaitWorkflow.json
@@ -0,0 +1,76 @@
+{
+ "nodes": [
+ {
+ "parameters": {},
+ "type": "n8n-nodes-base.manualTrigger",
+ "typeVersion": 1,
+ "position": [200, -360],
+ "id": "0889ff06-41e2-4786-a0fe-ca330c3711e7",
+ "name": "When clicking ‘Test workflow’"
+ },
+ {
+ "parameters": {
+ "resource": "workflow",
+ "operation": "dispatchAndWait",
+ "owner": {
+ "__rl": true,
+ "value": "Owner",
+ "mode": "list",
+ "cachedResultName": "Owner",
+ "cachedResultUrl": "https://github.com/Owner"
+ },
+ "repository": {
+ "__rl": true,
+ "value": "test-github-actions",
+ "mode": "list",
+ "cachedResultName": "test-github-actions",
+ "cachedResultUrl": "https://github.com/Owner/test-github-actions"
+ },
+ "workflowId": {
+ "__rl": true,
+ "value": 145370278,
+ "mode": "list",
+ "cachedResultName": "New Test Workflow"
+ },
+ "ref": {
+ "__rl": true,
+ "value": "test-branch",
+ "mode": "list",
+ "cachedResultName": "test-branch"
+ }
+ },
+ "type": "n8n-nodes-base.github",
+ "typeVersion": 1.1,
+ "position": [220, 0],
+ "id": "105cb5f0-bcc3-4397-ba06-bda8cf46c71b",
+ "name": "Dispatch and Wait for Completion",
+ "webhookId": "02bfeade-db6c-412a-8627-fe3a9952e8ee",
+ "credentials": {
+ "githubApi": {
+ "id": "RtvkwCTqGZ2sLhB8",
+ "name": "GitHub account 3"
+ }
+ }
+ }
+ ],
+ "connections": {
+ "When clicking ‘Test workflow’": {
+ "main": [
+ [
+ {
+ "node": "Dispatch and Wait for Completion",
+ "type": "main",
+ "index": 0
+ }
+ ]
+ ]
+ }
+ },
+ "pinData": {
+ "Dispatch and Wait for Completion": [
+ {
+ "json": {}
+ }
+ ]
+ }
+}
diff --git a/packages/nodes-base/nodes/Github/__tests__/GithubTestWorkflow.json b/packages/nodes-base/nodes/Github/__tests__/node/GithubTestWorkflow.json
similarity index 93%
rename from packages/nodes-base/nodes/Github/__tests__/GithubTestWorkflow.json
rename to packages/nodes-base/nodes/Github/__tests__/node/GithubTestWorkflow.json
index 7404c5afa9..1ea3cd487e 100644
--- a/packages/nodes-base/nodes/Github/__tests__/GithubTestWorkflow.json
+++ b/packages/nodes-base/nodes/Github/__tests__/node/GithubTestWorkflow.json
@@ -79,8 +79,5 @@
"json": {}
}
]
- },
- "meta": {
- "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
}
}