/* * Copyright (C) 2019-2022 Open StApps * 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 * Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ import { Api, AccessLevel, MembershipScope, MergeRequestMergeStatus, MergeRequestState, Scope, User, } from '@openstapps/gitlab-api'; import {Logger} from '@openstapps/logger'; import {WebClient} from '@slack/web-api'; import {CONCURRENCY, GROUPS, MAX_DEPTH_FOR_REMINDER, NOTE_PREFIX, SLACK_CHANNEL} from '../configuration.js'; import {mapAsyncLimit} from '@openstapps/collection-utils'; /** * Remind people of open merge requests * @param api GitLab API to make requests with */ export async function remind(api: Api): Promise { // get a list of open merge requests const allMergeRequests = await api.getMergeRequests( MembershipScope.GROUPS, GROUPS[0], MergeRequestState.OPENED, ); const mergeRequests = allMergeRequests.filter(mergeRequest => { const parts = mergeRequest.web_url.split('/'); // remove protocol, server name and main group parts.splice(0, parts.indexOf('gitlab.com') + 1 + 1); // remove merge_requests and INDEX parts parts.splice(-1 - 1); return parts.length <= MAX_DEPTH_FOR_REMINDER; }); Logger.info(`Found ${mergeRequests.length} open merge requests.`); // instantiate slack client const client = process.env.SLACK_API_TOKEN === undefined ? undefined : new WebClient(process.env.SLACK_API_TOKEN); // get members of the main group const members = await api.getMembers(MembershipScope.GROUPS, GROUPS[0]); // filter members with at least maintainer status const maintainers = members.filter(member => member.access_level >= AccessLevel.Maintainer); // extract maintainer's usernames const maintainerUsernames = maintainers.map(maintainer => maintainer.username); // sort maintainer's usernames alphabetically maintainerUsernames.sort((a, b) => { return a.localeCompare(b); }); Logger.info(`Found ${maintainers.length} maintainer(s).`); await mapAsyncLimit( mergeRequests, 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 && (note.resolved === undefined || !note.resolved); }); }); if (hasUnresolvedDiscussions) { let recipient = mergeRequest.author.username; if (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} 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}!`, ); } } }, CONCURRENCY, ); }