-
Notifications
You must be signed in to change notification settings - Fork 71
/
Copy pathindex.js
376 lines (327 loc) · 10.4 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
371
372
373
374
375
376
// @ts-check
const core = require('@actions/core')
const exec = require('@actions/exec')
const io = require('@actions/io')
const hasha = require('hasha')
const cache = require('@actions/cache')
const fs = require('fs')
const os = require('os')
const path = require('path')
const quote = require('quote')
/**
* Grabs a boolean GitHub Action parameter input and casts it.
* @param {string} name - parameter name
* @param {boolean} defaultValue - default value to use if the parameter was not specified
* @returns {boolean} converted input argument or default value
*/
const getInputBool = (name, defaultValue = false) => {
const param = core.getInput(name)
if (param === 'true' || param === '1') {
return true
}
if (param === 'false' || param === '0') {
return false
}
return defaultValue
}
const restoreCachedNpm = npmCache => {
console.log('trying to restore cached NPM modules')
console.log('cache key %s', npmCache.primaryKey)
console.log('restore keys %o', npmCache.restoreKeys)
console.log('input paths %o', npmCache.inputPaths)
return cache
.restoreCache(
npmCache.inputPaths,
npmCache.primaryKey,
npmCache.restoreKeys
)
.then(cache => {
if (typeof cache === 'undefined') {
console.log('npm cache miss')
} else {
console.log('npm cache hit key', cache)
}
return cache
})
.catch(e => {
console.warn(
`caught error ${e} retrieving cache, installing from scratch`
)
})
}
const saveCachedNpm = npmCache => {
console.log('saving NPM modules under key %s', npmCache.primaryKey)
console.log('input paths: %o', npmCache.inputPaths)
const started = +new Date()
return cache
.saveCache(npmCache.inputPaths, npmCache.primaryKey)
.then(() => {
const finished = +new Date()
console.log(
'npm cache saved for key %s, took %dms',
npmCache.primaryKey,
finished - started
)
})
.catch(err => {
// don't throw an error if cache already exists, which may happen due to
// race conditions
if (err instanceof cache.ReserveCacheError) {
console.warn(err.message)
return -1
}
// do not rethrow here or github actions will break (https://github.com/bahmutov/npm-install/issues/142)
console.warn(`saving npm cache failed with ${err}, continuing...`)
console.warn('cache key %s', npmCache.primaryKey)
console.warn('input paths %o', npmCache.inputPaths)
})
}
const hasOption = (name, o) => name in o
const install = (opts = {}) => {
// Note: need to quote found tool to avoid Windows choking on
// npm paths with spaces like "C:\Program Files\nodejs\npm.cmd ci"
if (!hasOption('useYarn', opts)) {
console.error('passed options %o', opts)
throw new Error('Missing useYarn option')
}
if (!hasOption('usePackageLock', opts)) {
console.error('passed options %o', opts)
throw new Error('Missing usePackageLock option')
}
if (!hasOption('workingDirectory', opts)) {
console.error('passed options %o', opts)
throw new Error('Missing workingDirectory option')
}
const shouldUseYarn = opts.useYarn
const shouldUsePackageLock = opts.usePackageLock
const npmCacheFolder = opts.npmCacheFolder
if (!npmCacheFolder) {
console.error('passed opts %o', opts)
throw new Error('Missing npm cache folder to use')
}
// set the NPM cache config in case there is custom npm install command
core.exportVariable('npm_config_cache', npmCacheFolder)
const options = {
cwd: path.resolve(opts.workingDirectory)
}
if (opts.installCommand) {
core.debug(`installing using custom command "${opts.installCommand}"`)
return exec.exec(opts.installCommand, [], options)
}
if (shouldUseYarn) {
console.log('installing NPM dependencies using Yarn')
return io.which('yarn', true).then(yarnPath => {
console.log('yarn at "%s"', yarnPath)
const args = shouldUsePackageLock ? ['--frozen-lockfile'] : []
core.debug(
`yarn command: "${yarnPath}" ${args} ${JSON.stringify(options)}`
)
return exec.exec(quote(yarnPath), args, options)
})
} else {
console.log('installing NPM dependencies')
return io.which('npm', true).then(npmPath => {
console.log('npm at "%s"', npmPath)
const args = shouldUsePackageLock ? ['ci'] : ['install']
core.debug(`npm command: "${npmPath}" ${args} ${JSON.stringify(options)}`)
return exec.exec(quote(npmPath), args, options)
})
}
}
const getPlatformAndArch = () => `${process.platform}-${process.arch}`
const getNow = () => new Date()
const getLockFilename = usePackageLock => workingDirectory => {
const packageFilename = path.join(workingDirectory, 'package.json')
const yarnFilename = path.join(workingDirectory, 'yarn.lock')
const useYarn = fs.existsSync(yarnFilename)
if (!usePackageLock) {
return {
useYarn,
lockFilename: packageFilename
}
}
core.debug(`yarn lock file "${yarnFilename}" exists? ${useYarn}`)
const npmShrinkwrapFilename = path.join(
workingDirectory,
'npm-shrinkwrap.json'
)
const packageLockFilename = path.join(workingDirectory, 'package-lock.json')
const npmFilename =
!useYarn && fs.existsSync(npmShrinkwrapFilename)
? npmShrinkwrapFilename
: packageLockFilename
const result = {
useYarn,
lockFilename: useYarn ? yarnFilename : npmFilename
}
return result
}
const getCacheParams = ({
useYarn,
useRollingCache,
homeDirectory,
npmCacheFolder,
lockHash,
cachePrefix
}) => {
const platformAndArch = api.utils.getPlatformAndArch()
core.debug(`platform and arch ${platformAndArch}`)
const primaryKeySegments = [platformAndArch]
if (cachePrefix) {
primaryKeySegments.unshift(cachePrefix)
}
let inputPaths, restoreKeys
if (useYarn) {
inputPaths = [path.join(homeDirectory, '.cache', 'yarn')]
primaryKeySegments.unshift('yarn')
} else {
inputPaths = [npmCacheFolder]
primaryKeySegments.unshift('npm')
}
if (useRollingCache) {
const now = api.utils.getNow()
primaryKeySegments.push(
String(now.getFullYear()),
String(now.getMonth()),
lockHash
)
restoreKeys = [
primaryKeySegments.join('-'),
primaryKeySegments.slice(0, -1).join('-')
]
} else {
primaryKeySegments.push(lockHash)
restoreKeys = [primaryKeySegments.join('-')]
}
return { primaryKey: primaryKeySegments.join('-'), inputPaths, restoreKeys }
}
const installInOneFolder = ({
usePackageLock,
workingDirectory,
useRollingCache,
installCommand,
cachePrefix
}) => {
core.debug(`usePackageLock? ${usePackageLock}`)
core.debug(`working directory ${workingDirectory}`)
const lockInfo = getLockFilename(usePackageLock)(workingDirectory)
const lockHash = hasha.fromFileSync(lockInfo.lockFilename)
if (!lockHash) {
throw new Error(
`could not compute hash from file "${lockInfo.lockFilename}"`
)
}
core.debug(`lock filename ${lockInfo.lockFilename}`)
core.debug(`file hash ${lockHash}`)
// if the user provided a custom command like "npm ...", then we cannot
// use Yarn cache paths
let useYarn = lockInfo.useYarn
if (useYarn && installCommand && installCommand.startsWith('npm')) {
core.debug('using NPM command, not using Yarn cache paths')
useYarn = false
}
// enforce the same NPM cache folder across different operating systems
const homeDirectory = os.homedir()
const NPM_CACHE_FOLDER = path.join(homeDirectory, '.npm')
const NPM_CACHE = getCacheParams({
useYarn,
homeDirectory,
useRollingCache,
npmCacheFolder: NPM_CACHE_FOLDER,
lockHash,
cachePrefix
})
const opts = {
useYarn,
usePackageLock,
workingDirectory,
npmCacheFolder: NPM_CACHE_FOLDER,
installCommand
}
return api.utils.restoreCachedNpm(NPM_CACHE).then(npmCacheHit => {
return api.utils.install(opts).then(() => {
if (npmCacheHit) {
return
}
return api.utils.saveCachedNpm(NPM_CACHE)
})
})
}
const npmInstallAction = async () => {
const usePackageLock = getInputBool('useLockFile', true)
const useRollingCache = getInputBool('useRollingCache', false)
const cachePrefix = core.getInput('cache-key-prefix') || ''
core.debug(`usePackageLock? ${usePackageLock}`)
core.debug(`useRollingCache? ${useRollingCache}`)
core.debug(`cache prefix "${cachePrefix}"`)
// Note: working directory for "actions/exec" should be absolute
const wds = core.getInput('working-directory') || process.cwd()
const workingDirectories = wds
.split('\n')
.map(s => s.trim())
.filter(Boolean)
core.debug(`iterating over working ${workingDirectories.length} folder(s)`)
const installCommand = core.getInput('install-command')
try {
for (const workingDirectory of workingDirectories) {
const started = +new Date()
await api.utils.installInOneFolder({
usePackageLock,
useRollingCache,
workingDirectory,
installCommand,
cachePrefix
})
const finished = +new Date()
core.debug(
`installing in ${workingDirectory} took ${finished - started}ms`
)
}
// node will stay alive if any promises are not resolved,
// which is a possibility if HTTP requests are dangling
// due to retries or timeouts. We know that if we got here
// that all promises that we care about have successfully
// resolved, so simply exit with success.
// From: https://github.com/actions/cache/blob/a2ed59d39b352305bdd2f628719a53b2cc4f9613/src/saveImpl.ts#L96
if (process.env.NODE_ENV !== 'test') {
process.exit(0)
} else {
core.debug('skip process.exit(0) in test mode')
}
} catch (err) {
console.error(err)
core.setFailed(err.message)
process.exit(1)
}
}
/**
* Object of exports, useful to easy testing when mocking individual methods
*/
const api = {
npmInstallAction,
// export functions mostly for testing
utils: {
restoreCachedNpm,
install,
saveCachedNpm,
getPlatformAndArch,
getNow,
installInOneFolder
}
}
module.exports = api
// @ts-ignore
if (!module.parent) {
console.log('running npm-install GitHub Action')
const started = +new Date()
npmInstallAction()
.then(() => {
console.log('all done, exiting')
const finished = +new Date()
core.debug(`npm-install took ${finished - started}ms`)
})
.catch(error => {
console.log(error)
core.setFailed(error.message)
})
}