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

View File

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

View File

@@ -23,13 +23,14 @@ import {tidy} from './tasks/tidy';
import {unlabel} from './tasks/unlabel'; import {unlabel} from './tasks/unlabel';
// add default handler for unhandled rejections // add default handler for unhandled rejections
process.on('unhandledRejection', (err) => { process.on('unhandledRejection', async (err) => {
Logger.error('UNHANDLED REJECTION', err.stack); await Logger.error('UNHANDLED REJECTION', err.stack);
process.exit(1); process.exit(1);
}); });
// check that environment variable GITLAB_PRIVATE_TOKEN is set // check that environment variable GITLAB_PRIVATE_TOKEN is set
if (typeof process.env.GITLAB_PRIVATE_TOKEN !== 'string' || process.env.GITLAB_PRIVATE_TOKEN.length === 0) { 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!'); Logger.error('Environment variable GITLAB_PRIVATE_TOKEN is not set!');
process.exit(1); 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); const gitlabApi = new Api(GITLAB_API_URL, process.env.GITLAB_PRIVATE_TOKEN as string);
if (existsSync(join(__dirname, 'package.json'))) { 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 commander

View File

@@ -18,8 +18,16 @@ import {Group, Project} from '@openstapps/gitlab-api/lib/types';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {readFile, writeFile} from 'fs'; import {readFile, writeFile} from 'fs';
import {promisify} from 'util'; import {promisify} from 'util';
import {CONCURRENCY} from './configuration';
/**
* Promisified version of readFile
*/
export const readFilePromisified = promisify(readFile); export const readFilePromisified = promisify(readFile);
/**
* Promisified version of writeFile
*/
export const writeFilePromisified = promisify(writeFile); export const writeFilePromisified = promisify(writeFile);
/** /**
@@ -29,15 +37,15 @@ export const writeFilePromisified = promisify(writeFile);
* @param groups List of groups * @param groups List of groups
*/ */
export async function getProjects(api: Api, groups: number[]): Promise<Project[]> { 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); return api.getProjectsForGroup(groupId);
}); });
const projects = flatten2dArray(projectResults); const projects = flatten2dArray(projectResults);
Logger.log('Fetched ' + projects.length + ' project(s).'); Logger.log(`Fetched ${projects.length} project(s).`);
return projects; return projects;
} }
@@ -49,8 +57,8 @@ export async function getProjects(api: Api, groups: number[]): Promise<Project[]
* @param groups List of groups * @param groups List of groups
*/ */
export async function getSubGroups(api: Api, groups: number[]): Promise<Group[]> { export async function getSubGroups(api: Api, groups: number[]): Promise<Group[]> {
return flatten2dArray(await asyncPool(2, groups, async (groupId) => { return flatten2dArray(await asyncPool(CONCURRENCY, groups, async (groupId) => {
return await api.getSubGroupsForGroup(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 * 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 * 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]
return SCHOOLS[school]; .concat(Object.keys(SCHOOLS)
})); .map((school) => {
return SCHOOLS[school];
}),
);
/** /**
* *
*/ */
export const LABEL_WEIGHTS: any = { export const LABEL_WEIGHTS: { [key: string]: number; } = {
'bug': 1, 'bug': 1,
'critical': 2, 'critical': 2,
}; };
@@ -67,12 +73,11 @@ export const PROTECTED_BRANCHES = [
/** /**
* Labels to add to all projects * Labels to add to all projects
*/ */
export const NEEDED_LABELS: Label[] = [ export const NEEDED_LABELS: Label[] = [{
{ color: '#FF0000',
color: '#FF0000', description: 'An error/something that is not working as expected',
description: 'An error/something that is not working as expected', name: 'bug',
name: 'bug', },
},
{ {
color: '#5CB85C', color: '#5CB85C',
name: 'consistency', name: 'consistency',
@@ -154,20 +159,23 @@ export const NEEDED_LABELS: Label[] = [
color: '#428BCA', color: '#428BCA',
description: 'Feedback from the feedback-module of the app', description: 'Feedback from the feedback-module of the app',
name: 'user-feedback', name: 'user-feedback',
}, }]
].concat(Object.keys(SCHOOLS).map((school) => { .concat(Object.keys(SCHOOLS)
return { .map((school) => {
color: '#F0AD4E', return {
description: 'An issue that specifically applies to this school', color: '#F0AD4E',
name: `school-${school}`, description: 'An issue that specifically applies to this school',
}; name: `school-${school}`,
})).concat(['android', 'iOS', 'web', 'node'].map((platform) => { };
return { }))
color: '#FFECDB', .concat(['android', 'iOS', 'web', 'node']
description: 'An issue that specifically applies to this platform', .map((platform) => {
name: 'platform-' + platform, return {
}; color: '#FFECDB',
})); description: 'An issue that specifically applies to this platform',
name: `platform-${platform}`,
};
}));
/** /**
* Prefix for automatically created notes * Prefix for automatically created notes
@@ -178,3 +186,8 @@ export const NOTE_PREFIX = '`openstapps/projectmanagement`';
* Slack channel to post messages to * Slack channel to post messages to
*/ */
export const SLACK_CHANNEL = 'C762UG76Z'; 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'; } from '@openstapps/gitlab-api/lib/types';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {WebClient} from '@slack/client'; 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> { export async function remind(api: Api): Promise<void> {
// get list of open merge requests // get list of open merge requests
const mergeRequests = await api.getMergeRequests(MembershipScope.GROUPS, GROUPS[0], MergeRequestState.OPENED); 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).`); 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 // check if merge request is WIP
if (mergeRequest.work_in_progress) { if (mergeRequest.work_in_progress) {
Logger.info(`Merge request '${mergeRequest.title}' is WIP.`); Logger.info(`Merge request '${mergeRequest.title}' is WIP.`);
return; return;
} }
@@ -67,14 +73,14 @@ export async function remind(api: Api): Promise<void> {
// check if at least one of the discussions is unresolved // check if at least one of the discussions is unresolved
const hasUnresolvedDiscussions = discussions.some((discussion) => { const hasUnresolvedDiscussions = discussions.some((discussion) => {
return discussion.notes.some((note) => { return discussion.notes.some((note) => {
return note.resolvable && !note.resolved; return note.resolvable && (typeof note.resolved === 'undefined' || !note.resolved);
}); });
}); });
if (hasUnresolvedDiscussions) { if (hasUnresolvedDiscussions) {
let recipient = mergeRequest.author.username; let recipient = mergeRequest.author.username;
if (typeof mergeRequest.assignee !== 'undefined') { if (typeof mergeRequest.assignee !== 'undefined' && mergeRequest.assignee !== null) {
recipient = mergeRequest.assignee.username; recipient = mergeRequest.assignee.username;
} }
@@ -100,11 +106,16 @@ export async function remind(api: Api): Promise<void> {
return true; 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; return approver.user.username !== username;
}); });
}) })
.map((username) => '@' + username) .map((username) => `@${username}`)
.join(', '); .join(', ');
// send message to slack // send message to slack
@@ -131,7 +142,7 @@ export async function remind(api: Api): Promise<void> {
// prefix maintainers with '@' and join with commas // prefix maintainers with '@' and join with commas
const possibleMergers = maintainerUsernames const possibleMergers = maintainerUsernames
.map((username) => '@' + username) .map((username) => `@${username}`)
.join(', '); .join(', ');
// create note in merge request // create note in merge request

View File

@@ -21,14 +21,23 @@ import {render} from 'mustache';
import {join, resolve} from 'path'; import {join, resolve} from 'path';
import {cwd} from 'process'; import {cwd} from 'process';
import {flatten2dArray, getProjects, readFilePromisified, writeFilePromisified} from '../common'; 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 * A structure for template compilation
*/ */
export interface StructureForTemplate { export interface StructureForTemplate {
/**
* List of issues by assignee
*/
issuesByAssignee: AssigneeWithIssues[]; issuesByAssignee: AssigneeWithIssues[];
/**
* Meeting day
*/
meetingDay: string; meetingDay: string;
/**
* Timestamp, when the report was generated
*/
timestamp: string; timestamp: string;
} }
@@ -43,12 +52,30 @@ export interface IssuesGroupedByAssigneeId {
* An assignee with assigned issues * An assignee with assigned issues
*/ */
export interface AssigneeWithIssues { export interface AssigneeWithIssues {
/**
* Assignee of the issues
*/
assignee: User; assignee: User;
/**
* Counts of issues
*/
issueCounts: { issueCounts: {
/**
* Count of closed issues
*/
closed: number; closed: number;
/**
* Count of opened issues
*/
opened: number; opened: number;
}; };
/**
* List of issues
*/
issues: IssueWithMeta[]; issues: IssueWithMeta[];
/**
* Quota of closed issues
*/
quota: number; quota: number;
} }
@@ -56,14 +83,38 @@ export interface AssigneeWithIssues {
* Issue with meta information * Issue with meta information
*/ */
export interface IssueWithMeta extends Issue { export interface IssueWithMeta extends Issue {
/**
* Whether or not an issue branch exists
*/
$branchExists: boolean; $branchExists: boolean;
/**
* Whether or not the issue is closed
*/
$closed: boolean; $closed: boolean;
/**
* List of labels
*/
$labels: Array<{ $labels: Array<{
/**
* Whether or not to print the label bold
*/
bold: boolean; bold: boolean;
/**
* Actual label
*/
label: string; label: string;
}>; }>;
/**
* URL of merge request
*/
$mergeRequestUrl: string; $mergeRequestUrl: string;
/**
* Name of project
*/
$project: string; $project: string;
/**
* Number of weeks the issue is open
*/
$weeksOpen: number; $weeksOpen: number;
} }
@@ -72,7 +123,13 @@ export interface IssueWithMeta extends Issue {
*/ */
export interface MergeRequestsForProjects { export interface MergeRequestsForProjects {
[projectId: string]: Array<{ [projectId: string]: Array<{
/**
* IID of issue
*/
issue_iid: number; issue_iid: number;
/**
* URL of issue
*/
web_url: string; web_url: string;
}>; }>;
} }
@@ -100,11 +157,13 @@ export function getMergeRequestUrls(projectMergeRequests: MergeRequestsForProjec
return []; return [];
} }
return projectMergeRequests[projectId].filter((obj) => { return projectMergeRequests[projectId]
return obj.issue_iid === issueIid; .filter((obj) => {
}).map((obj) => { return obj.issue_iid === issueIid;
return obj.web_url; })
}); .map((obj) => {
return obj.web_url;
});
} }
/** /**
@@ -115,17 +174,18 @@ export function getMergeRequestUrls(projectMergeRequests: MergeRequestsForProjec
* @param groups List of groups to get issues for * @param groups List of groups to get issues for
*/ */
export async function getIssues(api: Api, label: string, groups: number[]): Promise<Issue[]> { 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({ return api.getIssues({
groupId: groupId, groupId: groupId,
}); });
}); });
const issues = flatten2dArray(issueResults).filter((issue) => { const issues = flatten2dArray(issueResults)
return issue.labels.indexOf(label) >= 0; .filter((issue) => {
}); return issue.labels.indexOf(label) >= 0;
});
Logger.log('Fetched ' + issues.length + ' issue(s).'); Logger.log(`Fetched ${issues.length} issue(s).`);
return issues; return issues;
} }
@@ -138,20 +198,23 @@ export async function getIssues(api: Api, label: string, groups: number[]): Prom
*/ */
export async function getIssueBranches( export async function getIssueBranches(
api: Api, api: Api,
projects: Project[]): Promise<{ [k: string]: number[] }> { projects: Project[]): Promise<{ [k: string]: number[]; }> {
const projectBranches: { [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); const branches = await api.getBranchesForProject(project.id);
// extract issue number from branch // extract issue number from branch
projectBranches[project.id] = branches.map((branch) => { projectBranches[project.id] = branches
return branch.name.split('-')[0]; .map((branch) => {
}).filter((branchNameStart) => { return branch.name.split('-')[0];
return branchNameStart.match(/^[0-9]+$/); })
}).map((branchNameStart) => { .filter((branchNameStart) => {
return parseInt(branchNameStart, 10); return branchNameStart.match(/^[0-9]+$/);
}); })
.map((branchNameStart) => {
return parseInt(branchNameStart, 10);
});
}); });
return projectBranches; return projectBranches;
@@ -166,7 +229,7 @@ export async function getIssueBranches(
export async function getIssuesGroupedByAssignees(api: Api, label: string): Promise<AssigneeWithIssues[]> { export async function getIssuesGroupedByAssignees(api: Api, label: string): Promise<AssigneeWithIssues[]> {
const issuesByAssignee: IssuesGroupedByAssigneeId = {}; 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 (await api.getSubGroupsForGroup(groupId)).map((group) => {
return group.id; return group.id;
}); });
@@ -184,6 +247,7 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
issues.forEach((issue) => { issues.forEach((issue) => {
if (issue.assignee === null) { if (issue.assignee === null) {
Logger.warn('Issue without assignee!', issue.web_url); Logger.warn('Issue without assignee!', issue.web_url);
return; return;
} }
@@ -217,8 +281,11 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
}; };
}), }),
$mergeRequestUrl: getMergeRequestUrls(mergeRequests, issue.project_id, issue.iid)[0], $mergeRequestUrl: getMergeRequestUrls(mergeRequests, issue.project_id, issue.iid)[0],
$project: issue.web_url.replace('https://gitlab.com/', '').split('/issues/')[0], $project: issue.web_url
$weeksOpen: moment().diff(moment(issue.created_at), 'weeks'), .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 // calculate quota
Object.keys(issuesByAssignee).forEach((_assigneeId) => { for (const _assigneeId in issuesByAssignee) {
if (!issuesByAssignee.hasOwnProperty(_assigneeId)) {
continue;
}
const assigneeId = parseInt(_assigneeId, 10); const assigneeId = parseInt(_assigneeId, 10);
issuesByAssignee[assigneeId].quota = Math.floor( issuesByAssignee[assigneeId].quota = Math.floor(
issuesByAssignee[assigneeId].issueCounts.closed issuesByAssignee[assigneeId].issueCounts.closed
/ (issuesByAssignee[assigneeId].issueCounts.opened / (issuesByAssignee[assigneeId].issueCounts.opened
// tslint:disable-next-line:no-magic-numbers
+ issuesByAssignee[assigneeId].issueCounts.closed) * 100, + issuesByAssignee[assigneeId].issueCounts.closed) * 100,
); );
}); }
// sort issues by weight of labels and status // 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); const assigneeId = parseInt(_assigneeId, 10);
issuesByAssignee[assigneeId].issues.sort((a, b) => { issuesByAssignee[assigneeId].issues.sort((a, b) => {
let weightA = 0; let weightA = 0;
let weightB = 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) { if (a.labels.indexOf(issueLabel) >= 0) {
weightA += LABEL_WEIGHTS[issueLabel]; weightA += LABEL_WEIGHTS[issueLabel];
} }
@@ -255,19 +335,21 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
if (b.labels.indexOf(issueLabel) >= 0) { if (b.labels.indexOf(issueLabel) >= 0) {
weightB += LABEL_WEIGHTS[issueLabel]; weightB += LABEL_WEIGHTS[issueLabel];
} }
}); }
if (a.state === IssueState.CLOSED) { if (a.state === IssueState.CLOSED) {
// tslint:disable-next-line:no-magic-numbers
weightA -= 10; weightA -= 10;
} }
if (b.state === IssueState.CLOSED) { if (b.state === IssueState.CLOSED) {
// tslint:disable-next-line:no-magic-numbers
weightB -= 10; weightB -= 10;
} }
return weightB - weightA; return weightB - weightA;
}); });
}); }
return Object.values(issuesByAssignee); return Object.values(issuesByAssignee);
} }
@@ -280,7 +362,12 @@ export function getNextMeetingDay() {
const now = moment(); const now = moment();
// get first wednesday of month // 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)) { while (meetingDayMoment.isBefore(now)) {
// add one week until meeting day is after now // add one week until meeting day is after now
@@ -290,7 +377,7 @@ export function getNextMeetingDay() {
const meetingDay = meetingDayMoment.format('YYYY-MM-DD'); const meetingDay = meetingDayMoment.format('YYYY-MM-DD');
// log found meeting day // 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; return meetingDay;
} }
@@ -306,7 +393,7 @@ export async function getMergeRequests(api: Api,
const projectMergeRequests: MergeRequestsForProjects = {}; const projectMergeRequests: MergeRequestsForProjects = {};
// iterate over projects // iterate over projects
await asyncPool(2, projects, async (project) => { await asyncPool(CONCURRENCY, projects, async (project) => {
// check if project can have merge requests // check if project can have merge requests
if (!project.merge_requests_enabled) { if (!project.merge_requests_enabled) {
return; return;
@@ -316,17 +403,20 @@ export async function getMergeRequests(api: Api,
const mergeRequests = await api.getMergeRequests(MembershipScope.PROJECTS, project.id, MergeRequestState.OPENED); const mergeRequests = await api.getMergeRequests(MembershipScope.PROJECTS, project.id, MergeRequestState.OPENED);
// extract issue number from merge request // extract issue number from merge request
projectMergeRequests[project.id] = mergeRequests.map((mergeRequest) => { projectMergeRequests[project.id] = mergeRequests
// keep information about web url too .map((mergeRequest) => {
return {issue_iid: mergeRequest.source_branch.split('-')[0], web_url: mergeRequest.web_url}; // keep information about web url too
}).filter((branchNameStartAndUrl) => { return {issue_iid: mergeRequest.source_branch.split('-')[0], web_url: mergeRequest.web_url};
return branchNameStartAndUrl.issue_iid.match(/^[0-9]+$/); })
}).map((branchNameStartAndUrl) => { .filter((branchNameStartAndUrl) => {
return { return branchNameStartAndUrl.issue_iid.match(/^[0-9]+$/);
issue_iid: parseInt(branchNameStartAndUrl.issue_iid, 10), })
web_url: branchNameStartAndUrl.web_url, .map((branchNameStartAndUrl) => {
}; return {
}); issue_iid: parseInt(branchNameStartAndUrl.issue_iid, 10),
web_url: branchNameStartAndUrl.web_url,
};
});
}); });
return projectMergeRequests; return projectMergeRequests;
@@ -345,7 +435,8 @@ export async function generateReport(api: Api, label: string, template: string):
const structureForTemplate: StructureForTemplate = { const structureForTemplate: StructureForTemplate = {
issuesByAssignee: issuesGroupedByAssignee, issuesByAssignee: issuesGroupedByAssignee,
meetingDay: getNextMeetingDay(), meetingDay: getNextMeetingDay(),
timestamp: moment().format('LLL'), timestamp: moment()
.format('LLL'),
}; };
return render( return render(
@@ -369,7 +460,7 @@ export async function report(api: Api, label: string) {
(await readFilePromisified(resolve(__dirname, '..', '..', 'templates', 'report.md.mustache'))).toString(), (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') { if (label !== 'meeting') {
filename = join(cwd(), 'reports', `${label}.md`); 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 {AccessLevel, IssueState, MembershipScope, Milestone, Project, Scope} from '@openstapps/gitlab-api/lib/types';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {flatten2dArray, getProjects} from '../common'; 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 * 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> { export async function tidyIssuesWithoutMilestone(api: Api): Promise<void> {
// fetch issues without milestone from all groups // 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({ return api.getIssues({
groupId: groupId, groupId: groupId,
milestone: 'No Milestone', 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 // flatten structure, e.g. put all issues in one array
const issuesWithoutMilestone = flatten2dArray(issueResults); 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') { if (typeof milestoneCache[issue.project_id] === 'undefined') {
milestoneCache[issue.project_id] = await api.getMilestonesForProject(issue.project_id); 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) { 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; return;
} }
await api.setMilestoneForIssue(issue, milestoneId); 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( await api.createNote(
issue.project_id, issue.project_id,
Scope.ISSUES, Scope.ISSUES,
issue.iid, 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> { export async function tidyOpenIssuesWithoutMeetingLabel(api: Api): Promise<void> {
// fetch all open issues // fetch all open issues
const issueResults = await asyncPool(3, GROUPS, (groupId) => { const issueResults = await asyncPool(CONCURRENCY, GROUPS, async (groupId) => {
return api.getIssues({ return api.getIssues({
groupId: groupId, groupId: groupId,
state: IssueState.OPENED, 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'.`); 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') { if (issue.milestone !== null && issue.milestone.title === 'Backlog') {
Logger.info(`Skipping issue "${issue.title}" because it is in backlog.`); Logger.info(`Skipping issue "${issue.title}" because it is in backlog.`);
return; return;
} }
@@ -127,7 +137,7 @@ export async function tidyOpenIssuesWithoutMeetingLabel(api: Api): Promise<void>
* @param projects List of projects to tidy labels on * @param projects List of projects to tidy labels on
*/ */
export async function tidyLabels(api: Api, projects: Project[]): Promise<void> { 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 labels = await api.getLabels(project.id);
const neededLabels = NEEDED_LABELS.slice(0); 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); 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) => { // 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 * @param projects List of projects to tidy milestones on
*/ */
export async function tidyMilestones(api: Api, projects: Project[]): Promise<void> { 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 milestones = await api.getMilestonesForProject(project.id);
const missingMilestones = NEEDED_MILESTONES.slice(0); const missingMilestones = NEEDED_MILESTONES.slice(0);
@@ -184,9 +194,9 @@ export async function tidyMilestones(api: Api, projects: Project[]): Promise<voi
}); });
if (missingMilestones.length > 0) { if (missingMilestones.length > 0) {
await asyncPool(2, missingMilestones, async (milestone) => { await asyncPool(CONCURRENCY, missingMilestones, async (milestone) => {
await api.createMilestone(project.id, 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 * @param projects List of projects to tidy milestones on
*/ */
export async function tidyProtectedBranches(api: Api, projects: Project[]): Promise<void> { 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 branches = await api.getBranchesForProject(project.id);
const protectableBranches = branches.filter((branch) => { const protectableBranches = branches.filter((branch) => {
@@ -212,10 +222,10 @@ export async function tidyProtectedBranches(api: Api, projects: Project[]): Prom
return !branch.protected; return !branch.protected;
}); });
await asyncPool(2, unprotectedBranches, async (branch) => { await asyncPool(CONCURRENCY, unprotectedBranches, async (branch) => {
await api.protectBranch(project.id, branch.name); 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 * @param projects List of projects to tidy protected tags on
*/ */
export async function tidyProtectedTags(api: Api, projects: Project[]): Promise<void> { 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<{ const protectedTags: Array<{
/**
* List of access levels to create a tag
*/
create_access_levels: Array<{ create_access_levels: Array<{
access_level: AccessLevel, /**
* Access level
*/
access_level: AccessLevel;
/**
* Description of access level
*/
access_level_description: string; access_level_description: string;
}>; }>;
/**
* Name of the tag
*/
name: string; name: string;
}> = await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`); }> = await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`);
if (!protectedTags.find((protectedTag) => { if (protectedTags.findIndex((protectedTag) => {
return protectedTag.name === 'v*'; return protectedTag.name === 'v*';
})) { }) >= 0) {
await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`, { await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`, {
data: { data: {
create_access_level: AccessLevel.Maintainer, 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 stappsMembers = await api.getMembers(MembershipScope.GROUPS, GROUPS[0]);
const stappsMemberIds = stappsMembers.map((member) => member.id); const stappsMemberIds = stappsMembers.map((member) => member.id);
const groupIdsToSchool: any = {}; const groupIdsToSchool: { [id: number]: string; } = {};
Object.keys(SCHOOLS).map((school) => { Object.keys(SCHOOLS)
groupIdsToSchool[SCHOOLS[school]] = school; .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 members = await api.getMembers(MembershipScope.GROUPS, groupId);
const memberIds = members.map((member) => member.id); 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) { if (memberIds.indexOf(stappsMember.id) === -1) {
await api.addMember(MembershipScope.GROUPS, groupId, stappsMember.id, AccessLevel.Developer); 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) { if (stappsMemberIds.indexOf(member.id) === -1) {
await api.deleteMember(MembershipScope.GROUPS, groupId, member.id); 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. * 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> { export async function tidyIssuesWithoutAssignee(api: Api): Promise<void> {
// fetch issues without milestone from all groups // 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({ return api.getIssues({
groupId: groupId, groupId: groupId,
state: IssueState.OPENED, state: IssueState.OPENED,
@@ -318,12 +342,12 @@ export async function tidyIssuesWithoutAssignee(api: Api): Promise<void> {
return issue.assignee === null; 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); 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( await api.createNote(
issue.project_id, 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 {IssueState, Scope} from '@openstapps/gitlab-api/lib/types';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {flatten2dArray} from '../common'; import {flatten2dArray} from '../common';
import {GROUPS, NOTE_PREFIX} from '../configuration'; import {CONCURRENCY, GROUPS, NOTE_PREFIX} from '../configuration';
/** /**
* Remove label `meeting` from closed issues * 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 * @param api Instance of GitLabAPI to send requests with
*/ */
export async function unlabel(api: Api) { export async function unlabel(api: Api) {
const issueResults = await asyncPool(3, GROUPS, (groupId) => { const issueResults = await asyncPool(CONCURRENCY, GROUPS, async (groupId) => {
return api.getIssues({ return api.getIssues({
groupId: groupId, groupId: groupId,
state: IssueState.CLOSED, state: IssueState.CLOSED,
@@ -34,9 +34,9 @@ export async function unlabel(api: Api) {
const issues = flatten2dArray(issueResults); 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) { if (issue.labels.indexOf('meeting') >= 0) {
Logger.info(`Issue ${issue.title} is closed and has label "meeting". Removing it.`); Logger.info(`Issue ${issue.title} is closed and has label "meeting". Removing it.`);