mirror of https://github.com/actions/toolkit
Add tool-cache (#12)
* Add tool-cache * Format * Fix linux bug * Update readme * Package zip/unzip * Reorganize * Format * unzip somehow got corrupted, fixing * Resolve remaining todos * Don't log everything * Pass error codes * linting * Branding and nits * Fix for mac * clean up windows/nix logic * Clean up tests * Export HTTPError * Add option for custom 7z tool * Lint/format * Dont wipe temp/tool directoriespull/14/head
parent
9bf86bb363
commit
71a9b2d3ed
|
@ -6,7 +6,6 @@
|
||||||
"core",
|
"core",
|
||||||
"actions"
|
"actions"
|
||||||
],
|
],
|
||||||
"author": "Bryan MacFarlane <bryanmac@microsoft.com>",
|
|
||||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/core",
|
"homepage": "https://github.com/actions/toolkit/tree/master/packages/core",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "lib/core.js",
|
"main": "lib/core.js",
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
"exec",
|
"exec",
|
||||||
"actions"
|
"actions"
|
||||||
],
|
],
|
||||||
"author": "Bryan MacFarlane <bryanmac@microsoft.com>",
|
|
||||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/exec",
|
"homepage": "https://github.com/actions/toolkit/tree/master/packages/exec",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "lib/exec.js",
|
"main": "lib/exec.js",
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
"io",
|
"io",
|
||||||
"actions"
|
"actions"
|
||||||
],
|
],
|
||||||
"author": "Danny McCormick <damccorm@microsoft.com>",
|
|
||||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/io",
|
"homepage": "https://github.com/actions/toolkit/tree/master/packages/io",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "lib/io.js",
|
"main": "lib/io.js",
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
# `@actions/tool-cache`
|
||||||
|
|
||||||
|
> Functions necessary for downloading and caching tools.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
See [src/tool-cache.ts](src/tool-cache.ts).
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,338 @@
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as nock from 'nock'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as io from '@actions/io'
|
||||||
|
import * as exec from '@actions/exec'
|
||||||
|
|
||||||
|
const cachePath = path.join(__dirname, 'CACHE')
|
||||||
|
const tempPath = path.join(__dirname, 'TEMP')
|
||||||
|
// Set temp and tool directories before importing (used to set global state)
|
||||||
|
process.env['RUNNER_TEMPDIRECTORY'] = tempPath
|
||||||
|
process.env['RUNNER_TOOLSDIRECTORY'] = cachePath
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/first
|
||||||
|
import * as tc from '../src/tool-cache'
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
|
describe('@actions/tool-cache', function() {
|
||||||
|
beforeAll(function() {
|
||||||
|
nock('http://example.com')
|
||||||
|
.persist()
|
||||||
|
.get('/bytes/35')
|
||||||
|
.reply(200, {
|
||||||
|
username: 'abc',
|
||||||
|
password: 'def'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async function() {
|
||||||
|
await io.rmRF(cachePath)
|
||||||
|
await io.rmRF(tempPath)
|
||||||
|
await io.mkdirP(cachePath)
|
||||||
|
await io.mkdirP(tempPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async function() {
|
||||||
|
await io.rmRF(tempPath)
|
||||||
|
await io.rmRF(cachePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('downloads a 35 byte file', async () => {
|
||||||
|
const downPath: string = await tc.downloadTool(
|
||||||
|
'http://example.com/bytes/35'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(fs.existsSync(downPath)).toBeTruthy()
|
||||||
|
expect(fs.statSync(downPath).size).toBe(35)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('downloads a 35 byte file after a redirect', async () => {
|
||||||
|
nock('http://example.com')
|
||||||
|
.get('/redirect-to')
|
||||||
|
.reply(303, undefined, {
|
||||||
|
location: 'http://example.com/bytes/35'
|
||||||
|
})
|
||||||
|
|
||||||
|
const downPath: string = await tc.downloadTool(
|
||||||
|
'http://example.com/redirect-to'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(fs.existsSync(downPath)).toBeTruthy()
|
||||||
|
expect(fs.statSync(downPath).size).toBe(35)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has status code in exception dictionary for HTTP error code responses', async () => {
|
||||||
|
nock('http://example.com')
|
||||||
|
.get('/bytes/bad')
|
||||||
|
.reply(400, {
|
||||||
|
username: 'bad',
|
||||||
|
password: 'file'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect.assertions(2)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const errorCodeUrl = 'http://example.com/bytes/bad'
|
||||||
|
await tc.downloadTool(errorCodeUrl)
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.toString()).toContain('Unexpected HTTP response: 400')
|
||||||
|
expect(err['httpStatusCode']).toBe(400)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works with redirect code 302', async function() {
|
||||||
|
nock('http://example.com')
|
||||||
|
.get('/redirect-to')
|
||||||
|
.reply(302, undefined, {
|
||||||
|
location: 'http://example.com/bytes/35'
|
||||||
|
})
|
||||||
|
|
||||||
|
const downPath: string = await tc.downloadTool(
|
||||||
|
'http://example.com/redirect-to'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(fs.existsSync(downPath)).toBeTruthy()
|
||||||
|
expect(fs.statSync(downPath).size).toBe(35)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('installs a binary tool and finds it', async () => {
|
||||||
|
const downPath: string = await tc.downloadTool(
|
||||||
|
'http://example.com/bytes/35'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(fs.existsSync(downPath)).toBeTruthy()
|
||||||
|
|
||||||
|
await tc.cacheFile(downPath, 'foo', 'foo', '1.1.0')
|
||||||
|
|
||||||
|
const toolPath: string = tc.find('foo', '1.1.0')
|
||||||
|
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||||
|
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||||
|
|
||||||
|
const binaryPath: string = path.join(toolPath, 'foo')
|
||||||
|
expect(fs.existsSync(binaryPath)).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
it('installs a 7z and finds it', async () => {
|
||||||
|
const tempDir = path.join(__dirname, 'test-install-7z')
|
||||||
|
try {
|
||||||
|
await io.mkdirP(tempDir)
|
||||||
|
|
||||||
|
// copy the 7z file to the test dir
|
||||||
|
const _7zFile: string = path.join(tempDir, 'test.7z')
|
||||||
|
await io.cp(path.join(__dirname, 'data', 'test.7z'), _7zFile)
|
||||||
|
|
||||||
|
// extract/cache
|
||||||
|
const extPath: string = await tc.extract7z(_7zFile)
|
||||||
|
await tc.cacheDir(extPath, 'my-7z-contents', '1.1.0')
|
||||||
|
const toolPath: string = tc.find('my-7z-contents', '1.1.0')
|
||||||
|
|
||||||
|
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||||
|
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||||
|
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(toolPath, 'file-with-ç-character.txt'))
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
|
||||||
|
).toBeTruthy()
|
||||||
|
} finally {
|
||||||
|
await io.rmRF(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('extract 7z using custom 7z tool', async function() {
|
||||||
|
const tempDir = path.join(
|
||||||
|
__dirname,
|
||||||
|
'test-extract-7z-using-custom-7z-tool'
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
await io.mkdirP(tempDir)
|
||||||
|
// create mock7zr.cmd
|
||||||
|
const mock7zrPath: string = path.join(tempDir, 'mock7zr.cmd')
|
||||||
|
fs.writeFileSync(
|
||||||
|
mock7zrPath,
|
||||||
|
[
|
||||||
|
'echo %* > "%~dp0mock7zr-args.txt"',
|
||||||
|
`"${path.join(
|
||||||
|
__dirname,
|
||||||
|
'..',
|
||||||
|
'scripts',
|
||||||
|
'externals',
|
||||||
|
'7zdec.exe'
|
||||||
|
)}" x %5`
|
||||||
|
].join('\r\n')
|
||||||
|
)
|
||||||
|
|
||||||
|
// copy the 7z file to the test dir
|
||||||
|
const _7zFile: string = path.join(tempDir, 'test.7z')
|
||||||
|
await io.cp(path.join(__dirname, 'data', 'test.7z'), _7zFile)
|
||||||
|
|
||||||
|
// extract
|
||||||
|
const extPath: string = await tc.extract7z(
|
||||||
|
_7zFile,
|
||||||
|
undefined,
|
||||||
|
mock7zrPath
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(fs.existsSync(extPath)).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(tempDir, 'mock7zr-args.txt'))
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
fs
|
||||||
|
.readFileSync(path.join(tempDir, 'mock7zr-args.txt'))
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
).toBe(`x -bb1 -bd -sccUTF-8 ${_7zFile}`)
|
||||||
|
expect(fs.existsSync(path.join(extPath, 'file.txt'))).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(extPath, 'file-with-ç-character.txt'))
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(extPath, 'folder', 'nested-file.txt'))
|
||||||
|
).toBeTruthy()
|
||||||
|
} finally {
|
||||||
|
await io.rmRF(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
it('installs a zip and finds it', async () => {
|
||||||
|
const tempDir = path.join(__dirname, 'test-install-zip')
|
||||||
|
try {
|
||||||
|
await io.mkdirP(tempDir)
|
||||||
|
|
||||||
|
// stage the layout for a zip file:
|
||||||
|
// file.txt
|
||||||
|
// folder/nested-file.txt
|
||||||
|
const stagingDir = path.join(tempDir, 'zip-staging')
|
||||||
|
await io.mkdirP(path.join(stagingDir, 'folder'))
|
||||||
|
fs.writeFileSync(path.join(stagingDir, 'file.txt'), '')
|
||||||
|
fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '')
|
||||||
|
|
||||||
|
// create the zip
|
||||||
|
const zipFile = path.join(tempDir, 'test.zip')
|
||||||
|
await io.rmRF(zipFile)
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const escapedStagingPath = stagingDir.replace(/'/g, "''") // double-up single quotes
|
||||||
|
const escapedZipFile = zipFile.replace(/'/g, "''")
|
||||||
|
const powershellPath = await io.which('powershell', true)
|
||||||
|
const args = [
|
||||||
|
'-NoLogo',
|
||||||
|
'-Sta',
|
||||||
|
'-NoProfile',
|
||||||
|
'-NonInteractive',
|
||||||
|
'-ExecutionPolicy',
|
||||||
|
'Unrestricted',
|
||||||
|
'-Command',
|
||||||
|
`$ErrorActionPreference = 'Stop' ; Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::CreateFromDirectory('${escapedStagingPath}', '${escapedZipFile}')`
|
||||||
|
]
|
||||||
|
await exec.exec(`"${powershellPath}"`, args)
|
||||||
|
} else {
|
||||||
|
const zipPath: string = path.join(__dirname, 'externals', 'zip')
|
||||||
|
await exec.exec(`"${zipPath}`, [zipFile, '-r', '.'], {cwd: stagingDir})
|
||||||
|
}
|
||||||
|
|
||||||
|
const extPath: string = await tc.extractZip(zipFile)
|
||||||
|
await tc.cacheDir(extPath, 'foo', '1.1.0')
|
||||||
|
const toolPath: string = tc.find('foo', '1.1.0')
|
||||||
|
|
||||||
|
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||||
|
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||||
|
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
|
||||||
|
).toBeTruthy()
|
||||||
|
} finally {
|
||||||
|
await io.rmRF(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('installs a zip and extracts it to specified directory', async function() {
|
||||||
|
const tempDir = path.join(__dirname, 'test-install-zip')
|
||||||
|
try {
|
||||||
|
await io.mkdirP(tempDir)
|
||||||
|
|
||||||
|
// stage the layout for a zip file:
|
||||||
|
// file.txt
|
||||||
|
// folder/nested-file.txt
|
||||||
|
const stagingDir = path.join(tempDir, 'zip-staging')
|
||||||
|
await io.mkdirP(path.join(stagingDir, 'folder'))
|
||||||
|
fs.writeFileSync(path.join(stagingDir, 'file.txt'), '')
|
||||||
|
fs.writeFileSync(path.join(stagingDir, 'folder', 'nested-file.txt'), '')
|
||||||
|
|
||||||
|
// create the zip
|
||||||
|
const zipFile = path.join(tempDir, 'test.zip')
|
||||||
|
await io.rmRF(zipFile)
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const escapedStagingPath = stagingDir.replace(/'/g, "''") // double-up single quotes
|
||||||
|
const escapedZipFile = zipFile.replace(/'/g, "''")
|
||||||
|
const powershellPath = await io.which('powershell', true)
|
||||||
|
const args = [
|
||||||
|
'-NoLogo',
|
||||||
|
'-Sta',
|
||||||
|
'-NoProfile',
|
||||||
|
'-NonInteractive',
|
||||||
|
'-ExecutionPolicy',
|
||||||
|
'Unrestricted',
|
||||||
|
'-Command',
|
||||||
|
`$ErrorActionPreference = 'Stop' ; Add-Type -AssemblyName System.IO.Compression.FileSystem ; [System.IO.Compression.ZipFile]::CreateFromDirectory('${escapedStagingPath}', '${escapedZipFile}')`
|
||||||
|
]
|
||||||
|
await exec.exec(`"${powershellPath}"`, args)
|
||||||
|
} else {
|
||||||
|
const zipPath = path.join(__dirname, 'externals', 'zip')
|
||||||
|
await exec.exec(zipPath, [zipFile, '-r', '.'], {cwd: stagingDir})
|
||||||
|
}
|
||||||
|
|
||||||
|
const destDir = path.join(__dirname, 'unzip-dest')
|
||||||
|
await io.rmRF(destDir)
|
||||||
|
await io.mkdirP(destDir)
|
||||||
|
try {
|
||||||
|
const extPath: string = await tc.extractZip(zipFile, destDir)
|
||||||
|
await tc.cacheDir(extPath, 'foo', '1.1.0')
|
||||||
|
const toolPath: string = tc.find('foo', '1.1.0')
|
||||||
|
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||||
|
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||||
|
expect(fs.existsSync(path.join(toolPath, 'file.txt'))).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
fs.existsSync(path.join(toolPath, 'folder', 'nested-file.txt'))
|
||||||
|
).toBeTruthy()
|
||||||
|
} finally {
|
||||||
|
await io.rmRF(destDir)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await io.rmRF(tempDir)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works with a 502 temporary failure', async function() {
|
||||||
|
nock('http://example.com')
|
||||||
|
.get('/temp502')
|
||||||
|
.twice()
|
||||||
|
.reply(502, undefined)
|
||||||
|
nock('http://example.com')
|
||||||
|
.get('/temp502')
|
||||||
|
.reply(200, undefined)
|
||||||
|
|
||||||
|
const statusCodeUrl = 'http://example.com/temp502'
|
||||||
|
await tc.downloadTool(statusCodeUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("doesn't retry 502s more than 3 times", async function() {
|
||||||
|
nock('http://example.com')
|
||||||
|
.get('/perm502')
|
||||||
|
.times(3)
|
||||||
|
.reply(502, undefined)
|
||||||
|
|
||||||
|
expect.assertions(1)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const statusCodeUrl = 'http://example.com/perm502'
|
||||||
|
await tc.downloadTool(statusCodeUrl)
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.toString()).toContain('502')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,205 @@
|
||||||
|
{
|
||||||
|
"name": "@actions/tool-cache",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/nock": {
|
||||||
|
"version": "10.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz",
|
||||||
|
"integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/node": {
|
||||||
|
"version": "12.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
|
||||||
|
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/semver": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-OO0srjOGH99a4LUN2its3+r6CBYcplhJ466yLqs+zvAWgphCpS8hYZEZ797tRDP/QKcqTdb/YCN6ifASoAWkrQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/uuid": {
|
||||||
|
"version": "3.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz",
|
||||||
|
"integrity": "sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"assertion-error": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"chai": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"assertion-error": "^1.1.0",
|
||||||
|
"check-error": "^1.0.2",
|
||||||
|
"deep-eql": "^3.0.1",
|
||||||
|
"get-func-name": "^2.0.0",
|
||||||
|
"pathval": "^1.1.0",
|
||||||
|
"type-detect": "^4.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"check-error": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deep-eql": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"type-detect": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deep-equal": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"get-func-name": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"json-stringify-safe": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||||
|
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"lodash": {
|
||||||
|
"version": "4.17.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
|
||||||
|
"integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"minimist": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
|
||||||
|
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"mkdirp": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
|
||||||
|
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"minimist": "0.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ms": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"nock": {
|
||||||
|
"version": "10.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz",
|
||||||
|
"integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"chai": "^4.1.2",
|
||||||
|
"debug": "^4.1.0",
|
||||||
|
"deep-equal": "^1.0.0",
|
||||||
|
"json-stringify-safe": "^5.0.1",
|
||||||
|
"lodash": "^4.17.5",
|
||||||
|
"mkdirp": "^0.5.0",
|
||||||
|
"propagate": "^1.0.0",
|
||||||
|
"qs": "^6.5.1",
|
||||||
|
"semver": "^5.5.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": {
|
||||||
|
"version": "5.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
|
||||||
|
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pathval": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz",
|
||||||
|
"integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"propagate": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"qs": {
|
||||||
|
"version": "6.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
|
||||||
|
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"semver": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-kCqEOOHoBcFs/2Ccuk4Xarm/KiWRSLEX9CAZF8xkJ6ZPlIoTZ8V5f7J16vYLJqDbR7KrxTJpR2lqjIEm2Qx9cQ=="
|
||||||
|
},
|
||||||
|
"tunnel": {
|
||||||
|
"version": "0.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz",
|
||||||
|
"integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM="
|
||||||
|
},
|
||||||
|
"type-detect": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"typed-rest-client": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-f+3+X13CIpkv0WvFERkXq4aH5BYzyeYclf8t+X7oa/YaE80EjYW12kphY0aEQBaL9RzChP0MSbsVhB4X+bzyDw==",
|
||||||
|
"requires": {
|
||||||
|
"tunnel": "0.0.4",
|
||||||
|
"underscore": "1.8.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"underscore": {
|
||||||
|
"version": "1.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
|
||||||
|
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI="
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"name": "@actions/tool-cache",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Actions tool-cache lib",
|
||||||
|
"keywords": [
|
||||||
|
"exec",
|
||||||
|
"actions"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/actions/toolkit/tree/master/packages/exec",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "lib/tool-cache.js",
|
||||||
|
"directories": {
|
||||||
|
"lib": "lib",
|
||||||
|
"test": "__tests__"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib",
|
||||||
|
"scripts"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/actions/toolkit.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: run tests from root\" && exit 1",
|
||||||
|
"tsc": "tsc"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/actions/toolkit/issues"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^6.1.0",
|
||||||
|
"typed-rest-client": "^1.4.0",
|
||||||
|
"uuid": "^3.3.2",
|
||||||
|
"@actions/core": "^0.1.0",
|
||||||
|
"@actions/io": "^1.0.0",
|
||||||
|
"@actions/exec": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"nock": "^10.0.6",
|
||||||
|
"@types/nock": "^10.0.3",
|
||||||
|
"@types/semver": "^6.0.0",
|
||||||
|
"@types/uuid": "^3.4.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Source,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string]$Target)
|
||||||
|
|
||||||
|
# This script translates the output from 7zdec into UTF8. Node has limited
|
||||||
|
# built-in support for encodings.
|
||||||
|
#
|
||||||
|
# 7zdec uses the system default code page. The system default code page varies
|
||||||
|
# depending on the locale configuration. On an en-US box, the system default code
|
||||||
|
# page is Windows-1252.
|
||||||
|
#
|
||||||
|
# Note, on a typical en-US box, testing with the 'ç' character is a good way to
|
||||||
|
# determine whether data is passed correctly between processes. This is because
|
||||||
|
# the 'ç' character has a different code point across each of the common encodings
|
||||||
|
# on a typical en-US box, i.e.
|
||||||
|
# 1) the default console-output code page (IBM437)
|
||||||
|
# 2) the system default code page (i.e. CP_ACP) (Windows-1252)
|
||||||
|
# 3) UTF8
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# Redefine the wrapper over STDOUT to use UTF8. Node expects UTF8 by default.
|
||||||
|
$stdout = [System.Console]::OpenStandardOutput()
|
||||||
|
$utf8 = New-Object System.Text.UTF8Encoding($false) # do not emit BOM
|
||||||
|
$writer = New-Object System.IO.StreamWriter($stdout, $utf8)
|
||||||
|
[System.Console]::SetOut($writer)
|
||||||
|
|
||||||
|
# All subsequent output must be written using [System.Console]::WriteLine(). In
|
||||||
|
# PowerShell 4, Write-Host and Out-Default do not consider the updated stream writer.
|
||||||
|
|
||||||
|
Set-Location -LiteralPath $Target
|
||||||
|
|
||||||
|
# Print the ##command.
|
||||||
|
$_7zdec = Join-Path -Path "$PSScriptRoot" -ChildPath "externals/7zdec.exe"
|
||||||
|
[System.Console]::WriteLine("##[command]$_7zdec x `"$Source`"")
|
||||||
|
|
||||||
|
# The $OutputEncoding variable instructs PowerShell how to interpret the output
|
||||||
|
# from the external command.
|
||||||
|
$OutputEncoding = [System.Text.Encoding]::Default
|
||||||
|
|
||||||
|
# Note, the output from 7zdec.exe needs to be iterated over. Otherwise PowerShell.exe
|
||||||
|
# will launch the external command in such a way that it inherits the streams.
|
||||||
|
& $_7zdec x $Source 2>&1 |
|
||||||
|
ForEach-Object {
|
||||||
|
if ($_ -is [System.Management.Automation.ErrorRecord]) {
|
||||||
|
[System.Console]::WriteLine($_.Exception.Message)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[System.Console]::WriteLine($_)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[System.Console]::WriteLine("##[debug]7zdec.exe exit code '$LASTEXITCODE'")
|
||||||
|
[System.Console]::Out.Flush()
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
exit $LASTEXITCODE
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,473 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as io from '@actions/io'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as os from 'os'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as httpm from 'typed-rest-client/HttpClient'
|
||||||
|
import * as semver from 'semver'
|
||||||
|
import * as uuidV4 from 'uuid/v4'
|
||||||
|
import {exec} from '@actions/exec/lib/exec'
|
||||||
|
import {ExecOptions} from '@actions/exec/lib/interfaces'
|
||||||
|
import {ok} from 'assert'
|
||||||
|
|
||||||
|
export class HTTPError extends Error {
|
||||||
|
constructor(readonly httpStatusCode: number | undefined) {
|
||||||
|
super(`Unexpected HTTP response: ${httpStatusCode}`)
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
const userAgent = 'actions/tool-cache'
|
||||||
|
|
||||||
|
// On load grab temp directory and cache directory and remove them from env (currently don't want to expose this)
|
||||||
|
let tempDirectory: string = process.env['RUNNER_TEMPDIRECTORY'] || ''
|
||||||
|
let cacheRoot: string = process.env['RUNNER_TOOLSDIRECTORY'] || ''
|
||||||
|
// If directories not found, place them in common temp locations
|
||||||
|
if (!tempDirectory || !cacheRoot) {
|
||||||
|
let baseLocation: string
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
// On windows use the USERPROFILE env variable
|
||||||
|
baseLocation = process.env['USERPROFILE'] || 'C:\\'
|
||||||
|
} else {
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
baseLocation = '/Users'
|
||||||
|
} else {
|
||||||
|
baseLocation = '/home'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!tempDirectory) {
|
||||||
|
tempDirectory = path.join(baseLocation, 'actions', 'temp')
|
||||||
|
}
|
||||||
|
if (!cacheRoot) {
|
||||||
|
cacheRoot = path.join(baseLocation, 'actions', 'cache')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a tool from an url and stream it into a file
|
||||||
|
*
|
||||||
|
* @param url url of tool to download
|
||||||
|
* @returns path to downloaded tool
|
||||||
|
*/
|
||||||
|
export async function downloadTool(url: string): Promise<string> {
|
||||||
|
// Wrap in a promise so that we can resolve from within stream callbacks
|
||||||
|
return new Promise<string>(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const http = new httpm.HttpClient(userAgent, [], {
|
||||||
|
allowRetries: true,
|
||||||
|
maxRetries: 3
|
||||||
|
})
|
||||||
|
const destPath = path.join(tempDirectory, uuidV4())
|
||||||
|
|
||||||
|
await io.mkdirP(tempDirectory)
|
||||||
|
core.debug(`Downloading ${url}`)
|
||||||
|
core.debug(`Downloading ${destPath}`)
|
||||||
|
|
||||||
|
if (fs.existsSync(destPath)) {
|
||||||
|
throw new Error(`Destination file path ${destPath} already exists`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: httpm.HttpClientResponse = await http.get(url)
|
||||||
|
|
||||||
|
if (response.message.statusCode !== 200) {
|
||||||
|
const err = new HTTPError(response.message.statusCode)
|
||||||
|
core.debug(
|
||||||
|
`Failed to download from "${url}". Code(${
|
||||||
|
response.message.statusCode
|
||||||
|
}) Message(${response.message.statusMessage})`
|
||||||
|
)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
const file: NodeJS.WritableStream = fs.createWriteStream(destPath)
|
||||||
|
file.on('open', async () => {
|
||||||
|
try {
|
||||||
|
const stream = response.message.pipe(file)
|
||||||
|
stream.on('close', () => {
|
||||||
|
core.debug('download complete')
|
||||||
|
resolve(destPath)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
core.debug(
|
||||||
|
`Failed to download from "${url}". Code(${
|
||||||
|
response.message.statusCode
|
||||||
|
}) Message(${response.message.statusMessage})`
|
||||||
|
)
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
file.on('error', err => {
|
||||||
|
file.end()
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a .7z file
|
||||||
|
*
|
||||||
|
* @param file path to the .7z file
|
||||||
|
* @param dest destination directory. Optional.
|
||||||
|
* @param _7zPath path to 7zr.exe. Optional, for long path support. Most .7z archives do not have this
|
||||||
|
* problem. If your .7z archive contains very long paths, you can pass the path to 7zr.exe which will
|
||||||
|
* gracefully handle long paths. By default 7zdec.exe is used because it is a very small program and is
|
||||||
|
* bundled with the tool lib. However it does not support long paths. 7zr.exe is the reduced command line
|
||||||
|
* interface, it is smaller than the full command line interface, and it does support long paths. At the
|
||||||
|
* time of this writing, it is freely available from the LZMA SDK that is available on the 7zip website.
|
||||||
|
* Be sure to check the current license agreement. If 7zr.exe is bundled with your action, then the path
|
||||||
|
* to 7zr.exe can be pass to this function.
|
||||||
|
* @returns path to the destination directory
|
||||||
|
*/
|
||||||
|
export async function extract7z(
|
||||||
|
file: string,
|
||||||
|
dest?: string,
|
||||||
|
_7zPath?: string
|
||||||
|
): Promise<string> {
|
||||||
|
ok(IS_WINDOWS, 'extract7z() not supported on current OS')
|
||||||
|
ok(file, 'parameter "file" is required')
|
||||||
|
|
||||||
|
dest = dest || (await _createExtractFolder(dest))
|
||||||
|
|
||||||
|
const originalCwd = process.cwd()
|
||||||
|
process.chdir(dest)
|
||||||
|
if (_7zPath) {
|
||||||
|
try {
|
||||||
|
const args: string[] = [
|
||||||
|
'x', // eXtract files with full paths
|
||||||
|
'-bb1', // -bb[0-3] : set output log level
|
||||||
|
'-bd', // disable progress indicator
|
||||||
|
'-sccUTF-8', // set charset for for console input/output
|
||||||
|
file
|
||||||
|
]
|
||||||
|
const options: ExecOptions = {
|
||||||
|
silent: true
|
||||||
|
}
|
||||||
|
await exec(`"${_7zPath}"`, args, options)
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const escapedScript = path
|
||||||
|
.join(__dirname, '..', 'scripts', 'Invoke-7zdec.ps1')
|
||||||
|
.replace(/'/g, "''")
|
||||||
|
.replace(/"|\n|\r/g, '') // double-up single quotes, remove double quotes and newlines
|
||||||
|
const escapedFile = file.replace(/'/g, "''").replace(/"|\n|\r/g, '')
|
||||||
|
const escapedTarget = dest.replace(/'/g, "''").replace(/"|\n|\r/g, '')
|
||||||
|
const command = `& '${escapedScript}' -Source '${escapedFile}' -Target '${escapedTarget}'`
|
||||||
|
const args: string[] = [
|
||||||
|
'-NoLogo',
|
||||||
|
'-Sta',
|
||||||
|
'-NoProfile',
|
||||||
|
'-NonInteractive',
|
||||||
|
'-ExecutionPolicy',
|
||||||
|
'Unrestricted',
|
||||||
|
'-Command',
|
||||||
|
command
|
||||||
|
]
|
||||||
|
const options: ExecOptions = {
|
||||||
|
silent: true
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const powershellPath: string = await io.which('powershell', true)
|
||||||
|
await exec(`"${powershellPath}"`, args, options)
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a tar
|
||||||
|
*
|
||||||
|
* @param file path to the tar
|
||||||
|
* @param dest destination directory. Optional.
|
||||||
|
* @returns path to the destination directory
|
||||||
|
*/
|
||||||
|
export async function extractTar(file: string, dest?: string): Promise<string> {
|
||||||
|
if (!file) {
|
||||||
|
throw new Error("parameter 'file' is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
dest = dest || (await _createExtractFolder(dest))
|
||||||
|
const tarPath: string = await io.which('tar', true)
|
||||||
|
await exec(`"${tarPath}"`, ['xzC', dest, '-f', file])
|
||||||
|
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a zip
|
||||||
|
*
|
||||||
|
* @param file path to the zip
|
||||||
|
* @param dest destination directory. Optional.
|
||||||
|
* @returns path to the destination directory
|
||||||
|
*/
|
||||||
|
export async function extractZip(file: string, dest?: string): Promise<string> {
|
||||||
|
if (!file) {
|
||||||
|
throw new Error("parameter 'file' is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
dest = dest || (await _createExtractFolder(dest))
|
||||||
|
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
await extractZipWin(file, dest)
|
||||||
|
} else {
|
||||||
|
await extractZipNix(file, dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractZipWin(file: string, dest: string): Promise<void> {
|
||||||
|
// build the powershell command
|
||||||
|
const escapedFile = file.replace(/'/g, "''").replace(/"|\n|\r/g, '') // double-up single quotes, remove double quotes and newlines
|
||||||
|
const escapedDest = dest.replace(/'/g, "''").replace(/"|\n|\r/g, '')
|
||||||
|
const command = `$ErrorActionPreference = 'Stop' ; try { Add-Type -AssemblyName System.IO.Compression.FileSystem } catch { } ; [System.IO.Compression.ZipFile]::ExtractToDirectory('${escapedFile}', '${escapedDest}')`
|
||||||
|
|
||||||
|
// run powershell
|
||||||
|
const powershellPath = await io.which('powershell')
|
||||||
|
const args = [
|
||||||
|
'-NoLogo',
|
||||||
|
'-Sta',
|
||||||
|
'-NoProfile',
|
||||||
|
'-NonInteractive',
|
||||||
|
'-ExecutionPolicy',
|
||||||
|
'Unrestricted',
|
||||||
|
'-Command',
|
||||||
|
command
|
||||||
|
]
|
||||||
|
await exec(`"${powershellPath}"`, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractZipNix(file: string, dest: string): Promise<void> {
|
||||||
|
const unzipPath = path.join(__dirname, '..', 'scripts', 'externals', 'unzip')
|
||||||
|
await exec(`"${unzipPath}"`, [file], {cwd: dest})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches a directory and installs it into the tool cacheDir
|
||||||
|
*
|
||||||
|
* @param sourceDir the directory to cache into tools
|
||||||
|
* @param tool tool name
|
||||||
|
* @param version version of the tool. semver format
|
||||||
|
* @param arch architecture of the tool. Optional. Defaults to machine architecture
|
||||||
|
*/
|
||||||
|
export async function cacheDir(
|
||||||
|
sourceDir: string,
|
||||||
|
tool: string,
|
||||||
|
version: string,
|
||||||
|
arch?: string
|
||||||
|
): Promise<string> {
|
||||||
|
version = semver.clean(version) || version
|
||||||
|
arch = arch || os.arch()
|
||||||
|
core.debug(`Caching tool ${tool} ${version} ${arch}`)
|
||||||
|
|
||||||
|
core.debug(`source dir: ${sourceDir}`)
|
||||||
|
if (!fs.statSync(sourceDir).isDirectory()) {
|
||||||
|
throw new Error('sourceDir is not a directory')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the tool dir
|
||||||
|
const destPath: string = await _createToolPath(tool, version, arch)
|
||||||
|
// copy each child item. do not move. move can fail on Windows
|
||||||
|
// due to anti-virus software having an open handle on a file.
|
||||||
|
for (const itemName of fs.readdirSync(sourceDir)) {
|
||||||
|
const s = path.join(sourceDir, itemName)
|
||||||
|
await io.cp(s, destPath, {recursive: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
// write .complete
|
||||||
|
_completeToolPath(tool, version, arch)
|
||||||
|
|
||||||
|
return destPath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches a downloaded file (GUID) and installs it
|
||||||
|
* into the tool cache with a given targetName
|
||||||
|
*
|
||||||
|
* @param sourceFile the file to cache into tools. Typically a result of downloadTool which is a guid.
|
||||||
|
* @param targetFile the name of the file name in the tools directory
|
||||||
|
* @param tool tool name
|
||||||
|
* @param version version of the tool. semver format
|
||||||
|
* @param arch architecture of the tool. Optional. Defaults to machine architecture
|
||||||
|
*/
|
||||||
|
export async function cacheFile(
|
||||||
|
sourceFile: string,
|
||||||
|
targetFile: string,
|
||||||
|
tool: string,
|
||||||
|
version: string,
|
||||||
|
arch?: string
|
||||||
|
): Promise<string> {
|
||||||
|
version = semver.clean(version) || version
|
||||||
|
arch = arch || os.arch()
|
||||||
|
core.debug(`Caching tool ${tool} ${version} ${arch}`)
|
||||||
|
|
||||||
|
core.debug(`source file: ${sourceFile}`)
|
||||||
|
if (!fs.statSync(sourceFile).isFile()) {
|
||||||
|
throw new Error('sourceFile is not a file')
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the tool dir
|
||||||
|
const destFolder: string = await _createToolPath(tool, version, arch)
|
||||||
|
|
||||||
|
// copy instead of move. move can fail on Windows due to
|
||||||
|
// anti-virus software having an open handle on a file.
|
||||||
|
const destPath: string = path.join(destFolder, targetFile)
|
||||||
|
core.debug(`destination file ${destPath}`)
|
||||||
|
await io.cp(sourceFile, destPath)
|
||||||
|
|
||||||
|
// write .complete
|
||||||
|
_completeToolPath(tool, version, arch)
|
||||||
|
|
||||||
|
return destFolder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* finds the path to a tool in the local installed tool cache
|
||||||
|
*
|
||||||
|
* @param toolName name of the tool
|
||||||
|
* @param versionSpec version of the tool
|
||||||
|
* @param arch optional arch. defaults to arch of computer
|
||||||
|
*/
|
||||||
|
export function find(
|
||||||
|
toolName: string,
|
||||||
|
versionSpec: string,
|
||||||
|
arch?: string
|
||||||
|
): string {
|
||||||
|
if (!toolName) {
|
||||||
|
throw new Error('toolName parameter is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!versionSpec) {
|
||||||
|
throw new Error('versionSpec parameter is required')
|
||||||
|
}
|
||||||
|
|
||||||
|
arch = arch || os.arch()
|
||||||
|
|
||||||
|
// attempt to resolve an explicit version
|
||||||
|
if (!_isExplicitVersion(versionSpec)) {
|
||||||
|
const localVersions: string[] = _findLocalToolVersions(toolName, arch)
|
||||||
|
const match = _evaluateVersions(localVersions, versionSpec)
|
||||||
|
versionSpec = match
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for the explicit version in the cache
|
||||||
|
let toolPath = ''
|
||||||
|
if (versionSpec) {
|
||||||
|
versionSpec = semver.clean(versionSpec) || ''
|
||||||
|
const cachePath = path.join(cacheRoot, toolName, versionSpec, arch)
|
||||||
|
core.debug(`checking cache: ${cachePath}`)
|
||||||
|
if (fs.existsSync(cachePath) && fs.existsSync(`${cachePath}.complete`)) {
|
||||||
|
core.debug(`Found tool in cache ${toolName} ${versionSpec} ${arch}`)
|
||||||
|
toolPath = cachePath
|
||||||
|
} else {
|
||||||
|
core.debug('not found')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toolPath
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _createExtractFolder(dest?: string): Promise<string> {
|
||||||
|
if (!dest) {
|
||||||
|
// create a temp dir
|
||||||
|
dest = path.join(tempDirectory, uuidV4())
|
||||||
|
}
|
||||||
|
await io.mkdirP(dest)
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _createToolPath(
|
||||||
|
tool: string,
|
||||||
|
version: string,
|
||||||
|
arch?: string
|
||||||
|
): Promise<string> {
|
||||||
|
const folderPath = path.join(
|
||||||
|
cacheRoot,
|
||||||
|
tool,
|
||||||
|
semver.clean(version) || version,
|
||||||
|
arch || ''
|
||||||
|
)
|
||||||
|
core.debug(`destination ${folderPath}`)
|
||||||
|
const markerPath = `${folderPath}.complete`
|
||||||
|
await io.rmRF(folderPath)
|
||||||
|
await io.rmRF(markerPath)
|
||||||
|
await io.mkdirP(folderPath)
|
||||||
|
return folderPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function _completeToolPath(tool: string, version: string, arch?: string): void {
|
||||||
|
const folderPath = path.join(
|
||||||
|
cacheRoot,
|
||||||
|
tool,
|
||||||
|
semver.clean(version) || version,
|
||||||
|
arch || ''
|
||||||
|
)
|
||||||
|
const markerPath = `${folderPath}.complete`
|
||||||
|
fs.writeFileSync(markerPath, '')
|
||||||
|
core.debug('finished caching tool')
|
||||||
|
}
|
||||||
|
|
||||||
|
function _isExplicitVersion(versionSpec: string): boolean {
|
||||||
|
const c = semver.clean(versionSpec) || ''
|
||||||
|
core.debug(`isExplicit: ${c}`)
|
||||||
|
|
||||||
|
const valid = semver.valid(c) != null
|
||||||
|
core.debug(`explicit? ${valid}`)
|
||||||
|
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
|
||||||
|
function _evaluateVersions(versions: string[], versionSpec: string): string {
|
||||||
|
let version = ''
|
||||||
|
core.debug(`evaluating ${versions.length} versions`)
|
||||||
|
versions = versions.sort((a, b) => {
|
||||||
|
if (semver.gt(a, b)) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
})
|
||||||
|
for (let i = versions.length - 1; i >= 0; i--) {
|
||||||
|
const potential: string = versions[i]
|
||||||
|
const satisfied: boolean = semver.satisfies(potential, versionSpec)
|
||||||
|
if (satisfied) {
|
||||||
|
version = potential
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version) {
|
||||||
|
core.debug(`matched: ${version}`)
|
||||||
|
} else {
|
||||||
|
core.debug('match not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
function _findLocalToolVersions(toolName: string, arch?: string): string[] {
|
||||||
|
const versions: string[] = []
|
||||||
|
|
||||||
|
arch = arch || os.arch()
|
||||||
|
const toolPath = path.join(cacheRoot, toolName)
|
||||||
|
|
||||||
|
if (fs.existsSync(toolPath)) {
|
||||||
|
const children: string[] = fs.readdirSync(toolPath)
|
||||||
|
for (const child of children) {
|
||||||
|
if (_isExplicitVersion(child)) {
|
||||||
|
const fullPath = path.join(toolPath, child, arch || '')
|
||||||
|
if (fs.existsSync(fullPath) && fs.existsSync(`${fullPath}.complete`)) {
|
||||||
|
versions.push(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"outDir": "./lib",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue