Merge pull request #13 from nmetulev/autoLabelPRinProgress
Auto label PR in progress
This commit is contained in:
Коммит
20b93aaf4d
|
@ -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.
|
||||
|
||||
### 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?
|
||||
|
||||
1. First, build the project using `tsc` command line.
|
||||
|
|
|
@ -16,4 +16,11 @@ exports.completeFunctionBySendingMail = function (context, personalizations, mai
|
|||
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
|
|
@ -13,4 +13,13 @@ export const completeFunctionBySendingMail = (context: any, personalizations: an
|
|||
subject: subject,
|
||||
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) {
|
||||
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) {
|
||||
performGitHubGraphqlRequest(headers, {
|
||||
query: commentGitHubIssueMutation(issueId, comment)
|
||||
|
@ -112,4 +132,9 @@ exports.closeGitHubIssue = function (headers, owner, repo, issueNumber, issueId)
|
|||
var closeGitHubIssueMutation = function (issueId) {
|
||||
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
|
|
@ -1,5 +1,5 @@
|
|||
import { performHttpRequest } from './http';
|
||||
import { IssueNode, PullRequestNode, IssueOrPullRequestLinkNode, Milestone } from './models';
|
||||
import { IssueNode, PullRequestNode, IssueOrPullRequestLinkNode, Milestone, IssueWithLabels } from './models';
|
||||
|
||||
// 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, {
|
||||
query: getAllMilestonesQuery(repoOwner, repoName)
|
||||
}, (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
|
||||
|
||||
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) => {
|
||||
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';
|
||||
dueOn: string;
|
||||
number: number;
|
||||
}
|
||||
|
||||
export type IssueWithLabels = {
|
||||
number: number;
|
||||
labels: string[];
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
// Array utils
|
||||
|
||||
export const distinct = (array: any[]): any[] => {
|
||||
export const distinct = <T>(array: T[]): T[] => {
|
||||
return array.filter((x, i, a) => {
|
||||
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;
|
||||
if (!hasAlreadyGotTheMessage) {
|
||||
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); }, []);
|
||||
var linkedItemsNubmersInBodyMessage = searchLinkedItemsNumbersInComment(pullRequest.body);
|
||||
var linkedItemsNubmersInBodyMessage = functions_1.searchLinkedItemsNumbersInComment(pullRequest.body);
|
||||
var linkedItemsNumbers = linkedItemsNumbersInComments.concat(linkedItemsNubmersInBodyMessage);
|
||||
var distinctLinkedItemsNumbers = utils_1.distinct(linkedItemsNumbers);
|
||||
return distinctLinkedItemsNumbers;
|
||||
}
|
||||
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
|
|
@ -1,5 +1,5 @@
|
|||
import { distinct } from '../shared/utils';
|
||||
import { completeFunction } from '../shared/functions';
|
||||
import { completeFunction, searchLinkedItemsNumbersInComment } from '../shared/functions';
|
||||
import { PullRequestNode } from '../shared/models';
|
||||
import { getPullRequest, getIssueOrPullRequestLinks, commentGitHubIssue } from '../shared/github';
|
||||
|
||||
|
@ -81,14 +81,5 @@ const getLinkedItemsNumbersInPullRequest = (botUsername: string, pullRequest: Pu
|
|||
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 [];
|
||||
}
|
Загрузка…
Ссылка в новой задаче