From 1b009956aa5702a7536311725abd9826a2e1b333 Mon Sep 17 00:00:00 2001 From: Uku Pattak Date: Fri, 26 Jul 2024 22:03:38 +0300 Subject: [PATCH] Feature trello-switch-members-in-review vol 2 (#117) --- README.md | 2 +- dist/index.js | 319 +++++++++++++--------- package.json | 6 +- src/actions/addCardLinksToPullRequest.ts | 7 +- src/actions/addLabelToCards.ts | 17 +- src/actions/addPullRequestLinkToCards.ts | 5 +- src/actions/api/github.ts | 5 +- src/actions/api/trello.ts | 59 ++-- src/actions/getCardIds.ts | 15 +- src/actions/moveOrArchiveCards.ts | 15 +- src/actions/updateCardMembers.test.ts | 54 +++- src/actions/updateCardMembers.ts | 214 ++++++++------- src/actions/utils/isPullRequestInDraft.ts | 4 +- src/actions/utils/logger.ts | 12 + 14 files changed, 451 insertions(+), 283 deletions(-) create mode 100644 src/actions/utils/logger.ts diff --git a/README.md b/README.md index 9578753..787c66f 100644 --- a/README.md +++ b/README.md @@ -200,7 +200,7 @@ Default: `true` #### 3. `trello-switch-members-in-review` -Replaces Trello card members with PR reviewers when PR is opened. It reassigns the PR author, contributors and assignees when the card is moved away from trello-list-id-pr-open. +Replaces Trello card members with PR reviewers when PR is opened. It reassigns the PR author, contributors and assignees when the card is moved away from `trello-list-id-pr-open`. Default: `false` diff --git a/dist/index.js b/dist/index.js index 51ea2b5..0318af1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -33957,17 +33957,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", ({ value: true })); const github_1 = __nccwpck_require__(2649); const trello_1 = __nccwpck_require__(9763); +const logger_1 = __importDefault(__nccwpck_require__(2358)); const matchCardIds_1 = __importDefault(__nccwpck_require__(9812)); async function addCardLinksToPullRequest(conf, cardIds) { + logger_1.default.log('--- ADD CARD LINKS TO PR ---'); const bodyCardIds = await getCardIdsFromBody(conf); const commentsCardIds = await getCardIdsFromComments(conf); const linkedCardIds = [...bodyCardIds, ...commentsCardIds]; const unlinkedCardIds = cardIds.filter((id) => !linkedCardIds.includes(id)); if (!unlinkedCardIds.length) { - console.log('Skipping card linking as all cards are already mentioned under the PR'); + logger_1.default.log('Skipping card linking as all cards are already mentioned under the PR'); return; } - console.log('Commenting Trello card URLs to PR', unlinkedCardIds); + logger_1.default.log('Commenting Trello card URLs to PR', unlinkedCardIds); const cards = await Promise.all(unlinkedCardIds.map((id) => (0, trello_1.getCardInfo)(id))); const urls = cards.map((card) => card.shortUrl); const comment = conf.githubRequireKeywordPrefix ? `Closes ${urls.join(' ')}` : urls.join('\n'); @@ -33991,29 +33993,33 @@ async function getCardIdsFromComments(conf) { /***/ }), /***/ 7340: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); const github_1 = __nccwpck_require__(2649); const trello_1 = __nccwpck_require__(9763); +const logger_1 = __importDefault(__nccwpck_require__(2358)); async function addLabelToCards(conf, cardIds, head) { + logger_1.default.log('--- ADD LABEL TO CARDS ---'); if (!conf.trelloAddLabelsToCards) { - console.log('Skipping label adding'); + logger_1.default.log('Skipping label adding'); return; } - console.log('Starting to add labels to cards'); const branchLabel = await getBranchLabel(head); if (!branchLabel) { - console.log('Could not find branch label'); + logger_1.default.log('Could not find branch label'); return; } return Promise.all(cardIds.map(async (cardId) => { const cardInfo = await (0, trello_1.getCardInfo)(cardId); const hasConflictingLabel = cardInfo.labels.find((label) => conf.trelloConflictingLabels?.includes(label.name) || label.name === branchLabel); if (hasConflictingLabel) { - console.log('Skipping label adding to a card because it has a conflicting label', cardInfo.labels); + logger_1.default.log('Skipping label adding to a card as it has a conflicting label', cardInfo.labels); return; } const boardLabels = await (0, trello_1.getBoardLabels)(cardInfo.idBoard); @@ -34022,7 +34028,7 @@ async function addLabelToCards(conf, cardIds, head) { await (0, trello_1.addLabelToCard)(cardId, matchingLabel.id); } else { - console.log('Could not find a matching label from the board', branchLabel, boardLabels); + logger_1.default.log('Could not find a matching label from the board', { branchLabel, boardLabels }); } })); } @@ -34034,7 +34040,7 @@ async function getBranchLabel(prHead) { return matches[1]; } else { - console.log('Did not find branch label', branchName); + logger_1.default.log('Did not find branch label', branchName); } } function findMatchingLabel(branchLabel, boardLabels) { @@ -34042,7 +34048,7 @@ function findMatchingLabel(branchLabel, boardLabels) { if (match) { return match; } - console.log('Could not match the exact label name, trying to find partially matching label'); + logger_1.default.log('Could not match the exact label name, trying to find partially matching label'); return boardLabels.find((label) => branchLabel.startsWith(label.name)); } @@ -34050,18 +34056,23 @@ function findMatchingLabel(branchLabel, boardLabels) { /***/ }), /***/ 37: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); const trello_1 = __nccwpck_require__(9763); +const logger_1 = __importDefault(__nccwpck_require__(2358)); async function addPullRequestLinkToCards(cardIds, pr) { + logger_1.default.log('--- ADD PR LINK TO CARDS ---'); const link = pr.html_url || pr.url; return Promise.all(cardIds.map(async (cardId) => { const existingAttachments = await (0, trello_1.getCardAttachments)(cardId); if (existingAttachments?.some((it) => it.url.includes(link))) { - console.log('Found existing attachment, skipping adding attachment', cardId, link); + logger_1.default.log('Found existing attachment, skipping adding attachment', { cardId, link }); return; } return (0, trello_1.addAttachmentToCard)(cardId, link); @@ -34073,14 +34084,18 @@ exports["default"] = addPullRequestLinkToCards; /***/ }), /***/ 2649: -/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.updatePullRequestBody = exports.createComment = exports.getPullRequestRequestedReviewers = exports.getPullRequestReviews = exports.isPullRequestMerged = exports.getCommits = exports.getBranchName = exports.getPullRequest = exports.getPullRequestComments = void 0; const core_1 = __nccwpck_require__(2186); const github_1 = __nccwpck_require__(5438); +const logger_1 = __importDefault(__nccwpck_require__(2358)); const githubToken = (0, core_1.getInput)('github-token', { required: true }); const octokit = (0, github_1.getOctokit)(githubToken); const payload = github_1.context.payload; @@ -34156,7 +34171,7 @@ async function getPullRequestRequestedReviewers() { } exports.getPullRequestRequestedReviewers = getPullRequestRequestedReviewers; async function createComment(shortUrl) { - console.log('Creating PR comment', shortUrl); + logger_1.default.log('Creating PR comment', shortUrl); await octokit.rest.issues.createComment({ owner, repo, @@ -34166,7 +34181,7 @@ async function createComment(shortUrl) { } exports.createComment = createComment; async function updatePullRequestBody(newBody) { - console.log('Updating PR body', newBody); + logger_1.default.log('Updating PR body', newBody); await octokit.rest.issues.update({ owner, repo, @@ -34211,9 +34226,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.createCard = exports.getMemberInfo = exports.archiveCard = exports.moveCardToList = exports.removeMemberFromCard = exports.addLabelToCard = exports.getBoardLists = exports.getBoardLabels = exports.addMemberToCard = exports.addAttachmentToCard = exports.getCardAttachments = exports.getCardInfo = exports.searchTrelloCards = void 0; +exports.createCard = exports.archiveCard = exports.moveCardToList = exports.getBoardLists = exports.removeMemberFromCard = exports.addMemberToCard = exports.addLabelToCard = exports.getBoardLabels = exports.addAttachmentToCard = exports.getCardAttachments = exports.getMemberInfo = exports.getCardInfo = exports.searchTrelloCards = void 0; const axios_1 = __importDefault(__nccwpck_require__(8757)); const core = __importStar(__nccwpck_require__(2186)); +const logger_1 = __importDefault(__nccwpck_require__(2358)); const trelloApiKey = core.getInput('trello-api-key', { required: true }); const trelloAuthToken = core.getInput('trello-auth-token', { required: true }); const trelloCardPosition = core.getInput('trello-card-position'); @@ -34231,23 +34247,23 @@ async function getCardInfo(cardId) { return response?.data; } exports.getCardInfo = getCardInfo; +async function getMemberInfo(username) { + const response = await makeRequest('get', `https://api.trello.com/1/members/${username}`, { + organizations: 'all', + }); + return response?.data; +} +exports.getMemberInfo = getMemberInfo; async function getCardAttachments(cardId) { const response = await makeRequest('get', `https://api.trello.com/1/cards/${cardId}/attachments`); return response?.data || null; } exports.getCardAttachments = getCardAttachments; async function addAttachmentToCard(cardId, link) { - console.log('Adding attachment to the card', cardId, link); + logger_1.default.log('Adding attachment to the card', { cardId, link }); return makeRequest('post', `https://api.trello.com/1/cards/${cardId}/attachments`, { url: link }); } exports.addAttachmentToCard = addAttachmentToCard; -async function addMemberToCard(cardId, memberId) { - console.log('Adding member to a card', cardId, memberId); - return makeRequest('post', `https://api.trello.com/1/cards/${cardId}/idMembers`, { - value: memberId, - }); -} -exports.addMemberToCard = addMemberToCard; async function getBoardLabels(boardId) { const response = await makeRequest('get', `https://api.trello.com/1/boards/${boardId}/labels`); // Filters out board labels that have no name to avoid assigning them to every PR @@ -34255,25 +34271,32 @@ async function getBoardLabels(boardId) { return response?.data?.filter((label) => label.name); } exports.getBoardLabels = getBoardLabels; -async function getBoardLists(boardId) { - const response = await makeRequest('get', `https://api.trello.com/1/boards/${boardId}/lists`); - return response?.data; -} -exports.getBoardLists = getBoardLists; async function addLabelToCard(cardId, labelId) { - console.log('Adding label to a card', cardId, labelId); + logger_1.default.log('Adding label to a card', { cardId, labelId }); return makeRequest('post', `https://api.trello.com/1/cards/${cardId}/idLabels`, { value: labelId, }); } exports.addLabelToCard = addLabelToCard; +async function addMemberToCard(cardId, memberId) { + logger_1.default.log('Adding member to a card', { cardId, memberId }); + return makeRequest('post', `https://api.trello.com/1/cards/${cardId}/idMembers`, { + value: memberId, + }); +} +exports.addMemberToCard = addMemberToCard; async function removeMemberFromCard(cardId, memberId) { - console.log('Removing card member', cardId, memberId); + logger_1.default.log('Removing member from a card', { cardId, memberId }); return makeRequest('delete', `https://api.trello.com/1/cards/${cardId}/idMembers/${memberId}`); } exports.removeMemberFromCard = removeMemberFromCard; +async function getBoardLists(boardId) { + const response = await makeRequest('get', `https://api.trello.com/1/boards/${boardId}/lists`); + return response?.data; +} +exports.getBoardLists = getBoardLists; async function moveCardToList(cardId, listId, boardId) { - console.log('Moving card to list', cardId, listId); + logger_1.default.log('Moving card to list', { cardId, listId, boardId }); return makeRequest('put', `https://api.trello.com/1/cards/${cardId}`, { pos: trelloCardPosition, idList: listId, @@ -34282,21 +34305,14 @@ async function moveCardToList(cardId, listId, boardId) { } exports.moveCardToList = moveCardToList; async function archiveCard(cardId) { - console.log('Archiving card', cardId); + logger_1.default.log('ARCHIVE: Archiving card', { cardId }); return makeRequest('put', `https://api.trello.com/1/cards/${cardId}`, { closed: true, }); } exports.archiveCard = archiveCard; -async function getMemberInfo(username) { - const response = await makeRequest('get', `https://api.trello.com/1/members/${username}`, { - organizations: 'all', - }); - return response?.data; -} -exports.getMemberInfo = getMemberInfo; async function createCard(listId, title, body) { - console.log('Creating card based on PR info', title, body); + logger_1.default.log('Creating card based on PR info', { title, body }); const response = await makeRequest('post', `https://api.trello.com/1/cards`, { idList: listId, name: title, @@ -34332,7 +34348,7 @@ async function makeRequest(method, url, params) { message: error.message, }, }; - console.error(JSON.stringify(errorMessage, null, 2)); + logger_1.default.error(JSON.stringify(errorMessage, null, 2)); throw error; } } @@ -34354,8 +34370,9 @@ const github_1 = __nccwpck_require__(2649); const trello_1 = __nccwpck_require__(9763); const matchCardIds_1 = __importDefault(__nccwpck_require__(9812)); const isPullRequestInDraft_1 = __importDefault(__nccwpck_require__(3031)); +const logger_1 = __importDefault(__nccwpck_require__(2358)); async function getCardIds(conf, pr) { - console.log('Searching for card IDs'); + logger_1.default.log('--- FIND CARDS ---'); const latestPRInfo = (await (0, github_1.getPullRequest)()) || pr; let cardIds = (0, matchCardIds_1.default)(conf, latestPRInfo.body || ''); if (conf.githubIncludePrComments) { @@ -34381,11 +34398,11 @@ async function getCardIds(conf, pr) { } } if (cardIds.length) { - console.log('Found card IDs', cardIds); + logger_1.default.log('Found card IDs', cardIds); return [...new Set(cardIds)]; } else { - console.log('Could not find card IDs'); + logger_1.default.log('Could not find card IDs'); if (conf.githubRequireTrelloCard) { (0, core_1.setFailed)('The PR does not contain a link to a Trello card'); } @@ -34395,11 +34412,11 @@ async function getCardIds(conf, pr) { exports["default"] = getCardIds; async function getCardIdsFromBranchName(conf, prHead) { const branchName = prHead?.ref || (await (0, github_1.getBranchName)()); - console.log('Searching cards from branch name', branchName); + logger_1.default.log('Searching cards from branch name', branchName); if (conf.githubAllowMultipleCardsInPrBranchName) { const shortIdMatches = branchName.match(/(?<=^|\/)\d+(?:-\d+)+/gi)?.[0].split('-'); if (shortIdMatches && shortIdMatches.length > 1) { - console.log('Matched multiple potential Trello short IDs from branch name', shortIdMatches); + logger_1.default.log('Matched multiple potential Trello short IDs from branch name', shortIdMatches); const potentialCardIds = await Promise.all(shortIdMatches.map((shortId) => getTrelloCardByShortId(shortId, conf.trelloBoardId))); const cardIds = potentialCardIds.filter((c) => c); if (cardIds.length) { @@ -34409,12 +34426,12 @@ async function getCardIdsFromBranchName(conf, prHead) { } const matches = branchName.match(/(?<=^|\/)(\d+)-\S+/i); if (matches) { - console.log('Matched one potential card from branch name', matches); + logger_1.default.log('Matched one potential card from branch name', matches); const cardsWithExactMatch = await (0, trello_1.searchTrelloCards)(matches[0]); if (cardsWithExactMatch?.length) { return [cardsWithExactMatch[0].shortLink]; } - console.log('Could not find Trello card with branch name, trying only with short ID', matches[1]); + logger_1.default.log('Could not find Trello card with branch name, trying only with short ID', matches[1]); const cardId = await getTrelloCardByShortId(matches[1]); if (cardId) { return [cardId]; @@ -34484,29 +34501,31 @@ const trello_1 = __nccwpck_require__(9763); const isChangesRequestedInReview_1 = __importDefault(__nccwpck_require__(4028)); const isPullRequestInDraft_1 = __importDefault(__nccwpck_require__(3031)); const isPullRequestApproved_1 = __importDefault(__nccwpck_require__(4414)); +const logger_1 = __importDefault(__nccwpck_require__(2358)); async function moveOrArchiveCards(conf, cardIds, pr) { + logger_1.default.log('--- MOVE OR ARCHIVE CARDS ---'); const isDraft = (0, isPullRequestInDraft_1.default)(pr); const isChangesRequested = await (0, isChangesRequestedInReview_1.default)(); const isApproved = await (0, isPullRequestApproved_1.default)(); const isMerged = await (0, github_1.isPullRequestMerged)(); if (pr.state === 'open' && isDraft && conf.trelloListIdPrDraft) { await moveCardsToList(cardIds, conf.trelloListIdPrDraft, conf.trelloBoardId); - console.log('Moved cards to draft PR list'); + logger_1.default.log('Moved cards to draft PR list'); return; } if (pr.state === 'open' && !isDraft && isChangesRequested && conf.trelloListIdPrChangesRequested) { await moveCardsToList(cardIds, conf.trelloListIdPrChangesRequested, conf.trelloBoardId); - console.log('Moved cards to changes requested PR list'); + logger_1.default.log('Moved cards to changes requested PR list'); return; } if (pr.state === 'open' && !isDraft && !isChangesRequested && isApproved && conf.trelloListIdPrApproved) { await moveCardsToList(cardIds, conf.trelloListIdPrApproved, conf.trelloBoardId); - console.log('Moved cards to approved PR list'); + logger_1.default.log('Moved cards to approved PR list'); return; } if (pr.state === 'open' && !isDraft && conf.trelloListIdPrOpen) { await moveCardsToList(cardIds, conf.trelloListIdPrOpen, conf.trelloBoardId); - console.log('Moved cards to opened PR list'); + logger_1.default.log('Moved cards to opened PR list'); return; } if (pr.state === 'closed' && isMerged && conf.trelloArchiveOnMerge) { @@ -34515,10 +34534,10 @@ async function moveOrArchiveCards(conf, cardIds, pr) { } if (pr.state === 'closed' && conf.trelloListIdPrClosed) { await moveCardsToList(cardIds, conf.trelloListIdPrClosed, conf.trelloBoardId); - console.log('Moved cards to closed PR list'); + logger_1.default.log('Moved cards to closed PR list'); return; } - console.log('Skipping moving and archiving the cards', { state: pr.state, isDraft, isMerged }); + logger_1.default.log('Skipping moving and archiving the cards', { state: pr.state, isDraft, isMerged }); } exports["default"] = moveOrArchiveCards; async function moveCardsToList(cardIds, listId, boardId) { @@ -34556,49 +34575,35 @@ const trello_1 = __nccwpck_require__(9763); const isChangesRequestedInReview_1 = __importDefault(__nccwpck_require__(4028)); const isPullRequestInDraft_1 = __importDefault(__nccwpck_require__(3031)); const isPullRequestApproved_1 = __importDefault(__nccwpck_require__(4414)); +const logger_1 = __importDefault(__nccwpck_require__(2358)); async function updateCardMembers(conf, cardIds, pr) { + logger_1.default.log('--- UPDATE CARD MEMBERS ---'); if (!conf.trelloAddMembersToCards) { - console.log('Skipping members updating'); - return; - } - console.log('Starting to update card members'); - if (conf.trelloSwitchMembersInReview) { - // Assigns PR reviewers to the card when the PR is in review - const inReview = await isPullRequestInReview(conf, pr); - if (inReview) { - await switchCardMembersToReviewers(conf, cardIds); - return; - } + return logger_1.default.log('Skipping members updating'); } - // Assigns PR author, committers and assignees to the PR - const contributors = await getPullRequestContributors(); - if (!contributors.length) { - console.log('No PR contributors found'); - return; + const inReview = await isPullRequestInReview(conf, pr); + if (inReview) { + await assignReviewers(conf, cardIds); } - const memberIds = await getTrelloMemberIds(conf, contributors); - if (!memberIds.length) { - console.log('No Trello members found based on PR contributors'); - return; + else { + await assignContributors(conf, cardIds); } - return Promise.all(cardIds.map(async (cardId) => { - const cardInfo = await (0, trello_1.getCardInfo)(cardId); - await addMembers(cardInfo, memberIds); - if (conf.trelloRemoveUnrelatedMembers) { - await removeUnrelatedMembers(cardInfo, memberIds); - } - })); } exports["default"] = updateCardMembers; async function isPullRequestInReview(conf, pr) { + const isInDraft = (0, isPullRequestInDraft_1.default)(pr); + const isChangesRequested = await (0, isChangesRequestedInReview_1.default)(); + const isApproved = await (0, isPullRequestApproved_1.default)(); + logger_1.default.log('Checking if PR is in review', { prState: pr.state, isInDraft, isChangesRequested, isApproved }); + if (!conf.trelloSwitchMembersInReview) { + return false; + } if (pr.state !== 'open') { return false; } if ((0, isPullRequestInDraft_1.default)(pr)) { return false; } - const isChangesRequested = await (0, isChangesRequestedInReview_1.default)(); - const isApproved = await (0, isPullRequestApproved_1.default)(); if (isChangesRequested && conf.trelloListIdPrChangesRequested) { return false; } @@ -34607,25 +34612,32 @@ async function isPullRequestInReview(conf, pr) { } return true; } -async function switchCardMembersToReviewers(conf, cardIds) { +async function assignReviewers(conf, cardIds) { const reviewers = await getReviewers(); + const memberIds = await getTrelloMemberIds(conf, reviewers); + logger_1.default.log('Removing contributors and assigning reviewers', { reviewers, memberIds }); return Promise.all(cardIds.map(async (cardId) => { const cardInfo = await (0, trello_1.getCardInfo)(cardId); - // Removes all current members from the card - await Promise.all(cardInfo.idMembers.map((memberId) => (0, trello_1.removeMemberFromCard)(cardInfo.id, memberId))); - // Assigns PR reviewers to the Trello card - const memberIds = await getTrelloMemberIds(conf, reviewers); - await addMembers(cardInfo, memberIds); + await removeMembers(cardId, cardInfo.idMembers); + await addMembers(cardId, memberIds); })); } -async function getReviewers() { - const reviews = await (0, github_1.getPullRequestReviews)(); - const requestedReviewers = await (0, github_1.getPullRequestRequestedReviewers)(); - const allReviewers = [ - ...reviews.filter((r) => r.state !== 'PENDING').map((r) => r.user?.login), - ...requestedReviewers?.users?.map((u) => u.login), - ].filter((username) => username !== undefined); - return allReviewers; +async function assignContributors(conf, cardIds) { + const contributors = await getPullRequestContributors(); + if (!contributors.length) { + logger_1.default.log('No PR contributors found'); + return; + } + const memberIds = await getTrelloMemberIds(conf, contributors); + if (!memberIds.length) { + logger_1.default.log('No Trello members found based on PR contributors'); + return; + } + return Promise.all(cardIds.map(async (cardId) => { + await addMembers(cardId, memberIds); + await removeUnrelatedMembers(conf, cardId, memberIds); + await removeReviewers(conf, cardId); + })); } async function getPullRequestContributors() { const pr = await (0, github_1.getPullRequest)(); @@ -34647,25 +34659,77 @@ async function getPullRequestContributors() { } return Array.from(contributors); } +async function addMembers(cardId, memberIds) { + const cardInfo = await (0, trello_1.getCardInfo)(cardId); + const filtered = memberIds.filter((id) => !cardInfo.idMembers.includes(id)); + if (!filtered.length) { + logger_1.default.log('All members are already assigned to the card'); + return; + } + return Promise.all(filtered.map((memberId) => (0, trello_1.addMemberToCard)(cardInfo.id, memberId))); +} +async function removeUnrelatedMembers(conf, cardId, memberIds) { + if (!conf.trelloRemoveUnrelatedMembers) { + return; + } + logger_1.default.log('Starting to remove unrelated members'); + const cardInfo = await (0, trello_1.getCardInfo)(cardId); + const filtered = cardInfo.idMembers.filter((id) => !memberIds.includes(id)); + if (!filtered.length) { + logger_1.default.log('Did not find any unrelated members'); + return; + } + return removeMembers(cardInfo.id, filtered); +} +async function removeReviewers(conf, cardId) { + if (!conf.trelloSwitchMembersInReview) { + return; + } + logger_1.default.log('Starting to remove reviewers from the card'); + const reviewers = await getReviewers(); + const memberIds = await getTrelloMemberIds(conf, reviewers); + const cardInfo = await (0, trello_1.getCardInfo)(cardId); + const filtered = memberIds.filter((id) => cardInfo.idMembers.includes(id)); + if (!filtered.length) { + logger_1.default.log('Did not find any reviewers assigned to the card'); + return; + } + return removeMembers(cardInfo.id, filtered); +} +async function removeMembers(cardId, memberIds) { + return Promise.all(memberIds.map((memberId) => (0, trello_1.removeMemberFromCard)(cardId, memberId))); +} +async function getReviewers() { + const reviews = await (0, github_1.getPullRequestReviews)(); + const requestedReviewers = await (0, github_1.getPullRequestRequestedReviewers)(); + return [ + ...reviews.filter((r) => r.state !== 'PENDING').map((r) => r.user?.login), + ...requestedReviewers.users?.map((u) => u.login), + ].filter((username) => username); +} async function getTrelloMemberIds(conf, githubUsernames) { - const result = await Promise.all(githubUsernames.map(async (githubUsername) => { + const memberIds = await Promise.all(githubUsernames.map(async (githubUsername) => { const username = getTrelloUsername(conf, githubUsername); - console.log('Searching Trello member id by username', username); + logger_1.default.log('Searching Trello member id by username', username); const member = await (0, trello_1.getMemberInfo)(username); if (!member) { return; } - console.log('Found member id by username', member.id, username); + logger_1.default.log('Found member id by username', { memberId: member.id, username }); if (conf.trelloOrganizationName) { const hasAccess = member.organizations?.some((org) => org.name === conf.trelloOrganizationName); if (!hasAccess) { - console.log('...but the member has no access to the org', conf.trelloOrganizationName); + logger_1.default.log('The member has no access to the org', { + orgName: conf.trelloOrganizationName, + memberId: member.id, + username, + }); return; } } return member.id; })); - return result.filter((id) => id); + return memberIds.filter((id) => id); } function getTrelloUsername(conf, githubUsername) { const username = githubUsername?.replace('-', '_'); @@ -34673,11 +34737,10 @@ function getTrelloUsername(conf, githubUsername) { if (!usernamesMap) { return username; } - console.log('Mapping Github users to Trello users'); for (const line of usernamesMap.split(/[\r\n]/)) { const parts = line.trim().split(':'); if (parts.length < 2) { - console.error('Mapping of Github user to Trello does not contain 2 usernames separated by ":"', line); + logger_1.default.error('Mapping of Github user to Trello does not contain 2 usernames separated by ":"', line); continue; } if (parts[0].trim() === githubUsername && parts[1].trim() !== '') { @@ -34686,22 +34749,6 @@ function getTrelloUsername(conf, githubUsername) { } return username; } -async function addMembers(cardInfo, memberIds) { - const filtered = memberIds.filter((id) => !cardInfo.idMembers.includes(id)); - if (!filtered.length) { - console.log('All members are already assigned to the card'); - return; - } - return Promise.all(filtered.map((memberId) => (0, trello_1.addMemberToCard)(cardInfo.id, memberId))); -} -async function removeUnrelatedMembers(cardInfo, memberIds) { - const filtered = cardInfo.idMembers.filter((id) => !memberIds.includes(id)); - if (!filtered.length) { - console.log('Did not find any unrelated members'); - return; - } - return Promise.all(filtered.map((unrelatedMemberId) => (0, trello_1.removeMemberFromCard)(cardInfo.id, unrelatedMemberId))); -} /***/ }), @@ -34772,11 +34819,15 @@ exports["default"] = isPullRequestApproved; /***/ }), /***/ 3031: -/***/ ((__unused_webpack_module, exports) => { +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); +const logger_1 = __importDefault(__nccwpck_require__(2358)); function isPullRequestInDraft(pr) { // Treat PRs with “draft” or “wip” in brackets at the start or // end of the titles like drafts. Useful for orgs on unpaid @@ -34785,13 +34836,35 @@ function isPullRequestInDraft(pr) { const isRealDraft = pr.draft === true; const isFauxDraft = Boolean(pr.title.match(titleDraftRegExp)); if (isFauxDraft) { - console.log('This PR is in faux draft'); + logger_1.default.log('This PR is in faux draft'); } return isRealDraft || isFauxDraft; } exports["default"] = isPullRequestInDraft; +/***/ }), + +/***/ 2358: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +exports["default"] = { + log: (...message) => { + if (!process.env.JEST_WORKER_ID) { + console.log(...message); // eslint-disable-line no-console + } + }, + error: (...message) => { + if (!process.env.JEST_WORKER_ID) { + console.error(...message); // eslint-disable-line no-console + } + }, +}; + + /***/ }), /***/ 9812: diff --git a/package.json b/package.json index c7b7e0b..2cabac2 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "collectCoverageFrom": [ "src/**", "!src/index.ts", + "!src/actions/utils/logger.ts", "!src/actions/api/**" ] }, @@ -63,10 +64,7 @@ ], "ignorePatterns": [ "dist" - ], - "rules": { - "no-console": 0 - } + ] }, "packageManager": "yarn@4.1.0+sha512.5b7bc055cad63273dda27df1570a5d2eb4a9f03b35b394d3d55393c2a5560a17f5cef30944b11d6a48bcbcfc1c3a26d618aae77044774c529ba36cb771ad5b0f" } diff --git a/src/actions/addCardLinksToPullRequest.ts b/src/actions/addCardLinksToPullRequest.ts index 5726a3a..4e46804 100644 --- a/src/actions/addCardLinksToPullRequest.ts +++ b/src/actions/addCardLinksToPullRequest.ts @@ -1,9 +1,12 @@ import { Conf } from '../types' import { createComment, getPullRequest, getPullRequestComments } from './api/github' import { getCardInfo } from './api/trello' +import logger from './utils/logger' import matchCardIds from './utils/matchCardIds' export default async function addCardLinksToPullRequest(conf: Conf, cardIds: string[]) { + logger.log('--- ADD CARD LINKS TO PR ---') + const bodyCardIds = await getCardIdsFromBody(conf) const commentsCardIds = await getCardIdsFromComments(conf) const linkedCardIds = [...bodyCardIds, ...commentsCardIds] @@ -11,11 +14,11 @@ export default async function addCardLinksToPullRequest(conf: Conf, cardIds: str const unlinkedCardIds = cardIds.filter((id) => !linkedCardIds.includes(id)) if (!unlinkedCardIds.length) { - console.log('Skipping card linking as all cards are already mentioned under the PR') + logger.log('Skipping card linking as all cards are already mentioned under the PR') return } - console.log('Commenting Trello card URLs to PR', unlinkedCardIds) + logger.log('Commenting Trello card URLs to PR', unlinkedCardIds) const cards = await Promise.all(unlinkedCardIds.map((id) => getCardInfo(id))) const urls = cards.map((card) => card.shortUrl) diff --git a/src/actions/addLabelToCards.ts b/src/actions/addLabelToCards.ts index 15d976a..e6d1729 100644 --- a/src/actions/addLabelToCards.ts +++ b/src/actions/addLabelToCards.ts @@ -1,19 +1,20 @@ import { BoardLabel, Conf, PRHead } from '../types' import { getBranchName } from './api/github' import { addLabelToCard, getBoardLabels, getCardInfo } from './api/trello' +import logger from './utils/logger' export default async function addLabelToCards(conf: Conf, cardIds: string[], head?: PRHead) { + logger.log('--- ADD LABEL TO CARDS ---') + if (!conf.trelloAddLabelsToCards) { - console.log('Skipping label adding') + logger.log('Skipping label adding') return } - console.log('Starting to add labels to cards') - const branchLabel = await getBranchLabel(head) if (!branchLabel) { - console.log('Could not find branch label') + logger.log('Could not find branch label') return } @@ -26,7 +27,7 @@ export default async function addLabelToCards(conf: Conf, cardIds: string[], hea ) if (hasConflictingLabel) { - console.log('Skipping label adding to a card because it has a conflicting label', cardInfo.labels) + logger.log('Skipping label adding to a card as it has a conflicting label', cardInfo.labels) return } @@ -36,7 +37,7 @@ export default async function addLabelToCards(conf: Conf, cardIds: string[], hea if (matchingLabel) { await addLabelToCard(cardId, matchingLabel.id) } else { - console.log('Could not find a matching label from the board', branchLabel, boardLabels) + logger.log('Could not find a matching label from the board', { branchLabel, boardLabels }) } }), ) @@ -49,7 +50,7 @@ async function getBranchLabel(prHead?: PRHead) { if (matches) { return matches[1] } else { - console.log('Did not find branch label', branchName) + logger.log('Did not find branch label', branchName) } } @@ -59,7 +60,7 @@ function findMatchingLabel(branchLabel: string, boardLabels: BoardLabel[]) { if (match) { return match } - console.log('Could not match the exact label name, trying to find partially matching label') + logger.log('Could not match the exact label name, trying to find partially matching label') return boardLabels.find((label) => branchLabel.startsWith(label.name)) } diff --git a/src/actions/addPullRequestLinkToCards.ts b/src/actions/addPullRequestLinkToCards.ts index 0f022a3..4a40087 100644 --- a/src/actions/addPullRequestLinkToCards.ts +++ b/src/actions/addPullRequestLinkToCards.ts @@ -1,7 +1,10 @@ import { PR } from '../types' import { addAttachmentToCard, getCardAttachments } from './api/trello' +import logger from './utils/logger' export default async function addPullRequestLinkToCards(cardIds: string[], pr: PR) { + logger.log('--- ADD PR LINK TO CARDS ---') + const link = pr.html_url || pr.url return Promise.all( @@ -9,7 +12,7 @@ export default async function addPullRequestLinkToCards(cardIds: string[], pr: P const existingAttachments = await getCardAttachments(cardId) if (existingAttachments?.some((it) => it.url.includes(link))) { - console.log('Found existing attachment, skipping adding attachment', cardId, link) + logger.log('Found existing attachment, skipping adding attachment', { cardId, link }) return } diff --git a/src/actions/api/github.ts b/src/actions/api/github.ts index a57fc80..abb90fd 100644 --- a/src/actions/api/github.ts +++ b/src/actions/api/github.ts @@ -1,5 +1,6 @@ import { getInput } from '@actions/core' import { getOctokit, context } from '@actions/github' +import logger from '../utils/logger' const githubToken = getInput('github-token', { required: true }) @@ -84,7 +85,7 @@ export async function getPullRequestRequestedReviewers() { } export async function createComment(shortUrl: string) { - console.log('Creating PR comment', shortUrl) + logger.log('Creating PR comment', shortUrl) await octokit.rest.issues.createComment({ owner, @@ -95,7 +96,7 @@ export async function createComment(shortUrl: string) { } export async function updatePullRequestBody(newBody: string) { - console.log('Updating PR body', newBody) + logger.log('Updating PR body', newBody) await octokit.rest.issues.update({ owner, diff --git a/src/actions/api/trello.ts b/src/actions/api/trello.ts index 52640d7..f02d1ec 100644 --- a/src/actions/api/trello.ts +++ b/src/actions/api/trello.ts @@ -1,6 +1,7 @@ import axios from 'axios' import * as core from '@actions/core' import { BoardLabel } from '../../types' +import logger from '../utils/logger' const trelloApiKey = core.getInput('trello-api-key', { required: true }) const trelloAuthToken = core.getInput('trello-auth-token', { required: true }) @@ -27,6 +28,14 @@ export async function getCardInfo( return response?.data } +export async function getMemberInfo(username?: string): Promise<{ id: string; organizations: { name: string }[] }> { + const response = await makeRequest('get', `https://api.trello.com/1/members/${username}`, { + organizations: 'all', + }) + + return response?.data +} + export async function getCardAttachments(cardId: string): Promise<{ url: string }[]> { const response = await makeRequest('get', `https://api.trello.com/1/cards/${cardId}/attachments`) @@ -34,19 +43,11 @@ export async function getCardAttachments(cardId: string): Promise<{ url: string } export async function addAttachmentToCard(cardId: string, link: string) { - console.log('Adding attachment to the card', cardId, link) + logger.log('Adding attachment to the card', { cardId, link }) return makeRequest('post', `https://api.trello.com/1/cards/${cardId}/attachments`, { url: link }) } -export async function addMemberToCard(cardId: string, memberId: string) { - console.log('Adding member to a card', cardId, memberId) - - return makeRequest('post', `https://api.trello.com/1/cards/${cardId}/idMembers`, { - value: memberId, - }) -} - export async function getBoardLabels(boardId: string): Promise { const response = await makeRequest('get', `https://api.trello.com/1/boards/${boardId}/labels`) @@ -55,28 +56,36 @@ export async function getBoardLabels(boardId: string): Promise { return response?.data?.filter((label: { name: string }) => label.name) } -export async function getBoardLists(boardId: string): Promise<{ id: string }[]> { - const response = await makeRequest('get', `https://api.trello.com/1/boards/${boardId}/lists`) - - return response?.data -} - export async function addLabelToCard(cardId: string, labelId: string) { - console.log('Adding label to a card', cardId, labelId) + logger.log('Adding label to a card', { cardId, labelId }) return makeRequest('post', `https://api.trello.com/1/cards/${cardId}/idLabels`, { value: labelId, }) } +export async function addMemberToCard(cardId: string, memberId: string) { + logger.log('Adding member to a card', { cardId, memberId }) + + return makeRequest('post', `https://api.trello.com/1/cards/${cardId}/idMembers`, { + value: memberId, + }) +} + export async function removeMemberFromCard(cardId: string, memberId: string) { - console.log('Removing card member', cardId, memberId) + logger.log('Removing member from a card', { cardId, memberId }) return makeRequest('delete', `https://api.trello.com/1/cards/${cardId}/idMembers/${memberId}`) } +export async function getBoardLists(boardId: string): Promise<{ id: string }[]> { + const response = await makeRequest('get', `https://api.trello.com/1/boards/${boardId}/lists`) + + return response?.data +} + export async function moveCardToList(cardId: string, listId: string, boardId?: string) { - console.log('Moving card to list', cardId, listId) + logger.log('Moving card to list', { cardId, listId, boardId }) return makeRequest('put', `https://api.trello.com/1/cards/${cardId}`, { pos: trelloCardPosition, @@ -86,27 +95,19 @@ export async function moveCardToList(cardId: string, listId: string, boardId?: s } export async function archiveCard(cardId: string) { - console.log('Archiving card', cardId) + logger.log('ARCHIVE: Archiving card', { cardId }) return makeRequest('put', `https://api.trello.com/1/cards/${cardId}`, { closed: true, }) } -export async function getMemberInfo(username?: string): Promise<{ id: string; organizations: { name: string }[] }> { - const response = await makeRequest('get', `https://api.trello.com/1/members/${username}`, { - organizations: 'all', - }) - - return response?.data -} - export async function createCard( listId: string, title: string, body?: string, ): Promise<{ id: string; url: string; shortLink: string }> { - console.log('Creating card based on PR info', title, body) + logger.log('Creating card based on PR info', { title, body }) const response = await makeRequest('post', `https://api.trello.com/1/cards`, { idList: listId, @@ -144,7 +145,7 @@ async function makeRequest(method: 'get' | 'put' | 'post' | 'delete', url: strin message: error.message, }, } - console.error(JSON.stringify(errorMessage, null, 2)) + logger.error(JSON.stringify(errorMessage, null, 2)) throw error } diff --git a/src/actions/getCardIds.ts b/src/actions/getCardIds.ts index d68947c..a2e0c9a 100644 --- a/src/actions/getCardIds.ts +++ b/src/actions/getCardIds.ts @@ -4,9 +4,10 @@ import { getBranchName, getCommits, getPullRequest, getPullRequestComments, upda import { createCard, searchTrelloCards } from './api/trello' import matchCardIds from './utils/matchCardIds' import isPullRequestInDraft from './utils/isPullRequestInDraft' +import logger from './utils/logger' export default async function getCardIds(conf: Conf, pr: PR) { - console.log('Searching for card IDs') + logger.log('--- FIND CARDS ---') const latestPRInfo = (await getPullRequest()) || pr let cardIds = matchCardIds(conf, latestPRInfo.body || '') @@ -42,11 +43,11 @@ export default async function getCardIds(conf: Conf, pr: PR) { } if (cardIds.length) { - console.log('Found card IDs', cardIds) + logger.log('Found card IDs', cardIds) return [...new Set(cardIds)] } else { - console.log('Could not find card IDs') + logger.log('Could not find card IDs') if (conf.githubRequireTrelloCard) { setFailed('The PR does not contain a link to a Trello card') @@ -59,13 +60,13 @@ export default async function getCardIds(conf: Conf, pr: PR) { async function getCardIdsFromBranchName(conf: Conf, prHead?: PRHead) { const branchName = prHead?.ref || (await getBranchName()) - console.log('Searching cards from branch name', branchName) + logger.log('Searching cards from branch name', branchName) if (conf.githubAllowMultipleCardsInPrBranchName) { const shortIdMatches = branchName.match(/(?<=^|\/)\d+(?:-\d+)+/gi)?.[0].split('-') if (shortIdMatches && shortIdMatches.length > 1) { - console.log('Matched multiple potential Trello short IDs from branch name', shortIdMatches) + logger.log('Matched multiple potential Trello short IDs from branch name', shortIdMatches) const potentialCardIds = await Promise.all( shortIdMatches.map((shortId: string) => getTrelloCardByShortId(shortId, conf.trelloBoardId)), @@ -80,7 +81,7 @@ async function getCardIdsFromBranchName(conf: Conf, prHead?: PRHead) { const matches = branchName.match(/(?<=^|\/)(\d+)-\S+/i) if (matches) { - console.log('Matched one potential card from branch name', matches) + logger.log('Matched one potential card from branch name', matches) const cardsWithExactMatch = await searchTrelloCards(matches[0]) @@ -88,7 +89,7 @@ async function getCardIdsFromBranchName(conf: Conf, prHead?: PRHead) { return [cardsWithExactMatch[0].shortLink] } - console.log('Could not find Trello card with branch name, trying only with short ID', matches[1]) + logger.log('Could not find Trello card with branch name, trying only with short ID', matches[1]) const cardId = await getTrelloCardByShortId(matches[1]) diff --git a/src/actions/moveOrArchiveCards.ts b/src/actions/moveOrArchiveCards.ts index c36ca84..7ee746f 100644 --- a/src/actions/moveOrArchiveCards.ts +++ b/src/actions/moveOrArchiveCards.ts @@ -4,8 +4,11 @@ import { archiveCard, getBoardLists, getCardInfo, moveCardToList } from './api/t import isChangesRequestedInReview from './utils/isChangesRequestedInReview' import isPullRequestInDraft from './utils/isPullRequestInDraft' import isPullRequestApproved from './utils/isPullRequestApproved' +import logger from './utils/logger' export default async function moveOrArchiveCards(conf: Conf, cardIds: string[], pr: PR) { + logger.log('--- MOVE OR ARCHIVE CARDS ---') + const isDraft = isPullRequestInDraft(pr) const isChangesRequested = await isChangesRequestedInReview() const isApproved = await isPullRequestApproved() @@ -13,28 +16,28 @@ export default async function moveOrArchiveCards(conf: Conf, cardIds: string[], if (pr.state === 'open' && isDraft && conf.trelloListIdPrDraft) { await moveCardsToList(cardIds, conf.trelloListIdPrDraft, conf.trelloBoardId) - console.log('Moved cards to draft PR list') + logger.log('Moved cards to draft PR list') return } if (pr.state === 'open' && !isDraft && isChangesRequested && conf.trelloListIdPrChangesRequested) { await moveCardsToList(cardIds, conf.trelloListIdPrChangesRequested, conf.trelloBoardId) - console.log('Moved cards to changes requested PR list') + logger.log('Moved cards to changes requested PR list') return } if (pr.state === 'open' && !isDraft && !isChangesRequested && isApproved && conf.trelloListIdPrApproved) { await moveCardsToList(cardIds, conf.trelloListIdPrApproved, conf.trelloBoardId) - console.log('Moved cards to approved PR list') + logger.log('Moved cards to approved PR list') return } if (pr.state === 'open' && !isDraft && conf.trelloListIdPrOpen) { await moveCardsToList(cardIds, conf.trelloListIdPrOpen, conf.trelloBoardId) - console.log('Moved cards to opened PR list') + logger.log('Moved cards to opened PR list') return } @@ -47,12 +50,12 @@ export default async function moveOrArchiveCards(conf: Conf, cardIds: string[], if (pr.state === 'closed' && conf.trelloListIdPrClosed) { await moveCardsToList(cardIds, conf.trelloListIdPrClosed, conf.trelloBoardId) - console.log('Moved cards to closed PR list') + logger.log('Moved cards to closed PR list') return } - console.log('Skipping moving and archiving the cards', { state: pr.state, isDraft, isMerged }) + logger.log('Skipping moving and archiving the cards', { state: pr.state, isDraft, isMerged }) } async function moveCardsToList(cardIds: string[], listId: string, boardId?: string) { diff --git a/src/actions/updateCardMembers.test.ts b/src/actions/updateCardMembers.test.ts index e3abb77..48fea35 100644 --- a/src/actions/updateCardMembers.test.ts +++ b/src/actions/updateCardMembers.test.ts @@ -68,6 +68,28 @@ it('adds committer to the card', async () => { expect(addMemberToCard).toHaveBeenCalledWith('card', 'john-id') }) +it('ignores incorrectly configured usernames mapping', async () => { + await updateCardMembers({ ...conf, githubUsersToTrelloUsers: 'phil' }, ['card'], pr) + + expect(getMemberInfoMock).toHaveBeenCalledWith('phil') +}) + +it('removes only reviewers when unrelated members removing is turned off but switching members in review is on', async () => { + getPullRequestRequestedReviewersMock.mockResolvedValue({ users: [] }) + getPullRequestReviewsMock.mockResolvedValue([{ state: 'ACTIVE', user: { login: 'amy' } }]) + getMemberInfoMock.mockImplementation((username) => ({ id: username })) + getCardInfoMock.mockResolvedValue({ id: 'card', idMembers: ['amy1993', 'jones'] }) + + await updateCardMembers( + { ...conf, trelloRemoveUnrelatedMembers: false, trelloSwitchMembersInReview: true }, + ['card'], + pr, + ) + + expect(removeMemberFromCard).toHaveBeenCalledTimes(1) + expect(removeMemberFromCard).toHaveBeenCalledWith('card', 'amy1993') +}) + it('skips removing unrelated members when none found', async () => { getMemberInfoMock.mockResolvedValue({ id: 'phil-id' }) getCardInfoMock.mockResolvedValue({ id: 'card', idMembers: [] }) @@ -77,6 +99,21 @@ it('skips removing unrelated members when none found', async () => { expect(removeMemberFromCard).not.toHaveBeenCalled() }) +it('skips removing reviewers when none found', async () => { + getPullRequestRequestedReviewersMock.mockResolvedValue({ users: [] }) + getPullRequestReviewsMock.mockResolvedValue([]) + getMemberInfoMock.mockImplementation((username) => ({ id: username })) + getCardInfoMock.mockResolvedValue({ id: 'card', idMembers: ['amy', 'jones'] }) + + await updateCardMembers( + { ...conf, trelloRemoveUnrelatedMembers: false, trelloSwitchMembersInReview: true }, + ['card'], + pr, + ) + + expect(removeMemberFromCard).not.toHaveBeenCalled() +}) + it('skips removing unrelated members when turned off', async () => { getMemberInfoMock.mockResolvedValue({ id: 'phil-id' }) getCardInfoMock.mockResolvedValue({ id: 'card', idMembers: ['jones-id'] }) @@ -95,12 +132,6 @@ it('skips adding when all members are already assigned to the card', async () => expect(addMemberToCard).not.toHaveBeenCalled() }) -it('ignores incorrectly configured usernames mapping', async () => { - await updateCardMembers({ ...conf, githubUsersToTrelloUsers: 'phil' }, ['card'], pr) - - expect(getMemberInfoMock).toHaveBeenCalledWith('phil') -}) - it('skips adding when member not found with GitHub username', async () => { getMemberInfoMock.mockResolvedValue(undefined) @@ -140,7 +171,7 @@ describe('switching card members with reviewers when PR is in review', () => { pr = { ...pr, draft: false } conf = { ...conf, githubUsersToTrelloUsers: undefined, trelloSwitchMembersInReview: true } - getMemberInfoMock.mockImplementation((username) => ({ id: username })) + getMemberInfoMock.mockImplementation((username) => (username !== 'phil' ? { id: username } : null)) getCardInfoMock.mockResolvedValue({ id: 'card', idMembers: ['phil'] }) getPullRequestRequestedReviewersMock.mockResolvedValue({ users: [{ login: 'amy' }] }) getPullRequestReviewsMock.mockResolvedValue([ @@ -158,6 +189,15 @@ describe('switching card members with reviewers when PR is in review', () => { expect(addMemberToCard).toHaveBeenCalledWith('card', 'mike') }) + it('only removes all existing members when reviewers missing', async () => { + getMemberInfoMock.mockImplementation(() => null) + + await updateCardMembers(conf, ['card'], pr) + + expect(removeMemberFromCard).toHaveBeenCalledWith('card', 'phil') + expect(addMemberToCard).not.toHaveBeenCalled() + }) + it('skips when PR not open', async () => { await updateCardMembers(conf, ['card'], { ...pr, state: 'closed' }) diff --git a/src/actions/updateCardMembers.ts b/src/actions/updateCardMembers.ts index 2594c77..5e324c5 100644 --- a/src/actions/updateCardMembers.ts +++ b/src/actions/updateCardMembers.ts @@ -1,68 +1,42 @@ -import { Card, Conf, PR } from '../types' +import { Conf, PR } from '../types' import { getCommits, getPullRequest, getPullRequestRequestedReviewers, getPullRequestReviews } from './api/github' import { addMemberToCard, getCardInfo, getMemberInfo, removeMemberFromCard } from './api/trello' import isChangesRequestedInReview from './utils/isChangesRequestedInReview' import isPullRequestInDraft from './utils/isPullRequestInDraft' import isPullRequestApproved from './utils/isPullRequestApproved' +import logger from './utils/logger' export default async function updateCardMembers(conf: Conf, cardIds: string[], pr: PR) { - if (!conf.trelloAddMembersToCards) { - console.log('Skipping members updating') + logger.log('--- UPDATE CARD MEMBERS ---') - return + if (!conf.trelloAddMembersToCards) { + return logger.log('Skipping members updating') } - console.log('Starting to update card members') - - if (conf.trelloSwitchMembersInReview) { - // Assigns PR reviewers to the card when the PR is in review - const inReview = await isPullRequestInReview(conf, pr) + const inReview = await isPullRequestInReview(conf, pr) - if (inReview) { - await switchCardMembersToReviewers(conf, cardIds) - - return - } + if (inReview) { + await assignReviewers(conf, cardIds) + } else { + await assignContributors(conf, cardIds) } +} - // Assigns PR author, committers and assignees to the PR - const contributors = await getPullRequestContributors() - - if (!contributors.length) { - console.log('No PR contributors found') - - return - } - const memberIds = await getTrelloMemberIds(conf, contributors) +async function isPullRequestInReview(conf: Conf, pr: PR) { + const isInDraft = isPullRequestInDraft(pr) + const isChangesRequested = await isChangesRequestedInReview() + const isApproved = await isPullRequestApproved() - if (!memberIds.length) { - console.log('No Trello members found based on PR contributors') + logger.log('Checking if PR is in review', { prState: pr.state, isInDraft, isChangesRequested, isApproved }) - return + if (!conf.trelloSwitchMembersInReview) { + return false } - - return Promise.all( - cardIds.map(async (cardId) => { - const cardInfo = await getCardInfo(cardId) - - await addMembers(cardInfo, memberIds) - - if (conf.trelloRemoveUnrelatedMembers) { - await removeUnrelatedMembers(cardInfo, memberIds) - } - }), - ) -} - -async function isPullRequestInReview(conf: Conf, pr: PR) { if (pr.state !== 'open') { return false } if (isPullRequestInDraft(pr)) { return false } - const isChangesRequested = await isChangesRequestedInReview() - const isApproved = await isPullRequestApproved() - if (isChangesRequested && conf.trelloListIdPrChangesRequested) { return false } @@ -73,32 +47,45 @@ async function isPullRequestInReview(conf: Conf, pr: PR) { return true } -async function switchCardMembersToReviewers(conf: Conf, cardIds: string[]) { +async function assignReviewers(conf: Conf, cardIds: string[]) { const reviewers = await getReviewers() + const memberIds = await getTrelloMemberIds(conf, reviewers) + + logger.log('Removing contributors and assigning reviewers', { reviewers, memberIds }) return Promise.all( cardIds.map(async (cardId) => { const cardInfo = await getCardInfo(cardId) - // Removes all current members from the card - await Promise.all(cardInfo.idMembers.map((memberId: string) => removeMemberFromCard(cardInfo.id, memberId))) - - // Assigns PR reviewers to the Trello card - const memberIds = await getTrelloMemberIds(conf, reviewers) - await addMembers(cardInfo, memberIds) + await removeMembers(cardId, cardInfo.idMembers) + await addMembers(cardId, memberIds) }), ) } -async function getReviewers() { - const reviews = await getPullRequestReviews() - const requestedReviewers = await getPullRequestRequestedReviewers() - const allReviewers = [ - ...reviews.filter((r) => r.state !== 'PENDING').map((r) => r.user?.login), - ...requestedReviewers?.users?.map((u) => u.login), - ].filter((username) => username !== undefined) +async function assignContributors(conf: Conf, cardIds: string[]) { + const contributors = await getPullRequestContributors() + + if (!contributors.length) { + logger.log('No PR contributors found') + + return + } + const memberIds = await getTrelloMemberIds(conf, contributors) + + if (!memberIds.length) { + logger.log('No Trello members found based on PR contributors') + + return + } - return allReviewers as string[] + return Promise.all( + cardIds.map(async (cardId) => { + await addMembers(cardId, memberIds) + await removeUnrelatedMembers(conf, cardId, memberIds) + await removeReviewers(conf, cardId) + }), + ) } async function getPullRequestContributors() { @@ -127,25 +114,95 @@ async function getPullRequestContributors() { return Array.from(contributors) } +async function addMembers(cardId: string, memberIds: string[]) { + const cardInfo = await getCardInfo(cardId) + const filtered = memberIds.filter((id) => !cardInfo.idMembers.includes(id)) + + if (!filtered.length) { + logger.log('All members are already assigned to the card') + + return + } + + return Promise.all(filtered.map((memberId) => addMemberToCard(cardInfo.id, memberId))) +} + +async function removeUnrelatedMembers(conf: Conf, cardId: string, memberIds: string[]) { + if (!conf.trelloRemoveUnrelatedMembers) { + return + } + logger.log('Starting to remove unrelated members') + + const cardInfo = await getCardInfo(cardId) + const filtered = cardInfo.idMembers.filter((id) => !memberIds.includes(id)) + + if (!filtered.length) { + logger.log('Did not find any unrelated members') + + return + } + + return removeMembers(cardInfo.id, filtered) +} + +async function removeReviewers(conf: Conf, cardId: string) { + if (!conf.trelloSwitchMembersInReview) { + return + } + + logger.log('Starting to remove reviewers from the card') + + const reviewers = await getReviewers() + const memberIds = await getTrelloMemberIds(conf, reviewers) + const cardInfo = await getCardInfo(cardId) + const filtered = memberIds.filter((id) => cardInfo.idMembers.includes(id)) + + if (!filtered.length) { + logger.log('Did not find any reviewers assigned to the card') + + return + } + + return removeMembers(cardInfo.id, filtered) +} + +async function removeMembers(cardId: string, memberIds: string[]) { + return Promise.all(memberIds.map((memberId) => removeMemberFromCard(cardId, memberId))) +} + +async function getReviewers() { + const reviews = await getPullRequestReviews() + const requestedReviewers = await getPullRequestRequestedReviewers() + + return [ + ...reviews.filter((r) => r.state !== 'PENDING').map((r) => r.user?.login), + ...requestedReviewers.users?.map((u) => u.login), + ].filter((username) => username) as string[] +} + async function getTrelloMemberIds(conf: Conf, githubUsernames: string[]) { - const result = await Promise.all( + const memberIds = await Promise.all( githubUsernames.map(async (githubUsername) => { const username = getTrelloUsername(conf, githubUsername) - console.log('Searching Trello member id by username', username) + logger.log('Searching Trello member id by username', username) const member = await getMemberInfo(username) if (!member) { return } - console.log('Found member id by username', member.id, username) + logger.log('Found member id by username', { memberId: member.id, username }) if (conf.trelloOrganizationName) { const hasAccess = member.organizations?.some((org) => org.name === conf.trelloOrganizationName) if (!hasAccess) { - console.log('...but the member has no access to the org', conf.trelloOrganizationName) + logger.log('The member has no access to the org', { + orgName: conf.trelloOrganizationName, + memberId: member.id, + username, + }) return } @@ -155,7 +212,7 @@ async function getTrelloMemberIds(conf: Conf, githubUsernames: string[]) { }), ) - return result.filter((id) => id) as string[] + return memberIds.filter((id) => id) as string[] } function getTrelloUsername(conf: Conf, githubUsername?: string) { @@ -165,13 +222,12 @@ function getTrelloUsername(conf: Conf, githubUsername?: string) { if (!usernamesMap) { return username } - console.log('Mapping Github users to Trello users') for (const line of usernamesMap.split(/[\r\n]/)) { const parts = line.trim().split(':') if (parts.length < 2) { - console.error('Mapping of Github user to Trello does not contain 2 usernames separated by ":"', line) + logger.error('Mapping of Github user to Trello does not contain 2 usernames separated by ":"', line) continue } if (parts[0].trim() === githubUsername && parts[1].trim() !== '') { @@ -181,29 +237,3 @@ function getTrelloUsername(conf: Conf, githubUsername?: string) { return username } - -async function addMembers(cardInfo: Card, memberIds: string[]) { - const filtered = memberIds.filter((id) => !cardInfo.idMembers.includes(id)) - - if (!filtered.length) { - console.log('All members are already assigned to the card') - - return - } - - return Promise.all(filtered.map((memberId) => addMemberToCard(cardInfo.id, memberId))) -} - -async function removeUnrelatedMembers(cardInfo: Card, memberIds: string[]) { - const filtered = cardInfo.idMembers.filter((id: string) => !memberIds.includes(id)) - - if (!filtered.length) { - console.log('Did not find any unrelated members') - - return - } - - return Promise.all( - filtered.map((unrelatedMemberId: string) => removeMemberFromCard(cardInfo.id, unrelatedMemberId)), - ) -} diff --git a/src/actions/utils/isPullRequestInDraft.ts b/src/actions/utils/isPullRequestInDraft.ts index 6bc39a4..cbf18a3 100644 --- a/src/actions/utils/isPullRequestInDraft.ts +++ b/src/actions/utils/isPullRequestInDraft.ts @@ -1,3 +1,5 @@ +import logger from './logger' + export default function isPullRequestInDraft(pr: any) { // Treat PRs with “draft” or “wip” in brackets at the start or // end of the titles like drafts. Useful for orgs on unpaid @@ -7,7 +9,7 @@ export default function isPullRequestInDraft(pr: any) { const isFauxDraft = Boolean(pr.title.match(titleDraftRegExp)) if (isFauxDraft) { - console.log('This PR is in faux draft') + logger.log('This PR is in faux draft') } return isRealDraft || isFauxDraft diff --git a/src/actions/utils/logger.ts b/src/actions/utils/logger.ts new file mode 100644 index 0000000..78aec4a --- /dev/null +++ b/src/actions/utils/logger.ts @@ -0,0 +1,12 @@ +export default { + log: (...message: any[]) => { + if (!process.env.JEST_WORKER_ID) { + console.log(...message) // eslint-disable-line no-console + } + }, + error: (...message: any[]) => { + if (!process.env.JEST_WORKER_ID) { + console.error(...message) // eslint-disable-line no-console + } + }, +}