refactor: adjust code to updated dependencies

This commit is contained in:
Karl-Philipp Wulfert
2019-06-03 12:32:35 +02:00
parent 2ec80fbb2f
commit ce58450c54
9 changed files with 363 additions and 184 deletions

123
package-lock.json generated
View File

@@ -22,14 +22,6 @@
"chalk": "^2.0.0",
"esutils": "^2.0.2",
"js-tokens": "^4.0.0"
},
"dependencies": {
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
}
}
},
"@babel/runtime": {
@@ -47,33 +39,62 @@
"integrity": "sha512-PbDyjVme3HR8CrMI04SokU97Enq/+txP5fS2O0XYVSmMYteJ7Q9CLO2y0t8PmNZkt4YCxmHgaNEdMs+/Ki+PAA=="
},
"@openstapps/configuration": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/@openstapps/configuration/-/configuration-0.16.1.tgz",
"integrity": "sha512-GEYYfL0do3jikl2UyfvNdGJoQZGeo9sCYkfDrCsOYDZNxuHkHq5fzOPx8OJtMLblNzLgN65tiW+JPRWw6wTwKg==",
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/@openstapps/configuration/-/configuration-0.18.0.tgz",
"integrity": "sha512-Ufi3jzCozVqCymNeaeRzuOHO2Yd5qXJ10uF4xNHk6Q4LFD9NAMMBkYbawkjmecZoNR+Llqs4AnwSxIkuEAxcxA==",
"dev": true,
"requires": {
"@types/node": "10.14.4",
"@types/node": "10.14.7",
"@types/semver": "6.0.0",
"@types/yaml": "1.0.2",
"chalk": "2.4.2",
"commander": "2.20.0",
"semver": "6.0.0",
"semver": "6.1.0",
"tslint": "5.16.0",
"tslint-eslint-rules": "5.4.0",
"yaml": "1.5.0"
"yaml": "1.6.0"
},
"dependencies": {
"@types/node": {
"version": "10.14.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.4.tgz",
"integrity": "sha512-DT25xX/YgyPKiHFOpNuANIQIVvYEwCWXgK2jYYwqgaMrYE6+tq+DtmMwlD3drl6DJbUwtlIDnn0d7tIn/EbXBg==",
"version": "10.14.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.7.tgz",
"integrity": "sha512-on4MmIDgHXiuJDELPk1NFaKVUxxCFr37tm8E9yN6rAiF5Pzp/9bBfBHkoexqRiY+hk/Z04EJU9kKEb59YqJ82A==",
"dev": true
},
"semver": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz",
"integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.1.0.tgz",
"integrity": "sha512-kCqEOOHoBcFs/2Ccuk4Xarm/KiWRSLEX9CAZF8xkJ6ZPlIoTZ8V5f7J16vYLJqDbR7KrxTJpR2lqjIEm2Qx9cQ==",
"dev": true
},
"tslint": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-5.16.0.tgz",
"integrity": "sha512-UxG2yNxJ5pgGwmMzPMYh/CCnCnh0HfPgtlVRDs1ykZklufFBL1ZoTlWFRz2NQjcoEiDoRp+JyT0lhBbbH/obyA==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"builtin-modules": "^1.1.1",
"chalk": "^2.3.0",
"commander": "^2.12.1",
"diff": "^3.2.0",
"glob": "^7.1.1",
"js-yaml": "^3.13.0",
"minimatch": "^3.0.4",
"mkdirp": "^0.5.1",
"resolve": "^1.3.2",
"semver": "^5.3.0",
"tslib": "^1.8.0",
"tsutils": "^2.29.0"
},
"dependencies": {
"semver": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"dev": true
}
}
}
}
},
@@ -115,26 +136,26 @@
}
},
"@openstapps/logger": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@openstapps/logger/-/logger-0.1.0.tgz",
"integrity": "sha512-5z7Yf3WrzayEVNPp1TBoGiCVgPlQtqzOFh0yQ06gac/vFedWLPLBmENGDdRoEKar8bXzghkxDLy6Rvj/8HEQaQ==",
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@openstapps/logger/-/logger-0.2.1.tgz",
"integrity": "sha512-6+F1nxEBuNTrd3hhBxKnvkH8B84HvB/dVmoMP9Pmv2g3mL3pYJ9l2BBGaACDRA7oUCyLpbNQw+4Kf+VdyzOttw==",
"requires": {
"@types/node": "10.14.6",
"@types/nodemailer": "4.6.8",
"@types/node": "10.14.7",
"@types/nodemailer": "6.1.0",
"chalk": "2.4.2",
"flatted": "2.0.0",
"nodemailer": "6.1.1"
},
"dependencies": {
"@types/node": {
"version": "10.14.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.6.tgz",
"integrity": "sha512-Fvm24+u85lGmV4hT5G++aht2C5I4Z4dYlWZIh62FAfFO/TfzXtPpoLI6I7AuBWkIFqZCnhFOoTT7RjjaIL5Fjg=="
"version": "10.14.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.7.tgz",
"integrity": "sha512-on4MmIDgHXiuJDELPk1NFaKVUxxCFr37tm8E9yN6rAiF5Pzp/9bBfBHkoexqRiY+hk/Z04EJU9kKEb59YqJ82A=="
},
"@types/nodemailer": {
"version": "4.6.8",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-4.6.8.tgz",
"integrity": "sha512-IX1P3bxDP1VIdZf6/kIWYNmSejkYm9MOyMEtoDFi4DVzKjJ3kY4GhOcOAKs6lZRjqVVmF9UjPOZXuQczlpZThw==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.1.0.tgz",
"integrity": "sha512-WysSJ4sGW2Aum1Cs6HFosZdlR3WUzX0XoSLsI53q77gLd4wDfE84OXffZu5/nLIjeKh4SwfTsdrKsgBL9WowMA==",
"requires": {
"@types/node": "*"
}
@@ -314,9 +335,9 @@
"integrity": "sha512-RTVWV485OOf4+nO2+feurk0chzHkSjkjALiejpHltyuMf/13fGymbbNNFrSKdSSUg1TIwzszXdWsVirxgqYiFA=="
},
"@types/node": {
"version": "10.14.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.7.tgz",
"integrity": "sha512-on4MmIDgHXiuJDELPk1NFaKVUxxCFr37tm8E9yN6rAiF5Pzp/9bBfBHkoexqRiY+hk/Z04EJU9kKEb59YqJ82A=="
"version": "10.14.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.14.8.tgz",
"integrity": "sha512-I4+DbJEhLEg4/vIy/2gkWDvXBOOtPKV9EnLhYjMoqxcRW+TTZtUftkHktz/a8suoD5mUL7m6ReLrkPvSsCQQmw=="
},
"@types/nodemailer": {
"version": "4.6.5",
@@ -1535,6 +1556,12 @@
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true
},
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
@@ -2479,9 +2506,9 @@
"dev": true
},
"tslint": {
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-5.16.0.tgz",
"integrity": "sha512-UxG2yNxJ5pgGwmMzPMYh/CCnCnh0HfPgtlVRDs1ykZklufFBL1ZoTlWFRz2NQjcoEiDoRp+JyT0lhBbbH/obyA==",
"version": "5.17.0",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-5.17.0.tgz",
"integrity": "sha512-pflx87WfVoYepTet3xLfDOLDm9Jqi61UXIKePOuca0qoAZyrGWonDG9VTbji58Fy+8gciUn8Bt7y69+KEVjc/w==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
@@ -2490,7 +2517,7 @@
"commander": "^2.12.1",
"diff": "^3.2.0",
"glob": "^7.1.1",
"js-yaml": "^3.13.0",
"js-yaml": "^3.13.1",
"minimatch": "^3.0.4",
"mkdirp": "^0.5.1",
"resolve": "^1.3.2",
@@ -2517,9 +2544,9 @@
"dev": true
},
"tsutils": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.10.0.tgz",
"integrity": "sha512-q20XSMq7jutbGB8luhKKsQldRKWvyBO2BGqni3p4yq8Ys9bEP/xQw3KepKmMRt9gJ4lvQSScrihJrcKdKoSU7Q==",
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.13.0.tgz",
"integrity": "sha512-wRtEjVU8Su72sDIDoqno5Scwt8x4eaF0teKO3m4hu8K1QFPnIZMM88CLafs2tapUeWnY9SwwO3bWeOt2uauBcg==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
@@ -2590,9 +2617,9 @@
"dev": true
},
"typescript": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.5.tgz",
"integrity": "sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw==",
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.1.tgz",
"integrity": "sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw==",
"dev": true
},
"uglify-js": {
@@ -2678,12 +2705,12 @@
"dev": true
},
"yaml": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.5.0.tgz",
"integrity": "sha512-nKxSWOa7vxAP2pikrGxbkZsG/garQseRiLn9mIDjzwoQsyVy7ZWIpLoARejnINGGLA4fttuzRFFNxxbsztdJgw==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.6.0.tgz",
"integrity": "sha512-iZfse3lwrJRoSlfs/9KQ9iIXxs9++RvBFVzAqbbBiFT+giYtyanevreF9r61ZTbGMgWQBxAua3FzJiniiJXWWw==",
"dev": true,
"requires": {
"@babel/runtime": "^7.4.3"
"@babel/runtime": "^7.4.5"
}
},
"yn": {

View File

@@ -12,8 +12,11 @@
"check-configuration": "openstapps-configuration",
"compile": "rimraf lib && tsc && prepend lib/cli.js '#!/usr/bin/env node\n'",
"documentation": "typedoc --includeDeclarations --mode modules --out docs --readme README.md --listInvalidSymbolLinks src",
"postversion": "npm run changelog",
"prepublishOnly": "npm ci && npm run build",
"tslint": "tslint 'src/**/*.ts'"
"preversion": "npm run prepublishOnly",
"push": "git push && git push origin \"v$npm_package_version\"",
"tslint": "tslint -p tsconfig.json -c tslint.json 'src/**/*.ts'"
},
"author": "Karl-Philipp Wulfert <krlwlfrt@gmail.com>",
"contributors": [
@@ -25,25 +28,25 @@
"dependencies": {
"@krlwlfrt/async-pool": "0.1.0",
"@openstapps/gitlab-api": "0.5.1",
"@openstapps/logger": "0.1.0",
"@openstapps/logger": "0.2.1",
"@slack/client": "5.0.1",
"@types/glob": "7.1.1",
"@types/mustache": "0.8.32",
"@types/node": "10.14.7",
"@types/node": "10.14.8",
"commander": "2.20.0",
"glob": "7.1.4",
"moment": "2.24.0",
"mustache": "3.0.1"
},
"devDependencies": {
"@openstapps/configuration": "0.16.1",
"@openstapps/configuration": "0.18.0",
"conventional-changelog-cli": "2.0.21",
"prepend-file-cli": "1.0.6",
"rimraf": "2.6.3",
"ts-node": "8.2.0",
"tslint": "5.16.0",
"tslint": "5.17.0",
"typedoc": "0.14.2",
"typescript": "3.4.5"
"typescript": "3.5.1"
},
"main": "lib/common.js",
"typings": "lib/common.d.ts",

View File

@@ -23,13 +23,14 @@ import {tidy} from './tasks/tidy';
import {unlabel} from './tasks/unlabel';
// add default handler for unhandled rejections
process.on('unhandledRejection', (err) => {
Logger.error('UNHANDLED REJECTION', err.stack);
process.on('unhandledRejection', async (err) => {
await Logger.error('UNHANDLED REJECTION', err.stack);
process.exit(1);
});
// check that environment variable GITLAB_PRIVATE_TOKEN is set
if (typeof process.env.GITLAB_PRIVATE_TOKEN !== 'string' || process.env.GITLAB_PRIVATE_TOKEN.length === 0) {
// tslint:disable-next-line:no-floating-promises
Logger.error('Environment variable GITLAB_PRIVATE_TOKEN is not set!');
process.exit(1);
}
@@ -37,7 +38,8 @@ if (typeof process.env.GITLAB_PRIVATE_TOKEN !== 'string' || process.env.GITLAB_P
const gitlabApi = new Api(GITLAB_API_URL, process.env.GITLAB_PRIVATE_TOKEN as string);
if (existsSync(join(__dirname, 'package.json'))) {
commander.version(JSON.parse(readFileSync(join(__dirname, '..', 'package.json')).toString()).version);
commander.version(JSON.parse(readFileSync(join(__dirname, '..', 'package.json'))
.toString()).version);
}
commander

View File

@@ -18,8 +18,16 @@ import {Group, Project} from '@openstapps/gitlab-api/lib/types';
import {Logger} from '@openstapps/logger';
import {readFile, writeFile} from 'fs';
import {promisify} from 'util';
import {CONCURRENCY} from './configuration';
/**
* Promisified version of readFile
*/
export const readFilePromisified = promisify(readFile);
/**
* Promisified version of writeFile
*/
export const writeFilePromisified = promisify(writeFile);
/**
@@ -29,15 +37,15 @@ export const writeFilePromisified = promisify(writeFile);
* @param groups List of groups
*/
export async function getProjects(api: Api, groups: number[]): Promise<Project[]> {
Logger.info('Fetching all projects for specified groups (' + groups.length + ')...');
Logger.info(`Fetching all projects for specified groups (${groups.length})...`);
const projectResults = await asyncPool(3, groups, (groupId) => {
const projectResults = await asyncPool(CONCURRENCY, groups, async (groupId) => {
return api.getProjectsForGroup(groupId);
});
const projects = flatten2dArray(projectResults);
Logger.log('Fetched ' + projects.length + ' project(s).');
Logger.log(`Fetched ${projects.length} project(s).`);
return projects;
}
@@ -49,8 +57,8 @@ export async function getProjects(api: Api, groups: number[]): Promise<Project[]
* @param groups List of groups
*/
export async function getSubGroups(api: Api, groups: number[]): Promise<Group[]> {
return flatten2dArray(await asyncPool(2, groups, async (groupId) => {
return await api.getSubGroupsForGroup(groupId);
return flatten2dArray(await asyncPool(CONCURRENCY, groups, async (groupId) => {
return api.getSubGroupsForGroup(groupId);
}));
}

View File

@@ -17,21 +17,27 @@ import {Label} from '@openstapps/gitlab-api/lib/types';
/**
* List of schools with their IDs
*/
export const SCHOOLS: { [school: string]: number } = {};
export const SCHOOLS: { [school: string]: number; } = {};
/**
* ID OF openstapps main group
*/
const STAPPS_GROUP_ID = 4088298;
/**
* List of group IDs to fetch issues for
*
* 4088298 is openstapps main group
*/
export const GROUPS: number[] = [4088298].concat(Object.keys(SCHOOLS).map((school) => {
export const GROUPS: number[] = [STAPPS_GROUP_ID]
.concat(Object.keys(SCHOOLS)
.map((school) => {
return SCHOOLS[school];
}));
}),
);
/**
*
*/
export const LABEL_WEIGHTS: any = {
export const LABEL_WEIGHTS: { [key: string]: number; } = {
'bug': 1,
'critical': 2,
};
@@ -67,8 +73,7 @@ export const PROTECTED_BRANCHES = [
/**
* Labels to add to all projects
*/
export const NEEDED_LABELS: Label[] = [
{
export const NEEDED_LABELS: Label[] = [{
color: '#FF0000',
description: 'An error/something that is not working as expected',
name: 'bug',
@@ -154,18 +159,21 @@ export const NEEDED_LABELS: Label[] = [
color: '#428BCA',
description: 'Feedback from the feedback-module of the app',
name: 'user-feedback',
},
].concat(Object.keys(SCHOOLS).map((school) => {
}]
.concat(Object.keys(SCHOOLS)
.map((school) => {
return {
color: '#F0AD4E',
description: 'An issue that specifically applies to this school',
name: `school-${school}`,
};
})).concat(['android', 'iOS', 'web', 'node'].map((platform) => {
}))
.concat(['android', 'iOS', 'web', 'node']
.map((platform) => {
return {
color: '#FFECDB',
description: 'An issue that specifically applies to this platform',
name: 'platform-' + platform,
name: `platform-${platform}`,
};
}));
@@ -178,3 +186,8 @@ export const NOTE_PREFIX = '`openstapps/projectmanagement`';
* Slack channel to post messages to
*/
export const SLACK_CHANNEL = 'C762UG76Z';
/**
* Concurrency for async pool
*/
export const CONCURRENCY = 3;

View File

@@ -24,8 +24,13 @@ import {
} from '@openstapps/gitlab-api/lib/types';
import {Logger} from '@openstapps/logger';
import {WebClient} from '@slack/client';
import {GROUPS, NOTE_PREFIX, SLACK_CHANNEL} from '../configuration';
import {CONCURRENCY, GROUPS, NOTE_PREFIX, SLACK_CHANNEL} from '../configuration';
/**
* Remind people of open merge requests
*
* @param api GitLab API to make requests with
*/
export async function remind(api: Api): Promise<void> {
// get list of open merge requests
const mergeRequests = await api.getMergeRequests(MembershipScope.GROUPS, GROUPS[0], MergeRequestState.OPENED);
@@ -51,10 +56,11 @@ export async function remind(api: Api): Promise<void> {
Logger.info(`Found ${maintainers.length} maintainer(s).`);
await asyncPool(2, mergeRequests, async (mergeRequest) => {
await asyncPool(CONCURRENCY, mergeRequests, async (mergeRequest) => {
// check if merge request is WIP
if (mergeRequest.work_in_progress) {
Logger.info(`Merge request '${mergeRequest.title}' is WIP.`);
return;
}
@@ -67,14 +73,14 @@ export async function remind(api: Api): Promise<void> {
// check if at least one of the discussions is unresolved
const hasUnresolvedDiscussions = discussions.some((discussion) => {
return discussion.notes.some((note) => {
return note.resolvable && !note.resolved;
return note.resolvable && (typeof note.resolved === 'undefined' || !note.resolved);
});
});
if (hasUnresolvedDiscussions) {
let recipient = mergeRequest.author.username;
if (typeof mergeRequest.assignee !== 'undefined') {
if (typeof mergeRequest.assignee !== 'undefined' && mergeRequest.assignee !== null) {
recipient = mergeRequest.assignee.username;
}
@@ -100,11 +106,16 @@ export async function remind(api: Api): Promise<void> {
return true;
}
return approval.approved_by.find((approver: { user: User }) => {
return approval.approved_by.find((approver: {
/**
* Possible approver
*/
user: User;
}) => {
return approver.user.username !== username;
});
})
.map((username) => '@' + username)
.map((username) => `@${username}`)
.join(', ');
// send message to slack
@@ -131,7 +142,7 @@ export async function remind(api: Api): Promise<void> {
// prefix maintainers with '@' and join with commas
const possibleMergers = maintainerUsernames
.map((username) => '@' + username)
.map((username) => `@${username}`)
.join(', ');
// create note in merge request

View File

@@ -21,14 +21,23 @@ import {render} from 'mustache';
import {join, resolve} from 'path';
import {cwd} from 'process';
import {flatten2dArray, getProjects, readFilePromisified, writeFilePromisified} from '../common';
import {BOLD_LABELS, GROUPS, LABEL_WEIGHTS} from '../configuration';
import {BOLD_LABELS, CONCURRENCY, GROUPS, LABEL_WEIGHTS} from '../configuration';
/**
* A structure for template compilation
*/
export interface StructureForTemplate {
/**
* List of issues by assignee
*/
issuesByAssignee: AssigneeWithIssues[];
/**
* Meeting day
*/
meetingDay: string;
/**
* Timestamp, when the report was generated
*/
timestamp: string;
}
@@ -43,12 +52,30 @@ export interface IssuesGroupedByAssigneeId {
* An assignee with assigned issues
*/
export interface AssigneeWithIssues {
/**
* Assignee of the issues
*/
assignee: User;
/**
* Counts of issues
*/
issueCounts: {
/**
* Count of closed issues
*/
closed: number;
/**
* Count of opened issues
*/
opened: number;
};
/**
* List of issues
*/
issues: IssueWithMeta[];
/**
* Quota of closed issues
*/
quota: number;
}
@@ -56,14 +83,38 @@ export interface AssigneeWithIssues {
* Issue with meta information
*/
export interface IssueWithMeta extends Issue {
/**
* Whether or not an issue branch exists
*/
$branchExists: boolean;
/**
* Whether or not the issue is closed
*/
$closed: boolean;
/**
* List of labels
*/
$labels: Array<{
/**
* Whether or not to print the label bold
*/
bold: boolean;
/**
* Actual label
*/
label: string;
}>;
/**
* URL of merge request
*/
$mergeRequestUrl: string;
/**
* Name of project
*/
$project: string;
/**
* Number of weeks the issue is open
*/
$weeksOpen: number;
}
@@ -72,7 +123,13 @@ export interface IssueWithMeta extends Issue {
*/
export interface MergeRequestsForProjects {
[projectId: string]: Array<{
/**
* IID of issue
*/
issue_iid: number;
/**
* URL of issue
*/
web_url: string;
}>;
}
@@ -100,9 +157,11 @@ export function getMergeRequestUrls(projectMergeRequests: MergeRequestsForProjec
return [];
}
return projectMergeRequests[projectId].filter((obj) => {
return projectMergeRequests[projectId]
.filter((obj) => {
return obj.issue_iid === issueIid;
}).map((obj) => {
})
.map((obj) => {
return obj.web_url;
});
}
@@ -115,17 +174,18 @@ export function getMergeRequestUrls(projectMergeRequests: MergeRequestsForProjec
* @param groups List of groups to get issues for
*/
export async function getIssues(api: Api, label: string, groups: number[]): Promise<Issue[]> {
const issueResults = await asyncPool(2, groups, (groupId) => {
const issueResults = await asyncPool(CONCURRENCY, groups, async (groupId) => {
return api.getIssues({
groupId: groupId,
});
});
const issues = flatten2dArray(issueResults).filter((issue) => {
const issues = flatten2dArray(issueResults)
.filter((issue) => {
return issue.labels.indexOf(label) >= 0;
});
Logger.log('Fetched ' + issues.length + ' issue(s).');
Logger.log(`Fetched ${issues.length} issue(s).`);
return issues;
}
@@ -138,18 +198,21 @@ export async function getIssues(api: Api, label: string, groups: number[]): Prom
*/
export async function getIssueBranches(
api: Api,
projects: Project[]): Promise<{ [k: string]: number[] }> {
const projectBranches: { [k: string]: number[] } = {};
projects: Project[]): Promise<{ [k: string]: number[]; }> {
const projectBranches: { [k: string]: number[]; } = {};
await asyncPool(2, projects, async (project) => {
await asyncPool(CONCURRENCY, projects, async (project) => {
const branches = await api.getBranchesForProject(project.id);
// extract issue number from branch
projectBranches[project.id] = branches.map((branch) => {
projectBranches[project.id] = branches
.map((branch) => {
return branch.name.split('-')[0];
}).filter((branchNameStart) => {
})
.filter((branchNameStart) => {
return branchNameStart.match(/^[0-9]+$/);
}).map((branchNameStart) => {
})
.map((branchNameStart) => {
return parseInt(branchNameStart, 10);
});
});
@@ -166,7 +229,7 @@ export async function getIssueBranches(
export async function getIssuesGroupedByAssignees(api: Api, label: string): Promise<AssigneeWithIssues[]> {
const issuesByAssignee: IssuesGroupedByAssigneeId = {};
const groups = flatten2dArray(await asyncPool(2, GROUPS, async (groupId) => {
const groups = flatten2dArray(await asyncPool(CONCURRENCY, GROUPS, async (groupId) => {
return (await api.getSubGroupsForGroup(groupId)).map((group) => {
return group.id;
});
@@ -184,6 +247,7 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
issues.forEach((issue) => {
if (issue.assignee === null) {
Logger.warn('Issue without assignee!', issue.web_url);
return;
}
@@ -217,8 +281,11 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
};
}),
$mergeRequestUrl: getMergeRequestUrls(mergeRequests, issue.project_id, issue.iid)[0],
$project: issue.web_url.replace('https://gitlab.com/', '').split('/issues/')[0],
$weeksOpen: moment().diff(moment(issue.created_at), 'weeks'),
$project: issue.web_url
.replace('https://gitlab.com/', '')
.split('/issues/')[0],
$weeksOpen: moment()
.diff(moment(issue.created_at), 'weeks'),
},
};
@@ -229,25 +296,38 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
});
// calculate quota
Object.keys(issuesByAssignee).forEach((_assigneeId) => {
for (const _assigneeId in issuesByAssignee) {
if (!issuesByAssignee.hasOwnProperty(_assigneeId)) {
continue;
}
const assigneeId = parseInt(_assigneeId, 10);
issuesByAssignee[assigneeId].quota = Math.floor(
issuesByAssignee[assigneeId].issueCounts.closed
/ (issuesByAssignee[assigneeId].issueCounts.opened
// tslint:disable-next-line:no-magic-numbers
+ issuesByAssignee[assigneeId].issueCounts.closed) * 100,
);
});
}
// sort issues by weight of labels and status
Object.keys(issuesByAssignee).forEach((_assigneeId) => {
for (const _assigneeId in issuesByAssignee) {
if (!issuesByAssignee.hasOwnProperty(_assigneeId)) {
continue;
}
const assigneeId = parseInt(_assigneeId, 10);
issuesByAssignee[assigneeId].issues.sort((a, b) => {
let weightA = 0;
let weightB = 0;
Object.keys(LABEL_WEIGHTS).forEach((issueLabel) => {
for (const issueLabel in LABEL_WEIGHTS) {
if (!LABEL_WEIGHTS.hasOwnProperty(issueLabel)) {
continue;
}
if (a.labels.indexOf(issueLabel) >= 0) {
weightA += LABEL_WEIGHTS[issueLabel];
}
@@ -255,19 +335,21 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
if (b.labels.indexOf(issueLabel) >= 0) {
weightB += LABEL_WEIGHTS[issueLabel];
}
});
}
if (a.state === IssueState.CLOSED) {
// tslint:disable-next-line:no-magic-numbers
weightA -= 10;
}
if (b.state === IssueState.CLOSED) {
// tslint:disable-next-line:no-magic-numbers
weightB -= 10;
}
return weightB - weightA;
});
});
}
return Object.values(issuesByAssignee);
}
@@ -280,7 +362,12 @@ export function getNextMeetingDay() {
const now = moment();
// get first wednesday of month
const meetingDayMoment = moment().startOf('month').hour(10).isoWeekday(3);
const meetingDayMoment = moment()
.startOf('month')
// tslint:disable-next-line:no-magic-numbers
.hour(10)
// tslint:disable-next-line:no-magic-numbers
.isoWeekday(3);
while (meetingDayMoment.isBefore(now)) {
// add one week until meeting day is after now
@@ -290,7 +377,7 @@ export function getNextMeetingDay() {
const meetingDay = meetingDayMoment.format('YYYY-MM-DD');
// log found meeting day
Logger.info('Generating report for ' + meetingDay + ' of ' + GROUPS.length + ' group(s)...');
Logger.info(`Generating report for '${meetingDay}' of '${GROUPS.length}' group(s)...`);
return meetingDay;
}
@@ -306,7 +393,7 @@ export async function getMergeRequests(api: Api,
const projectMergeRequests: MergeRequestsForProjects = {};
// iterate over projects
await asyncPool(2, projects, async (project) => {
await asyncPool(CONCURRENCY, projects, async (project) => {
// check if project can have merge requests
if (!project.merge_requests_enabled) {
return;
@@ -316,12 +403,15 @@ export async function getMergeRequests(api: Api,
const mergeRequests = await api.getMergeRequests(MembershipScope.PROJECTS, project.id, MergeRequestState.OPENED);
// extract issue number from merge request
projectMergeRequests[project.id] = mergeRequests.map((mergeRequest) => {
projectMergeRequests[project.id] = mergeRequests
.map((mergeRequest) => {
// keep information about web url too
return {issue_iid: mergeRequest.source_branch.split('-')[0], web_url: mergeRequest.web_url};
}).filter((branchNameStartAndUrl) => {
})
.filter((branchNameStartAndUrl) => {
return branchNameStartAndUrl.issue_iid.match(/^[0-9]+$/);
}).map((branchNameStartAndUrl) => {
})
.map((branchNameStartAndUrl) => {
return {
issue_iid: parseInt(branchNameStartAndUrl.issue_iid, 10),
web_url: branchNameStartAndUrl.web_url,
@@ -345,7 +435,8 @@ export async function generateReport(api: Api, label: string, template: string):
const structureForTemplate: StructureForTemplate = {
issuesByAssignee: issuesGroupedByAssignee,
meetingDay: getNextMeetingDay(),
timestamp: moment().format('LLL'),
timestamp: moment()
.format('LLL'),
};
return render(
@@ -369,7 +460,7 @@ export async function report(api: Api, label: string) {
(await readFilePromisified(resolve(__dirname, '..', '..', 'templates', 'report.md.mustache'))).toString(),
);
let filename = join(cwd(), 'reports', meetingDay + '.md');
let filename = join(cwd(), 'reports', `${meetingDay}.md`);
if (label !== 'meeting') {
filename = join(cwd(), 'reports', `${label}.md`);

View File

@@ -17,7 +17,15 @@ import {Api} from '@openstapps/gitlab-api';
import {AccessLevel, IssueState, MembershipScope, Milestone, Project, Scope} from '@openstapps/gitlab-api/lib/types';
import {Logger} from '@openstapps/logger';
import {flatten2dArray, getProjects} from '../common';
import {GROUPS, NEEDED_LABELS, NEEDED_MILESTONES, NOTE_PREFIX, PROTECTED_BRANCHES, SCHOOLS} from '../configuration';
import {
CONCURRENCY,
GROUPS,
NEEDED_LABELS,
NEEDED_MILESTONES,
NOTE_PREFIX,
PROTECTED_BRANCHES,
SCHOOLS,
} from '../configuration';
/**
* Tidy issues without milestone
@@ -28,7 +36,7 @@ import {GROUPS, NEEDED_LABELS, NEEDED_MILESTONES, NOTE_PREFIX, PROTECTED_BRANCHE
*/
export async function tidyIssuesWithoutMilestone(api: Api): Promise<void> {
// fetch issues without milestone from all groups
const issueResults = await asyncPool(3, GROUPS, (groupId) => {
const issueResults = await asyncPool(CONCURRENCY, GROUPS, async (groupId) => {
return api.getIssues({
groupId: groupId,
milestone: 'No Milestone',
@@ -39,11 +47,11 @@ export async function tidyIssuesWithoutMilestone(api: Api): Promise<void> {
// flatten structure, e.g. put all issues in one array
const issuesWithoutMilestone = flatten2dArray(issueResults);
Logger.info('Found `' + issuesWithoutMilestone.length + '` issue(s) without milestone.');
Logger.info(`Found '${issuesWithoutMilestone.length}' issue(s) without milestone.`);
const milestoneCache: { [s: number]: Milestone[] } = {};
const milestoneCache: { [s: number]: Milestone[]; } = {};
await asyncPool(5, issuesWithoutMilestone, async (issue) => {
await asyncPool(CONCURRENCY, issuesWithoutMilestone, async (issue) => {
if (typeof milestoneCache[issue.project_id] === 'undefined') {
milestoneCache[issue.project_id] = await api.getMilestonesForProject(issue.project_id);
}
@@ -57,19 +65,20 @@ export async function tidyIssuesWithoutMilestone(api: Api): Promise<void> {
});
if (milestoneId === null) {
Logger.warn('Milestone `Meeting` was not available for issue ' + issue.title + ' (' + issue.web_url + ').');
Logger.warn(`Milestone 'Meeting' was not available for issue ${issue.title} (${issue.web_url}).`);
return;
}
await api.setMilestoneForIssue(issue, milestoneId);
Logger.log('Milestone was set to `Meeting` for issue ' + issue.title + ' (' + issue.web_url + ').');
Logger.log(`Milestone was set to 'Meeting' for issue ${issue.title} (${issue.web_url})`);
await api.createNote(
issue.project_id,
Scope.ISSUES,
issue.iid,
`${NOTE_PREFIX} Milestone was set automatically to \`Meeting\`.`,
`${NOTE_PREFIX} Milestone was set automatically to 'Meeting'.`,
);
});
@@ -85,7 +94,7 @@ export async function tidyIssuesWithoutMilestone(api: Api): Promise<void> {
*/
export async function tidyOpenIssuesWithoutMeetingLabel(api: Api): Promise<void> {
// fetch all open issues
const issueResults = await asyncPool(3, GROUPS, (groupId) => {
const issueResults = await asyncPool(CONCURRENCY, GROUPS, async (groupId) => {
return api.getIssues({
groupId: groupId,
state: IssueState.OPENED,
@@ -104,9 +113,10 @@ export async function tidyOpenIssuesWithoutMeetingLabel(api: Api): Promise<void>
Logger.info(`Filtered ${openIssuesWithoutMeetingLabel.length} open issue(s) without label 'meeting'.`);
await asyncPool(5, openIssuesWithoutMeetingLabel, async (issue) => {
await asyncPool(CONCURRENCY, openIssuesWithoutMeetingLabel, async (issue) => {
if (issue.milestone !== null && issue.milestone.title === 'Backlog') {
Logger.info(`Skipping issue "${issue.title}" because it is in backlog.`);
return;
}
@@ -127,7 +137,7 @@ export async function tidyOpenIssuesWithoutMeetingLabel(api: Api): Promise<void>
* @param projects List of projects to tidy labels on
*/
export async function tidyLabels(api: Api, projects: Project[]): Promise<void> {
await asyncPool(5, projects, async (project) => {
await asyncPool(CONCURRENCY, projects, async (project) => {
const labels = await api.getLabels(project.id);
const neededLabels = NEEDED_LABELS.slice(0);
@@ -148,10 +158,10 @@ export async function tidyLabels(api: Api, projects: Project[]): Promise<void> {
} */
});
await asyncPool(2, neededLabels, async (neededLabel) => {
await asyncPool(CONCURRENCY, neededLabels, async (neededLabel) => {
await api.createLabel(project.id, neededLabel.name, neededLabel.description, neededLabel.color);
Logger.log('Created label `' + neededLabel.name + '` in ' + project.name_with_namespace + '.');
Logger.log(`Created label '${neededLabel.name}' in '${project.name_with_namespace}'.`);
});
// await asyncPool(2, extraneousLabels, async (extraneousLabel) => {
@@ -171,7 +181,7 @@ export async function tidyLabels(api: Api, projects: Project[]): Promise<void> {
* @param projects List of projects to tidy milestones on
*/
export async function tidyMilestones(api: Api, projects: Project[]): Promise<void> {
await asyncPool(5, projects, async (project) => {
await asyncPool(CONCURRENCY, projects, async (project) => {
const milestones = await api.getMilestonesForProject(project.id);
const missingMilestones = NEEDED_MILESTONES.slice(0);
@@ -184,9 +194,9 @@ export async function tidyMilestones(api: Api, projects: Project[]): Promise<voi
});
if (missingMilestones.length > 0) {
await asyncPool(2, missingMilestones, async (milestone) => {
await asyncPool(CONCURRENCY, missingMilestones, async (milestone) => {
await api.createMilestone(project.id, milestone);
Logger.log('Created milestone ' + milestone + ' for project ' + project.name_with_namespace + '.');
Logger.log(`Created milestone '${milestone}' for project ${project.name_with_namespace}'.`);
});
}
});
@@ -201,7 +211,7 @@ export async function tidyMilestones(api: Api, projects: Project[]): Promise<voi
* @param projects List of projects to tidy milestones on
*/
export async function tidyProtectedBranches(api: Api, projects: Project[]): Promise<void> {
await asyncPool(2, projects, async (project) => {
await asyncPool(CONCURRENCY, projects, async (project) => {
const branches = await api.getBranchesForProject(project.id);
const protectableBranches = branches.filter((branch) => {
@@ -212,10 +222,10 @@ export async function tidyProtectedBranches(api: Api, projects: Project[]): Prom
return !branch.protected;
});
await asyncPool(2, unprotectedBranches, async (branch) => {
await asyncPool(CONCURRENCY, unprotectedBranches, async (branch) => {
await api.protectBranch(project.id, branch.name);
Logger.log('Added protected branch `' + branch.name + '` in project `' + project.name_with_namespace + '`...');
Logger.log(`Added protected branch '${branch.name}' in project '${project.name_with_namespace}'...`);
});
});
@@ -229,18 +239,31 @@ export async function tidyProtectedBranches(api: Api, projects: Project[]): Prom
* @param projects List of projects to tidy protected tags on
*/
export async function tidyProtectedTags(api: Api, projects: Project[]): Promise<void> {
await asyncPool(2, projects, async (project) => {
await asyncPool(CONCURRENCY, projects, async (project) => {
// TODO: move this to GitLab API
const protectedTags: Array<{
/**
* List of access levels to create a tag
*/
create_access_levels: Array<{
access_level: AccessLevel,
/**
* Access level
*/
access_level: AccessLevel;
/**
* Description of access level
*/
access_level_description: string;
}>;
/**
* Name of the tag
*/
name: string;
}> = await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`);
if (!protectedTags.find((protectedTag) => {
if (protectedTags.findIndex((protectedTag) => {
return protectedTag.name === 'v*';
})) {
}) >= 0) {
await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`, {
data: {
create_access_level: AccessLevel.Maintainer,
@@ -265,34 +288,35 @@ export async function tidySubGroupMembers(api: Api): Promise<void> {
const stappsMembers = await api.getMembers(MembershipScope.GROUPS, GROUPS[0]);
const stappsMemberIds = stappsMembers.map((member) => member.id);
const groupIdsToSchool: any = {};
const groupIdsToSchool: { [id: number]: string; } = {};
Object.keys(SCHOOLS).map((school) => {
Object.keys(SCHOOLS)
.map((school) => {
groupIdsToSchool[SCHOOLS[school]] = school;
});
await asyncPool(2, GROUPS.slice(1), async (groupId) => {
await asyncPool(CONCURRENCY, GROUPS.slice(1), async (groupId) => {
const members = await api.getMembers(MembershipScope.GROUPS, groupId);
const memberIds = members.map((member) => member.id);
await asyncPool(2, stappsMembers, async (stappsMember) => {
await asyncPool(CONCURRENCY, stappsMembers, async (stappsMember) => {
if (memberIds.indexOf(stappsMember.id) === -1) {
await api.addMember(MembershipScope.GROUPS, groupId, stappsMember.id, AccessLevel.Developer);
Logger.log('Added ' + stappsMember.name + ' to group ' + groupIdsToSchool[groupId] + '.');
Logger.log(`Added '${stappsMember.name}' to group '${groupIdsToSchool[groupId]}'.`);
}
});
await asyncPool(2, members, async (member) => {
await asyncPool(CONCURRENCY, members, async (member) => {
if (stappsMemberIds.indexOf(member.id) === -1) {
await api.deleteMember(MembershipScope.GROUPS, groupId, member.id);
Logger.log('Deleted member ' + member.name + ' from group ' + groupIdsToSchool[groupId] + '.');
Logger.log(`Deleted member '${member.name}' from group '${groupIdsToSchool[groupId]}'.`);
}
});
});
Logger.ok('Tidied "sub" group members.');
Logger.ok(`Tidied 'sub' group members.`);
}
/**
@@ -300,11 +324,11 @@ export async function tidySubGroupMembers(api: Api): Promise<void> {
*
* Set assignee to author if no assignee is set.
*
* @param api
* @param api GitLab API instance to use for the requests
*/
export async function tidyIssuesWithoutAssignee(api: Api): Promise<void> {
// fetch issues without milestone from all groups
const issueResults = await asyncPool(3, GROUPS, (groupId) => {
const issueResults = await asyncPool(CONCURRENCY, GROUPS, async (groupId) => {
return api.getIssues({
groupId: groupId,
state: IssueState.OPENED,
@@ -318,12 +342,12 @@ export async function tidyIssuesWithoutAssignee(api: Api): Promise<void> {
return issue.assignee === null;
});
Logger.info('Found `' + issuesWithoutAssignee.length + '` issue(s) without assignee.');
Logger.info(`Found '${issuesWithoutAssignee.length}' issue(s) without assignee.`);
await asyncPool(3, issuesWithoutAssignee, async (issue) => {
await asyncPool(CONCURRENCY, issuesWithoutAssignee, async (issue) => {
await api.setAssigneeForIssue(issue, issue.author.id);
Logger.log('Set assignee for `' + issue.title + '` to ' + issue.author.name + '.');
Logger.log(`Set assignee for '${issue.title}' to '${issue.author.name}'.`);
await api.createNote(
issue.project_id,

View File

@@ -17,7 +17,7 @@ import {Api} from '@openstapps/gitlab-api';
import {IssueState, Scope} from '@openstapps/gitlab-api/lib/types';
import {Logger} from '@openstapps/logger';
import {flatten2dArray} from '../common';
import {GROUPS, NOTE_PREFIX} from '../configuration';
import {CONCURRENCY, GROUPS, NOTE_PREFIX} from '../configuration';
/**
* Remove label `meeting` from closed issues
@@ -25,7 +25,7 @@ import {GROUPS, NOTE_PREFIX} from '../configuration';
* @param api Instance of GitLabAPI to send requests with
*/
export async function unlabel(api: Api) {
const issueResults = await asyncPool(3, GROUPS, (groupId) => {
const issueResults = await asyncPool(CONCURRENCY, GROUPS, async (groupId) => {
return api.getIssues({
groupId: groupId,
state: IssueState.CLOSED,
@@ -34,9 +34,9 @@ export async function unlabel(api: Api) {
const issues = flatten2dArray(issueResults);
Logger.log('Fetched ' + issues.length + ' closed issue(s).');
Logger.log(`Fetched ${issues.length} closed issue(s).`);
await asyncPool(1, issues, async (issue) => {
await asyncPool(CONCURRENCY, issues, async (issue) => {
if (issue.labels.indexOf('meeting') >= 0) {
Logger.info(`Issue ${issue.title} is closed and has label "meeting". Removing it.`);