refactor: move to eslint

This commit is contained in:
Rainer Killinger
2022-10-07 08:24:47 +00:00
parent 612648e5d7
commit 5ddff5fe12
15 changed files with 1716 additions and 736 deletions

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
resources
openapi

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "@openstapps"
}

View File

@@ -3,6 +3,10 @@ image: registry.gitlab.com/openstapps/projectmanagement/node
before_script: before_script:
- npm ci - npm ci
default:
tags:
- performance
stages: stages:
- build - build
- test - test
@@ -39,7 +43,7 @@ docker image builder:
- docker build -t registry.gitlab.com/openstapps/projectmanagement/builder -f images/builder/Dockerfile . - docker build -t registry.gitlab.com/openstapps/projectmanagement/builder -f images/builder/Dockerfile .
- docker push registry.gitlab.com/openstapps/projectmanagement/builder - docker push registry.gitlab.com/openstapps/projectmanagement/builder
tags: tags:
- docker - secrecy
docker image node: docker image node:
image: registry.gitlab.com/openstapps/projectmanagement/builder image: registry.gitlab.com/openstapps/projectmanagement/builder
@@ -56,7 +60,7 @@ docker image node:
- docker build -t registry.gitlab.com/openstapps/projectmanagement/node -f images/node/Dockerfile images/node - docker build -t registry.gitlab.com/openstapps/projectmanagement/node -f images/node/Dockerfile images/node
- docker push registry.gitlab.com/openstapps/projectmanagement/node - docker push registry.gitlab.com/openstapps/projectmanagement/node
tags: tags:
- docker - secrecy
npm audit: npm audit:
allow_failure: true allow_failure: true
@@ -119,7 +123,7 @@ pages:
paths: paths:
- public - public
package: npm package:
dependencies: dependencies:
- npm build - npm build
tags: tags:
@@ -166,7 +170,7 @@ renovate:
variables: variables:
- $RENOVATE == "true" - $RENOVATE == "true"
tags: tags:
- gitlab-org-docker - secrecy
artifacts: artifacts:
when: always when: always
expire_in: 1d expire_in: 1d

1600
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,17 +7,17 @@
"url": "git@gitlab.com:openstapps/projectmanagement.git" "url": "git@gitlab.com:openstapps/projectmanagement.git"
}, },
"scripts": { "scripts": {
"build": "npm run tslint && npm run compile", "build": "npm run lint && npm run compile",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md && git commit -m 'docs: update changelog'", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md && git commit -m 'docs: update changelog'",
"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 --out docs --readme README.md --listInvalidSymbolLinks --entryPointStrategy expand src", "documentation": "typedoc --out docs --readme README.md --listInvalidSymbolLinks --entryPointStrategy expand src",
"postversion": "npm run changelog", "postversion": "npm run changelog",
"prepublishOnly": "npm ci && npm run build", "prepublishOnly": "npm ci && npm run build && npm run test",
"preversion": "npm run prepublishOnly", "preversion": "npm run prepublishOnly",
"push": "git push && git push origin \"v$npm_package_version\"", "push": "git push && git push origin \"v$npm_package_version\"",
"test": "nyc mocha --require ts-node/register 'test/**/*.spec.ts'", "test": "nyc mocha --require ts-node/register 'test/**/*.spec.ts'",
"tslint": "tslint -p tsconfig.json -c tslint.json 'src/**/*.ts'" "lint": "eslint --ext .ts src/"
}, },
"author": "Karl-Philipp Wulfert <krlwlfrt@gmail.com>", "author": "Karl-Philipp Wulfert <krlwlfrt@gmail.com>",
"contributors": [ "contributors": [
@@ -31,32 +31,41 @@
"dependencies": { "dependencies": {
"@krlwlfrt/async-pool": "0.4.1", "@krlwlfrt/async-pool": "0.4.1",
"@openstapps/gitlab-api": "0.9.0", "@openstapps/gitlab-api": "0.9.0",
"@openstapps/logger": "0.8.1", "@openstapps/logger": "1.0.0",
"@slack/client": "5.0.2", "@slack/client": "5.0.2",
"@types/glob": "7.2.0", "@types/glob": "7.2.0",
"@types/mustache": "4.1.3", "@types/mustache": "4.2.1",
"@types/node": "14.18.21", "@types/node": "14.18.21",
"@types/tmp": "0.2.3", "@types/tmp": "0.2.3",
"commander": "9.3.0", "commander": "9.4.1",
"glob": "8.0.3", "glob": "8.0.3",
"moment": "2.29.3", "moment": "2.29.4",
"mustache": "4.2.0", "mustache": "4.2.0",
"tmp": "0.2.1" "tmp": "0.2.1"
}, },
"devDependencies": { "devDependencies": {
"@openstapps/configuration": "0.29.1", "@openstapps/configuration": "0.33.0",
"@testdeck/mocha": "0.2.0", "@openstapps/eslint-config": "1.1.0",
"@types/chai": "4.3.1", "@testdeck/mocha": "0.2.1",
"@types/chai": "4.3.3",
"@types/chai-as-promised": "7.1.5", "@types/chai-as-promised": "7.1.5",
"@types/mocha": "9.1.1", "@types/mocha": "10.0.0",
"@typescript-eslint/eslint-plugin": "5.39.0",
"@typescript-eslint/parser": "5.39.0",
"chai": "4.3.6", "chai": "4.3.6",
"chai-as-promised": "7.1.1", "chai-as-promised": "7.1.1",
"conventional-changelog-cli": "2.2.2", "conventional-changelog-cli": "2.2.2",
"mocha": "9.2.2", "eslint": "8.24.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-jsdoc": "39.3.6",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-unicorn": "44.0.2",
"mocha": "10.0.0",
"nyc": "15.1.0", "nyc": "15.1.0",
"prepend-file-cli": "1.0.6", "prepend-file-cli": "1.0.6",
"prettier": "2.7.1",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-node": "10.8.2", "ts-node": "10.9.1",
"tslint": "6.1.3", "tslint": "6.1.3",
"typedoc": "0.22.17", "typedoc": "0.22.17",
"typescript": "4.4.4" "typescript": "4.4.4"

View File

@@ -53,7 +53,6 @@
"reviewers": [ "reviewers": [
"abcdev", "abcdev",
"jovankrunic", "jovankrunic",
"FrankNagel",
"theaninova" "theaninova"
] ]
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 StApps * Copyright (C) 2019-2022 Open StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -18,7 +18,7 @@ import {AddLogLevel} from '@openstapps/logger/lib/transformations/add-log-level'
import {Colorize} from '@openstapps/logger/lib/transformations/colorize'; import {Colorize} from '@openstapps/logger/lib/transformations/colorize';
import {Command} from 'commander'; import {Command} from 'commander';
import {existsSync, readFileSync} from 'fs'; import {existsSync, readFileSync} from 'fs';
import {join, resolve} from 'path'; import path from 'path';
import {cwd, stdout} from 'process'; import {cwd, stdout} from 'process';
import {GITLAB_API_URL} from './configuration'; import {GITLAB_API_URL} from './configuration';
import {getUsedVersionMajorMinor} from './tasks/get-used-version'; import {getUsedVersionMajorMinor} from './tasks/get-used-version';
@@ -27,12 +27,10 @@ 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', async (reason) => { process.on('unhandledRejection', async reason => {
if (reason instanceof Error) { await (reason instanceof Error
await Logger.error('Unhandled rejection', reason.stack); ? Logger.error('Unhandled rejection', reason.stack)
} else { : Logger.error('Unhandled rejection', reason));
await Logger.error('Unhandled rejection', reason);
}
process.exit(1); process.exit(1);
}); });
@@ -42,54 +40,46 @@ const commander = new Command('openstapps-projectmanagement');
// error on unknown commands // error on unknown commands
commander.on('command:*', async () => { commander.on('command:*', async () => {
await Logger.error('Invalid command: %s\nSee --help for a list of available commands.', commander.args.join(' ')); await Logger.error(
'Invalid command: %s\nSee --help for a list of available commands.',
commander.args.join(' '),
);
process.exit(1); process.exit(1);
}); });
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);
Logger.setTransformations([ Logger.setTransformations([new AddLogLevel(), new Colorize()]);
new AddLogLevel(),
new Colorize(),
]);
if (existsSync(join(__dirname, 'package.json'))) { // eslint-disable-next-line unicorn/prefer-module
commander.version(JSON.parse(readFileSync(join(__dirname, '..', 'package.json')) if (existsSync(path.join(__dirname, 'package.json'))) {
.toString()).version); // eslint-disable-next-line unicorn/prefer-module
commander.version(JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json')).toString()).version);
} }
commander commander.command('unlabel').action(async () => {
.command('unlabel') await unlabel(gitlabApi);
.action(async () => { Logger.ok('Done!');
await unlabel(gitlabApi); });
Logger.ok('Done!');
});
commander commander.command('tidy').action(async () => {
.command('tidy') await tidy(gitlabApi);
.action(async () => { Logger.ok('Done!');
await tidy(gitlabApi); });
Logger.ok('Done!');
});
commander commander.command('remind').action(async () => {
.command('remind') await remind(gitlabApi);
.action(async () => { Logger.ok('Done!');
await remind(gitlabApi); });
Logger.ok('Done!');
});
commander commander.command('get-used-version <dependency> [path]').action(async (dependency, filePath) => {
.command('get-used-version <dependency> [path]') let fallbackPath = cwd();
.action(async (dependency, path) => { if (typeof filePath === 'string' && filePath.length > 0) {
let fallbackPath = cwd(); fallbackPath = path.resolve(filePath);
if (typeof path === 'string' && path.length > 0) { }
fallbackPath = resolve(path);
}
stdout.write(await getUsedVersionMajorMinor(fallbackPath, dependency)); stdout.write(await getUsedVersionMajorMinor(fallbackPath, dependency));
}); });
commander commander.parse(process.argv);
.parse(process.argv);

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018, 2019 StApps * Copyright (C) 2018-2022 Open StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -39,7 +39,7 @@ export const writeFilePromisified = promisify(writeFile);
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(CONCURRENCY, groups, async (groupId) => { const projectResults = await asyncPool(CONCURRENCY, groups, async groupId => {
return api.getProjectsForGroup(groupId); return api.getProjectsForGroup(groupId);
}); });
@@ -57,16 +57,19 @@ 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(CONCURRENCY, groups, async (groupId) => { return flatten2dArray(
return api.getSubGroupsForGroup(groupId); await asyncPool(CONCURRENCY, groups, async groupId => {
})); return api.getSubGroupsForGroup(groupId);
}),
);
} }
/** /**
* Flatten 2d array * Flatten 2d array
* *
* @param arr Flattened array * @param array Flattened array
*/ */
export function flatten2dArray<T>(arr: T[][]): T[] { export function flatten2dArray<T>(array: T[][]): T[] {
return ([] as T[]).concat(...arr); // eslint-disable-next-line unicorn/prefer-spread
return ([] as T[]).concat(...array);
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018, 2019 StApps * Copyright (C) 2018-2022 Open StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -18,38 +18,35 @@ import moment from 'moment';
/** /**
* 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 * ID OF openstapps main group
*/ */
const STAPPS_GROUP_ID = 4088298; const STAPPS_GROUP_ID = 4_088_298;
/** /**
* List of group IDs to fetch issues for * List of group IDs to fetch issues for
*/ */
export const GROUPS: number[] = [STAPPS_GROUP_ID] export const GROUPS: number[] = [
.concat(Object.keys(SCHOOLS) STAPPS_GROUP_ID,
.map((school) => { ...Object.keys(SCHOOLS).map(school => {
return SCHOOLS[school]; return SCHOOLS[school];
}), }),
); ];
/** /**
* *
*/ */
export const LABEL_WEIGHTS: { [key: string]: number; } = { export const LABEL_WEIGHTS: {[key: string]: number} = {
'bug': 1, bug: 1,
'critical': 2, critical: 2,
}; };
/** /**
* List of labels to print bold in report * List of labels to print bold in report
*/ */
export const BOLD_LABELS: string[] = [ export const BOLD_LABELS: string[] = ['bug', 'critical'];
'bug',
'critical',
];
/** /**
* GitLab API URL * GitLab API URL
@@ -59,124 +56,122 @@ export const GITLAB_API_URL = 'https://gitlab.com/api/v4/';
/** /**
* Milestones to add to projects * Milestones to add to projects
*/ */
export const NEEDED_MILESTONES = [ export const NEEDED_MILESTONES = ['Backlog'];
'Backlog',
];
/** /**
* Protected branches * Protected branches
*/ */
export const PROTECTED_BRANCHES = [ export const PROTECTED_BRANCHES = ['develop', 'master'];
'develop',
'master',
];
/** /**
* Labels to add to all projects * Labels to add to all projects
*/ */
export const NEEDED_LABELS: Label[] = [{ export const NEEDED_LABELS: Label[] = [
color: '#FF0000', // eslint-disable-next-line unicorn/no-useless-spread
description: 'An error/something that is not working as expected', ...[
name: 'bug', {
}, color: '#FF0000',
{ description: 'An error/something that is not working as expected',
color: '#5CB85C', name: 'bug',
name: 'consistency', },
}, {
{ color: '#5CB85C',
color: '#FF0000', name: 'consistency',
name: 'confirmed', },
}, {
{ color: '#FF0000',
color: '#FF0000', name: 'confirmed',
description: 'A blocking issue/something that needs to be fixed ASAP', },
name: 'critical', {
}, color: '#FF0000',
{ description: 'A blocking issue/something that needs to be fixed ASAP',
color: '#428BCA', name: 'critical',
name: 'design', },
}, {
{ color: '#428BCA',
color: '#0033CC', name: 'design',
description: 'An issue about the documentation of the software', },
name: 'documentation', {
}, color: '#0033CC',
{ description: 'An issue about the documentation of the software',
color: '#5CB85C', name: 'documentation',
name: 'Doing', },
}, {
{ color: '#5CB85C',
color: '#5CB85C', name: 'Doing',
description: 'A feature proposal/something that will be developed', },
name: 'feature', {
}, color: '#5CB85C',
{ description: 'A feature proposal/something that will be developed',
color: '#7F8C8D', name: 'feature',
description: 'An issue that is unimportant or invalid', },
name: 'invalid', {
}, color: '#7F8C8D',
{ description: 'An issue that is unimportant or invalid',
color: '#FFFF88', name: 'invalid',
name: 'meeting', },
}, {
{ color: '#FFFF88',
color: '#8E44AD', name: 'meeting',
name: 'organization', },
}, {
{ color: '#8E44AD',
color: '#FF0000', name: 'organization',
description: 'An issue with the performance of the software', },
name: 'performance', {
}, color: '#FF0000',
{ description: 'An issue with the performance of the software',
color: '#69D100', name: 'performance',
name: 'refactoring', },
}, {
{ color: '#69D100',
color: '#FF0000', name: 'refactoring',
description: 'An issue with the security of the software', },
name: 'security', {
}, color: '#FF0000',
{ description: 'An issue with the security of the software',
color: '#D1D100', name: 'security',
description: 'An issue about the testing procedure of the software', },
name: 'testing', {
}, color: '#D1D100',
{ description: 'An issue about the testing procedure of the software',
color: '#F0AD4E', name: 'testing',
name: 'To Do', },
}, {
{ color: '#F0AD4E',
color: '#A8D695', name: 'To Do',
description: 'An issue with low priority', },
name: 'unimportant', {
}, color: '#A8D695',
{ description: 'An issue with low priority',
color: '#D10069', name: 'unimportant',
description: 'An issue with the usability of the software', },
name: 'usability', {
}, color: '#D10069',
{ description: 'An issue with the usability of the software',
color: '#428BCA', name: 'usability',
description: 'Feedback from the feedback-module of the app', },
name: 'user-feedback', {
}] color: '#428BCA',
.concat(Object.keys(SCHOOLS) description: 'Feedback from the feedback-module of the app',
.map((school) => { name: 'user-feedback',
return { },
color: '#F0AD4E', ],
description: 'An issue that specifically applies to this school', ...Object.keys(SCHOOLS).map(school => {
name: `school-${school}`, return {
}; color: '#F0AD4E',
})) description: 'An issue that specifically applies to this school',
.concat(['android', 'iOS', 'web', 'node'] name: `school-${school}`,
.map((platform) => { };
return { }),
color: '#FFECDB', ...['android', 'iOS', 'web', 'node'].map(platform => {
description: 'An issue that specifically applies to this platform', return {
name: `platform::${platform}`, color: '#FFECDB',
}; description: 'An issue that specifically applies to this platform',
})); name: `platform::${platform}`,
};
}),
];
/** /**
* Prefix for automatically created notes * Prefix for automatically created notes
@@ -215,5 +210,4 @@ if (NEXT_MEETING.isBefore(moment())) {
/** /**
* Last meeting * Last meeting
*/ */
export const LAST_MEETING = moment(NEXT_MEETING) export const LAST_MEETING = moment(NEXT_MEETING).subtract(1, 'week');
.subtract(1, 'week');

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 StApps * Copyright (C) 2019-2022 Open StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -13,33 +13,33 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {existsSync, PathLike} from 'fs'; import {existsSync, PathLike} from 'fs';
import {join} from 'path'; import path from 'path';
import {readFilePromisified} from '../common'; import {readFilePromisified} from '../common';
/** /**
* Get used version of a dependency of a project referenced by a path * Get used version of a dependency of a project referenced by a path
* *
* @param path Path to a Node.js project directory * @param directoryPath Path to a Node.js project directory
* @param dependency Dependency to get used version of * @param dependency Dependency to get used version of
*/ */
export async function getUsedVersion(path: PathLike, dependency: string): Promise<string> { export async function getUsedVersion(directoryPath: PathLike, dependency: string): Promise<string> {
if (!existsSync(join(path.toString(), 'package.json'))) { if (!existsSync(path.join(directoryPath.toString(), 'package.json'))) {
throw new Error(`'package.json' does not exist in '${path}'. Not a Node.js project?`); throw new Error(`'package.json' does not exist in '${directoryPath}'. Not a Node.js project?`);
} }
const buffer = await readFilePromisified(join(path.toString(), 'package.json')); const buffer = await readFilePromisified(path.join(directoryPath.toString(), 'package.json'));
const content = buffer.toString(); const content = buffer.toString();
const pkgJson = JSON.parse(content); const packageJson = JSON.parse(content);
if (typeof pkgJson.dependencies !== 'object') { if (typeof packageJson.dependencies !== 'object') {
throw new Error(`Project in '${path}' has no dependencies!`); throw new TypeError(`Project in '${directoryPath}' has no dependencies!`);
} }
if (typeof pkgJson.dependencies[dependency] !== 'string') { if (typeof packageJson.dependencies[dependency] !== 'string') {
throw new Error(`Project in '${path}' does not depend on '${dependency}'.`); throw new TypeError(`Project in '${directoryPath}' does not depend on '${dependency}'.`);
} }
return pkgJson.dependencies[dependency]; return packageJson.dependencies[dependency];
} }
/** /**
@@ -51,7 +51,8 @@ export async function getUsedVersion(path: PathLike, dependency: string): Promis
* @param dependency see [[getUsedVersion]] * @param dependency see [[getUsedVersion]]
*/ */
export async function getUsedVersionMajorMinor(path: PathLike, dependency: string): Promise<string> { export async function getUsedVersionMajorMinor(path: PathLike, dependency: string): Promise<string> {
const versionMatch = (await getUsedVersion(path, dependency)).match(/([0-9]+\.[0-9]+)\.[0-9]+/); const usedVersions = await getUsedVersion(path, dependency);
const versionMatch = usedVersions.match(/([0-9]+\.[0-9]+)\.[0-9]+/);
// istanbul ignore if // istanbul ignore if
if (versionMatch === null) { if (versionMatch === null) {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 StApps * Copyright (C) 2019-2022 Open StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -33,17 +33,20 @@ import {CONCURRENCY, GROUPS, MAX_DEPTH_FOR_REMINDER, NOTE_PREFIX, SLACK_CHANNEL}
*/ */
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 allMergeRequests = await api const allMergeRequests = await api.getMergeRequests(
.getMergeRequests(MembershipScope.GROUPS, GROUPS[0], MergeRequestState.OPENED); MembershipScope.GROUPS,
GROUPS[0],
MergeRequestState.OPENED,
);
const mergeRequests = allMergeRequests.filter((mergeRequest) => { const mergeRequests = allMergeRequests.filter(mergeRequest => {
const parts = mergeRequest.web_url.split('/'); const parts = mergeRequest.web_url.split('/');
// remove protocol, server name and main group // remove protocol, server name and main group
parts.splice(0, parts.indexOf('gitlab.com') + 1 + 1); parts.splice(0, parts.indexOf('gitlab.com') + 1 + 1);
// remove merge_requests and INDEX parts // remove merge_requests and INDEX parts
parts.splice(parts.length - 1 - 1); parts.splice(-1 - 1);
return parts.length <= MAX_DEPTH_FOR_REMINDER; return parts.length <= MAX_DEPTH_FOR_REMINDER;
}); });
@@ -51,18 +54,19 @@ export async function remind(api: Api): Promise<void> {
Logger.info(`Found ${mergeRequests.length} open merge requests.`); Logger.info(`Found ${mergeRequests.length} open merge requests.`);
// instantiate slack client // instantiate slack client
const client = typeof process.env.SLACK_API_TOKEN !== 'undefined' ? const client =
new WebClient(process.env.SLACK_API_TOKEN) : typeof process.env.SLACK_API_TOKEN !== 'undefined'
undefined; ? new WebClient(process.env.SLACK_API_TOKEN)
: undefined;
// get members of main group // get members of main group
const members = await api.getMembers(MembershipScope.GROUPS, GROUPS[0]); const members = await api.getMembers(MembershipScope.GROUPS, GROUPS[0]);
// filter members with at least maintainer status // filter members with at least maintainer status
const maintainers = members.filter((member) => member.access_level >= AccessLevel.Maintainer); const maintainers = members.filter(member => member.access_level >= AccessLevel.Maintainer);
// extract maintainer's usernames // extract maintainer's usernames
const maintainerUsernames = maintainers.map((maintainer) => maintainer.username); const maintainerUsernames = maintainers.map(maintainer => maintainer.username);
// sort maintainer's usernames alphabetically // sort maintainer's usernames alphabetically
maintainerUsernames.sort((a, b) => { maintainerUsernames.sort((a, b) => {
@@ -71,7 +75,7 @@ export async function remind(api: Api): Promise<void> {
Logger.info(`Found ${maintainers.length} maintainer(s).`); Logger.info(`Found ${maintainers.length} maintainer(s).`);
await asyncPool(CONCURRENCY, 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.`);
@@ -86,8 +90,8 @@ export async function remind(api: Api): Promise<void> {
const discussions = await api.getMergeRequestDiscussions(mergeRequest.project_id, mergeRequest.iid); const discussions = await api.getMergeRequestDiscussions(mergeRequest.project_id, mergeRequest.iid);
// 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 && (typeof note.resolved === 'undefined' || !note.resolved); return note.resolvable && (typeof note.resolved === 'undefined' || !note.resolved);
}); });
}); });
@@ -116,7 +120,7 @@ export async function remind(api: Api): Promise<void> {
// get possible appropers, prefixed with '@' and joined with commas // get possible appropers, prefixed with '@' and joined with commas
const possibleApprovers = maintainerUsernames const possibleApprovers = maintainerUsernames
.filter((username) => { .filter(username => {
if (mergeRequest.assignee.username === username) { if (mergeRequest.assignee.username === username) {
return false; return false;
} }
@@ -128,22 +132,24 @@ export async function remind(api: Api): Promise<void> {
return true; return true;
} }
return approval.approved_by.find((approver: { return approval.approved_by.find(
/** (approver: {
* Possible approver /**
*/ * Possible approver
user: User; */
}) => { 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
await client?.chat.postMessage({ await client?.chat.postMessage({
channel: SLACK_CHANNEL, channel: SLACK_CHANNEL,
text: `Merge request '${mergeRequest.title}' needs more approvals! See ${mergeRequest.web_url}!`, text: `Merge request '${mergeRequest.title}' needs more approvals! See ${mergeRequest.web_url}!`,
}); });
// assign reviewers // assign reviewers
@@ -153,22 +159,21 @@ export async function remind(api: Api): Promise<void> {
mergeRequest.iid, mergeRequest.iid,
`/assign_reviewer ${possibleApprovers}`, `/assign_reviewer ${possibleApprovers}`,
); );
} else { } else {
Logger.log(`Merge request '${mergeRequest.title}' is ready to be merged!`); Logger.log(`Merge request '${mergeRequest.title}' is ready to be merged!`);
// send message to slack // send message to slack
await client?.chat.postMessage({ await client?.chat.postMessage({
channel: SLACK_CHANNEL, channel: SLACK_CHANNEL,
text: `Merge request '${mergeRequest.title}' is ready to be merged! See ${mergeRequest.web_url}!`, text: `Merge request '${mergeRequest.title}' is ready to be merged! See ${mergeRequest.web_url}!`,
}); });
// prefix maintainers with '@' and join with commas // prefix maintainers with '@' and join with commas
const possibleMergers = maintainerUsernames const possibleMergers = maintainerUsernames
.filter((username) => { .filter(username => {
return mergeRequest.assignee.username !== username; return mergeRequest.assignee.username !== username;
}) })
.map((username) => `@${username}`) .map(username => `@${username}`)
.join(', '); .join(', ');
// create note in merge request // create note in merge request

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018, 2019 StApps * Copyright (C) 2018-2022 Open StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -14,11 +14,18 @@
*/ */
import {asyncPool} from '@krlwlfrt/async-pool'; import {asyncPool} from '@krlwlfrt/async-pool';
import {Api} from '@openstapps/gitlab-api'; import {Api} from '@openstapps/gitlab-api';
import {Issue, IssueState, MembershipScope, MergeRequestState, Project, User} from '@openstapps/gitlab-api/lib/types'; import {
Issue,
IssueState,
MembershipScope,
MergeRequestState,
Project,
User,
} from '@openstapps/gitlab-api/lib/types';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import moment from 'moment'; import moment from 'moment';
import {render} from 'mustache'; import {render} from 'mustache';
import {join, resolve} from 'path'; import path 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, CONCURRENCY, GROUPS, LABEL_WEIGHTS, NEXT_MEETING} from '../configuration'; import {BOLD_LABELS, CONCURRENCY, GROUPS, LABEL_WEIGHTS, NEXT_MEETING} from '../configuration';
@@ -139,8 +146,10 @@ export interface MergeRequestsForProjects {
* *
* @param state State to check * @param state State to check
*/ */
export function issueStateIsOpenedOrClosed(state: IssueState): state is IssueState.OPENED | IssueState.CLOSED { export function issueStateIsOpenedOrClosed(
return ['opened', 'closed'].indexOf(state) >= 0; state: IssueState,
): state is IssueState.OPENED | IssueState.CLOSED {
return ['opened', 'closed'].includes(state);
} }
/** /**
@@ -150,19 +159,24 @@ export function issueStateIsOpenedOrClosed(state: IssueState): state is IssueSta
* @param projectId Project ID to get data about merge requests for * @param projectId Project ID to get data about merge requests for
* @param issueIid Issue IID in certain project (relative ID, and not issue's GitLab API ID) * @param issueIid Issue IID in certain project (relative ID, and not issue's GitLab API ID)
*/ */
export function getMergeRequestUrls(projectMergeRequests: MergeRequestsForProjects, export function getMergeRequestUrls(
projectId: number, projectMergeRequests: MergeRequestsForProjects,
issueIid: number): string[] { projectId: number,
if (typeof projectMergeRequests[projectId] === 'undefined' || projectMergeRequests[projectId].length === 0) { issueIid: number,
): string[] {
if (
typeof projectMergeRequests[projectId] === 'undefined' ||
projectMergeRequests[projectId].length === 0
) {
return []; return [];
} }
return projectMergeRequests[projectId] return projectMergeRequests[projectId]
.filter((obj) => { .filter(object => {
return obj.issue_iid === issueIid; return object.issue_iid === issueIid;
}) })
.map((obj) => { .map(object => {
return obj.web_url; return object.web_url;
}); });
} }
@@ -174,16 +188,15 @@ 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(CONCURRENCY, groups, async (groupId) => { const issueResults = await asyncPool(CONCURRENCY, groups, async groupId => {
return api.getIssues({ return api.getIssues({
groupId: groupId, groupId: groupId,
}); });
}); });
const issues = flatten2dArray(issueResults) const issues = flatten2dArray(issueResults).filter(issue => {
.filter((issue) => { return issue.labels.includes(label);
return issue.labels.indexOf(label) >= 0; });
});
Logger.log(`Fetched ${issues.length} issue(s).`); Logger.log(`Fetched ${issues.length} issue(s).`);
@@ -196,24 +209,22 @@ export async function getIssues(api: Api, label: string, groups: number[]): Prom
* @param api GitLab API To make requests with * @param api GitLab API To make requests with
* @param projects List of projects * @param projects List of projects
*/ */
export async function getIssueBranches( export async function getIssueBranches(api: Api, projects: Project[]): Promise<{[k: string]: number[]}> {
api: Api, const projectBranches: {[k: string]: number[]} = {};
projects: Project[]): Promise<{ [k: string]: number[]; }> {
const projectBranches: { [k: string]: number[]; } = {};
await asyncPool(CONCURRENCY, 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 projectBranches[project.id] = branches
.map((branch) => { .map(branch => {
return branch.name.split('-')[0]; return branch.name.split('-')[0];
}) })
.filter((branchNameStart) => { .filter(branchNameStart => {
return branchNameStart.match(/^[0-9]+$/); return branchNameStart.match(/^[0-9]+$/);
}) })
.map((branchNameStart) => { .map(branchNameStart => {
return parseInt(branchNameStart, 10); return Number.parseInt(branchNameStart, 10);
}); });
}); });
@@ -229,26 +240,26 @@ 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(CONCURRENCY, GROUPS, async (groupId) => { const groups = flatten2dArray(
return (await api.getSubGroupsForGroup(groupId)).map((group) => { await asyncPool(CONCURRENCY, GROUPS, async groupId => {
return group.id; const subGroups = await api.getSubGroupsForGroup(groupId);
}); return subGroups.map(group => {
})); return group.id;
groups.push.apply(groups, GROUPS); });
}),
);
groups.push(...groups, ...GROUPS);
const [issues, projects] = await Promise.all([ const [issues, projects] = await Promise.all([getIssues(api, label, groups), getProjects(api, groups)]);
getIssues(api, label, groups),
getProjects(api, groups),
]);
const issueBranches = await getIssueBranches(api, projects); const issueBranches = await getIssueBranches(api, projects);
const mergeRequests = await getMergeRequests(api, projects); const mergeRequests = await getMergeRequests(api, projects);
issues.forEach((issue) => { for (const issue of issues) {
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; continue;
} }
if (typeof issuesByAssignee[issue.assignee.id] === 'undefined') { if (typeof issuesByAssignee[issue.assignee.id] === 'undefined') {
@@ -267,33 +278,33 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
issue.state = IssueState.OPENED; issue.state = IssueState.OPENED;
} }
const issueMeta = {
$branchExists:
typeof issueBranches[issue.project_id] !== 'undefined' &&
issueBranches[issue.project_id].includes(issue.iid),
$closed: issue.state === IssueState.CLOSED,
$issue: issue,
$labels: issue.labels.map((issueLabel: string) => {
return {
bold: BOLD_LABELS.includes(issueLabel),
label: issueLabel,
};
}),
$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'),
};
const issueWithMeta: IssueWithMeta = { const issueWithMeta: IssueWithMeta = {
...issue, ...issue,
...{ ...issueMeta,
$branchExists: typeof issueBranches[issue.project_id] !== 'undefined'
&& issueBranches[issue.project_id].indexOf(issue.iid) >= 0,
$closed: issue.state === IssueState.CLOSED,
$issue: issue,
$labels: issue.labels.map((issueLabel: string) => {
return {
bold: BOLD_LABELS.includes(issueLabel),
label: issueLabel,
};
}),
$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'),
},
}; };
if (issueStateIsOpenedOrClosed(issue.state)) { if (issueStateIsOpenedOrClosed(issue.state)) {
issuesByAssignee[issue.assignee.id].issueCounts[issue.state]++; issuesByAssignee[issue.assignee.id].issueCounts[issue.state]++;
issuesByAssignee[issue.assignee.id].issues.push(issueWithMeta); issuesByAssignee[issue.assignee.id].issues.push(issueWithMeta);
} }
}); }
// calculate quota // calculate quota
for (const _assigneeId in issuesByAssignee) { for (const _assigneeId in issuesByAssignee) {
@@ -301,13 +312,14 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
continue; continue;
} }
const assigneeId = parseInt(_assigneeId, 10); const assigneeId = Number.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 // tslint:disable-next-line:no-magic-numbers
+ issuesByAssignee[assigneeId].issueCounts.closed) * 100, issuesByAssignee[assigneeId].issueCounts.closed)) *
100,
); );
} }
@@ -317,7 +329,7 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
continue; continue;
} }
const assigneeId = parseInt(_assigneeId, 10); const assigneeId = Number.parseInt(_assigneeId, 10);
issuesByAssignee[assigneeId].issues.sort((a, b) => { issuesByAssignee[assigneeId].issues.sort((a, b) => {
let weightA = 0; let weightA = 0;
@@ -328,11 +340,11 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
continue; continue;
} }
if (a.labels.indexOf(issueLabel) >= 0) { if (a.labels.includes(issueLabel)) {
weightA += LABEL_WEIGHTS[issueLabel]; weightA += LABEL_WEIGHTS[issueLabel];
} }
if (b.labels.indexOf(issueLabel) >= 0) { if (b.labels.includes(issueLabel)) {
weightB += LABEL_WEIGHTS[issueLabel]; weightB += LABEL_WEIGHTS[issueLabel];
} }
} }
@@ -372,32 +384,35 @@ export function getNextMeetingDay() {
* @param api GitLab API to make requests with * @param api GitLab API to make requests with
* @param projects List of projects * @param projects List of projects
*/ */
export async function getMergeRequests(api: Api, export async function getMergeRequests(api: Api, projects: Project[]): Promise<MergeRequestsForProjects> {
projects: Project[]): Promise<MergeRequestsForProjects> {
const projectMergeRequests: MergeRequestsForProjects = {}; const projectMergeRequests: MergeRequestsForProjects = {};
// iterate over projects // iterate over projects
await asyncPool(CONCURRENCY, 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;
} }
// get all merge requests for project // get all merge requests for project
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 projectMergeRequests[project.id] = mergeRequests
.map((mergeRequest) => { .map(mergeRequest => {
// keep information about web url too // keep information about web url too
return {issue_iid: mergeRequest.source_branch.split('-')[0], web_url: mergeRequest.web_url}; return {issue_iid: mergeRequest.source_branch.split('-')[0], web_url: mergeRequest.web_url};
}) })
.filter((branchNameStartAndUrl) => { .filter(branchNameStartAndUrl => {
return branchNameStartAndUrl.issue_iid.match(/^[0-9]+$/); return branchNameStartAndUrl.issue_iid.match(/^[0-9]+$/);
}) })
.map((branchNameStartAndUrl) => { .map(branchNameStartAndUrl => {
return { return {
issue_iid: parseInt(branchNameStartAndUrl.issue_iid, 10), issue_iid: Number.parseInt(branchNameStartAndUrl.issue_iid, 10),
web_url: branchNameStartAndUrl.web_url, web_url: branchNameStartAndUrl.web_url,
}; };
}); });
@@ -419,14 +434,10 @@ 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() timestamp: moment().format('LLL'),
.format('LLL'),
}; };
return render( return render(template, structureForTemplate);
template,
structureForTemplate,
);
} }
/** /**
@@ -438,16 +449,17 @@ export async function generateReport(api: Api, label: string, template: string):
export async function report(api: Api, label: string) { export async function report(api: Api, label: string) {
const meetingDay = getNextMeetingDay(); const meetingDay = getNextMeetingDay();
const markdown = await generateReport( const readReportFile = await readFilePromisified(
api, // eslint-disable-next-line unicorn/prefer-module
label, path.resolve(__dirname, '..', '..', 'templates', 'report.md.mustache'),
(await readFilePromisified(resolve(__dirname, '..', '..', 'templates', 'report.md.mustache'))).toString(),
); );
let filename = join(cwd(), 'reports', `${meetingDay}.md`); const markdown = await generateReport(api, label, readReportFile.toString());
let filename = path.join(cwd(), 'reports', `${meetingDay}.md`);
if (label !== 'meeting') { if (label !== 'meeting') {
filename = join(cwd(), 'reports', `${label}.md`); filename = path.join(cwd(), 'reports', `${label}.md`);
} }
await writeFilePromisified(filename, markdown); await writeFilePromisified(filename, markdown);

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018, 2019 StApps * Copyright (C) 2018-2022 Open StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -14,7 +14,15 @@
*/ */
import {asyncPool} from '@krlwlfrt/async-pool'; import {asyncPool} from '@krlwlfrt/async-pool';
import {Api} from '@openstapps/gitlab-api'; import {Api} from '@openstapps/gitlab-api';
import {AccessLevel, IssueState, MembershipScope, MergeRequestState, Milestone, Project, Scope} from '@openstapps/gitlab-api/lib/types'; import {
AccessLevel,
IssueState,
MembershipScope,
MergeRequestState,
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 { import {
@@ -36,7 +44,7 @@ import {
*/ */
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(CONCURRENCY, GROUPS, async (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',
@@ -49,22 +57,22 @@ export async function tidyIssuesWithoutMilestone(api: Api): Promise<void> {
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(CONCURRENCY, 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);
} }
let milestoneId = null; let milestoneId;
milestoneCache[issue.project_id].forEach((milestone) => { for (const milestone of milestoneCache[issue.project_id]) {
if (milestone.title === 'Meeting') { if (milestone.title === 'Meeting') {
milestoneId = milestone.id; milestoneId = milestone.id;
} }
}); }
if (milestoneId === null) { if (typeof milestoneId === 'undefined') {
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;
@@ -94,7 +102,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(CONCURRENCY, GROUPS, async (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,
@@ -107,13 +115,13 @@ export async function tidyOpenIssuesWithoutMeetingLabel(api: Api): Promise<void>
Logger.info(`Found ${openIssues.length} open issue(s).`); Logger.info(`Found ${openIssues.length} open issue(s).`);
// filter issues without meeting label // filter issues without meeting label
const openIssuesWithoutMeetingLabel = openIssues.filter((openIssue) => { const openIssuesWithoutMeetingLabel = openIssues.filter(openIssue => {
return openIssue.labels.indexOf('meeting') === -1; return !openIssue.labels.includes('meeting');
}); });
Logger.info(`Filtered ${openIssuesWithoutMeetingLabel.length} open issue(s) without label 'meeting'.`); Logger.info(`Filtered ${openIssuesWithoutMeetingLabel.length} open issue(s) without label 'meeting'.`);
await asyncPool(CONCURRENCY, 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.`);
@@ -124,7 +132,8 @@ export async function tidyOpenIssuesWithoutMeetingLabel(api: Api): Promise<void>
issue.project_id, issue.project_id,
Scope.ISSUES, Scope.ISSUES,
issue.iid, issue.iid,
`${NOTE_PREFIX} Automatically adding label 'meeting'\n\n/label ~meeting`); `${NOTE_PREFIX} Automatically adding label 'meeting'\n\n/label ~meeting`,
);
}); });
Logger.ok(`Tidied open issues without label 'meeting'.`); Logger.ok(`Tidied open issues without label 'meeting'.`);
@@ -137,28 +146,28 @@ 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(CONCURRENCY, 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];
// const extraneousLabels: Label[] = []; // const extraneousLabels: Label[] = [];
labels.forEach((label) => { for (const label of labels) {
// let needed = false; // let needed = false;
neededLabels.forEach((neededLabel, neededLabelIdx) => { for (const [neededLabelIndex, neededLabel] of neededLabels.entries()) {
if (neededLabel.name.toLowerCase() === label.name.toLowerCase()) { if (neededLabel.name.toLowerCase() === label.name.toLowerCase()) {
neededLabels.splice(neededLabelIdx, 1); neededLabels.splice(neededLabelIndex, 1);
// needed = true; // needed = true;
} }
}); }
/* if (!needed) { /* if (!needed) {
extraneousLabels.push(label); extraneousLabels.push(label);
} */ } */
}); }
await asyncPool(CONCURRENCY, 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}'.`);
@@ -181,20 +190,20 @@ 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(CONCURRENCY, 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];
milestones.forEach((milestone) => { for (const milestone of milestones) {
const idx = missingMilestones.indexOf(milestone.title); const index = missingMilestones.indexOf(milestone.title);
if (idx >= 0) { if (index >= 0) {
missingMilestones.splice(idx, 1); missingMilestones.splice(index, 1);
} }
}); }
if (missingMilestones.length > 0 && !project.archived) { if (missingMilestones.length > 0 && !project.archived) {
await asyncPool(CONCURRENCY, 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}'.`);
}); });
@@ -211,18 +220,18 @@ 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(CONCURRENCY, 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 => {
return PROTECTED_BRANCHES.indexOf(branch.name) >= 0; return PROTECTED_BRANCHES.includes(branch.name);
}); });
const unprotectedBranches = protectableBranches.filter((branch) => { const unprotectedBranches = protectableBranches.filter(branch => {
return !branch.protected; return !branch.protected;
}); });
await asyncPool(CONCURRENCY, 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}'...`);
@@ -239,9 +248,9 @@ 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(CONCURRENCY, projects, async (project) => { await asyncPool(CONCURRENCY, projects, async project => {
// TODO: move this to GitLab API // TODO: move this to GitLab API
const protectedTags = await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`) as Array<{ const protectedTags = (await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`)) as Array<{
/** /**
* List of access levels to create a tag * List of access levels to create a tag
*/ */
@@ -261,9 +270,11 @@ export async function tidyProtectedTags(api: Api, projects: Project[]): Promise<
name: string; name: string;
}>; }>;
if (protectedTags.findIndex((protectedTag) => { if (
return protectedTag.name === 'v*'; protectedTags.findIndex(protectedTag => {
}) === -1) { return protectedTag.name === 'v*';
}) === -1
) {
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,
@@ -286,29 +297,28 @@ export async function tidyProtectedTags(api: Api, projects: Project[]): Promise<
*/ */
export async function tidySubGroupMembers(api: Api): Promise<void> { 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 = new Set(stappsMembers.map(member => member.id));
const groupIdsToSchool: { [id: number]: string; } = {}; const groupIdsToSchool: {[id: number]: string} = {};
Object.keys(SCHOOLS) Object.keys(SCHOOLS).map(school => {
.map((school) => { groupIdsToSchool[SCHOOLS[school]] = school;
groupIdsToSchool[SCHOOLS[school]] = school; });
});
await asyncPool(CONCURRENCY, 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 = new Set(members.map(member => member.id));
await asyncPool(CONCURRENCY, stappsMembers, async (stappsMember) => { await asyncPool(CONCURRENCY, stappsMembers, async stappsMember => {
if (memberIds.indexOf(stappsMember.id) === -1) { if (!memberIds.has(stappsMember.id)) {
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(CONCURRENCY, members, async (member) => { await asyncPool(CONCURRENCY, members, async member => {
if (stappsMemberIds.indexOf(member.id) === -1) { if (!stappsMemberIds.has(member.id)) {
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]}'.`);
@@ -328,7 +338,7 @@ export async function tidySubGroupMembers(api: Api): Promise<void> {
*/ */
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(CONCURRENCY, GROUPS, async (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,
@@ -338,13 +348,13 @@ export async function tidyIssuesWithoutAssignee(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 issues = flatten2dArray(issueResults); const issues = flatten2dArray(issueResults);
const issuesWithoutAssignee = issues.filter((issue) => { const issuesWithoutAssignee = issues.filter(issue => {
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(CONCURRENCY, 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}'.`);
@@ -368,24 +378,20 @@ export async function tidyIssuesWithoutAssignee(api: Api): Promise<void> {
* @param api GitLab API instance to use for the requests * @param api GitLab API instance to use for the requests
*/ */
export async function tidyMergeRequestsWithoutAssignee(api: Api): Promise<void> { export async function tidyMergeRequestsWithoutAssignee(api: Api): Promise<void> {
const mergeRequestResults = await asyncPool(CONCURRENCY, GROUPS, async (groupId) => { const mergeRequestResults = await asyncPool(CONCURRENCY, GROUPS, async groupId => {
return api.getMergeRequests( return api.getMergeRequests(MembershipScope.GROUPS, groupId, MergeRequestState.OPENED);
MembershipScope.GROUPS,
groupId,
MergeRequestState.OPENED,
);
}); });
// flatten structure, e.g. put all issues in one array // flatten structure, e.g. put all issues in one array
const mergeRequests = flatten2dArray(mergeRequestResults); const mergeRequests = flatten2dArray(mergeRequestResults);
const mergeRequestsWithoutAssignee = mergeRequests.filter((mergeRequest) => { const mergeRequestsWithoutAssignee = mergeRequests.filter(mergeRequest => {
return mergeRequest.assignee === null; return mergeRequest.assignee === null;
}); });
Logger.info(`Found '${mergeRequestsWithoutAssignee.length}' merge requests without assignee.`); Logger.info(`Found '${mergeRequestsWithoutAssignee.length}' merge requests without assignee.`);
await asyncPool(CONCURRENCY, mergeRequestsWithoutAssignee, async (mergeRequest) => { await asyncPool(CONCURRENCY, mergeRequestsWithoutAssignee, async mergeRequest => {
await api.setAssigneeForMergeRequest(mergeRequest, mergeRequest.author.id); await api.setAssigneeForMergeRequest(mergeRequest, mergeRequest.author.id);
Logger.log(`Set assignee for '${mergeRequest.title}' to '${mergeRequest.author.name}'.`); Logger.log(`Set assignee for '${mergeRequest.title}' to '${mergeRequest.author.name}'.`);
@@ -408,8 +414,9 @@ export async function tidyMergeRequestsWithoutAssignee(api: Api): Promise<void>
*/ */
export async function tidy(api: Api) { export async function tidy(api: Api) {
// get first level sub groups // get first level sub groups
const groups = GROUPS.slice(); const groups = [...GROUPS];
groups.push.apply(groups, (await api.getSubGroupsForGroup(groups[0])).map((group) => group.id)); const subGroups = await api.getSubGroupsForGroup(groups[0]);
groups.push(...groups, ...subGroups.map(group => group.id));
// get non archived projects of groups // get non archived projects of groups
let projects = await getProjects(api, groups); let projects = await getProjects(api, groups);

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2018, 2019 StApps * Copyright (C) 2018-2022 Open StApps
* This program is free software: you can redistribute it and/or modify it * This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -26,7 +26,7 @@ import {CONCURRENCY, GROUPS, LAST_MEETING, 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(CONCURRENCY, GROUPS, async (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,
@@ -37,21 +37,21 @@ export async function unlabel(api: Api) {
Logger.log(`Fetched ${issues.length} closed issue(s).`); Logger.log(`Fetched ${issues.length} closed issue(s).`);
await asyncPool(CONCURRENCY, issues, async (issue) => { await asyncPool(CONCURRENCY, issues, async issue => {
if (issue.labels.indexOf('meeting') >= 0 && issue.closed_at !== null) { if (
issue.labels.includes('meeting') &&
issue.closed_at !== null &&
moment(issue.closed_at).isBefore(LAST_MEETING)
) {
Logger.info(`Issue ${issue.title} is closed before last meeting and has label "meeting". Removing it.`);
if (moment(issue.closed_at) await api.createNote(
.isBefore(LAST_MEETING)) { issue.project_id,
Logger.info(`Issue ${issue.title} is closed before last meeting and has label "meeting". Removing it.`); Scope.ISSUES,
issue.iid,
await api.createNote( `${NOTE_PREFIX} Removed label \`meeting\` automatically.
issue.project_id,
Scope.ISSUES,
issue.iid,
`${NOTE_PREFIX} Removed label \`meeting\` automatically.
/unlabel ~meeting`, /unlabel ~meeting`,
); );
}
} }
}); });

View File

@@ -1,3 +0,0 @@
{
"extends": "./node_modules/@openstapps/configuration/tslint.json"
}