mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-20 16:42:56 +00:00
fix: build
This commit is contained in:
@@ -14,7 +14,7 @@
|
||||
*/
|
||||
import {existsSync, PathLike} from 'fs';
|
||||
import path from 'path';
|
||||
import {readFilePromisified} from '../common';
|
||||
import {readFile} from 'fs/promises';
|
||||
|
||||
/**
|
||||
* Get used version of a dependency of a project referenced by a path
|
||||
@@ -27,7 +27,7 @@ export async function getUsedVersion(directoryPath: PathLike, dependency: string
|
||||
throw new Error(`'package.json' does not exist in '${directoryPath}'. Not a Node.js project?`);
|
||||
}
|
||||
|
||||
const buffer = await readFilePromisified(path.join(directoryPath.toString(), 'package.json'));
|
||||
const buffer = await readFile(path.join(directoryPath.toString(), 'package.json'));
|
||||
const content = buffer.toString();
|
||||
const packageJson = JSON.parse(content);
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {asyncPool} from '@krlwlfrt/async-pool';
|
||||
import {Api} from '@openstapps/gitlab-api';
|
||||
import {
|
||||
AccessLevel,
|
||||
@@ -23,8 +22,8 @@ import {
|
||||
User,
|
||||
} from '@openstapps/gitlab-api/lib/types';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {WebClient} from '@slack/client';
|
||||
import {CONCURRENCY, GROUPS, MAX_DEPTH_FOR_REMINDER, NOTE_PREFIX, SLACK_CHANNEL} from '../configuration';
|
||||
import {WebClient} from '@slack/web-api';
|
||||
import {GROUPS, MAX_DEPTH_FOR_REMINDER, NOTE_PREFIX, SLACK_CHANNEL} from '../configuration';
|
||||
|
||||
/**
|
||||
* Remind people of open merge requests
|
||||
@@ -55,9 +54,9 @@ export async function remind(api: Api): Promise<void> {
|
||||
|
||||
// instantiate slack client
|
||||
const client =
|
||||
typeof process.env.SLACK_API_TOKEN !== 'undefined'
|
||||
? new WebClient(process.env.SLACK_API_TOKEN)
|
||||
: undefined;
|
||||
typeof process.env.SLACK_API_TOKEN === 'undefined'
|
||||
? undefined
|
||||
: new WebClient(process.env.SLACK_API_TOKEN);
|
||||
|
||||
// get members of main group
|
||||
const members = await api.getMembers(MembershipScope.GROUPS, GROUPS[0]);
|
||||
@@ -75,115 +74,117 @@ export async function remind(api: Api): Promise<void> {
|
||||
|
||||
Logger.info(`Found ${maintainers.length} maintainer(s).`);
|
||||
|
||||
await asyncPool(CONCURRENCY, mergeRequests, async mergeRequest => {
|
||||
// check if merge request is WIP
|
||||
if (mergeRequest.work_in_progress) {
|
||||
Logger.info(`Merge request '${mergeRequest.title}' is WIP.`);
|
||||
await Promise.all(
|
||||
mergeRequests.map(async mergeRequest => {
|
||||
// check if merge request is WIP
|
||||
if (mergeRequest.work_in_progress) {
|
||||
Logger.info(`Merge request '${mergeRequest.title}' is WIP.`);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// get merge request approval
|
||||
const approval = await api.getMergeRequestApproval(mergeRequest.project_id, mergeRequest.iid);
|
||||
|
||||
// get merge request discussions
|
||||
const discussions = await api.getMergeRequestDiscussions(mergeRequest.project_id, mergeRequest.iid);
|
||||
|
||||
// check if at least one of the discussions is unresolved
|
||||
const hasUnresolvedDiscussions = discussions.some(discussion => {
|
||||
return discussion.notes.some(note => {
|
||||
return note.resolvable && (typeof note.resolved === 'undefined' || !note.resolved);
|
||||
});
|
||||
});
|
||||
|
||||
if (hasUnresolvedDiscussions) {
|
||||
let recipient = mergeRequest.author.username;
|
||||
|
||||
if (typeof mergeRequest.assignee !== 'undefined' && mergeRequest.assignee !== null) {
|
||||
recipient = mergeRequest.assignee.username;
|
||||
return;
|
||||
}
|
||||
|
||||
// create note in merge request
|
||||
await api.createNote(
|
||||
mergeRequest.project_id,
|
||||
Scope.MERGE_REQUESTS,
|
||||
mergeRequest.iid,
|
||||
`${NOTE_PREFIX} Please resolve pending discussions, @${recipient}!`,
|
||||
);
|
||||
// get merge request approval
|
||||
const approval = await api.getMergeRequestApproval(mergeRequest.project_id, mergeRequest.iid);
|
||||
|
||||
return;
|
||||
}
|
||||
// get merge request discussions
|
||||
const discussions = await api.getMergeRequestDiscussions(mergeRequest.project_id, mergeRequest.iid);
|
||||
|
||||
if (approval.merge_status === MergeRequestMergeStatus.CAN_BE_MERGED) {
|
||||
if (approval.approvals_left > 0) {
|
||||
Logger.warn(`Merge request '${mergeRequest.title}' needs more approvals!`);
|
||||
|
||||
// get possible appropers, prefixed with '@' and joined with commas
|
||||
const possibleApprovers = maintainerUsernames
|
||||
.filter(username => {
|
||||
if (mergeRequest.assignee.username === username) {
|
||||
return false;
|
||||
}
|
||||
if (username.includes('openstapps') || username.includes('kphilipp')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (approval.approved_by.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return approval.approved_by.find(
|
||||
(approver: {
|
||||
/**
|
||||
* Possible approver
|
||||
*/
|
||||
user: User;
|
||||
}) => {
|
||||
return approver.user.username !== username;
|
||||
},
|
||||
);
|
||||
})
|
||||
.map(username => `@${username}`)
|
||||
.join(' ');
|
||||
|
||||
// send message to slack
|
||||
await client?.chat.postMessage({
|
||||
channel: SLACK_CHANNEL,
|
||||
text: `Merge request '${mergeRequest.title}' needs more approvals! See ${mergeRequest.web_url}!`,
|
||||
// check if at least one of the discussions is unresolved
|
||||
const hasUnresolvedDiscussions = discussions.some(discussion => {
|
||||
return discussion.notes.some(note => {
|
||||
return note.resolvable && (typeof note.resolved === 'undefined' || !note.resolved);
|
||||
});
|
||||
});
|
||||
|
||||
// assign reviewers
|
||||
await api.createNote(
|
||||
mergeRequest.project_id,
|
||||
Scope.MERGE_REQUESTS,
|
||||
mergeRequest.iid,
|
||||
`/assign_reviewer ${possibleApprovers}`,
|
||||
);
|
||||
} else {
|
||||
Logger.log(`Merge request '${mergeRequest.title}' is ready to be merged!`);
|
||||
if (hasUnresolvedDiscussions) {
|
||||
let recipient = mergeRequest.author.username;
|
||||
|
||||
// send message to slack
|
||||
await client?.chat.postMessage({
|
||||
channel: SLACK_CHANNEL,
|
||||
text: `Merge request '${mergeRequest.title}' is ready to be merged! See ${mergeRequest.web_url}!`,
|
||||
});
|
||||
|
||||
// prefix maintainers with '@' and join with commas
|
||||
const possibleMergers = maintainerUsernames
|
||||
.filter(username => {
|
||||
return mergeRequest.assignee.username !== username;
|
||||
})
|
||||
.map(username => `@${username}`)
|
||||
.join(', ');
|
||||
if (typeof mergeRequest.assignee !== 'undefined' && mergeRequest.assignee !== null) {
|
||||
recipient = mergeRequest.assignee.username;
|
||||
}
|
||||
|
||||
// create note in merge request
|
||||
await api.createNote(
|
||||
mergeRequest.project_id,
|
||||
Scope.MERGE_REQUESTS,
|
||||
mergeRequest.iid,
|
||||
`${NOTE_PREFIX} Merge request is ready to be merged, ${possibleMergers}!`,
|
||||
`${NOTE_PREFIX} Please resolve pending discussions, @${recipient}!`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (approval.merge_status === MergeRequestMergeStatus.CAN_BE_MERGED) {
|
||||
if (approval.approvals_left > 0) {
|
||||
Logger.warn(`Merge request '${mergeRequest.title}' needs more approvals!`);
|
||||
|
||||
// get possible appropers, prefixed with '@' and joined with commas
|
||||
const possibleApprovers = maintainerUsernames
|
||||
.filter(username => {
|
||||
if (mergeRequest.assignee.username === username) {
|
||||
return false;
|
||||
}
|
||||
if (username.includes('openstapps') || username.includes('kphilipp')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (approval.approved_by.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return approval.approved_by.find(
|
||||
(approver: {
|
||||
/**
|
||||
* Possible approver
|
||||
*/
|
||||
user: User;
|
||||
}) => {
|
||||
return approver.user.username !== username;
|
||||
},
|
||||
);
|
||||
})
|
||||
.map(username => `@${username}`)
|
||||
.join(' ');
|
||||
|
||||
// send message to slack
|
||||
await client?.chat.postMessage({
|
||||
channel: SLACK_CHANNEL,
|
||||
text: `Merge request '${mergeRequest.title}' needs more approvals! See ${mergeRequest.web_url}!`,
|
||||
});
|
||||
|
||||
// assign reviewers
|
||||
await api.createNote(
|
||||
mergeRequest.project_id,
|
||||
Scope.MERGE_REQUESTS,
|
||||
mergeRequest.iid,
|
||||
`/assign_reviewer ${possibleApprovers}`,
|
||||
);
|
||||
} else {
|
||||
Logger.log(`Merge request '${mergeRequest.title}' is ready to be merged!`);
|
||||
|
||||
// send message to slack
|
||||
await client?.chat.postMessage({
|
||||
channel: SLACK_CHANNEL,
|
||||
text: `Merge request '${mergeRequest.title}' is ready to be merged! See ${mergeRequest.web_url}!`,
|
||||
});
|
||||
|
||||
// prefix maintainers with '@' and join with commas
|
||||
const possibleMergers = maintainerUsernames
|
||||
.filter(username => {
|
||||
return mergeRequest.assignee.username !== username;
|
||||
})
|
||||
.map(username => `@${username}`)
|
||||
.join(', ');
|
||||
|
||||
// create note in merge request
|
||||
await api.createNote(
|
||||
mergeRequest.project_id,
|
||||
Scope.MERGE_REQUESTS,
|
||||
mergeRequest.iid,
|
||||
`${NOTE_PREFIX} Merge request is ready to be merged, ${possibleMergers}!`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {asyncPool} from '@krlwlfrt/async-pool';
|
||||
import {Api} from '@openstapps/gitlab-api';
|
||||
import {
|
||||
Issue,
|
||||
@@ -23,12 +22,16 @@ import {
|
||||
User,
|
||||
} from '@openstapps/gitlab-api/lib/types';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import moment from 'moment';
|
||||
import {render} from 'mustache';
|
||||
import path from 'path';
|
||||
import {cwd} from 'process';
|
||||
import {flatten2dArray, getProjects, readFilePromisified, writeFilePromisified} from '../common';
|
||||
import {BOLD_LABELS, CONCURRENCY, GROUPS, LABEL_WEIGHTS, NEXT_MEETING} from '../configuration';
|
||||
import {getProjects} from '../common';
|
||||
import {BOLD_LABELS, GROUPS, LABEL_WEIGHTS, NEXT_MEETING} from '../configuration';
|
||||
import differenceInWeeks from 'date-fns/differenceInWeeks';
|
||||
import formatISO from 'date-fns/formatISO';
|
||||
import format from 'date-fns/format';
|
||||
import de from 'date-fns/locale/de';
|
||||
import {readFile, writeFile} from 'fs/promises';
|
||||
|
||||
/**
|
||||
* A structure for template compilation
|
||||
@@ -188,13 +191,15 @@ export function getMergeRequestUrls(
|
||||
* @param groups List of groups to get issues for
|
||||
*/
|
||||
export async function getIssues(api: Api, label: string, groups: number[]): Promise<Issue[]> {
|
||||
const issueResults = await asyncPool(CONCURRENCY, groups, async groupId => {
|
||||
return api.getIssues({
|
||||
groupId: groupId,
|
||||
});
|
||||
});
|
||||
const issueResults = await Promise.all(
|
||||
groups.map(groupId =>
|
||||
api.getIssues({
|
||||
groupId: groupId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const issues = flatten2dArray(issueResults).filter(issue => {
|
||||
const issues = issueResults.flat().filter(issue => {
|
||||
return issue.labels.includes(label);
|
||||
});
|
||||
|
||||
@@ -212,21 +217,23 @@ export async function getIssues(api: Api, label: string, groups: number[]): Prom
|
||||
export async function getIssueBranches(api: Api, projects: Project[]): Promise<{[k: string]: number[]}> {
|
||||
const projectBranches: {[k: string]: number[]} = {};
|
||||
|
||||
await asyncPool(CONCURRENCY, projects, async project => {
|
||||
const branches = await api.getBranchesForProject(project.id);
|
||||
await Promise.all(
|
||||
projects.map(async project => {
|
||||
const branches = await api.getBranchesForProject(project.id);
|
||||
|
||||
// extract issue number from branch
|
||||
projectBranches[project.id] = branches
|
||||
.map(branch => {
|
||||
return branch.name.split('-')[0];
|
||||
})
|
||||
.filter(branchNameStart => {
|
||||
return branchNameStart.match(/^[0-9]+$/);
|
||||
})
|
||||
.map(branchNameStart => {
|
||||
return Number.parseInt(branchNameStart, 10);
|
||||
});
|
||||
});
|
||||
// extract issue number from branch
|
||||
projectBranches[project.id] = branches
|
||||
.map(branch => {
|
||||
return branch.name.split('-')[0];
|
||||
})
|
||||
.filter(branchNameStart => {
|
||||
return branchNameStart.match(/^[0-9]+$/);
|
||||
})
|
||||
.map(branchNameStart => {
|
||||
return Number.parseInt(branchNameStart, 10);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return projectBranches;
|
||||
}
|
||||
@@ -240,14 +247,14 @@ export async function getIssueBranches(api: Api, projects: Project[]): Promise<{
|
||||
export async function getIssuesGroupedByAssignees(api: Api, label: string): Promise<AssigneeWithIssues[]> {
|
||||
const issuesByAssignee: IssuesGroupedByAssigneeId = {};
|
||||
|
||||
const groups = flatten2dArray(
|
||||
await asyncPool(CONCURRENCY, GROUPS, async groupId => {
|
||||
const groups = await Promise.all(
|
||||
GROUPS.map(async groupId => {
|
||||
const subGroups = await api.getSubGroupsForGroup(groupId);
|
||||
return subGroups.map(group => {
|
||||
return group.id;
|
||||
});
|
||||
}),
|
||||
);
|
||||
).then(it => it.flat());
|
||||
groups.push(...groups, ...GROUPS);
|
||||
|
||||
const [issues, projects] = await Promise.all([getIssues(api, label, groups), getProjects(api, groups)]);
|
||||
@@ -292,7 +299,7 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
|
||||
}),
|
||||
$mergeRequestUrl: getMergeRequestUrls(mergeRequests, issue.project_id, issue.iid)[0],
|
||||
$project: issue.web_url.replace('https://gitlab.com/', '').split('/-/issues/')[0],
|
||||
$weeksOpen: moment().diff(moment(issue.created_at), 'weeks'),
|
||||
$weeksOpen: differenceInWeeks(Date.now(), new Date(issue.created_at)),
|
||||
};
|
||||
|
||||
const issueWithMeta: IssueWithMeta = {
|
||||
@@ -370,7 +377,7 @@ export async function getIssuesGroupedByAssignees(api: Api, label: string): Prom
|
||||
* Get next meeting day
|
||||
*/
|
||||
export function getNextMeetingDay() {
|
||||
const meetingDay = NEXT_MEETING.format('YYYY-MM-DD');
|
||||
const meetingDay = formatISO(NEXT_MEETING, {representation: 'date'});
|
||||
|
||||
// log found meeting day
|
||||
Logger.info(`Generating report for '${meetingDay}' of '${GROUPS.length}' group(s)...`);
|
||||
@@ -388,35 +395,37 @@ export async function getMergeRequests(api: Api, projects: Project[]): Promise<M
|
||||
const projectMergeRequests: MergeRequestsForProjects = {};
|
||||
|
||||
// iterate over projects
|
||||
await asyncPool(CONCURRENCY, projects, async project => {
|
||||
// check if project can have merge requests
|
||||
if (!project.merge_requests_enabled) {
|
||||
return;
|
||||
}
|
||||
await Promise.all(
|
||||
projects.map(async project => {
|
||||
// check if project can have merge requests
|
||||
if (!project.merge_requests_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get all merge requests for project
|
||||
const mergeRequests = await api.getMergeRequests(
|
||||
MembershipScope.PROJECTS,
|
||||
project.id,
|
||||
MergeRequestState.OPENED,
|
||||
);
|
||||
// get all merge requests for project
|
||||
const mergeRequests = await api.getMergeRequests(
|
||||
MembershipScope.PROJECTS,
|
||||
project.id,
|
||||
MergeRequestState.OPENED,
|
||||
);
|
||||
|
||||
// extract issue number from merge request
|
||||
projectMergeRequests[project.id] = mergeRequests
|
||||
.map(mergeRequest => {
|
||||
// keep information about web url too
|
||||
return {issue_iid: mergeRequest.source_branch.split('-')[0], web_url: mergeRequest.web_url};
|
||||
})
|
||||
.filter(branchNameStartAndUrl => {
|
||||
return branchNameStartAndUrl.issue_iid.match(/^[0-9]+$/);
|
||||
})
|
||||
.map(branchNameStartAndUrl => {
|
||||
return {
|
||||
issue_iid: Number.parseInt(branchNameStartAndUrl.issue_iid, 10),
|
||||
web_url: branchNameStartAndUrl.web_url,
|
||||
};
|
||||
});
|
||||
});
|
||||
// extract issue number from merge request
|
||||
projectMergeRequests[project.id] = mergeRequests
|
||||
.map(mergeRequest => {
|
||||
// keep information about web url too
|
||||
return {issue_iid: mergeRequest.source_branch.split('-')[0], web_url: mergeRequest.web_url};
|
||||
})
|
||||
.filter(branchNameStartAndUrl => {
|
||||
return branchNameStartAndUrl.issue_iid.match(/^[0-9]+$/);
|
||||
})
|
||||
.map(branchNameStartAndUrl => {
|
||||
return {
|
||||
issue_iid: Number.parseInt(branchNameStartAndUrl.issue_iid, 10),
|
||||
web_url: branchNameStartAndUrl.web_url,
|
||||
};
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return projectMergeRequests;
|
||||
}
|
||||
@@ -434,7 +443,7 @@ export async function generateReport(api: Api, label: string, template: string):
|
||||
const structureForTemplate: StructureForTemplate = {
|
||||
issuesByAssignee: issuesGroupedByAssignee,
|
||||
meetingDay: getNextMeetingDay(),
|
||||
timestamp: moment().format('LLL'),
|
||||
timestamp: format(Date.now(), 'PPPPpp', {locale: de}),
|
||||
};
|
||||
|
||||
return render(template, structureForTemplate);
|
||||
@@ -449,7 +458,7 @@ export async function generateReport(api: Api, label: string, template: string):
|
||||
export async function report(api: Api, label: string) {
|
||||
const meetingDay = getNextMeetingDay();
|
||||
|
||||
const readReportFile = await readFilePromisified(
|
||||
const readReportFile = await readFile(
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
path.resolve(__dirname, '..', '..', 'templates', 'report.md.mustache'),
|
||||
);
|
||||
@@ -462,7 +471,7 @@ export async function report(api: Api, label: string) {
|
||||
filename = path.join(cwd(), 'reports', `${label}.md`);
|
||||
}
|
||||
|
||||
await writeFilePromisified(filename, markdown);
|
||||
await writeFile(filename, markdown);
|
||||
|
||||
Logger.ok(`Wrote file '${filename}'.`);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {asyncPool} from '@krlwlfrt/async-pool';
|
||||
import {Api} from '@openstapps/gitlab-api';
|
||||
import {
|
||||
AccessLevel,
|
||||
@@ -24,9 +23,8 @@ import {
|
||||
Scope,
|
||||
} from '@openstapps/gitlab-api/lib/types';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {flatten2dArray, getProjects} from '../common';
|
||||
import {getProjects} from '../common';
|
||||
import {
|
||||
CONCURRENCY,
|
||||
GROUPS,
|
||||
NEEDED_LABELS,
|
||||
NEEDED_MILESTONES,
|
||||
@@ -44,51 +42,52 @@ import {
|
||||
*/
|
||||
export async function tidyIssuesWithoutMilestone(api: Api): Promise<void> {
|
||||
// fetch issues without milestone from all groups
|
||||
const issueResults = await asyncPool(CONCURRENCY, GROUPS, async groupId => {
|
||||
return api.getIssues({
|
||||
groupId: groupId,
|
||||
milestone: 'No Milestone',
|
||||
state: IssueState.OPENED,
|
||||
});
|
||||
});
|
||||
|
||||
// flatten structure, e.g. put all issues in one array
|
||||
const issuesWithoutMilestone = flatten2dArray(issueResults);
|
||||
const issuesWithoutMilestone = await Promise.all(
|
||||
GROUPS.map(groupId =>
|
||||
api.getIssues({
|
||||
groupId: groupId,
|
||||
milestone: 'No Milestone',
|
||||
state: IssueState.OPENED,
|
||||
}),
|
||||
),
|
||||
).then(it => it.flat());
|
||||
|
||||
Logger.info(`Found '${issuesWithoutMilestone.length}' issue(s) without milestone.`);
|
||||
|
||||
const milestoneCache: {[s: number]: Milestone[]} = {};
|
||||
|
||||
await asyncPool(CONCURRENCY, issuesWithoutMilestone, async issue => {
|
||||
if (typeof milestoneCache[issue.project_id] === 'undefined') {
|
||||
milestoneCache[issue.project_id] = await api.getMilestonesForProject(issue.project_id);
|
||||
}
|
||||
|
||||
let milestoneId;
|
||||
|
||||
for (const milestone of milestoneCache[issue.project_id]) {
|
||||
if (milestone.title === 'Meeting') {
|
||||
milestoneId = milestone.id;
|
||||
await Promise.all(
|
||||
issuesWithoutMilestone.map(async issue => {
|
||||
if (typeof milestoneCache[issue.project_id] === 'undefined') {
|
||||
milestoneCache[issue.project_id] = await api.getMilestonesForProject(issue.project_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof milestoneId === 'undefined') {
|
||||
Logger.warn(`Milestone 'Meeting' was not available for issue ${issue.title} (${issue.web_url}).`);
|
||||
let milestoneId;
|
||||
|
||||
return;
|
||||
}
|
||||
for (const milestone of milestoneCache[issue.project_id]) {
|
||||
if (milestone.title === 'Meeting') {
|
||||
milestoneId = milestone.id;
|
||||
}
|
||||
}
|
||||
|
||||
await api.setMilestoneForIssue(issue, milestoneId);
|
||||
if (typeof milestoneId === 'undefined') {
|
||||
Logger.warn(`Milestone 'Meeting' was not available for issue ${issue.title} (${issue.web_url}).`);
|
||||
|
||||
Logger.log(`Milestone was set to 'Meeting' for issue ${issue.title} (${issue.web_url})`);
|
||||
return;
|
||||
}
|
||||
|
||||
await api.createNote(
|
||||
issue.project_id,
|
||||
Scope.ISSUES,
|
||||
issue.iid,
|
||||
`${NOTE_PREFIX} Milestone was set automatically to 'Meeting'.`,
|
||||
);
|
||||
});
|
||||
await api.setMilestoneForIssue(issue, milestoneId);
|
||||
|
||||
Logger.log(`Milestone was set to 'Meeting' for issue ${issue.title} (${issue.web_url})`);
|
||||
|
||||
await api.createNote(
|
||||
issue.project_id,
|
||||
Scope.ISSUES,
|
||||
issue.iid,
|
||||
`${NOTE_PREFIX} Milestone was set automatically to 'Meeting'.`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok('Tidied issues without milestones.');
|
||||
}
|
||||
@@ -102,15 +101,14 @@ export async function tidyIssuesWithoutMilestone(api: Api): Promise<void> {
|
||||
*/
|
||||
export async function tidyOpenIssuesWithoutMeetingLabel(api: Api): Promise<void> {
|
||||
// fetch all open issues
|
||||
const issueResults = await asyncPool(CONCURRENCY, GROUPS, async groupId => {
|
||||
return api.getIssues({
|
||||
groupId: groupId,
|
||||
state: IssueState.OPENED,
|
||||
});
|
||||
});
|
||||
|
||||
// flatten structure, e.g. put all issues in one array
|
||||
const openIssues = flatten2dArray(issueResults);
|
||||
const openIssues = await Promise.all(
|
||||
GROUPS.map(groupId =>
|
||||
api.getIssues({
|
||||
groupId: groupId,
|
||||
state: IssueState.OPENED,
|
||||
}),
|
||||
),
|
||||
).then(it => it.flat());
|
||||
|
||||
Logger.info(`Found ${openIssues.length} open issue(s).`);
|
||||
|
||||
@@ -121,20 +119,22 @@ export async function tidyOpenIssuesWithoutMeetingLabel(api: Api): Promise<void>
|
||||
|
||||
Logger.info(`Filtered ${openIssuesWithoutMeetingLabel.length} open issue(s) without label 'meeting'.`);
|
||||
|
||||
await asyncPool(CONCURRENCY, openIssuesWithoutMeetingLabel, async issue => {
|
||||
if (issue.milestone !== null && issue.milestone.title === 'Backlog') {
|
||||
Logger.info(`Skipping issue "${issue.title}" because it is in backlog.`);
|
||||
await Promise.all(
|
||||
openIssuesWithoutMeetingLabel.map(async issue => {
|
||||
if (issue.milestone !== null && issue.milestone.title === 'Backlog') {
|
||||
Logger.info(`Skipping issue "${issue.title}" because it is in backlog.`);
|
||||
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
return api.createNote(
|
||||
issue.project_id,
|
||||
Scope.ISSUES,
|
||||
issue.iid,
|
||||
`${NOTE_PREFIX} Automatically adding label 'meeting'\n\n/label ~meeting`,
|
||||
);
|
||||
});
|
||||
return api.createNote(
|
||||
issue.project_id,
|
||||
Scope.ISSUES,
|
||||
issue.iid,
|
||||
`${NOTE_PREFIX} Automatically adding label 'meeting'\n\n/label ~meeting`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok(`Tidied open issues without label 'meeting'.`);
|
||||
}
|
||||
@@ -146,39 +146,43 @@ export async function tidyOpenIssuesWithoutMeetingLabel(api: Api): Promise<void>
|
||||
* @param projects List of projects to tidy labels on
|
||||
*/
|
||||
export async function tidyLabels(api: Api, projects: Project[]): Promise<void> {
|
||||
await asyncPool(CONCURRENCY, projects, async project => {
|
||||
const labels = await api.getLabels(project.id);
|
||||
await Promise.all(
|
||||
projects.map(async project => {
|
||||
const labels = await api.getLabels(project.id);
|
||||
|
||||
const neededLabels = [...NEEDED_LABELS];
|
||||
// const extraneousLabels: Label[] = [];
|
||||
const neededLabels = [...NEEDED_LABELS];
|
||||
// const extraneousLabels: Label[] = [];
|
||||
|
||||
for (const label of labels) {
|
||||
// let needed = false;
|
||||
for (const label of labels) {
|
||||
// let needed = false;
|
||||
|
||||
for (const [neededLabelIndex, neededLabel] of neededLabels.entries()) {
|
||||
if (neededLabel.name.toLowerCase() === label.name.toLowerCase()) {
|
||||
neededLabels.splice(neededLabelIndex, 1);
|
||||
// needed = true;
|
||||
for (const [neededLabelIndex, neededLabel] of neededLabels.entries()) {
|
||||
if (neededLabel.name.toLowerCase() === label.name.toLowerCase()) {
|
||||
neededLabels.splice(neededLabelIndex, 1);
|
||||
// needed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* if (!needed) {
|
||||
/* if (!needed) {
|
||||
extraneousLabels.push(label);
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
await asyncPool(CONCURRENCY, neededLabels, async neededLabel => {
|
||||
await api.createLabel(project.id, neededLabel.name, neededLabel.description, neededLabel.color);
|
||||
await Promise.all(
|
||||
neededLabels.map(async neededLabel => {
|
||||
await api.createLabel(project.id, neededLabel.name, neededLabel.description, neededLabel.color);
|
||||
|
||||
Logger.log(`Created label '${neededLabel.name}' in '${project.name_with_namespace}'.`);
|
||||
});
|
||||
Logger.log(`Created label '${neededLabel.name}' in '${project.name_with_namespace}'.`);
|
||||
}),
|
||||
);
|
||||
|
||||
// await asyncPool(2, extraneousLabels, async (extraneousLabel) => {
|
||||
// await api.deleteLabel(project.id, extraneousLabel.name);
|
||||
//
|
||||
// Logger.log('Deleted label `' + extraneousLabel.name + '` from ' + project.name_with_namespace + '.');
|
||||
// });
|
||||
});
|
||||
// await asyncPool(2, extraneousLabels, async (extraneousLabel) => {
|
||||
// await api.deleteLabel(project.id, extraneousLabel.name);
|
||||
//
|
||||
// Logger.log('Deleted label `' + extraneousLabel.name + '` from ' + project.name_with_namespace + '.');
|
||||
// });
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok('Tidied labels.');
|
||||
}
|
||||
@@ -190,25 +194,29 @@ export async function tidyLabels(api: Api, projects: Project[]): Promise<void> {
|
||||
* @param projects List of projects to tidy milestones on
|
||||
*/
|
||||
export async function tidyMilestones(api: Api, projects: Project[]): Promise<void> {
|
||||
await asyncPool(CONCURRENCY, projects, async project => {
|
||||
const milestones = await api.getMilestonesForProject(project.id);
|
||||
const missingMilestones = [...NEEDED_MILESTONES];
|
||||
await Promise.all(
|
||||
projects.map(async project => {
|
||||
const milestones = await api.getMilestonesForProject(project.id);
|
||||
const missingMilestones = [...NEEDED_MILESTONES];
|
||||
|
||||
for (const milestone of milestones) {
|
||||
const index = missingMilestones.indexOf(milestone.title);
|
||||
for (const milestone of milestones) {
|
||||
const index = missingMilestones.indexOf(milestone.title);
|
||||
|
||||
if (index >= 0) {
|
||||
missingMilestones.splice(index, 1);
|
||||
if (index >= 0) {
|
||||
missingMilestones.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missingMilestones.length > 0 && !project.archived) {
|
||||
await asyncPool(CONCURRENCY, missingMilestones, async milestone => {
|
||||
await api.createMilestone(project.id, milestone);
|
||||
Logger.log(`Created milestone '${milestone}' for project ${project.name_with_namespace}'.`);
|
||||
});
|
||||
}
|
||||
});
|
||||
if (missingMilestones.length > 0 && !project.archived) {
|
||||
await Promise.all(
|
||||
missingMilestones.map(async milestone => {
|
||||
await api.createMilestone(project.id, milestone);
|
||||
Logger.log(`Created milestone '${milestone}' for project ${project.name_with_namespace}'.`);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok('Tidied milestones.');
|
||||
}
|
||||
@@ -220,23 +228,29 @@ export async function tidyMilestones(api: Api, projects: Project[]): Promise<voi
|
||||
* @param projects List of projects to tidy milestones on
|
||||
*/
|
||||
export async function tidyProtectedBranches(api: Api, projects: Project[]): Promise<void> {
|
||||
await asyncPool(CONCURRENCY, projects, async project => {
|
||||
const branches = await api.getBranchesForProject(project.id);
|
||||
await Promise.all(
|
||||
projects.map(async project => {
|
||||
const branches = await api.getBranchesForProject(project.id);
|
||||
|
||||
const protectableBranches = branches.filter(branch => {
|
||||
return PROTECTED_BRANCHES.includes(branch.name);
|
||||
});
|
||||
const protectableBranches = branches.filter(branch => {
|
||||
return PROTECTED_BRANCHES.includes(branch.name);
|
||||
});
|
||||
|
||||
const unprotectedBranches = protectableBranches.filter(branch => {
|
||||
return !branch.protected;
|
||||
});
|
||||
const unprotectedBranches = protectableBranches.filter(branch => {
|
||||
return !branch.protected;
|
||||
});
|
||||
|
||||
await asyncPool(CONCURRENCY, unprotectedBranches, async branch => {
|
||||
await api.protectBranch(project.id, branch.name);
|
||||
await Promise.all(
|
||||
unprotectedBranches.map(async branch => {
|
||||
await api.protectBranch(project.id, branch.name);
|
||||
|
||||
Logger.log(`Added protected branch '${branch.name}' in project '${project.name_with_namespace}'...`);
|
||||
});
|
||||
});
|
||||
Logger.log(
|
||||
`Added protected branch '${branch.name}' in project '${project.name_with_namespace}'...`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok('Tidied protected branches.');
|
||||
}
|
||||
@@ -248,44 +262,48 @@ export async function tidyProtectedBranches(api: Api, projects: Project[]): Prom
|
||||
* @param projects List of projects to tidy protected tags on
|
||||
*/
|
||||
export async function tidyProtectedTags(api: Api, projects: Project[]): Promise<void> {
|
||||
await asyncPool(CONCURRENCY, projects, async project => {
|
||||
// TODO: move this to GitLab API
|
||||
const protectedTags = (await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`)) as Array<{
|
||||
/**
|
||||
* List of access levels to create a tag
|
||||
*/
|
||||
create_access_levels: Array<{
|
||||
await Promise.all(
|
||||
projects.map(async project => {
|
||||
// TODO: move this to GitLab API
|
||||
const protectedTags = (await api.makeGitLabAPIRequest(
|
||||
`projects/${project.id}/protected_tags`,
|
||||
)) as Array<{
|
||||
/**
|
||||
* Access level
|
||||
* List of access levels to create a tag
|
||||
*/
|
||||
access_level: AccessLevel;
|
||||
create_access_levels: Array<{
|
||||
/**
|
||||
* Access level
|
||||
*/
|
||||
access_level: AccessLevel;
|
||||
/**
|
||||
* Description of access level
|
||||
*/
|
||||
access_level_description: string;
|
||||
}>;
|
||||
/**
|
||||
* Description of access level
|
||||
* Name of the tag
|
||||
*/
|
||||
access_level_description: string;
|
||||
name: string;
|
||||
}>;
|
||||
/**
|
||||
* Name of the tag
|
||||
*/
|
||||
name: string;
|
||||
}>;
|
||||
|
||||
if (
|
||||
protectedTags.findIndex(protectedTag => {
|
||||
return protectedTag.name === 'v*';
|
||||
}) === -1
|
||||
) {
|
||||
await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`, {
|
||||
data: {
|
||||
create_access_level: AccessLevel.Maintainer,
|
||||
name: 'v*',
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
if (
|
||||
protectedTags.findIndex(protectedTag => {
|
||||
return protectedTag.name === 'v*';
|
||||
}) === -1
|
||||
) {
|
||||
await api.makeGitLabAPIRequest(`projects/${project.id}/protected_tags`, {
|
||||
data: {
|
||||
create_access_level: AccessLevel.Maintainer,
|
||||
name: 'v*',
|
||||
},
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
Logger.log(`Added protected version tag in project '${project.name_with_namespace}'.`);
|
||||
}
|
||||
});
|
||||
Logger.log(`Added protected version tag in project '${project.name_with_namespace}'.`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok('Tidied protected tags.');
|
||||
}
|
||||
@@ -305,26 +323,32 @@ export async function tidySubGroupMembers(api: Api): Promise<void> {
|
||||
groupIdsToSchool[SCHOOLS[school]] = school;
|
||||
});
|
||||
|
||||
await asyncPool(CONCURRENCY, GROUPS.slice(1), async groupId => {
|
||||
const members = await api.getMembers(MembershipScope.GROUPS, groupId);
|
||||
const memberIds = new Set(members.map(member => member.id));
|
||||
await Promise.all(
|
||||
GROUPS.slice(1).map(async groupId => {
|
||||
const members = await api.getMembers(MembershipScope.GROUPS, groupId);
|
||||
const memberIds = new Set(members.map(member => member.id));
|
||||
|
||||
await asyncPool(CONCURRENCY, stappsMembers, async stappsMember => {
|
||||
if (!memberIds.has(stappsMember.id)) {
|
||||
await api.addMember(MembershipScope.GROUPS, groupId, stappsMember.id, AccessLevel.Developer);
|
||||
await Promise.all(
|
||||
stappsMembers.map(async stappsMember => {
|
||||
if (!memberIds.has(stappsMember.id)) {
|
||||
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 => {
|
||||
if (!stappsMemberIds.has(member.id)) {
|
||||
await api.deleteMember(MembershipScope.GROUPS, groupId, member.id);
|
||||
await Promise.all(
|
||||
members.map(async member => {
|
||||
if (!stappsMemberIds.has(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.`);
|
||||
}
|
||||
@@ -338,15 +362,14 @@ export async function tidySubGroupMembers(api: Api): Promise<void> {
|
||||
*/
|
||||
export async function tidyIssuesWithoutAssignee(api: Api): Promise<void> {
|
||||
// fetch issues without milestone from all groups
|
||||
const issueResults = await asyncPool(CONCURRENCY, GROUPS, async groupId => {
|
||||
return api.getIssues({
|
||||
groupId: groupId,
|
||||
state: IssueState.OPENED,
|
||||
});
|
||||
});
|
||||
|
||||
// flatten structure, e.g. put all issues in one array
|
||||
const issues = flatten2dArray(issueResults);
|
||||
const issues = await Promise.all(
|
||||
GROUPS.map(async groupId => {
|
||||
return api.getIssues({
|
||||
groupId: groupId,
|
||||
state: IssueState.OPENED,
|
||||
});
|
||||
}),
|
||||
).then(it => it.flat());
|
||||
|
||||
const issuesWithoutAssignee = issues.filter(issue => {
|
||||
return issue.assignee === null;
|
||||
@@ -354,18 +377,20 @@ export async function tidyIssuesWithoutAssignee(api: Api): Promise<void> {
|
||||
|
||||
Logger.info(`Found '${issuesWithoutAssignee.length}' issue(s) without assignee.`);
|
||||
|
||||
await asyncPool(CONCURRENCY, issuesWithoutAssignee, async issue => {
|
||||
await api.setAssigneeForIssue(issue, issue.author.id);
|
||||
await Promise.all(
|
||||
issuesWithoutAssignee.map(async issue => {
|
||||
await api.setAssigneeForIssue(issue, issue.author.id);
|
||||
|
||||
Logger.log(`Set assignee for '${issue.title}' to '${issue.author.name}'.`);
|
||||
Logger.log(`Set assignee for '${issue.title}' to '${issue.author.name}'.`);
|
||||
|
||||
await api.createNote(
|
||||
issue.project_id,
|
||||
Scope.ISSUES,
|
||||
issue.iid,
|
||||
`${NOTE_PREFIX} Assignee was set automatically to author.`,
|
||||
);
|
||||
});
|
||||
await api.createNote(
|
||||
issue.project_id,
|
||||
Scope.ISSUES,
|
||||
issue.iid,
|
||||
`${NOTE_PREFIX} Assignee was set automatically to author.`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok('Tidied issues without assignee.');
|
||||
}
|
||||
@@ -378,12 +403,11 @@ export async function tidyIssuesWithoutAssignee(api: Api): Promise<void> {
|
||||
* @param api GitLab API instance to use for the requests
|
||||
*/
|
||||
export async function tidyMergeRequestsWithoutAssignee(api: Api): Promise<void> {
|
||||
const mergeRequestResults = await asyncPool(CONCURRENCY, GROUPS, async groupId => {
|
||||
return api.getMergeRequests(MembershipScope.GROUPS, groupId, MergeRequestState.OPENED);
|
||||
});
|
||||
|
||||
// flatten structure, e.g. put all issues in one array
|
||||
const mergeRequests = flatten2dArray(mergeRequestResults);
|
||||
const mergeRequests = await Promise.all(
|
||||
GROUPS.map(async groupId => {
|
||||
return api.getMergeRequests(MembershipScope.GROUPS, groupId, MergeRequestState.OPENED);
|
||||
}),
|
||||
).then(it => it.flat());
|
||||
|
||||
const mergeRequestsWithoutAssignee = mergeRequests.filter(mergeRequest => {
|
||||
return mergeRequest.assignee === null;
|
||||
@@ -391,18 +415,20 @@ export async function tidyMergeRequestsWithoutAssignee(api: Api): Promise<void>
|
||||
|
||||
Logger.info(`Found '${mergeRequestsWithoutAssignee.length}' merge requests without assignee.`);
|
||||
|
||||
await asyncPool(CONCURRENCY, mergeRequestsWithoutAssignee, async mergeRequest => {
|
||||
await api.setAssigneeForMergeRequest(mergeRequest, mergeRequest.author.id);
|
||||
await Promise.all(
|
||||
mergeRequestsWithoutAssignee.map(async mergeRequest => {
|
||||
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}'.`);
|
||||
|
||||
await api.createNote(
|
||||
mergeRequest.project_id,
|
||||
Scope.MERGE_REQUESTS,
|
||||
mergeRequest.iid,
|
||||
`${NOTE_PREFIX} Assignee was set automatically to author.`,
|
||||
);
|
||||
});
|
||||
await api.createNote(
|
||||
mergeRequest.project_id,
|
||||
Scope.MERGE_REQUESTS,
|
||||
mergeRequest.iid,
|
||||
`${NOTE_PREFIX} Assignee was set automatically to author.`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok('Tidied merge requests without assignee.');
|
||||
}
|
||||
|
||||
@@ -12,13 +12,11 @@
|
||||
* You should have received a copy of the GNU General Public License along with
|
||||
* this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {asyncPool} from '@krlwlfrt/async-pool';
|
||||
import {Api} from '@openstapps/gitlab-api';
|
||||
import {IssueState, Scope} from '@openstapps/gitlab-api/lib/types';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import moment from 'moment';
|
||||
import {flatten2dArray} from '../common';
|
||||
import {CONCURRENCY, GROUPS, LAST_MEETING, NOTE_PREFIX} from '../configuration';
|
||||
import {GROUPS, LAST_MEETING, NOTE_PREFIX} from '../configuration';
|
||||
import isBefore from 'date-fns/isBefore';
|
||||
|
||||
/**
|
||||
* Remove label `meeting` from closed issues
|
||||
@@ -26,34 +24,38 @@ import {CONCURRENCY, GROUPS, LAST_MEETING, NOTE_PREFIX} from '../configuration';
|
||||
* @param api Instance of GitLabAPI to send requests with
|
||||
*/
|
||||
export async function unlabel(api: Api) {
|
||||
const issueResults = await asyncPool(CONCURRENCY, GROUPS, async groupId => {
|
||||
return api.getIssues({
|
||||
groupId: groupId,
|
||||
state: IssueState.CLOSED,
|
||||
});
|
||||
});
|
||||
|
||||
const issues = flatten2dArray(issueResults);
|
||||
const issues = await Promise.all(
|
||||
GROUPS.map(async groupId => {
|
||||
return api.getIssues({
|
||||
groupId: groupId,
|
||||
state: IssueState.CLOSED,
|
||||
});
|
||||
}),
|
||||
).then(it => it.flat());
|
||||
|
||||
Logger.log(`Fetched ${issues.length} closed issue(s).`);
|
||||
|
||||
await asyncPool(CONCURRENCY, issues, async issue => {
|
||||
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.`);
|
||||
await Promise.all(
|
||||
issues.map(async issue => {
|
||||
if (
|
||||
issue.labels.includes('meeting') &&
|
||||
issue.closed_at !== null &&
|
||||
isBefore(new Date(issue.closed_at), LAST_MEETING)
|
||||
) {
|
||||
Logger.info(
|
||||
`Issue ${issue.title} is closed before last meeting and has label "meeting". Removing it.`,
|
||||
);
|
||||
|
||||
await api.createNote(
|
||||
issue.project_id,
|
||||
Scope.ISSUES,
|
||||
issue.iid,
|
||||
`${NOTE_PREFIX} Removed label \`meeting\` automatically.
|
||||
await api.createNote(
|
||||
issue.project_id,
|
||||
Scope.ISSUES,
|
||||
issue.iid,
|
||||
`${NOTE_PREFIX} Removed label \`meeting\` automatically.
|
||||
/unlabel ~meeting`,
|
||||
);
|
||||
}
|
||||
});
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
Logger.ok('Label `meeting` has been removed from closed issues.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user