-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
370 lines (305 loc) · 12 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
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
const core = require('@actions/core');
const github = require('@actions/github');
const octokit = github.getOctokit(token);
// Javascript destructuring assignment. See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
const {owner, repo} = github.context.repo
const token = core.getInput('token');
// Determines if an object is an object
// @param {any} variable The object to check
// @returns {boolean} true if variable is an object, false otherwise
function isObject (variable) {
return typeof variable === 'object' && !Array.isArray(variable) && variable !== null
}
// Determines if an object is a nonempty string
// @param {any} str The object to check
// @returns {boolean} true if str is a nonempty string, false otherwise
function isNonEmptyString (str) {
return typeof str === 'string' && str.length
}
// Lists up to MAX_CARDS_PER_PAGE cards from a column
// @param {integer} columnId The id of the column containing the cards
// @param {integer} pageNumber The page of up to MAX_CARDS_PER_PAGE cards to retrieve
// default 1
// @return {Promise} A promise representing fetching the page of cards
// @fulfilled {Array} The card data as an array of objects
// @throws {TypeError} for a parameter of the incorrect type
// @throws {RangeError} if columnId is negative
// @throws {RangeError} if pageNumber is less than 1
// @throws {Error} if an error occurs while trying to fetch the card data
async function getCardPage (columnId, pageNumber = 1) {
if (typeof columnId === 'string') {
columnId = parseInt(columnId)
if (!columnId) { // The column id isn't going to be 0
throw new TypeError('Param columnId is not an integer')
}
}
if (typeof pageNumber === 'string') {
pageNumber = parseInt(pageNumber)
if (!pageNumber) { // The column id isn't going to be 0
throw new TypeError('Param pageNumber is not an integer')
}
}
if (!Number.isInteger(columnId)) {
throw new TypeError('Param columnId is not an integer')
} else if (columnId < 0) {
throw new RangeError('Param columnId cannot be negative')
}
if (!Number.isInteger(pageNumber)) {
throw new TypeError('Param pageNumber is not an integer')
} else if (pageNumber < 1) {
throw new RangeError('Param pageNumber cannot be less than 1')
}
return await octokit.projects.listCards({
column_id: columnId,
archived_state: 'not_archived',
page: pageNumber,
per_page: MAX_CARDS_PER_PAGE
})
}
// Get a column by name in a project
// @param {columnName} columnName The name of the column
// @param {integer} projectId The id of the project containing the column
// @return {Promise} A promise representing fetching of the column
// @fulfilled {Object} An object representing the first column with name matching columnName
// undefined if the column could not be found
// @throws {TypeError} for a parameter of the incorrect type
// @throws {RangeError} if projectId is less than 1
// @throws {Error} if an error occurs while trying to fetch the project data
async function getColumn (columnName, projectId) {
if (typeof projectId === 'string') {
columnId = parseInt(projectId)
if (!projectId) { // The project id isn't going to be 0
throw new TypeError('Param projectId is not an integer')
}
}
if (!Number.isInteger(projectId)) {
throw new TypeError('Param projectId is not an integer')
} else if (projectId < 0) {
throw new RangeError('Param projectId cannot be negative')
}
const columnList = await octokit.request('GET /projects/{project_id}/columns', {
project_id: projectId
})
return columnList.data.find((column) => {
return column.name === columnName
})
}
// Lists all the cards for a column that are issues
// @param {integer} columnId The id of the column containing the cards
// @return {Promise} A promise representing fetching of card data
// @fulfilled {Array} The card data as an array of objects
// @throws {TypeError} for a parameter of the incorrect type
// @throws {RangeError} if columnId is negative
// @throws {Error} if an error occurs while trying to fetch the card data
async function getColumnCardIssues (columnId) {
if (typeof columnId === 'string') {
columnId = parseInt(columnId)
if (!columnId) { // The column id isn't going to be 0
throw new TypeError('Param columnId is not an integer')
}
}
if (!Number.isInteger(columnId)) {
throw new TypeError('Param columnId is not an integer')
} else if (columnId < 0) {
throw new RangeError('Param columnId cannot be negative')
}
let cardIssues = []
let cardPage
let page = 1
do {
cardPage = await getCardPage(columnId, page)
// filter out non issue cards
let pageCardIssues = cardPage.data.filter((card) => {
return card.content_url
})
cardIssues.push(...pageCardIssues)
page++
} while (cardPage.data.length === MAX_CARDS_PER_PAGE)
return cardIssues
}
// Get a list of labels for an issue
// @param {number} issueNumber The number of the issue to fetch labels for
// @return {Promise} A promise representing fetching of the labels for an issue
// @fulfilled {Object} A list of objects each representing a label
// @throws {TypeError} for a parameter of the incorrect type
// @throws {RangeError} when issueNumber is less than 1
// @throws {Error} if an error occurs while trying to fetch the project data
async function getIssueLabels (issueNumber) {
if (!Number.isInteger(issueNumber)) {
throw new TypeError('Param issueNumber must be an integer')
}
return await octokit.request('GET /repos/{owner}/{repo}/issues/{issue_number}/labels', {
owner: owner,
repo: repo,
issue_number: issueNumber
})
}
// Get the project with name passed into projectName from the current repo
// @param {string} projectName The name of the project
// @return {Promise} A promise representing fetching of the project
// @fulfilled {Object} An object representing the first project with name matching projectName
// undefined if the project could not be found
// @throws {TypeError} for a parameter of the incorrect type
// @throws {Error} if an error occurs while trying to fetch the project data
async function getProject (projectName) {
if (!isNonEmptyString(projectName)) {
throw new TypeError('Param projectName must be a non empty string')
}
const repoProjects = await octokit.request('GET /repos/{owner}/{repo}/projects', {
owner: owner,
repo: repo
})
return repoProjects.data.find((project) => {
return project.name === projectName
})
}
// Adds a label to a card if it is an issue
// @param {object} card An object representing the card to be labeled
// @param {Array} labels The list of labels to be added
// @return {Promise} A promise representing the labeling of the card
// @throws {TypeError} for a parameter of the incorrect type
// @throws {Error} if an error occurs while labeling the card
async function labelCardIssue (card, labels) {
if (!isObject(card)) {
throw new TypeError('Param card is not an object')
}
if (!Array.isArray(labels)) {
reject(new TypeError('Param labels must be an array'))
}
if (!card.content_url) {
throw new ReferenceError(`Card with id: ${ card.id } is missing field "content_url"`)
}
const issueNumberMatchCapture = card.content_url.match(/\/issues\/(\d+)$/)
if (!issueNumberMatchCapture || issueNumberMatchCapture.length < 2) {
throw new Error(`Failed to extract issue number from url: ${card.content_url}`)
}
const issueNumber = issueNumberMatchCapture[1]
return octokit.issues.addLabels({
owner: owner,
repo: repo,
issue_number: issueNumber,
labels: labels
})
}
// Adds a github labeld to each card of a list
// @param {Array} cardData The list of cards to be labeled
// @param {Array} labels The list of labels to be added
// @return {Promise} A promise representing labeling the list of cards
// @fulfilled {integer} The number of cards successfully labeled
// @rejected {TypeError} for a parameter of the incorrect type
function labelCards(cardData, labels) {
const delayBetweenRequestsMS = cardData.length >= MAX_CARDS_PER_PAGE ? 1000 : 0
if (delayBetweenRequestsMS) {
console.log('INFO: A large number of label issue requests will be sent. Throttling requests.')
}
return new Promise((resolve, reject) => {
if (!Array.isArray(cardData)) {
reject(new TypeError('Param cardData must be an array'))
return
}
if (!(cardData.length)) {
resolve(0)
return
}
if (!Array.isArray(labels)) {
reject(new TypeError('Param labels must be an array'))
return
}
let cardLabelAttemptCount = 0
let cardsLabeledCount = 0
let requestSentCount = 0
const requestInterval = setInterval(() => {
const card = cardData[requestSentCount]
labelCardIssue(card, labels).then(() => {
cardsLabeledCount++
}).catch((e) => {
console.warn(`WARNING: Failed to label card with id: ${card.id}`)
console.warn(e.message)
}).finally(() => {
cardLabelAttemptCount++
if (cardLabelAttemptCount === cardData.length) {
resolve(cardsLabeledCount)
}
})
if (++requestSentCount >= cardData.length) {
clearInterval(requestInterval)
}
}, delayBetweenRequestsMS)
})
}
async function main () {
const validColumnsLabels = validateColumnsLabels(columns_labels)
for (const column_labels of validColumnsLabels) {
let columnId = column_labels['column_id']
console.log(`Labeling a column with the following column label data: ${ JSON.stringify(column_labels) }`)
if (!columnId) {
let project
try {
project = await getProject(column_labels['project_name'])
} catch (e) {
console.error(`ERROR: Failed to find project with name "${column_labels['project_name']}"`)
console.error(' Skipping labeling using the above data')
console.error(e.message)
continue
}
try {
const column = await getColumn(column_labels['column_name'], project.id)
columnId = column ? column.id : null
if (!columnId) {
throw new Error('')
}
} catch (e) {
console.error(`ERROR: Failed to find column with name ${column_labels['column_name']}`)
console.error(' Skipping labeling using the above data')
console.error(e.message)
continue
}
}
let cards
try {
cards = await getColumnCardIssues(columnId)
} catch (e) {
console.error('ERROR: Failed to fetch card data')
console.error(' Skipping labeling using the above data')
console.error(e.message)
continue
}
for (const card of cards) {
const issueNumberMatchCapture = card.content_url.match(/\/issues\/(\d+)$/)
if (!issueNumberMatchCapture || issueNumberMatchCapture.length < 2) {
console.warn(`Failed to extract issue number from url: ${card.content_url}`)
continue
}
const issueNumber = issueNumberMatchCapture[1]
console.log(await getIssueLabels(issueNumber))
}
//const cardsLabeledCount = await labelCards(cards, column_labels['labels'])
//console.log(`Labeled/relabeled ${cardsLabeledCount} of ${cards.length} card issues`)
}
return
/*// Remove the label from the cards
cards.data.forEach(async card => {
const matches = card.content_url.match(/\/issues\/(\d+)/);
if (!matches) {
console.log(`Couldn't match the regexp against '${card.content_url}'.`);
return true;
}
const issueNumber = matches[1];
try {
await octokit.issues.removeLabel({
owner: repoOwner,
repo: repo,
issue_number: issueNumber,
name: labelToRemove
});
}
catch (e) {
console.log(e.message);
return true;
}
})*/
}
main().catch((e) => {
console.error(e.message)
process.exit(1)
})