diff --git a/packages/nodes-base/credentials/GitlabApi.credentials.ts b/packages/nodes-base/credentials/GitlabApi.credentials.ts new file mode 100644 index 000000000..b3aceb121 --- /dev/null +++ b/packages/nodes-base/credentials/GitlabApi.credentials.ts @@ -0,0 +1,24 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class GitlabApi implements ICredentialType { + name = 'gitlabApi'; + displayName = 'Gitlab API'; + properties = [ + { + displayName: 'Gitlab Server', + name: 'server', + type: 'string' as NodePropertyTypes, + default: 'https://gitlab.com' + }, + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Gitlab/GenericFunctions.ts b/packages/nodes-base/nodes/Gitlab/GenericFunctions.ts new file mode 100644 index 000000000..1c665dbf4 --- /dev/null +++ b/packages/nodes-base/nodes/Gitlab/GenericFunctions.ts @@ -0,0 +1,52 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +/** + * Make an API request to Gitlab + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function gitlabApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query?: object): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('gitlabApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const options = { + method, + headers: { + 'Private-Token': `token ${credentials.accessToken}`, + }, + body, + qs: query, + uri: `${(credentials.server as string).replace(/\/$/, '')}/api/v4/${endpoint}`, + json: true + }; + + try { + return await this.helpers.request(options); + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The Gitlab credentials are not valid!'); + } + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Gitlab error response [${error.statusCode}]: ${error.response.body.message}`); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} diff --git a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts new file mode 100644 index 000000000..307e7441d --- /dev/null +++ b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts @@ -0,0 +1,252 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + gitlabApiRequest, +} from './GenericFunctions'; + + +export class GitlabTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Gitlab Trigger', + name: 'gitlabTrigger', + icon: 'file:gitlab.png', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}', + description: 'Starts the workflow when a Gitlab events occurs.', + defaults: { + name: 'Gitlab Trigger', + color: '#FC6D27', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'gitlabApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Repository Owner', + name: 'owner', + type: 'string', + default: '', + required: true, + placeholder: 'n8n-io', + description: 'Owner of the repsitory.', + }, + { + displayName: 'Repository Name', + name: 'repository', + type: 'string', + default: '', + required: true, + placeholder: 'n8n', + description: 'The name of the repsitory.', + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + options: [ + { + name: '*', + value: '*', + description: 'Any time any event is triggered (Wildcard Event).', + }, + { + name: 'Comment', + value: 'note', + description: 'Triggered when a new comment is made on commits, merge requests, issues, and code snippets.' + }, + { + name: 'Issue', + value: 'issues', + description: 'Triggered when a new issue is created or an existing issue was updated/closed/reopened.' + }, + { + name: 'Job', + value: 'job', + description: 'Triggered on status change of a job.' + }, + { + name: 'Merge Request', + value: 'merge_requests', + description: 'Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch.' + }, + { + name: 'Pipeline', + value: 'pipeline', + description: 'Triggered on status change of Pipeline.' + }, + { + name: 'Push', + value: 'push', + description: 'Triggered when you push to the repository except when pushing tags.' + }, + { + name: 'Tag', + value: 'tag_push', + description: 'Triggered when you create (or delete) tags to the repository.' + }, + { + name: 'Wiki Page', + value: 'wiki_page', + description: 'Triggered when a wiki page is created, updated or deleted.' + } + ], + required: true, + default: [], + description: 'The events to listen to.', + }, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId === undefined) { + // No webhook id is set so no webhook can exist + return false; + } + + // Webhook got created before so check if it still exists + const owner = this.getNodeParameter('owner') as string; + const repository = this.getNodeParameter('repository') as string; + const endpoint = `/projects/${owner}%2F${repository}/hooks/${webhookData.webhookId}`; + + try { + await gitlabApiRequest.call(this, 'GET', endpoint, {}); + } catch (e) { + if (e.message.includes('[404]:')) { + // Webhook does not exist + delete webhookData.webhookId; + delete webhookData.webhookEvents; + + return false; + } + + // Some error occured + throw e; + } + + // If it did not error then the webhook exists + return true; + }, + /** + * Gitlab API - Add project hook: + * https://docs.gitlab.com/ee/api/projects.html#add-project-hook + */ + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + + const owner = this.getNodeParameter('owner') as string; + const repository = this.getNodeParameter('repository') as string; + + let eventsArray = this.getNodeParameter('events', []) as string[]; + if (eventsArray.includes('*')) { + eventsArray = ['note', 'issues', 'job', 'merge_requests', 'pipeline', 'push', 'tag_push', 'wiki_page']; + } + + const events: { [key: string]: boolean } = { }; + for (const e of eventsArray) { + events[`${e}_events`] = true + } + + const endpoint = `/projects/${owner}%2F${repository}/hooks`; + + const body = { + url: webhookUrl, + events, + enable_ssl_verification: false, + }; + + + let responseData; + try { + responseData = await gitlabApiRequest.call(this, 'POST', endpoint, body); + } catch (e) { + throw e; + } + + if (responseData.id === undefined) { + // Required data is missing so was not successful + throw new Error('Gitlab webhook creation response did not contain the expected data.'); + } + + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = responseData.id as string; + webhookData.webhookEvents = eventsArray as string[]; + + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId !== undefined) { + const owner = this.getNodeParameter('owner') as string; + const repository = this.getNodeParameter('repository') as string; + const endpoint = `/projects/${owner}%2F${repository}/hooks/${webhookData.webhookId}`; + const body = {}; + + try { + await gitlabApiRequest.call(this, 'DELETE', endpoint, body); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + delete webhookData.webhookEvents; + } + + return true; + }, + }, + }; + + + + async webhook(this: IWebhookFunctions): Promise { + const bodyData = this.getBodyData(); + + const returnData: IDataObject[] = []; + + returnData.push( + { + body: bodyData, + headers: this.getHeaderData(), + query: this.getQueryData(), + } + ); + + return { + workflowData: [ + this.helpers.returnJsonArray(returnData) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Gitlab/gitlab.png b/packages/nodes-base/nodes/Gitlab/gitlab.png new file mode 100644 index 000000000..38ddea94d Binary files /dev/null and b/packages/nodes-base/nodes/Gitlab/gitlab.png differ