Merge pull request #13 from nmetulev/autoLabelPRinProgress

Auto label PR in progress
This commit is contained in:
Nikola Metulev 2018-03-19 19:35:54 -07:00 коммит произвёл GitHub
Родитель f3d2afcbdd b6d90f7509
Коммит 20b93aaf4d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
15 изменённых файлов: 274 добавлений и 23 удалений

Просмотреть файл

@ -0,0 +1,15 @@
{
"bindings": [
{
"webHookType": "github",
"name": "req",
"type": "httpTrigger",
"direction": "in"
},
{
"type": "http",
"direction": "out",
"name": "$return"
}
]
}

Просмотреть файл

@ -0,0 +1,59 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var github_1 = require("../shared/github");
var functions_1 = require("../shared/functions");
var utils_1 = require("../shared/utils");
var firstBlockTitle = '## PR Type';
var labelPRinProgress = 'PR in progress';
module.exports = function (context, req) {
var githubApiHeaders = {
'User-Agent': 'github-bot-uwp-toolkit',
'Authorization': 'token ' + process.env.GITHUB_BOT_UWP_TOOLKIT_ACCESS_TOKEN
};
var repoOwner = process.env.GITHUB_BOT_UWP_TOOLKIT_REPO_OWNER;
var repoName = process.env.GITHUB_BOT_UWP_TOOLKIT_REPO_NAME;
var pullRequestNumber = req.number;
github_1.getPullRequest(githubApiHeaders, repoOwner, repoName, pullRequestNumber, function (pullRequest) {
var creationMessage = pullRequest.body;
var firstBlockOfCreationMessage = creationMessage.split(firstBlockTitle)[0];
if (firstBlockOfCreationMessage) {
var linkedItemsNumbers = utils_1.distinct(functions_1.searchLinkedItemsNumbersInComment(firstBlockOfCreationMessage));
github_1.getIssueOrPullRequestLinks(githubApiHeaders, repoOwner, repoName, linkedItemsNumbers, function (results) {
var issuesNumber = results
.filter(function (r) { return r.__typename === 'Issue'; })
.map(function (r) { return r.__typename === 'Issue' ? r.number : null; })
.filter(function (n) { return !!n; });
if (issuesNumber.length <= 0) {
context.log('linked items are not issues');
functions_1.completeFunction(context, req, { status: 201, body: { success: false, message: 'Linked items are not issues.' } });
return;
}
if (process.env.GITHUB_BOT_UWP_TOOLKIT_ACTIVATE_MUTATION) {
github_1.getIssuesLabels(githubApiHeaders, repoOwner, repoName, issuesNumber, function (issuesWithLabels) {
if (req.action === 'closed') {
var issuesWithLabelsWithExpectedLabel = issuesWithLabels.filter(function (iwl) { return iwl.labels.some(function (label) { return label === labelPRinProgress; }); });
issuesWithLabelsWithExpectedLabel.map(function (issueWithLabels) {
var labels = utils_1.distinct(issueWithLabels.labels.filter(function (label) { return label !== labelPRinProgress; }));
github_1.setLabelsForIssue(githubApiHeaders, repoOwner, repoName, issueWithLabels.number, labels);
});
}
if (req.action === 'opened' || req.action === 'reopened') {
var issuesWithLabelsWithoutExpectedLabel = issuesWithLabels.filter(function (iwl) { return iwl.labels.every(function (label) { return label !== labelPRinProgress; }); });
issuesWithLabelsWithoutExpectedLabel.map(function (issueWithLabels) {
var labels = utils_1.distinct(issueWithLabels.labels.concat([labelPRinProgress]));
github_1.setLabelsForIssue(githubApiHeaders, repoOwner, repoName, issueWithLabels.number, labels);
});
}
});
}
context.log(issuesNumber);
functions_1.completeFunction(context, req, { status: 201, body: { success: true, message: issuesNumber } });
});
}
else {
context.log('no linked issues');
functions_1.completeFunction(context, req, { status: 201, body: { success: false, message: 'No linked issues.' } });
}
});
};
//# sourceMappingURL=index.js.map

Просмотреть файл

@ -0,0 +1,78 @@
import { getPullRequest, getIssueOrPullRequestLinks, setLabelsForIssue, getIssuesLabels } from '../shared/github';
import { searchLinkedItemsNumbersInComment, completeFunction } from '../shared/functions';
import { distinct } from '../shared/utils';
const firstBlockTitle = '## PR Type';
const labelPRinProgress = 'PR in progress';
module.exports = (context, req) => {
const githubApiHeaders = {
'User-Agent': 'github-bot-uwp-toolkit',
'Authorization': 'token ' + process.env.GITHUB_BOT_UWP_TOOLKIT_ACCESS_TOKEN
};
const repoOwner = process.env.GITHUB_BOT_UWP_TOOLKIT_REPO_OWNER;
const repoName = process.env.GITHUB_BOT_UWP_TOOLKIT_REPO_NAME;
const pullRequestNumber: number = req.number;
getPullRequest(
githubApiHeaders,
repoOwner,
repoName,
pullRequestNumber,
(pullRequest) => {
// retrieve first block of creation block where user puts the linked issues
const creationMessage = pullRequest.body;
const firstBlockOfCreationMessage = creationMessage.split(firstBlockTitle)[0];
if (firstBlockOfCreationMessage) {
const linkedItemsNumbers = distinct(searchLinkedItemsNumbersInComment(firstBlockOfCreationMessage));
getIssueOrPullRequestLinks(githubApiHeaders, repoOwner, repoName, linkedItemsNumbers, (results) => {
const issuesNumber = results
.filter(r => r.__typename === 'Issue')
.map(r => r.__typename === 'Issue' ? r.number : null)
.filter(n => !!n);
if (issuesNumber.length <= 0) {
context.log('linked items are not issues');
completeFunction(context, req, { status: 201, body: { success: false, message: 'Linked items are not issues.' } });
return;
}
if (process.env.GITHUB_BOT_UWP_TOOLKIT_ACTIVATE_MUTATION) {
getIssuesLabels(githubApiHeaders, repoOwner, repoName, issuesNumber, (issuesWithLabels) => {
if (req.action === 'closed') {
// filter issues which DOES already contain the label
const issuesWithLabelsWithExpectedLabel =
issuesWithLabels.filter(iwl => iwl.labels.some(label => label === labelPRinProgress));
// remove label 'PR in progress'
issuesWithLabelsWithExpectedLabel.map(issueWithLabels => {
const labels = distinct(issueWithLabels.labels.filter(label => label !== labelPRinProgress));
setLabelsForIssue(githubApiHeaders, repoOwner, repoName, issueWithLabels.number, labels);
});
}
if (req.action === 'opened' || req.action === 'reopened') {
// filter issues which does NOT already contain the label
const issuesWithLabelsWithoutExpectedLabel =
issuesWithLabels.filter(iwl => iwl.labels.every(label => label !== labelPRinProgress));
// add label 'PR in progress'
issuesWithLabelsWithoutExpectedLabel.map(issueWithLabels => {
const labels = distinct(issueWithLabels.labels.concat([labelPRinProgress]));
setLabelsForIssue(githubApiHeaders, repoOwner, repoName, issueWithLabels.number, labels);
});
}
})
}
context.log(issuesNumber);
completeFunction(context, req, { status: 201, body: { success: true, message: issuesNumber } });
});
} else {
context.log('no linked issues');
completeFunction(context, req, { status: 201, body: { success: false, message: 'No linked issues.' } });
}
});
};

Просмотреть файл

@ -0,0 +1,5 @@
{
"name": "auto-label-pr-in-progress",
"version": "0.0.1",
"description": "Automatically add/remove label PR in progress on issues linked to a PR"
}

Просмотреть файл

@ -26,6 +26,11 @@ Then, using the `pull_request` it will detect the linked issues that are not clo
This function detects issues with `pending-uservoice-creation` label. This function detects issues with `pending-uservoice-creation` label.
### autoLabelPRinProgress
This function listens a GitHub webhook event when a PR is created, closed, reopened or merged.
Then, it will detect the linked issues to this PR and update the `labels` of each of these issues by adding/removing the `PR in progress` label.
## How to use? ## How to use?
1. First, build the project using `tsc` command line. 1. First, build the project using `tsc` command line.

Просмотреть файл

@ -16,4 +16,11 @@ exports.completeFunctionBySendingMail = function (context, personalizations, mai
content: content content: content
}); });
}; };
exports.searchLinkedItemsNumbersInComment = function (message) {
var matches = message.match(/[#][0-9]+/g);
if (matches) {
return matches.map(function (m) { return parseInt(m.trim().substr(1)); });
}
return [];
};
//# sourceMappingURL=functions.js.map //# sourceMappingURL=functions.js.map

Просмотреть файл

@ -13,4 +13,13 @@ export const completeFunctionBySendingMail = (context: any, personalizations: an
subject: subject, subject: subject,
content: content content: content
}); });
}
export const searchLinkedItemsNumbersInComment = (message: string): number[] => {
const matches = message.match(/[#][0-9]+/g);
if (matches) {
return matches.map(m => parseInt(m.trim().substr(1)));
}
return [];
} }

Просмотреть файл

@ -88,6 +88,26 @@ exports.getAllMilestones = function (headers, repoOwner, repoName, callback) {
var getAllMilestonesQuery = function (repoOwner, repoName) { var getAllMilestonesQuery = function (repoOwner, repoName) {
return "\n query {\n repository(owner: \"" + repoOwner + "\", name: \"" + repoName + "\") {\n milestones(first: 100) {\n edges {\n node {\n id,\n state,\n dueOn,\n number\n }\n }\n }\n }\n }"; return "\n query {\n repository(owner: \"" + repoOwner + "\", name: \"" + repoName + "\") {\n milestones(first: 100) {\n edges {\n node {\n id,\n state,\n dueOn,\n number\n }\n }\n }\n }\n }";
}; };
exports.getIssuesLabels = function (headers, repoOwner, repoName, numbers, callback) {
performGitHubGraphqlRequest(headers, {
query: getIssuesLabelsQuery(repoOwner, repoName, numbers)
}, function (response) {
var results = numbers
.map(function (n, index) { return ({
number: n,
labels: response.data.repository['result' + index].labels.edges.map(function (edge) { return edge.node.name; })
}); });
callback(results);
});
};
var getIssuesLabelsQuery = function (repoOwner, repoName, numbers) {
var resultList = numbers
.map(function (n, index) {
return "\n result" + index + ": issue(number: " + n + ") {\n labels(first: 100) {\n edges {\n node {\n name\n }\n }\n }\n }";
})
.join(',');
return "\n query {\n repository(owner: \"" + repoOwner + "\", name: \"" + repoName + "\") {\n " + resultList + "\n }\n }";
};
exports.commentGitHubIssue = function (headers, issueId, comment) { exports.commentGitHubIssue = function (headers, issueId, comment) {
performGitHubGraphqlRequest(headers, { performGitHubGraphqlRequest(headers, {
query: commentGitHubIssueMutation(issueId, comment) query: commentGitHubIssueMutation(issueId, comment)
@ -112,4 +132,9 @@ exports.closeGitHubIssue = function (headers, owner, repo, issueNumber, issueId)
var closeGitHubIssueMutation = function (issueId) { var closeGitHubIssueMutation = function (issueId) {
return "\n mutation {\n closeIssue(input: { subjectId: \"" + issueId + "\" }) {\n subject {\n id\n }\n }\n }"; return "\n mutation {\n closeIssue(input: { subjectId: \"" + issueId + "\" }) {\n subject {\n id\n }\n }\n }";
}; };
exports.setLabelsForIssue = function (headers, owner, repo, issueNumber, labels) {
performGitHubRestRequest(headers, "/repos/" + owner + "/" + repo + "/issues/" + issueNumber, 'PATCH', {
labels: labels
});
};
//# sourceMappingURL=github.js.map //# sourceMappingURL=github.js.map

Просмотреть файл

@ -1,5 +1,5 @@
import { performHttpRequest } from './http'; import { performHttpRequest } from './http';
import { IssueNode, PullRequestNode, IssueOrPullRequestLinkNode, Milestone } from './models'; import { IssueNode, PullRequestNode, IssueOrPullRequestLinkNode, Milestone, IssueWithLabels } from './models';
// private functions // private functions
@ -178,7 +178,7 @@ const getIssueOrPullRequestLinksQuery = (repoOwner: string, repoName: string, nu
}`; }`;
} }
export const getAllMilestones = (headers: any, repoOwner: string, repoName: string, callback: (milestones: Milestone[]) => any) => { export const getAllMilestones = (headers: any, repoOwner: string, repoName: string, callback: (milestones: Milestone[]) => any) => {
performGitHubGraphqlRequest(headers, { performGitHubGraphqlRequest(headers, {
query: getAllMilestonesQuery(repoOwner, repoName) query: getAllMilestonesQuery(repoOwner, repoName)
}, (response) => { }, (response) => {
@ -203,6 +203,42 @@ const getAllMilestonesQuery = (repoOwner: string, repoName: string) => {
}`; }`;
} }
export const getIssuesLabels = (headers: any, repoOwner: string, repoName: string, numbers: number[], callback: (issuesWithLabels: IssueWithLabels[]) => any) => {
performGitHubGraphqlRequest(headers, {
query: getIssuesLabelsQuery(repoOwner, repoName, numbers)
}, (response) => {
const results = numbers
.map((n, index) => ({
number: n,
labels: response.data.repository['result' + index].labels.edges.map(edge => edge.node.name)
}));
callback(results);
});
}
const getIssuesLabelsQuery = (repoOwner: string, repoName: string, numbers: number[]) => {
const resultList = numbers
.map((n, index) => {
return `
result${index}: issue(number: ${n}) {
labels(first: 100) {
edges {
node {
name
}
}
}
}`;
})
.join(',');
return `
query {
repository(owner: "${repoOwner}", name: "${repoName}") {
${resultList}
}
}`;
}
// mutations // mutations
export const commentGitHubIssue = (headers: any, issueId: string, comment: string) => { export const commentGitHubIssue = (headers: any, issueId: string, comment: string) => {
@ -221,7 +257,7 @@ const commentGitHubIssueMutation = (issueId: string, comment: string): string =>
}`; }`;
} }
// this mutation is not currently available - using the REST API // these mutations are not currently available - using the REST API instead
export const closeGitHubIssue = (headers: any, owner: string, repo: string, issueNumber: number, issueId: string) => { export const closeGitHubIssue = (headers: any, owner: string, repo: string, issueNumber: number, issueId: string) => {
const useGraphql = false; const useGraphql = false;
@ -244,4 +280,10 @@ const closeGitHubIssueMutation = (issueId: string): string => {
} }
} }
}`; }`;
}
export const setLabelsForIssue = (headers: any, owner: string, repo: string, issueNumber: number, labels: string[]) => {
performGitHubRestRequest(headers, `/repos/${owner}/${repo}/issues/${issueNumber}`, 'PATCH', {
labels
});
} }

Просмотреть файл

@ -76,4 +76,9 @@ export type Milestone = {
state: 'CLOSED' | 'OPEN'; state: 'CLOSED' | 'OPEN';
dueOn: string; dueOn: string;
number: number; number: number;
}
export type IssueWithLabels = {
number: number;
labels: string[];
} }

Просмотреть файл

@ -1,6 +1,6 @@
// Array utils // Array utils
export const distinct = (array: any[]): any[] => { export const distinct = <T>(array: T[]): T[] => {
return array.filter((x, i, a) => { return array.filter((x, i, a) => {
return a.indexOf(x) === i; return a.indexOf(x) === i;
}); });

Просмотреть файл

@ -0,0 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tests_1 = require("../shared/tests");
var autoLabelPRinProgress = require('../autoLabelPRinProgress');
autoLabelPRinProgress(tests_1.createFakeContext('autoLabelPRinProgress'), {
number: 1824,
action: 'opened'
});
//# sourceMappingURL=autoLabelPRinProgress.js.map

Просмотреть файл

@ -0,0 +1,8 @@
import { createFakeContext } from '../shared/tests';
const autoLabelPRinProgress = require('../autoLabelPRinProgress');
autoLabelPRinProgress(createFakeContext('autoLabelPRinProgress'), {
number: 1824,
action: 'opened'
});

Просмотреть файл

@ -46,20 +46,13 @@ var getLinkedItemsNumbersInPullRequest = function (botUsername, pullRequest) {
}).length > 0; }).length > 0;
if (!hasAlreadyGotTheMessage) { if (!hasAlreadyGotTheMessage) {
var linkedItemsNumbersInComments = pullRequest.comments.edges.map(function (edge) { return edge.node; }) var linkedItemsNumbersInComments = pullRequest.comments.edges.map(function (edge) { return edge.node; })
.map(function (c) { return searchLinkedItemsNumbersInComment(c.body); }) .map(function (c) { return functions_1.searchLinkedItemsNumbersInComment(c.body); })
.reduce(function (a, b) { return a.concat(b); }, []); .reduce(function (a, b) { return a.concat(b); }, []);
var linkedItemsNubmersInBodyMessage = searchLinkedItemsNumbersInComment(pullRequest.body); var linkedItemsNubmersInBodyMessage = functions_1.searchLinkedItemsNumbersInComment(pullRequest.body);
var linkedItemsNumbers = linkedItemsNumbersInComments.concat(linkedItemsNubmersInBodyMessage); var linkedItemsNumbers = linkedItemsNumbersInComments.concat(linkedItemsNubmersInBodyMessage);
var distinctLinkedItemsNumbers = utils_1.distinct(linkedItemsNumbers); var distinctLinkedItemsNumbers = utils_1.distinct(linkedItemsNumbers);
return distinctLinkedItemsNumbers; return distinctLinkedItemsNumbers;
} }
return []; return [];
}; };
var searchLinkedItemsNumbersInComment = function (message) {
var matches = message.match(/[#][0-9]+/g);
if (matches) {
return matches.map(function (m) { return parseInt(m.trim().substr(1)); });
}
return [];
};
//# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map

Просмотреть файл

@ -1,5 +1,5 @@
import { distinct } from '../shared/utils'; import { distinct } from '../shared/utils';
import { completeFunction } from '../shared/functions'; import { completeFunction, searchLinkedItemsNumbersInComment } from '../shared/functions';
import { PullRequestNode } from '../shared/models'; import { PullRequestNode } from '../shared/models';
import { getPullRequest, getIssueOrPullRequestLinks, commentGitHubIssue } from '../shared/github'; import { getPullRequest, getIssueOrPullRequestLinks, commentGitHubIssue } from '../shared/github';
@ -81,14 +81,5 @@ const getLinkedItemsNumbersInPullRequest = (botUsername: string, pullRequest: Pu
return distinctLinkedItemsNumbers; return distinctLinkedItemsNumbers;
} }
return [];
}
const searchLinkedItemsNumbersInComment = (message: string): number[] => {
const matches = message.match(/[#][0-9]+/g);
if (matches) {
return matches.map(m => parseInt(m.trim().substr(1)));
}
return []; return [];
} }