This commit is contained in:
Odonno 2018-03-20 20:48:34 +01:00
Родитель b5416843c3 20b93aaf4d
Коммит 74fc3b809b
15 изменённых файлов: 273 добавлений и 22 удалений

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

@ -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"
}

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

@ -32,6 +32,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.

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

@ -23,4 +23,11 @@ exports.containsExclusiveLabels = function (rootNode, exclusiveLabels) {
return exclusiveLabels.some(function (l) { return l === label.name; }); return exclusiveLabels.some(function (l) { return l === label.name; });
}); });
}; };
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

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

@ -24,3 +24,12 @@ export const containsExclusiveLabels = (rootNode: IssueNode | PullRequest, exclu
return exclusiveLabels.some(l => l === label.name); return exclusiveLabels.some(l => l === label.name);
}); });
} }
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 [];
}

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

@ -98,6 +98,26 @@ exports.getAllOpenPullRequests = function (headers, repoOwner, repoName, callbac
var getAllOpenPullRequestsQuery = function (repoOwner, repoName) { var getAllOpenPullRequestsQuery = function (repoOwner, repoName) {
return "\n query { \n repository(owner: \"" + repoOwner + "\", name: \"" + repoName + "\") { \n pullRequests(states: [OPEN], first: 100) {\n edges {\n node {\n id,\n number,\n author {\n login\n },\n createdAt,\n comments {\n totalCount\n },\n lastComment: comments(last: 1) {\n edges {\n node {\n updatedAt\n }\n }\n },\n lastTwoComments: comments(last: 2) {\n edges {\n node {\n author {\n login\n },\n body\n }\n }\n },\n labels(first: 10) {\n edges {\n node {\n name\n }\n }\n },\n milestone {\n number\n }\n }\n }\n }\n }\n }"; return "\n query { \n repository(owner: \"" + repoOwner + "\", name: \"" + repoName + "\") { \n pullRequests(states: [OPEN], first: 100) {\n edges {\n node {\n id,\n number,\n author {\n login\n },\n createdAt,\n comments {\n totalCount\n },\n lastComment: comments(last: 1) {\n edges {\n node {\n updatedAt\n }\n }\n },\n lastTwoComments: comments(last: 2) {\n edges {\n node {\n author {\n login\n },\n body\n }\n }\n },\n labels(first: 10) {\n edges {\n node {\n name\n }\n }\n },\n milestone {\n number\n }\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: addGitHubCommentMutation(issueId, comment) query: addGitHubCommentMutation(issueId, comment)
@ -127,4 +147,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, PullRequest } from './models'; import { IssueNode, PullRequestNode, IssueOrPullRequestLinkNode, Milestone, PullRequest, IssueWithLabels } from './models';
// private functions // private functions
@ -260,6 +260,42 @@ const getAllOpenPullRequestsQuery = (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) => {
@ -283,7 +319,7 @@ const addGitHubCommentMutation = (subjectId: 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;
@ -307,3 +343,9 @@ 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
});
}

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

@ -116,3 +116,8 @@ export type PullRequest = {
}; };
milestone: MilestoneWithNumberAndState | undefined; milestone: MilestoneWithNumberAndState | undefined;
} }
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';
@ -83,12 +83,3 @@ const getLinkedItemsNumbersInPullRequest = (botUsername: string, pullRequest: Pu
return []; 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 [];
}