-
Notifications
You must be signed in to change notification settings - Fork 18
/
utils.js
393 lines (337 loc) · 14.5 KB
/
utils.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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
const { copy, remove } = require('fs-extra');
const { readdir, stat } = require('fs').promises;
const path = require('path');
const core = require('@actions/core');
const { getCommitFiles, getBranchesRemote } = require('./api-calls');
module.exports = { copyChangedFiles, parseCommaList, getListOfReposToIgnore, getBranchName, getListOfFilesToReplicate, getAuthanticatedUrl, isInitialized, getBranchesList, filterOutMissingBranches, filterOutFiles, getFilteredFilesList, getFileName, removeFiles, getFiles };
/**
* @param {Object} octokit GitHub API client instance
* @param {Object} commitId Id of the commit to check for files changes
* @param {String} owner org or user name
* @param {String} repo repo name
* @param {String} patternsToIgnore comma-separated list of file paths or directories that should be ignored
* @param {String} patternsToInclude comma-separated list of file paths or directories that should be replicated
* @param {String} triggerEventName name of the event that triggered the workflow
*
* @returns {Object<Array<String>>} list of filepaths of modified files
*/
async function getListOfFilesToReplicate(octokit, commitId, owner, repo, patternsToIgnore, patternsToInclude, triggerEventName) {
let filesToCheckForReplication;
let filesToCheckForRemoval;
core.startGroup('Getting list of workflow files that need to be replicated in other repositories');
if (triggerEventName === 'push') {
const commitFiles = await getCommitFiles(octokit, commitId, owner, repo);
core.debug(`DEBUG: list of files modified in commit ${commitId}. Full response from API:`);
core.debug(JSON.stringify(commitFiles, null, 2));
//filtering out files that show in commit as removed
filesToCheckForReplication = getFiles(commitFiles, false);
//remember files that show in commit as removed
filesToCheckForRemoval = getFiles(commitFiles, true);
}
if (triggerEventName === 'workflow_dispatch') {
const root = process.cwd();
filesToCheckForReplication = (await getFilesListRecursively(root)).map(filepath => path.relative(root, filepath));
filesToCheckForRemoval = [];
core.debug(`DEBUG: list of files from the repo is ${filesToCheckForReplication}`);
}
const filesForRemoval = getFilteredFilesList(filesToCheckForRemoval, patternsToIgnore, patternsToInclude);
const filesForReplication = getFilteredFilesList(filesToCheckForReplication, patternsToIgnore, patternsToInclude);
if (!filesForReplication.length) {
core.info('No changes were detected.');
} else {
core.info(`Files that need replication are: ${filesForReplication}.`);
}
core.endGroup();
return { filesForReplication, filesForRemoval };
}
/**
* Get a list of all files recursively in file path
*
* @param {String} filepath
*
* @returns {Array<String>} list of filepaths in path directory
*/
async function getFilesListRecursively(filepath) {
const paths = await readdir(filepath);
const fullpaths = paths.map(async filename => {
const fullpath = path.join(filepath, filename);
const stats = await stat(fullpath);
if (stats.isFile()) {
return fullpath;
} else if (stats.isDirectory()) {
return (await getFilesListRecursively(fullpath)).flat();
}
});
return (await Promise.all(fullpaths)).flat();
}
/**
* Get a list of files to replicate
*
* @param {Array} filesToCheckForReplication list of all paths that are suppose to be replicated
* @param {String} filesToIgnore Comma-separated list of file paths or directories to ignore
* @param {String} patternsToInclude Comma-separated list of file paths or directories to include
*
* @returns {Array}
*/
function getFilteredFilesList(filesToCheckForReplication, filesToIgnore, patternsToInclude) {
const filesWithoutIgnored = filterOutFiles(filesToCheckForReplication, filesToIgnore, true);
return filterOutFiles(filesWithoutIgnored, patternsToInclude, false);
}
/**
* Get list of files that should be replicated because they are supposed to be ignored, or because they should not be ignored
*
* @param {Array} filesToFilter list of all paths that are suppose to be replicated
* @param {String} patterns Comma-separated list of file paths or directories
* @param {Boolean} ignore true means files that matching patters should be filtered out, false means that only matching patterns should stay
*
* @returns {Array}
*/
function filterOutFiles(filesToFilter, patterns, ignore) {
const filteredList = [];
const includePatternsList = patterns ? parseCommaList(patterns) : [];
for (const filename of filesToFilter) {
const isMatching = !!includePatternsList.map(pattern => {
return filename.includes(pattern);
}).filter(Boolean).length;
if (!ignore && isMatching) filteredList.push(filename);
if (ignore && !isMatching) filteredList.push(filename);
}
return filteredList;
}
/**
* Assemble a list of repositories that should be ignored.
*
* @param {String} repo The current repository.
* @param {Array} reposList All the repositories.
* @param {String} inputs.reposToIgnore A comma separated list of repositories to ignore.
* @param {String} inputs.topicsToInclude A comma separated list of topics to include.
* @param {Boolean} inputs.excludePrivate Exclude private repositories.
* @param {Boolean} inputs.excludeForked Exclude forked repositories.
*
* @returns {Array}
*/
function getListOfReposToIgnore(repo, reposList, inputs) {
const {
reposToIgnore,
topicsToInclude,
excludePrivate,
excludeForked,
} = inputs;
core.startGroup('Getting list of repos to be ignored');
//manually ignored repositories.
const ignoredRepositories = reposToIgnore ? parseCommaList(reposToIgnore) : [];
// Exclude archived repositories by default. The action will fail otherwise.
const EXCLUDE_ARCHIVED = true;
if (EXCLUDE_ARCHIVED === true) {
ignoredRepositories.push(...archivedRepositories(reposList));
}
//by default repo where workflow runs should always be ignored.
ignoredRepositories.push(repo);
// if topics_to_ignore is set, get ignored repositories by topics.
if (topicsToInclude.length) {
ignoredRepositories.push(...ignoredByTopics(topicsToInclude, reposList));
}
// Exclude private repositories.
if (excludePrivate === true) {
ignoredRepositories.push(...privateRepositories(reposList));
}
// Exclude forked repositories
if (excludeForked === true) {
ignoredRepositories.push(...forkedRepositories(reposList));
}
if (!ignoredRepositories.length) {
core.info('No repositories will be ignored.');
} else {
core.info(`Repositories that will be ignored: ${ignoredRepositories}.`);
}
core.endGroup();
return ignoredRepositories;
}
/**
* @param {Array} filesList list of files that need to be copied
* @param {String} root root destination in the repo, always ./
* @param {String} destination in case files need to be copied to soom custom location in repo
*/
async function copyChangedFiles(filesList, root, destination) {
core.info('Copying files');
core.debug(`DEBUG: Copying files to root ${root} and destination ${destination} - if provided (${!!destination}). Where process.cwd() is ${process.cwd()}`);
await Promise.all(filesList.map(async filePath => {
return destination
? await copy(path.join(process.cwd(), filePath), path.join(root, destination, getFileName(filePath)))
: await copy(path.join(process.cwd(), filePath), path.join(root, filePath));
}));
}
/**
* @param {Array|String} toRemove comma-separated list of patterns that specify where and what should be removed or array of files to remove
* @param {String} root root of cloned repo
* @param {Object}options
* {String} patternsToIgnore comma-separated list of file paths or directories that should be ignored
* {String} destination in case files need to be removed from soom custom location in repo
*/
async function removeFiles(toRemove, root, { patternsToIgnore, destination }) {
let filesForRemoval;
const isListString = typeof toRemove === 'string';
core.info('Removing files');
if (!isListString) core.debug(`DEBUG: Removing to the following files: ${filesForRemoval}`);
core.debug(`DEBUG: Removing files from root ${root} Where process.cwd() is ${process.cwd()}`);
if (isListString) {
const filesToCheckForRemoval = (await getFilesListRecursively(root)).map(filepath => path.relative(root, filepath));
filesForRemoval = getFilteredFilesList(filesToCheckForRemoval, patternsToIgnore, toRemove);
core.debug(`DEBUG: Provided patterns ${toRemove} relate to the following files: ${filesForRemoval}`);
} else {
filesForRemoval = toRemove;
}
await Promise.all(filesForRemoval.map(async filePath => {
return await remove(destination ?
path.join(root, destination, getFileName(filePath)) :
path.join(root, filePath));
}));
}
/**
* @param {String} filePath full filepath to the file
* @returns {String} filename with extension
*/
function getFileName(filePath) {
return filePath.split('/').slice(-1)[0];
}
/**
* @param {String} list names of values that can be separated by comma
* @returns {Array<String>} input names not separated by string but as separate array items
*/
function parseCommaList(list) {
return list.split(',').map(i => i.trim().replace(/['"]+/g, ''));
}
/**
* Create a branch name.
* If commitId is not provided then it means action was not triggered by push and name must have some generated number and indicate manual run
*
* @param {String} commitId id of commit that should be added to branch name for better debugging of changes
* @param {String} branchName name of the branch that new branch will be cut from
* @returns {String}
*/
function getBranchName(commitId, branchName) {
return commitId ? `bot/update-global-workflow-${branchName}-${commitId}` : `bot/manual-update-global-workflow-${branchName}-${Math.random().toString(36).substring(7)}`;
}
/**
* Get list of branches that this action should operate on
* @param {Object} octokit GitHub API client instance
* @param {String} owner org or user name
* @param {String} repo repo name
* @param {String} branchesString comma-separated list of branches
* @param {String} defaultBranch name of the repo default branch
* @returns {Array<Object, Object>} first index is object with branches that user wants to operate on and that are in remote, next index has all remote branches
*/
async function getBranchesList(octokit, owner, repo, branchesString, defaultBranch) {
core.info('Getting list of branches the action should operate on');
const branchesFromRemote = await getBranchesRemote(octokit, owner, repo);
//we need to match if all branches that user wants this action to support are on the server and can actually be supported
//branches not available an remote will not be included
const filteredBranches = filterOutMissingBranches(branchesString, branchesFromRemote, defaultBranch);
core.info(`This is a final list of branches action will operate on: ${JSON.stringify(filteredBranches, null, 2)}`);
return [filteredBranches, branchesFromRemote];
}
/**
* Get array of branches without the ones that do not exist in remote
* @param {String} branchesRequested User requested branches
* @param {Array<Object>} branchesExisting Existing branches
* @param {String} defaultBranch Name of repo default branch
* @returns {Array<Object>}
*/
function filterOutMissingBranches(branchesRequested, branchesExisting, defaultBranch) {
const branchesArray = branchesRequested
? parseCommaList(branchesRequested)
: [`^${defaultBranch}$`];
core.info(`These were requested branches: ${branchesRequested}`);
core.info(`This is default branch: ${defaultBranch}`);
return branchesExisting.filter(branch => {
// return branchesArray.includes(branch.name);
return branchesArray.some(b => {
const regex = new RegExp(b);
return regex.test(branch.name);
});
});
}
/**
* Creates a url with authentication token in it
*
* @param {String} token access token to GitHub
* @param {String} url repo URL
* @returns {String}
*/
function getAuthanticatedUrl(token, url) {
const arr = url.split('//');
return `https://${token}@${arr[arr.length - 1]}.git`;
};
/**
* Checking if repo is initialized cause if it isn't we need to ignore it
*
* @param {Array<Object>} branches list of all local branches with detail info about them
* @param {String} defaultBranch name of default branch that is always set even if repo not initialized
* @returns {Boolean}
*/
function isInitialized(branches, defaultBranch) {
core.info('Checking if repo initialized.');
core.debug('DEBUG: list of local branches');
core.debug(JSON.stringify(branches.branches, null, 2));
return !!branches.branches[defaultBranch];
}
/**
* Getting list of topics that should be included if topics_to_include is set.
* Further on we will get a list of repositories that do not belong to any of the specified topics.
*
* @param {String} topicsToInclude Comma separated list of topics to include.
* @param {Array} reposList All the repositories.
* @returns {Array} List of all repositories to exclude.
*/
function ignoredByTopics(topicsToInclude, reposList) {
const includedTopics = topicsToInclude ? parseCommaList(topicsToInclude) : [];
if (!includedTopics.length) return;
return reposList.filter(repo => {
return includedTopics.some(topic => repo.topics.includes(topic)) === false;
}).map(reposList => reposList.name);
}
/**
* Returns a list of archived repositories.
*
* @param {Array} reposList All the repositories.
* @returns {Array}
*/
function archivedRepositories(reposList) {
return reposList.filter(repo => {
return repo.archived === true;
}).map(reposList => reposList.name);
}
/**
* Returns a list of private repositories.
*
* @param {Array} reposList All the repositories.
* @returns {Array}
*/
function privateRepositories(reposList) {
return reposList.filter(repo => {
return repo.private === true;
}).map(reposList => reposList.name);
}
/**
* Returns a list of forked repositories.
*
* @param {Array} reposList All the repositories.
* @returns {Array}
*/
function forkedRepositories(reposList) {
return reposList.filter(repo => {
return repo.fork === true;
}).map(reposList => reposList.name);
}
/**
* Returns a list of files that were removed or not
*
* @param {Array} filesList All the files objects.
* @param {Boolean} removed should return removed or not removed
* @returns {Array}
*/
function getFiles(filesList, removed) {
return filesList
.filter(fileObj => removed ? fileObj.status === 'removed' : fileObj.status !== 'removed')
.map(nonRemovedFile => nonRemovedFile.filename);
}