import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
INodeExecutionData,
INodeTypeDescription,
INodeType,
} from 'n8n-workflow';
import {
githubApiRequest,
getFileSha,
} from './GenericFunctions';
export class Github implements INodeType {
description: INodeTypeDescription = {
displayName: 'Github',
name: 'github',
icon: 'file:github.png',
group: ['input'],
version: 1,
description: 'Retrieve data from Github API.',
defaults: {
name: 'Github',
color: '#665533',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'githubApi',
required: true,
}
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
options: [
{
name: 'Create File',
value: 'createFile',
description: 'Creates a new file in a repository',
},
{
name: 'Create Issue',
value: 'createIssue',
description: 'Creates a new issue',
},
{
name: 'Create Issue Comment',
value: 'createIssueComment',
description: 'Creates a new comment on an issue',
},
{
name: 'Create Release',
value: 'createRelease',
description: 'Creates a new release',
},
{
name: 'Delete File',
value: 'deleteFile',
description: 'Deletes a file',
},
{
name: 'Edit Issue',
value: 'editIssue',
description: 'Edits an existing issue',
},
{
name: 'Get Community Profile',
value: 'getCommunityProfile',
description: 'Get the community profile of a repository with metrics, health score, description, license, ...',
},
{
name: 'Get File',
value: 'getFile',
description: 'Get the data of a file',
},
{
name: 'Get Issue',
value: 'getIssue',
description: 'Get the data of a single issues',
},
{
name: 'Get Repository Issues',
value: 'getRepositoryIssues',
description: 'Returns issues of a repository',
},
{
name: 'Get Repository License',
value: 'getRepositoryLicense',
description: 'Returns the contents of the repository\'s license file, if one is detected',
},
{
name: 'List Popular Paths',
value: 'listPopularPaths',
description: 'Get the top 10 popular content paths over the last 14 days.',
},
{
name: 'List Referrers',
value: 'listReferrers',
description: 'Get the top 10 referrering domains over the last 14 days',
},
{
name: 'List User Repositories',
value: 'listUserRepositories',
description: 'Returns the repositories of a user',
},
{
name: 'Lock Issue',
value: 'lockIssue',
description: 'Lock an issue',
},
{
name: 'Update File',
value: 'updateFile',
description: 'Updates an existing file',
},
],
default: 'createIssue',
description: 'The operation to perform.',
},
{
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,
displayOptions: {
hide: {
operation: [
'listUserRepositories',
],
},
},
placeholder: 'n8n',
description: 'The name of the repsitory.',
},
// ----------------------------------
// createFile / deleteFile / getFile / updateFile
// ----------------------------------
{
displayName: 'File Path',
name: 'filePath',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'createFile',
'deleteFile',
'getFile',
'updateFile',
],
},
},
placeholder: 'docs/README.md',
description: 'The file path of the file. Has to contain the full path.',
},
// ----------------------------------
// createFile / updateFile
// ----------------------------------
{
displayName: 'Binary Data',
name: 'binaryData',
type: 'boolean',
default: false,
required: true,
displayOptions: {
show: {
operation: [
'createFile',
'updateFile',
],
},
},
description: 'If the data to upload should be taken from binary field.',
},
{
displayName: 'File Content',
name: 'fileContent',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'createFile',
'updateFile',
],
binaryData: [
false,
],
},
},
placeholder: '',
description: 'The text content of the file.',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
operation: [
'createFile',
'updateFile',
],
binaryData: [
true,
],
},
},
placeholder: '',
description: 'Name of the binary property which contains
the data for the file.',
},
{
displayName: 'Commit Message',
name: 'commitMessage',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'createFile',
'deleteFile',
'updateFile',
],
},
},
description: 'The commit message.',
},
{
displayName: 'Additional Parameters',
name: 'additionalParameters',
placeholder: 'Add Parameter',
description: 'Additional fields to add.',
type: 'fixedCollection',
default: {},
displayOptions: {
show: {
operation: [
'createFile',
'deleteFile',
'updateFile',
],
},
},
options: [
{
name: 'author',
displayName: 'Author',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'The name of the author of the commit.',
},
{
displayName: 'EMail',
name: 'email',
type: 'string',
default: '',
description: 'The email of the author of the commit.',
},
]
},
{
name: 'branch',
displayName: 'Branch',
values: [
{
displayName: 'Branch',
name: 'branch',
type: 'string',
default: '',
description: 'The branch to commit to. If not set the repository’s default branch (usually master) is used.',
},
]
},
{
name: 'committer',
displayName: 'Committer',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'The name of the committer of the commit.',
},
{
displayName: 'EMail',
name: 'email',
type: 'string',
default: '',
description: 'The email of the committer of the commit.',
},
]
},
],
},
// ----------------------------------
// createIssue
// ----------------------------------
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'createIssue',
],
},
},
description: 'The title of the issue.',
},
{
displayName: 'Body',
name: 'body',
type: 'string',
typeOptions: {
rows: 5,
},
default: '',
displayOptions: {
show: {
operation: [
'createIssue',
],
},
},
description: 'The body of the issue.',
},
{
displayName: 'Labels',
name: 'labels',
type: 'collection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Label',
},
displayOptions: {
show: {
operation: [
'createIssue',
],
},
},
default: { 'label': '' },
options: [
{
displayName: 'Label',
name: 'label',
type: 'string',
default: '',
description: 'Label to add to issue.',
},
],
},
{
displayName: 'Assignees',
name: 'assignees',
type: 'collection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Assignee',
},
displayOptions: {
show: {
operation: [
'createIssue',
],
},
},
default: { 'assignee': '' },
options: [
{
displayName: 'Assignee',
name: 'assignee',
type: 'string',
default: '',
description: 'User to assign issue too.',
},
],
},
// ----------------------------------
// createIssueComment
// ----------------------------------
{
displayName: 'Issue Number',
name: 'issueNumber',
type: 'number',
default: 0,
required: true,
displayOptions: {
show: {
operation: [
'createIssueComment',
],
},
},
description: 'The number of the issue on which to create the comment on.',
},
{
displayName: 'Body',
name: 'body',
type: 'string',
typeOptions: {
rows: 5,
},
displayOptions: {
show: {
operation: [
'createIssueComment',
],
},
},
default: '',
description: 'The body of the comment.',
},
// ----------------------------------
// createRelease
// ----------------------------------
{
displayName: 'Tag',
name: 'releaseTag',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
operation: [
'createRelease',
],
},
},
description: 'The tag of the release.',
},
{
displayName: 'Edit Fields',
name: 'createReleaseFields',
type: 'collection',
typeOptions: {
multipleValueButtonText: 'Add Field',
},
displayOptions: {
show: {
operation: [
'createRelease'
],
},
},
default: {},
options: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'The name of the issue.',
},
{
displayName: 'Body',
name: 'body',
type: 'string',
typeOptions: {
rows: 5,
},
default: '',
description: 'The body of the release.',
},
{
displayName: 'Draft',
name: 'draft',
type: 'boolean',
default: false,
description: 'Set "true" to create a draft (unpublished) release, "false" to create a published one.',
},
{
displayName: 'Prerelease',
name: 'prerelease',
type: 'boolean',
default: false,
description: 'If set to "true" it will point out that the release is non-production ready.',
},
{
displayName: 'Target Commitish',
name: 'target_commitish',
type: 'string',
default: '',
description: 'Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Unused if the Git tag already exists. Default: the repository\'s default branch(usually master).',
},
],
},
// ----------------------------------
// editIssue
// ----------------------------------
{
displayName: 'Issue Number',
name: 'issueNumber',
type: 'number',
default: 0,
required: true,
displayOptions: {
show: {
operation: [
'editIssue',
],
},
},
description: 'The number of the issue edit.',
},
{
displayName: 'Edit Fields',
name: 'editFields',
type: 'collection',
typeOptions: {
multipleValueButtonText: 'Add Field',
},
displayOptions: {
show: {
operation: [
'editIssue'
],
},
},
default: {},
options: [
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
description: 'The title of the issue.',
},
{
displayName: 'Body',
name: 'body',
type: 'string',
typeOptions: {
rows: 5,
},
default: '',
description: 'The body of the issue.',
},
{
displayName: 'State',
name: 'state',
type: 'options',
options: [
{
name: 'Closed',
value: 'closed',
description: 'Set the state to "closed"',
},
{
name: 'Open',
value: 'open',
description: 'Set the state to "open"',
},
],
default: 'open',
description: 'The state to set.',
},
{
displayName: 'Labels',
name: 'labels',
type: 'collection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Label',
},
default: { 'label': '' },
options: [
{
displayName: 'Label',
name: 'label',
type: 'string',
default: '',
description: 'Label to add to issue.',
},
],
},
{
displayName: 'Assignees',
name: 'assignees',
type: 'collection',
typeOptions: {
multipleValues: true,
multipleValueButtonText: 'Add Assignee',
},
default: { 'assignee': '' },
options: [
{
displayName: 'Assignees',
name: 'assignee',
type: 'string',
default: '',
description: 'User to assign issue too.',
},
],
},
],
},
// ----------------------------------
// getFile
// ----------------------------------
{
displayName: 'As Binary Property',
name: 'asBinaryProperty',
type: 'boolean',
default: true,
displayOptions: {
show: {
operation: [
'getFile',
],
},
},
description: 'If set it will set the data of the file as binary property
instead of returning the raw API response.',
},
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
default: 'data',
required: true,
displayOptions: {
show: {
operation: [
'getFile',
],
asBinaryProperty: [
true,
],
},
},
placeholder: '',
description: 'Name of the binary property in which to save
the binary data of the received file.',
},
// ----------------------------------
// getIssue
// ----------------------------------
{
displayName: 'Issue Number',
name: 'issueNumber',
type: 'number',
default: 0,
required: true,
displayOptions: {
show: {
operation: [
'getIssue',
],
},
},
description: 'The number of the issue get data of.',
},
// ----------------------------------
// getRepositoryIssues
// ----------------------------------
{
displayName: 'Filters',
name: 'getRepositoryIssuesFilters',
type: 'collection',
typeOptions: {
multipleValueButtonText: 'Add Filter',
},
displayOptions: {
show: {
operation: [
'getRepositoryIssues'
],
},
},
default: {},
options: [
{
displayName: 'Assignee',
name: 'assignee',
type: 'string',
default: '',
description: 'Return only issuse which are assigned to a specific user.',
},
{
displayName: 'Creator',
name: 'creator',
type: 'string',
default: '',
description: 'Return only issuse which were created by a specific user.',
},
{
displayName: 'Mentioned',
name: 'mentioned',
type: 'string',
default: '',
description: 'Return only issuse in which a specific user was mentioned.',
},
{
displayName: 'Labels',
name: 'labels',
type: 'string',
default: '',
description: 'Return only issuse with the given labels. Multiple lables can be separated by comma.',
},
{
displayName: 'Updated Since',
name: 'since',
type: 'dateTime',
default: '',
description: 'Return only issues updated at or after this time.',
},
{
displayName: 'State',
name: 'state',
type: 'options',
options: [
{
name: 'All',
value: 'all',
description: 'Returns issues with any state',
},
{
name: 'Closed',
value: 'closed',
description: 'Return issues with "closed" state',
},
{
name: 'Open',
value: 'open',
description: 'Return issues with "open" state',
},
],
default: 'open',
description: 'The state to set.',
},
{
displayName: 'Sort',
name: 'sort',
type: 'options',
options: [
{
name: 'Created',
value: 'created',
description: 'Sort by created date',
},
{
name: 'Updated',
value: 'updated',
description: 'Sort by updated date',
},
{
name: 'Comments',
value: 'comments',
description: 'Sort by comments',
},
],
default: 'created',
description: 'The order the issues should be returned in.',
},
{
displayName: 'Direction',
name: 'direction',
type: 'options',
options: [
{
name: 'Ascending',
value: 'asc',
description: 'Sort in ascending order',
},
{
name: 'Descending',
value: 'desc',
description: 'Sort in descending order',
},
],
default: 'desc',
description: 'The sort order.',
},
],
},
// ----------------------------------
// lockIssue
// ----------------------------------
{
displayName: 'Issue Number',
name: 'issueNumber',
type: 'number',
default: 0,
required: true,
displayOptions: {
show: {
operation: [
'lockIssue',
],
},
},
description: 'The number of the issue to lock.',
},
{
displayName: 'Lock Reason',
name: 'lockReason',
type: 'options',
displayOptions: {
show: {
operation: [
'lockIssue'
],
},
},
options: [
{
name: 'Off-Topic',
value: 'off-topic',
description: 'The issue is Off-Topic',
},
{
name: 'Too Heated',
value: 'too heated',
description: 'The discussion is too heated',
},
{
name: 'Resolved',
value: 'resolved',
description: 'The issue got resolved',
},
{
name: 'Spam',
value: 'spam',
description: 'The issue is spam',
},
],
default: 'resolved',
description: 'The reason to lock the issue.',
},
],
};
async execute(this: IExecuteFunctions): Promise {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const credentials = this.getCredentials('githubApi');
if (credentials === undefined) {
throw new Error('No credentials got returned!');
}
// Operations which overwrite the returned data
const overwriteDataOperations = [
'createFile',
'createIssue',
'createIssueComment',
'createRelease',
'deleteFile',
'editIssue',
'getCommunityProfile',
'getFile',
'getIssue',
'getRepositoryLicense',
'updateFile',
];
// Operations which overwrite the returned data and return arrays
// and has so to be merged with the data of other items
const overwriteDataOperationsArray = [
'getRepositoryIssues',
'listPopularPaths',
'listReferrers',
'listUserRepositories',
];
// For Post
let body: IDataObject;
// For Query string
let qs: IDataObject;
let requestMethod: string;
let endpoint: string;
const operation = this.getNodeParameter('operation', 0) as string;
for (let i = 0; i < items.length; i++) {
// Reset all values
requestMethod = 'GET';
endpoint = '';
body = {};
qs = {};
// Request the parameters which almost all operations need
const owner = this.getNodeParameter('owner', i) as string;
let repository = '';
if (operation !== 'listUserRepositories') {
repository = this.getNodeParameter('repository', i) as string;
}
if (['createFile', 'updateFile'].includes(operation)) {
requestMethod = 'PUT';
const filePath = this.getNodeParameter('filePath', i) as string;
const additionalParameters = this.getNodeParameter('additionalParameters', i, {}) as IDataObject;
if (additionalParameters.author) {
body.author = additionalParameters.author;
}
if (additionalParameters.committer) {
body.committer = additionalParameters.committer;
}
if (additionalParameters.branch && (additionalParameters.branch as IDataObject).branch) {
body.branch = (additionalParameters.branch as IDataObject).branch;
}
if (operation === 'updateFile') {
// If the file should be updated the request has to contain the SHA
// of the file which gets replaced.
body.sha = await getFileSha.call(this, owner, repository, filePath, body.branch as string | undefined);
}
body.message = this.getNodeParameter('commitMessage', i) as string;
if (this.getNodeParameter('binaryData', i) === true) {
// Is binary file to upload
const item = items[i];
if (item.binary === undefined) {
throw new Error('No binary data exists on item!');
}
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
if (item.binary[binaryPropertyName] === undefined) {
throw new Error(`No binary data property "${binaryPropertyName}" does not exists on item!`);
}
// Currently internally n8n uses base64 and also Github expects it base64 encoded.
// If that ever changes the data has to get converted here.
body.content = item.binary[binaryPropertyName].data;
} else {
// Is text file
// body.content = Buffer.from(this.getNodeParameter('fileContent', i) as string, 'base64');
body.content = Buffer.from(this.getNodeParameter('fileContent', i) as string).toString('base64');
}
endpoint = `/repos/${owner}/${repository}/contents/${encodeURI(filePath)}`;
} else if (operation === 'createIssue') {
requestMethod = 'POST';
body.title = this.getNodeParameter('title', i) as string;
body.body = this.getNodeParameter('body', i) as string;
const labels = this.getNodeParameter('labels', i) as IDataObject[];
const assignees = this.getNodeParameter('assignees', i) as IDataObject[];
body.labels = labels.map((data) => data['label']);
body.assignees = assignees.map((data) => data['assignee']);
endpoint = `/repos/${owner}/${repository}/issues`;
} else if (operation === 'createIssueComment') {
requestMethod = 'POST';
const issueNumber = this.getNodeParameter('issueNumber', i) as string;
body.body = this.getNodeParameter('body', i) as string;
endpoint = `/repos/${owner}/${repository}/issues/${issueNumber}/comments`;
} else if (operation === 'createRelease') {
requestMethod = 'POST';
body = this.getNodeParameter('createReleaseFields', i, {}) as IDataObject;
body.tag_name = this.getNodeParameter('releaseTag', i) as string;
endpoint = `/repos/${owner}/${repository}/releases`;
} else if (operation === 'deleteFile') {
requestMethod = 'DELETE';
const additionalParameters = this.getNodeParameter('additionalParameters', i, {}) as IDataObject;
if (additionalParameters.author) {
body.author = additionalParameters.author;
}
if (additionalParameters.committer) {
body.committer = additionalParameters.committer;
}
if (additionalParameters.branch && (additionalParameters.branch as IDataObject).branch) {
body.branch = (additionalParameters.branch as IDataObject).branch;
}
const filePath = this.getNodeParameter('filePath', i) as string;
body.message = this.getNodeParameter('commitMessage', i) as string;
body.sha = await getFileSha.call(this, owner, repository, filePath, body.branch as string | undefined);
endpoint = `/repos/${owner}/${repository}/contents/${encodeURI(filePath)}`;
} else if (operation === 'editIssue') {
requestMethod = 'PATCH';
const issueNumber = this.getNodeParameter('issueNumber', i) as string;
body = this.getNodeParameter('editFields', i, {}) as IDataObject;
if (body.labels !== undefined) {
body.labels = (body.labels as IDataObject[]).map((data) => data['label']);
}
if (body.assignees !== undefined) {
body.assignees = (body.assignees as IDataObject[]).map((data) => data['assignee']);
}
endpoint = `/repos/${owner}/${repository}/issues/${issueNumber}`;
} else if (operation === 'getFile') {
requestMethod = 'GET';
const filePath = this.getNodeParameter('filePath', i) as string;
endpoint = `/repos/${owner}/${repository}/contents/${encodeURI(filePath)}`;
} else if (operation === 'getIssue') {
requestMethod = 'GET';
const issueNumber = this.getNodeParameter('issueNumber', i) as string;
endpoint = `/repos/${owner}/${repository}/issues/${issueNumber}`;
} else if (operation === 'listPopularPaths') {
requestMethod = 'GET';
endpoint = `/repos/${owner}/${repository}/traffic/popular/paths`;
} else if (operation === 'listReferrers') {
requestMethod = 'GET';
endpoint = `/repos/${owner}/${repository}/traffic/popular/referrers`;
} else if (operation === 'getRepositoryLicense') {
requestMethod = 'GET';
endpoint = `/repos/${owner}/${repository}/license`;
} else if (operation === 'getRepositoryIssues') {
requestMethod = 'GET';
qs = this.getNodeParameter('getRepositoryIssuesFilters', i) as IDataObject;
endpoint = `/repos/${owner}/${repository}/issues`;
} else if (operation === 'listUserRepositories') {
requestMethod = 'GET';
endpoint = `/users/${owner}/repos`;
} else if (operation === 'lockIssue') {
requestMethod = 'PUT';
const issueNumber = this.getNodeParameter('issueNumber', i) as string;
qs.lock_reason = this.getNodeParameter('lockReason', i) as string;
endpoint = `/repos/${owner}/${repository}/issues/${issueNumber}/lock`;
}
const responseData = await githubApiRequest.call(this, requestMethod, endpoint, body, qs);
if (operation === 'getFile') {
const asBinaryProperty = this.getNodeParameter('asBinaryProperty', i);
if (asBinaryProperty === true) {
// Add the returned data to the item as binary property
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', i) as string;
if (items[i].binary === undefined) {
items[i].binary = {};
}
items[i].binary![binaryPropertyName] = await this.helpers.prepareBinaryData(Buffer.from(responseData.content, 'base64'), responseData.path);
return this.prepareOutputData(items);
}
}
if (overwriteDataOperations.includes(operation)) {
returnData.push(responseData);
} else if (overwriteDataOperationsArray.includes(operation)) {
returnData.push.apply(returnData, responseData);
}
}
if (overwriteDataOperations.includes(operation) || overwriteDataOperationsArray.includes(operation)) {
// Return data gets replaced
return [this.helpers.returnJsonArray(returnData)];
} else {
// For all other ones simply return the unchanged items
return this.prepareOutputData(items);
}
}
}