Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New trello card command #71

Merged
merged 10 commits into from
Dec 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ This action scans PR description and comments for Trello card URL(s) or branch n
- Assigns a PR author and fellow assignees to a Trello card.
- And more...

You can also optionally use the action to create new Trello card by adding `/new-trello-card` in the PR description.

## Basic configuration

```yaml
Expand Down Expand Up @@ -51,6 +53,11 @@ github-include-pr-comments: true
# DEFAULT: false
github-include-pr-branch-name: false

# Creates a new Trello card from PR details if "/new-trello-card" is written in the PR description.
# Replaces "/new-trello-card" with the card link.
# DEFAULT: false
github-include-new-card-command: false

# Only matches Trello URLs prefixed with "Closes" etc.
# Just like https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword
# DEFAULT: false
Expand Down
5 changes: 4 additions & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ inputs:
github-include-pr-branch-name:
description: Include PR branch name when searching for Trello cards (e.g. "1234-card-title"). If card ID is found, it automatically comments card URL to the PR.
default: false
github-include-new-card-command:
description: Creates a new Trello card from PR details if "/new-trello-card" is written in the PR description. Replaces "/new-trello-card" with the card link.
default: false
github-users-to-trello-users:
description: |-
Newline-separated list of mapping between Github username and Trello username. Example:
Expand Down Expand Up @@ -51,7 +54,7 @@ inputs:
description: Position of the card after being moved to a list. Can be "top" or "bottom".
default: 'top'
trello-remove-unrelated-members:
description: Enable or disable the removal of unrelated users on Trello cards.
description: Removes card members who are not authors or assignees of the PR.
default: true
runs:
using: node20
Expand Down
79 changes: 63 additions & 16 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33154,7 +33154,7 @@ function wrappy (fn, cb) {
"use strict";

Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.createComment = exports.getBranchName = exports.getPullRequestAssignees = exports.getPullRequestComments = void 0;
exports.updatePullRequestBody = exports.createComment = exports.getBranchName = exports.getPullRequest = exports.getPullRequestComments = void 0;
const core_1 = __nccwpck_require__(2186);
const github_1 = __nccwpck_require__(5438);
const githubToken = (0, core_1.getInput)('github-token', { required: true });
Expand All @@ -33171,15 +33171,15 @@ async function getPullRequestComments() {
return response.data;
}
exports.getPullRequestComments = getPullRequestComments;
async function getPullRequestAssignees() {
async function getPullRequest() {
const response = await octokit.rest.issues.get({
owner: repoOwner,
repo: payload.repository.name,
issue_number: issueNumber,
});
return [...(response.data.assignees || []), response.data.user];
return response.data;
}
exports.getPullRequestAssignees = getPullRequestAssignees;
exports.getPullRequest = getPullRequest;
async function getBranchName() {
const response = await octokit.rest.pulls.get({
owner: repoOwner,
Expand All @@ -33190,6 +33190,7 @@ async function getBranchName() {
}
exports.getBranchName = getBranchName;
async function createComment(shortUrl) {
console.log('Creating PR comment', shortUrl);
await octokit.rest.issues.createComment({
owner: repoOwner,
repo: payload.repository.name,
Expand All @@ -33198,6 +33199,16 @@ async function createComment(shortUrl) {
});
}
exports.createComment = createComment;
async function updatePullRequestBody(newBody) {
console.log('Updating PR body', newBody);
await octokit.rest.issues.update({
owner: repoOwner,
repo: payload.repository.name,
issue_number: issueNumber,
body: newBody,
});
}
exports.updatePullRequestBody = updatePullRequestBody;


/***/ }),
Expand Down Expand Up @@ -33239,6 +33250,7 @@ const main_1 = __nccwpck_require__(399);
githubRequireTrelloCard: core.getBooleanInput('github-require-trello-card'),
githubIncludePrComments: core.getBooleanInput('github-include-pr-comments'),
githubIncludePrBranchName: core.getBooleanInput('github-include-pr-branch-name'),
githubIncludeNewCardCommand: core.getBooleanInput('github-include-new-card-command'),
githubUsersToTrelloUsers: core.getInput('github-users-to-trello-users'),
trelloOrganizationName: core.getInput('trello-organization-name'),
trelloListIdPrDraft: core.getInput('trello-list-id-pr-draft'),
Expand Down Expand Up @@ -33266,14 +33278,13 @@ const trelloRequests_1 = __nccwpck_require__(777);
async function run(pr, conf = {}) {
try {
const comments = await (0, githubRequests_1.getPullRequestComments)();
const cardIds = await getCardIds(conf, pr.head, pr.body, comments);
const cardIds = await getCardIds(conf, pr, comments);
if (cardIds.length) {
console.log('Found card IDs', cardIds);
await moveCards(conf, cardIds, pr);
await addPRLinkToCards(cardIds, pr.html_url || pr.url);
await addCardLinkToPR(conf, cardIds, pr.body, comments);
await updateCardMembers(conf, cardIds);
await addCardLinkToPR(conf, cardIds, pr, comments);
await addLabelToCards(conf, cardIds, pr.head);
await updateCardMembers(conf, cardIds);
}
}
catch (error) {
Expand All @@ -33282,20 +33293,26 @@ async function run(pr, conf = {}) {
}
}
exports.run = run;
async function getCardIds(conf, prHead, prBody = '', comments) {
async function getCardIds(conf, pr, comments) {
console.log('Searching for card ids');
let cardIds = matchCardIds(conf, prBody || '');
let cardIds = matchCardIds(conf, pr.body || '');
if (conf.githubIncludePrComments) {
for (const comment of comments) {
cardIds = [...cardIds, ...matchCardIds(conf, comment.body)];
}
}
const createdCardId = await createNewCard(conf, pr);
if (createdCardId) {
cardIds = [...cardIds, createdCardId];
}
if (cardIds.length) {
console.log('Found card IDs', cardIds);
return [...new Set(cardIds)];
}
if (conf.githubIncludePrBranchName) {
const cardId = await getCardIdFromBranch(prHead);
const cardId = await getCardIdFromBranch(pr.head);
if (cardId) {
console.log('Found card ID from branch name');
return [cardId];
}
}
Expand All @@ -33322,6 +33339,20 @@ function matchCardIds(conf, text) {
return cardIds;
})));
}
async function createNewCard(conf, pr) {
if (!conf.githubIncludeNewCardCommand) {
return;
}
const isDraft = isDraftPr(pr);
const listId = pr.state === 'open' && isDraft ? conf.trelloListIdPrDraft : conf.trelloListIdPrOpen;
const commandRegex = /(^|\s)\/new-trello-card(\s|$)/; // Avoids matching URLs
if (listId && pr.body && commandRegex.test(pr.body)) {
const card = await (0, trelloRequests_1.createCard)(listId, pr.title, pr.body.replace('/new-trello-card', ''));
await (0, githubRequests_1.updatePullRequestBody)(pr.body.replace('/new-trello-card', card.url));
return card.id;
}
return;
}
async function getCardIdFromBranch(prHead) {
console.log('Searching card from branch name');
const branchName = prHead?.ref || (await (0, githubRequests_1.getBranchName)());
Expand Down Expand Up @@ -33393,11 +33424,12 @@ async function addPRLinkToCards(cardIds, link) {
return (0, trelloRequests_1.addAttachmentToCard)(cardId, link);
}));
}
async function addCardLinkToPR(conf, cardIds, prBody = '', comments = []) {
async function addCardLinkToPR(conf, cardIds, pr, comments = []) {
if (!conf.githubIncludePrBranchName) {
return;
}
if (matchCardIds(conf, prBody || '')?.length) {
const pullRequest = conf.githubIncludeNewCardCommand ? await (0, githubRequests_1.getPullRequest)() : pr;
if (matchCardIds(conf, pullRequest.body || '')?.length) {
console.log('Card is already linked in the PR description');
return;
}
Expand All @@ -33412,7 +33444,7 @@ async function addCardLinkToPR(conf, cardIds, prBody = '', comments = []) {
await (0, githubRequests_1.createComment)(cardInfo.shortUrl);
}
async function updateCardMembers(conf, cardIds) {
const assignees = await (0, githubRequests_1.getPullRequestAssignees)();
const assignees = await getPullRequestAssignees();
console.log('Starting to update card members');
if (!assignees?.length) {
console.log('No PR assignees found');
Expand All @@ -33426,12 +33458,16 @@ async function updateCardMembers(conf, cardIds) {
}
return Promise.all(cardIds.map(async (cardId) => {
const cardInfo = await (0, trelloRequests_1.getCardInfo)(cardId);
await addNewMembers(cardInfo, memberIds);
if (conf.trelloRemoveUnrelatedMembers) {
await removeUnrelatedMembers(cardInfo, memberIds);
}
return addNewMembers(cardInfo, memberIds);
}));
}
async function getPullRequestAssignees() {
const pr = await (0, githubRequests_1.getPullRequest)();
return pr ? [...(pr.assignees || []), pr.user] : [];
}
async function getTrelloMemberId(conf, githubUserName) {
let username = githubUserName?.replace('-', '_');
if (conf.githubUsersToTrelloUsers?.trim()) {
Expand Down Expand Up @@ -33567,7 +33603,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getMemberInfo = 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.getMemberInfo = exports.moveCardToList = exports.removeMemberFromCard = exports.addLabelToCard = exports.getBoardLists = exports.getBoardLabels = exports.addMemberToCard = exports.addAttachmentToCard = exports.getCardAttachments = exports.getCardInfo = exports.searchTrelloCards = void 0;
const axios_1 = __importDefault(__nccwpck_require__(8757));
const core = __importStar(__nccwpck_require__(2186));
const trelloApiKey = core.getInput('trello-api-key', { required: true });
Expand Down Expand Up @@ -33643,6 +33679,17 @@ async function getMemberInfo(username) {
return response?.data;
}
exports.getMemberInfo = getMemberInfo;
async function createCard(listId, title, body) {
console.log('Creating card based on PR info', title, body);
const response = await makeRequest('post', `https://api.trello.com/1/cards`, {
idList: listId,
name: title,
desc: body,
pos: trelloCardPosition,
});
return response?.data;
}
exports.createCard = createCard;
async function makeRequest(method, url, params) {
try {
let response;
Expand Down
17 changes: 15 additions & 2 deletions src/githubRequests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ export async function getPullRequestComments() {
return response.data
}

export async function getPullRequestAssignees() {
export async function getPullRequest() {
const response = await octokit.rest.issues.get({
owner: repoOwner,
repo: payload.repository!.name,
issue_number: issueNumber!,
})

return [...(response.data.assignees || []), response.data.user]
return response.data
}

export async function getBranchName() {
Expand All @@ -39,10 +39,23 @@ export async function getBranchName() {
}

export async function createComment(shortUrl: string) {
console.log('Creating PR comment', shortUrl)

await octokit.rest.issues.createComment({
owner: repoOwner,
repo: payload.repository!.name,
issue_number: issueNumber!,
body: shortUrl,
})
}

export async function updatePullRequestBody(newBody: string) {
console.log('Updating PR body', newBody)

await octokit.rest.issues.update({
owner: repoOwner,
repo: payload.repository!.name,
issue_number: issueNumber!,
body: newBody,
})
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ run((context.payload.pull_request || context.payload.issue) as PR, {
githubRequireTrelloCard: core.getBooleanInput('github-require-trello-card'),
githubIncludePrComments: core.getBooleanInput('github-include-pr-comments'),
githubIncludePrBranchName: core.getBooleanInput('github-include-pr-branch-name'),
githubIncludeNewCardCommand: core.getBooleanInput('github-include-new-card-command'),
githubUsersToTrelloUsers: core.getInput('github-users-to-trello-users'),
trelloOrganizationName: core.getInput('trello-organization-name'),
trelloListIdPrDraft: core.getInput('trello-list-id-pr-draft'),
Expand Down
50 changes: 44 additions & 6 deletions src/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,22 @@ import {
getBoardLabels,
addLabelToCard,
getBoardLists,
createCard,
} from './trelloRequests'
import { getPullRequestComments, getBranchName, createComment, getPullRequestAssignees } from './githubRequests'
import {
getPullRequestComments,
getBranchName,
createComment,
getPullRequest,
updatePullRequestBody,
} from './githubRequests'

jest.mock('@actions/core')
jest.mock('@actions/github')
jest.mock('./githubRequests')
jest.mock('./trelloRequests')

const getPullRequestAssigneesMock = getPullRequestAssignees as jest.Mock
const getPullRequestMock = getPullRequest as jest.Mock
const getMemberInfoMock = getMemberInfo as jest.Mock
const getCardInfoMock = getCardInfo as jest.Mock
const getPullRequestCommentsMock = getPullRequestComments as jest.Mock
Expand All @@ -29,6 +36,7 @@ const searchTrelloCardsMock = searchTrelloCards as jest.Mock
const getCardAttachmentsMock = getCardAttachments as jest.Mock
const getBoardLabelsMock = getBoardLabels as jest.Mock
const getBoardListsMock = getBoardLists as jest.Mock
const createCardMock = createCard as jest.Mock

const basePR = { number: 0, state: 'open', title: 'Title' }

Expand Down Expand Up @@ -87,6 +95,36 @@ describe('Finding cards', () => {
})
})

describe('Creating new card', () => {
const pr = { ...basePR, body: '/new-trello-card Description' }
const conf = { trelloListIdPrOpen: 'open-list-id', githubIncludeNewCardCommand: true }

it('adds new card, updates PR body and adds to card ids list', async () => {
createCardMock.mockResolvedValueOnce({ id: 'card-id', url: 'card-url' })

await run(pr, conf)

expect(createCard).toHaveBeenCalledWith('open-list-id', 'Title', ' Description')
expect(updatePullRequestBody).toHaveBeenCalledWith('card-url Description')
expect(moveCardToList).toHaveBeenCalledWith('card-id', 'open-list-id', undefined)
})

it('skips when no command found', async () => {
await run({ ...pr, body: '' }, conf)
expect(createCard).not.toHaveBeenCalled()
})

it('skips when list is missing', async () => {
await run(pr, { ...conf, trelloListIdPrOpen: '' })
expect(createCard).not.toHaveBeenCalled()
})

it('skips when turned off', async () => {
await run(pr, { ...conf, githubIncludeNewCardCommand: false })
expect(createCard).not.toHaveBeenCalled()
})
})

describe('Moving cards', () => {
describe('PR is added to draft', () => {
const pr = { ...basePR, body: 'https://trello.com/c/card/title' }
Expand Down Expand Up @@ -222,7 +260,7 @@ describe('Updating card members', () => {
const conf = { githubUsersToTrelloUsers: 'jack: jones\namy: amy1993', trelloRemoveUnrelatedMembers: true }

it('adds PR author and assignees to the card and removes unrelated members', async () => {
getPullRequestAssigneesMock.mockResolvedValueOnce([{ login: 'phil' }, { login: 'amy' }])
getPullRequestMock.mockResolvedValueOnce({ user: { login: 'phil' }, assignees: [{ login: 'amy' }] })
getMemberInfoMock.mockImplementation((username) =>
username === 'amy1993' ? { id: 'amy-id' } : { id: 'phil-id' },
)
Expand All @@ -236,7 +274,7 @@ describe('Updating card members', () => {
})

it('skips removing unrelated members when turned off', async () => {
getPullRequestAssigneesMock.mockResolvedValueOnce([{ login: 'phil' }])
getPullRequestMock.mockResolvedValueOnce({ user: { login: 'phil' } })
getMemberInfoMock.mockResolvedValueOnce({ id: 'phil-id' })
getCardInfoMock.mockResolvedValueOnce({ id: 'card', idMembers: ['jones-id'] })

Expand All @@ -246,7 +284,7 @@ describe('Updating card members', () => {
})

it('skips adding when all members are already assigned to the card', async () => {
getPullRequestAssigneesMock.mockResolvedValueOnce([{ login: 'phil' }])
getPullRequestMock.mockResolvedValueOnce({ user: { login: 'phil' } })
getMemberInfoMock.mockResolvedValueOnce({ id: 'phil-id' })
getCardInfoMock.mockResolvedValueOnce({ id: 'card', idMembers: ['phil-id'] })

Expand All @@ -256,7 +294,7 @@ describe('Updating card members', () => {
})

it('skips adding when member not found with GitHub username', async () => {
getPullRequestAssigneesMock.mockResolvedValueOnce([{ login: 'phil' }])
getPullRequestMock.mockResolvedValueOnce({ user: { login: 'phil' } })
getMemberInfoMock.mockResolvedValue(undefined)

await run(pr)
Expand Down
Loading