forked from delivered/attach-to-trello-card-action
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
252 lines (203 loc) · 8.93 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
const util = require('util');
const core = require('@actions/core');
const github = require('@actions/github');
const axios = require('axios');
const supportedEvent = 'pull_request';
const supportedActions = ['opened', 'reopened', 'edited', 'labeled', 'unlabeled'];
const trelloReviewLabelId = '64fa916ea60ef5c4ba86301a'
//configured in workflow file, which in turn should use repo secrets settings
const trelloKey = core.getInput('trello-key', { required: true });
const trelloToken = core.getInput('trello-token', { required: true });
//adds extra (redundant) PR comment, to mimic normal behavior of trello GH powerup
const shouldAddPrComment = core.getInput('add-pr-comment') === 'true';
//token is NOT magically present in context as some docs seem to indicate - have to supply in workflow yaml to input var
const ghToken = core.getInput('repo-token');
const evthookPayload = github.context.payload;
const trelloClient = axios.create({
baseURL: 'https://api.trello.com',
});
const requestTrello = async (verb, url, body = null, extraParams = null) => {
try {
const params = {
...(extraParams || {}),
key: trelloKey,
token: trelloToken
};
const res = await trelloClient.request({
method: verb,
url: url,
data: body,
params: params
});
core.debug(`${verb} to ${url} completed with status: ${res.status}. data follows:`);
//BRH NOTE core.xxx logging methods explode with typeerror when given non-string object. TODO wrap.
core.debug(util.inspect(res.data));
return res.data;
} catch (err) {
core.error(`${verb} to ${url} errored: ${err}`);
if (err.response) {
core.error(util.inspect(err.response.data));
}
throw err;
}
};
const getCard = async (cardId) => {
return requestTrello('get', `/1/cards/${cardId}`);
};
const getCardAttachments = async (cardId) => {
return requestTrello('get', `/1/cards/${cardId}/attachments`);
};
const createCardAttachment = async (cardId, attachUrl) => {
return requestTrello('post', `/1/cards/${cardId}/attachments`, { url: attachUrl });
};
const getCardInfoSubset = async (cardId) => {
return requestTrello('get', `/1/cards/${cardId}`, null, { fields: 'name,url' });
};
const getTrelloCardLabels = async (cardId) => {
return requestTrello('get', `/1/cards/${cardId}/idLabels`);
};
const addTrelloCardLabel = async (cardId, labelId) => {
return requestTrello('post', `/1/cards/${cardId}/idLabels`, null, { value: labelId });
};
const removeTrelloCardLabel = async (cardId, labelId) => {
return requestTrello('delete', `/1/cards/${cardId}/idLabels/${labelId}`);
};
if (ghToken) {
const octokit = new github.getOctokit(ghToken);
}
const baseIssuesArgs = {
owner: (evthookPayload.organization || evthookPayload.repository.owner).login,
repo: evthookPayload.repository.name,
issue_number: evthookPayload.pull_request.number
};
const getPrComments = async () => {
if (!octokit) {
throw new Error('Could not get PR comments. Is the GH repo-token provided?');
}
return octokit.rest.issues.listComments(baseIssuesArgs);
};
const addPrComment = async (body) => {
if (!octokit) {
throw new Error('Could not get PR comments. Is the GH repo-token provided?');
}
return octokit.rest.issues.createComment({
...baseIssuesArgs,
body
});
};
// to stop looking when we get to what looks like pr description, use stopOnNonLink true. to allow interspersed lines of
// yada yada yada b/w Trello links, use false.
const extractTrelloCardIds = (prBody) => {
core.debug(`prBody: ${util.inspect(prBody)}`);
if (!prBody || prBody === '') {
return cardIds;
}
// browsers submit textareas with \r\n line breaks on all platforms
const browserEol = '\r\n';
const linkRegex = /(https\:\/\/trello\.com\/c\/(\w+)(\/\S*)?)/;
const cardIds = [];
const lines = prBody.split(browserEol);
//loop and gather up cardIds
for (const line of lines) {
const matches = linkRegex.exec(line);
if (matches) {
if (matches[2]) {
core.debug(`found id ${matches[2]}`);
cardIds.push(matches[2]);
}
}
};
return cardIds;
}
const commentsContainsTrelloLink = async (cardId) => {
const linkRegex = new RegExp(`\\[[^\\]]+\\]\\(https:\\/\\/trello.com\\/c\\/${cardId}(\\/[^)]*)?\\)`);
const comments = await getPrComments();
return comments.data.some((comment) => linkRegex.test(comment.body));
};
const buildTrelloLinkComment = async (cardId) => {
const cardInfo = await getCardInfoSubset(cardId);
return `![](https://github.trello.services/images/mini-trello-icon.png) [${cardInfo.name}](${cardInfo.url})`;
};
const syncUpLabel = async (cardId, pullRequestHasReviewLabel) => {
let trellolabels = await getTrelloCardLabels(cardId);
core.info("syncup-label:Trello Card's labels: " + JSON.stringify(trellolabels));
var cardHasReviewLabel = trellolabels.some(lb => lb == trelloReviewLabelId);
if (pullRequestHasReviewLabel && !cardHasReviewLabel) {
await addTrelloCardLabel(cardId, trelloReviewLabelId);
core.info(`syncup-label:add label [Ready for Team Review] to trello card`);
}
if (!pullRequestHasReviewLabel && cardHasReviewLabel) {
await removeTrelloCardLabel(cardId, trelloReviewLabelId);
core.info(`syncup-label:remove label [Ready for Team Review] from trello card`);
}
};
const syncUpAttachment = async (cardId) => {
let extantAttachments;
extantAttachments = await getCardAttachments(cardId);
//make sure not already attached
if (extantAttachments == null || !extantAttachments.some(it => it.url === evthookPayload.pull_request.html_url)) {
const createdAttachment = await createCardAttachment(cardId, evthookPayload.pull_request.html_url);
core.info(`syncup-attachment:created trello attachment for card ${cardId}.`);
core.debug(util.inspect(createdAttachment));
// BRH NOTE actually, the power-up doesn't check if it previously added comment, so this doesn't exactly match
// its fxnality.
if (shouldAddPrComment && !await commentsContainsTrelloLink(cardId)) {
core.debug(`syncup-attachment:adding pr comment for card ${cardId}.`);
const newComment = await buildTrelloLinkComment(cardId)
//comments as 'github actions' bot, at least when using token automatically generated for GH workflows
await addPrComment(newComment);
} else {
core.info(`syncup-attachment:pr comment already present or unwanted for card ${cardId} - skipped adding comment .`);
}
} else {
core.info(`syncup-attachment:trello attachment for card ${cardId} already exists - skipped creating attachment .`);
}
}
// Run everytime new commit is pushed
(async () => {
try {
// 1. Check Github action event
if (!(github.context.eventName === supportedEvent && supportedActions.some(el => el === evthookPayload.action))) {
core.info(`event-check:event/type not supported: ${github.context.eventName.eventName}.${evthookPayload.action}. skipping action.`);
return;
}
core.info(`event-check:action == ${evthookPayload.action}, passed`);
// 2. Check Trello card reference
// allow on pull request relates to only one cards
// but a trello card can have many pull requests.
const cardIds = extractTrelloCardIds(evthookPayload.pull_request.body);
if (!cardIds || cardIds.length == 0) {
throw new Error("trello-card-check: no card urls in pr comment, please attach your card to this pull request.")
}
if (cardIds.length != 1) {
throw new Error("trello-card-check: cannot have multiple trello cards on one pull request.");
}
const cardId = cardIds[0];
core.info(`trello-card-check:cardId = ${cardId}, passed`);
// 3. Sync Up Attachment
await syncUpAttachment(cardId);
// 4. Sync Up Label beteween pull request and trello card
const labelObjects = evthookPayload.pull_request.labels
const labels = labelObjects.map(function (object) {
return object['name'];
});
core.info("syncup-label:Pull Request's labels: " + JSON.stringify(labels));
const pullRequestHasReviewLabel = labels.some(label => label == "ready for review");
await syncUpLabel(cardId, pullRequestHasReviewLabel);
core.info("syncup-label:passed");
// 5. if pull request has [ready for review] label , continue to check if card has verification step provided
if (pullRequestHasReviewLabel) {
const verificationTexReg = /(verification|validation).*step/;
const cardObject = await getCard(cardId);
const matches = verificationTexReg.exec(cardObject.desc.toLowerCase());
if (!matches) {
throw Error("verification-step-check: there are no verification steps on the card yet, please put \"Verification Steps\" as text or remove the [ready for review] label to skip this error.")
}
core.info("verification-step-check:passed");
}
} catch (error) {
core.error(util.inspect(error));
//failure will stop PR from being mergeable if that setting enabled on the repo. there is not currently a neutral exit in actions v2.
core.setFailed(error.message);
}
})();