mirror of
https://github.com/actions/toolkit.git
synced 2026-08-11 00:00:28 +02:00
Merge remote-tracking branch 'upstream/master' into tool-cache-xar
# Conflicts: # packages/tool-cache/__tests__/tool-cache.test.ts
This commit is contained in:
@@ -22,11 +22,11 @@ These can then be extracted in platform specific ways:
|
||||
const tc = require('@actions/tool-cache');
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
const node12Path = tc.downloadTool('https://nodejs.org/dist/v12.7.0/node-v12.7.0-win-x64.zip');
|
||||
const node12Path = await tc.downloadTool('https://nodejs.org/dist/v12.7.0/node-v12.7.0-win-x64.zip');
|
||||
const node12ExtractedFolder = await tc.extractZip(node12Path, 'path/to/extract/to');
|
||||
|
||||
// Or alternately
|
||||
const node12Path = tc.downloadTool('https://nodejs.org/dist/v12.7.0/node-v12.7.0-win-x64.7z');
|
||||
const node12Path = await tc.downloadTool('https://nodejs.org/dist/v12.7.0/node-v12.7.0-win-x64.7z');
|
||||
const node12ExtractedFolder = await tc.extract7z(node12Path, 'path/to/extract/to');
|
||||
}
|
||||
else {
|
||||
@@ -37,7 +37,7 @@ else {
|
||||
|
||||
#### Cache
|
||||
|
||||
Finally, you can cache these directories in our tool-cache. This is useful if you want to switch back and forth between versions of a tool, or save a tool between runs for private runners (private runners are still in development but are on the roadmap).
|
||||
Finally, you can cache these directories in our tool-cache. This is useful if you want to switch back and forth between versions of a tool, or save a tool between runs for self-hosted runners.
|
||||
|
||||
You'll often want to add it to the path as part of this step:
|
||||
|
||||
@@ -57,7 +57,7 @@ You can also cache files for reuse.
|
||||
```js
|
||||
const tc = require('@actions/tool-cache');
|
||||
|
||||
tc.cacheFile('path/to/exe', 'destFileName.exe', 'myExeName', '1.1.0');
|
||||
const cachedPath = await tc.cacheFile('path/to/exe', 'destFileName.exe', 'myExeName', '1.1.0');
|
||||
```
|
||||
|
||||
#### Find
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
# @actions/tool-cache Releases
|
||||
|
||||
### 1.3.4
|
||||
|
||||
- [Update the http-client to 1.0.8 which had a security fix](https://github.com/actions/toolkit/pull/429)
|
||||
|
||||
Here is [the security issue](https://github.com/actions/http-client/pull/27) that was fixed in the http-client 1.0.8 release
|
||||
|
||||
### 1.3.3
|
||||
|
||||
- [Update downloadTool to only retry 500s and 408 and 429](https://github.com/actions/toolkit/pull/373)
|
||||
|
||||
### 1.3.2
|
||||
|
||||
- [Update downloadTool with better error handling and retries](https://github.com/actions/toolkit/pull/369)
|
||||
|
||||
### 1.3.1
|
||||
|
||||
- [Increase http-client min version](https://github.com/actions/toolkit/pull/314)
|
||||
|
||||
### 1.3.0
|
||||
|
||||
- [Uses @actions/http-client](https://github.com/actions/http-client)
|
||||
|
||||
### 1.2.0
|
||||
|
||||
- [Overload downloadTool to accept destination path](https://github.com/actions/toolkit/pull/257)
|
||||
- [Fix `extractTar` on Windows](https://github.com/actions/toolkit/pull/264)
|
||||
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
|
||||
|
||||
### 1.1.2
|
||||
|
||||
- [Use zip and unzip from PATH](https://github.com/actions/toolkit/pull/161)
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,125 @@
|
||||
import * as core from '@actions/core'
|
||||
import {RetryHelper} from '../src/retry-helper'
|
||||
|
||||
let info: string[]
|
||||
let retryHelper: RetryHelper
|
||||
|
||||
describe('retry-helper tests', () => {
|
||||
beforeAll(() => {
|
||||
// Mock @actions/core info()
|
||||
jest.spyOn(core, 'info').mockImplementation((message: string) => {
|
||||
info.push(message)
|
||||
})
|
||||
|
||||
retryHelper = new RetryHelper(3, 0, 0)
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset info
|
||||
info = []
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
// Restore
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('first attempt succeeds', async () => {
|
||||
const actual = await retryHelper.execute(async () => {
|
||||
return 'some result'
|
||||
})
|
||||
expect(actual).toBe('some result')
|
||||
expect(info).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('second attempt succeeds', async () => {
|
||||
let attempts = 0
|
||||
const actual = await retryHelper.execute(async () => {
|
||||
if (++attempts === 1) {
|
||||
throw new Error('some error')
|
||||
}
|
||||
|
||||
return Promise.resolve('some result')
|
||||
})
|
||||
expect(attempts).toBe(2)
|
||||
expect(actual).toBe('some result')
|
||||
expect(info).toHaveLength(2)
|
||||
expect(info[0]).toBe('some error')
|
||||
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
|
||||
})
|
||||
|
||||
it('third attempt succeeds', async () => {
|
||||
let attempts = 0
|
||||
const actual = await retryHelper.execute(async () => {
|
||||
if (++attempts < 3) {
|
||||
throw new Error(`some error ${attempts}`)
|
||||
}
|
||||
|
||||
return Promise.resolve('some result')
|
||||
})
|
||||
expect(attempts).toBe(3)
|
||||
expect(actual).toBe('some result')
|
||||
expect(info).toHaveLength(4)
|
||||
expect(info[0]).toBe('some error 1')
|
||||
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
|
||||
expect(info[2]).toBe('some error 2')
|
||||
expect(info[3]).toMatch(/Waiting .+ seconds before trying again/)
|
||||
})
|
||||
|
||||
it('all attempts fail', async () => {
|
||||
let attempts = 0
|
||||
let error: Error = (null as unknown) as Error
|
||||
try {
|
||||
await retryHelper.execute(() => {
|
||||
throw new Error(`some error ${++attempts}`)
|
||||
})
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
expect(error.message).toBe('some error 3')
|
||||
expect(attempts).toBe(3)
|
||||
expect(info).toHaveLength(4)
|
||||
expect(info[0]).toBe('some error 1')
|
||||
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
|
||||
expect(info[2]).toBe('some error 2')
|
||||
expect(info[3]).toMatch(/Waiting .+ seconds before trying again/)
|
||||
})
|
||||
|
||||
it('checks retryable after first attempt', async () => {
|
||||
let attempts = 0
|
||||
let error: Error = (null as unknown) as Error
|
||||
try {
|
||||
await retryHelper.execute(
|
||||
async () => {
|
||||
throw new Error(`some error ${++attempts}`)
|
||||
},
|
||||
() => false
|
||||
)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
expect(error.message).toBe('some error 1')
|
||||
expect(attempts).toBe(1)
|
||||
expect(info).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('checks retryable after second attempt', async () => {
|
||||
let attempts = 0
|
||||
let error: Error = (null as unknown) as Error
|
||||
try {
|
||||
await retryHelper.execute(
|
||||
async () => {
|
||||
throw new Error(`some error ${++attempts}`)
|
||||
},
|
||||
(e: Error) => e.message === 'some error 1'
|
||||
)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
expect(error.message).toBe('some error 2')
|
||||
expect(attempts).toBe(2)
|
||||
expect(info).toHaveLength(2)
|
||||
expect(info[0]).toBe('some error 1')
|
||||
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/)
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,9 @@
|
||||
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'
|
||||
import * as stream from 'stream'
|
||||
import nock from 'nock'
|
||||
|
||||
const cachePath = path.join(__dirname, 'CACHE')
|
||||
const tempPath = path.join(__dirname, 'TEMP')
|
||||
@@ -25,6 +26,8 @@ describe('@actions/tool-cache', function() {
|
||||
username: 'abc',
|
||||
password: 'def'
|
||||
})
|
||||
setGlobal('TEST_DOWNLOAD_TOOL_RETRY_MIN_SECONDS', 0)
|
||||
setGlobal('TEST_DOWNLOAD_TOOL_RETRY_MAX_SECONDS', 0)
|
||||
})
|
||||
|
||||
beforeEach(async function() {
|
||||
@@ -34,9 +37,15 @@ describe('@actions/tool-cache', function() {
|
||||
await io.mkdirP(tempPath)
|
||||
})
|
||||
|
||||
afterEach(function() {
|
||||
setResponseMessageFactory(undefined)
|
||||
})
|
||||
|
||||
afterAll(async function() {
|
||||
await io.rmRF(tempPath)
|
||||
await io.rmRF(cachePath)
|
||||
setGlobal('TEST_DOWNLOAD_TOOL_RETRY_MIN_SECONDS', undefined)
|
||||
setGlobal('TEST_DOWNLOAD_TOOL_RETRY_MAX_SECONDS', undefined)
|
||||
})
|
||||
|
||||
it('downloads a 35 byte file', async () => {
|
||||
@@ -48,8 +57,50 @@ describe('@actions/tool-cache', function() {
|
||||
expect(fs.statSync(downPath).size).toBe(35)
|
||||
})
|
||||
|
||||
it('downloads a 35 byte file (dest)', async () => {
|
||||
const dest = 'test-download-file'
|
||||
try {
|
||||
const downPath: string = await tc.downloadTool(
|
||||
'http://example.com/bytes/35',
|
||||
dest
|
||||
)
|
||||
|
||||
expect(downPath).toEqual(dest)
|
||||
expect(fs.existsSync(downPath)).toBeTruthy()
|
||||
expect(fs.statSync(downPath).size).toBe(35)
|
||||
} finally {
|
||||
try {
|
||||
await fs.promises.unlink(dest)
|
||||
} catch {
|
||||
// intentionally empty
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('downloads a 35 byte file (dest requires mkdirp)', async () => {
|
||||
const dest = 'test-download-dir/test-download-file'
|
||||
try {
|
||||
const downPath: string = await tc.downloadTool(
|
||||
'http://example.com/bytes/35',
|
||||
dest
|
||||
)
|
||||
|
||||
expect(downPath).toEqual(dest)
|
||||
expect(fs.existsSync(downPath)).toBeTruthy()
|
||||
expect(fs.statSync(downPath).size).toBe(35)
|
||||
} finally {
|
||||
try {
|
||||
await fs.promises.unlink(dest)
|
||||
await fs.promises.rmdir(path.dirname(dest))
|
||||
} catch {
|
||||
// intentionally empty
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('downloads a 35 byte file after a redirect', async () => {
|
||||
nock('http://example.com')
|
||||
.persist()
|
||||
.get('/redirect-to')
|
||||
.reply(303, undefined, {
|
||||
location: 'http://example.com/bytes/35'
|
||||
@@ -63,8 +114,75 @@ describe('@actions/tool-cache', function() {
|
||||
expect(fs.statSync(downPath).size).toBe(35)
|
||||
})
|
||||
|
||||
it('handles error from response message stream', async () => {
|
||||
nock('http://example.com')
|
||||
.persist()
|
||||
.get('/error-from-response-message-stream')
|
||||
.reply(200, {})
|
||||
|
||||
setResponseMessageFactory(() => {
|
||||
const readStream = new stream.Readable()
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
readStream._read = () => {
|
||||
readStream.destroy(new Error('uh oh'))
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/unbound-method */
|
||||
return readStream
|
||||
})
|
||||
|
||||
let error = new Error('unexpected')
|
||||
try {
|
||||
await tc.downloadTool(
|
||||
'http://example.com/error-from-response-message-stream'
|
||||
)
|
||||
} catch (err) {
|
||||
error = err
|
||||
}
|
||||
|
||||
expect(error).not.toBeUndefined()
|
||||
expect(error.message).toBe('uh oh')
|
||||
})
|
||||
|
||||
it('retries error from response message stream', async () => {
|
||||
nock('http://example.com')
|
||||
.persist()
|
||||
.get('/retries-error-from-response-message-stream')
|
||||
.reply(200, {})
|
||||
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
let attempt = 1
|
||||
setResponseMessageFactory(() => {
|
||||
const readStream = new stream.Readable()
|
||||
let failsafe = 0
|
||||
readStream._read = () => {
|
||||
// First attempt fails
|
||||
if (attempt++ === 1) {
|
||||
readStream.destroy(new Error('uh oh'))
|
||||
return
|
||||
}
|
||||
|
||||
// Write some data
|
||||
if (failsafe++ === 0) {
|
||||
readStream.push(''.padEnd(35))
|
||||
readStream.push(null) // no more data
|
||||
}
|
||||
}
|
||||
|
||||
return readStream
|
||||
})
|
||||
/* eslint-enable @typescript-eslint/unbound-method */
|
||||
|
||||
const downPath = await tc.downloadTool(
|
||||
'http://example.com/retries-error-from-response-message-stream'
|
||||
)
|
||||
|
||||
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')
|
||||
.persist()
|
||||
.get('/bytes/bad')
|
||||
.reply(400, {
|
||||
username: 'bad',
|
||||
@@ -84,6 +202,7 @@ describe('@actions/tool-cache', function() {
|
||||
|
||||
it('works with redirect code 302', async function() {
|
||||
nock('http://example.com')
|
||||
.persist()
|
||||
.get('/redirect-to')
|
||||
.reply(302, undefined, {
|
||||
location: 'http://example.com/bytes/35'
|
||||
@@ -143,6 +262,36 @@ describe('@actions/tool-cache', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('extracts a 7z to a directory that does not exist', async () => {
|
||||
const tempDir = path.join(__dirname, 'test-install-7z')
|
||||
const destDir = path.join(tempDir, 'not-exist')
|
||||
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, destDir)
|
||||
await tc.cacheDir(extPath, 'my-7z-contents', '1.1.0')
|
||||
const toolPath: string = tc.find('my-7z-contents', '1.1.0')
|
||||
|
||||
expect(extPath).toContain('not-exist')
|
||||
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,
|
||||
@@ -186,7 +335,7 @@ describe('@actions/tool-cache', function() {
|
||||
.readFileSync(path.join(tempDir, 'mock7zr-args.txt'))
|
||||
.toString()
|
||||
.trim()
|
||||
).toBe(`x -bb1 -bd -sccUTF-8 ${_7zFile}`)
|
||||
).toBe(`x -bb0 -bd -sccUTF-8 ${_7zFile}`)
|
||||
expect(fs.existsSync(path.join(extPath, 'file.txt'))).toBeTruthy()
|
||||
expect(
|
||||
fs.existsSync(path.join(extPath, 'file-with-ç-character.txt'))
|
||||
@@ -198,53 +347,20 @@ describe('@actions/tool-cache', function() {
|
||||
await io.rmRF(tempDir)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
if (IS_MAC) {
|
||||
it('extract .xar', async () => {
|
||||
const tempDir = path.join(tempPath, 'test-install.xar')
|
||||
|
||||
await io.mkdirP(tempDir)
|
||||
|
||||
// copy the .xar file to the test dir
|
||||
const _xarFile: string = path.join(tempDir, 'test.xar')
|
||||
await io.cp(path.join(__dirname, 'data', 'test.xar'), _xarFile)
|
||||
|
||||
// extract/cache
|
||||
const extPath: string = await tc.extractXar(_xarFile, undefined, '-x')
|
||||
await tc.cacheDir(extPath, 'my-xar-contents', '1.1.0')
|
||||
const toolPath: string = tc.find('my-xar-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()
|
||||
expect(
|
||||
fs.readFileSync(
|
||||
path.join(toolPath, 'folder', 'nested-file.txt'),
|
||||
'utf8'
|
||||
)
|
||||
).toBe('folder/nested-file.txt contents')
|
||||
})
|
||||
}
|
||||
|
||||
it('extract .tar.gz', async () => {
|
||||
const tempDir = path.join(tempPath, 'test-install-tar.gz')
|
||||
} else if (IS_MAC) {
|
||||
it('extract .xar', async () => {
|
||||
const tempDir = path.join(tempPath, 'test-install.xar')
|
||||
|
||||
await io.mkdirP(tempDir)
|
||||
|
||||
// copy the .tar.gz file to the test dir
|
||||
const _tgzFile: string = path.join(tempDir, 'test.tar.gz')
|
||||
await io.cp(path.join(__dirname, 'data', 'test.tar.gz'), _tgzFile)
|
||||
// copy the .xar file to the test dir
|
||||
const _xarFile: string = path.join(tempDir, 'test.xar')
|
||||
await io.cp(path.join(__dirname, 'data', 'test.xar'), _xarFile)
|
||||
|
||||
// extract/cache
|
||||
const extPath: string = await tc.extractTar(_tgzFile)
|
||||
await tc.cacheDir(extPath, 'my-tgz-contents', '1.1.0')
|
||||
const toolPath: string = tc.find('my-tgz-contents', '1.1.0')
|
||||
const extPath: string = await tc.extractXar(_xarFile, undefined, '-x')
|
||||
await tc.cacheDir(extPath, 'my-xar-contents', '1.1.0')
|
||||
const toolPath: string = tc.find('my-xar-contents', '1.1.0')
|
||||
|
||||
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||
@@ -262,33 +378,89 @@ describe('@actions/tool-cache', function() {
|
||||
)
|
||||
).toBe('folder/nested-file.txt contents')
|
||||
})
|
||||
|
||||
it('extract .tar.xz', async () => {
|
||||
const tempDir = path.join(tempPath, 'test-install-tar.xz')
|
||||
|
||||
await io.mkdirP(tempDir)
|
||||
|
||||
// copy the .tar.gz file to the test dir
|
||||
const _txzFile: string = path.join(tempDir, 'test.tar.xz')
|
||||
await io.cp(path.join(__dirname, 'data', 'test.tar.xz'), _txzFile)
|
||||
|
||||
// extract/cache
|
||||
const extPath: string = await tc.extractTar(_txzFile, undefined, 'x')
|
||||
await tc.cacheDir(extPath, 'my-txz-contents', '1.1.0')
|
||||
const toolPath: string = tc.find('my-txz-contents', '1.1.0')
|
||||
|
||||
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||
expect(fs.existsSync(path.join(toolPath, 'bar.txt'))).toBeTruthy()
|
||||
expect(
|
||||
fs.existsSync(path.join(toolPath, 'foo', 'hello.txt'))
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
fs.readFileSync(path.join(toolPath, 'foo', 'hello.txt'), 'utf8')
|
||||
).toBe('foo/hello: world')
|
||||
})
|
||||
}
|
||||
|
||||
it('extract .tar.gz', async () => {
|
||||
const tempDir = path.join(tempPath, 'test-install-tar.gz')
|
||||
|
||||
await io.mkdirP(tempDir)
|
||||
|
||||
// copy the .tar.gz file to the test dir
|
||||
const _tgzFile: string = path.join(tempDir, 'test.tar.gz')
|
||||
await io.cp(path.join(__dirname, 'data', 'test.tar.gz'), _tgzFile)
|
||||
|
||||
// extract/cache
|
||||
const extPath: string = await tc.extractTar(_tgzFile)
|
||||
await tc.cacheDir(extPath, 'my-tgz-contents', '1.1.0')
|
||||
const toolPath: string = tc.find('my-tgz-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()
|
||||
expect(
|
||||
fs.readFileSync(path.join(toolPath, 'folder', 'nested-file.txt'), 'utf8')
|
||||
).toBe('folder/nested-file.txt contents')
|
||||
})
|
||||
|
||||
it('extract .tar.gz to a directory that does not exist', async () => {
|
||||
const tempDir = path.join(tempPath, 'test-install-tar.gz')
|
||||
const destDir = path.join(tempDir, 'not-exist')
|
||||
|
||||
await io.mkdirP(tempDir)
|
||||
|
||||
// copy the .tar.gz file to the test dir
|
||||
const _tgzFile: string = path.join(tempDir, 'test.tar.gz')
|
||||
await io.cp(path.join(__dirname, 'data', 'test.tar.gz'), _tgzFile)
|
||||
|
||||
// extract/cache
|
||||
const extPath: string = await tc.extractTar(_tgzFile, destDir)
|
||||
await tc.cacheDir(extPath, 'my-tgz-contents', '1.1.0')
|
||||
const toolPath: string = tc.find('my-tgz-contents', '1.1.0')
|
||||
|
||||
expect(extPath).toContain('not-exist')
|
||||
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()
|
||||
expect(
|
||||
fs.readFileSync(path.join(toolPath, 'folder', 'nested-file.txt'), 'utf8')
|
||||
).toBe('folder/nested-file.txt contents')
|
||||
})
|
||||
|
||||
it('extract .tar.xz', async () => {
|
||||
const tempDir = path.join(tempPath, 'test-install-tar.xz')
|
||||
|
||||
await io.mkdirP(tempDir)
|
||||
|
||||
// copy the .tar.gz file to the test dir
|
||||
const _txzFile: string = path.join(tempDir, 'test.tar.xz')
|
||||
await io.cp(path.join(__dirname, 'data', 'test.tar.xz'), _txzFile)
|
||||
|
||||
// extract/cache
|
||||
const extPath: string = await tc.extractTar(_txzFile, undefined, 'x')
|
||||
await tc.cacheDir(extPath, 'my-txz-contents', '1.1.0')
|
||||
const toolPath: string = tc.find('my-txz-contents', '1.1.0')
|
||||
|
||||
expect(fs.existsSync(toolPath)).toBeTruthy()
|
||||
expect(fs.existsSync(`${toolPath}.complete`)).toBeTruthy()
|
||||
expect(fs.existsSync(path.join(toolPath, 'bar.txt'))).toBeTruthy()
|
||||
expect(fs.existsSync(path.join(toolPath, 'foo', 'hello.txt'))).toBeTruthy()
|
||||
expect(
|
||||
fs.readFileSync(path.join(toolPath, 'foo', 'hello.txt'), 'utf8')
|
||||
).toBe('foo/hello: world')
|
||||
})
|
||||
|
||||
it('installs a zip and finds it', async () => {
|
||||
const tempDir = path.join(__dirname, 'test-install-zip')
|
||||
try {
|
||||
@@ -321,7 +493,7 @@ describe('@actions/tool-cache', function() {
|
||||
]
|
||||
await exec.exec(`"${powershellPath}"`, args)
|
||||
} else {
|
||||
const zipPath: string = await io.which('zip')
|
||||
const zipPath: string = await io.which('zip', true)
|
||||
await exec.exec(`"${zipPath}`, [zipFile, '-r', '.'], {cwd: stagingDir})
|
||||
}
|
||||
|
||||
@@ -372,7 +544,7 @@ describe('@actions/tool-cache', function() {
|
||||
]
|
||||
await exec.exec(`"${powershellPath}"`, args)
|
||||
} else {
|
||||
const zipPath: string = await io.which('zip')
|
||||
const zipPath: string = await io.which('zip', true)
|
||||
await exec.exec(zipPath, [zipFile, '-r', '.'], {cwd: stagingDir})
|
||||
}
|
||||
|
||||
@@ -397,6 +569,60 @@ describe('@actions/tool-cache', function() {
|
||||
}
|
||||
})
|
||||
|
||||
it('extract zip to a directory that does not exist', 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: string = await io.which('zip', true)
|
||||
await exec.exec(zipPath, [zipFile, '-r', '.'], {cwd: stagingDir})
|
||||
}
|
||||
|
||||
const destDir = path.join(tempDir, 'not-exist')
|
||||
|
||||
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(extPath).toContain('not-exist')
|
||||
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('works with a 502 temporary failure', async function() {
|
||||
nock('http://example.com')
|
||||
.get('/temp502')
|
||||
@@ -425,4 +651,61 @@ describe('@actions/tool-cache', function() {
|
||||
expect(err.toString()).toContain('502')
|
||||
}
|
||||
})
|
||||
|
||||
it('retries 429s', async function() {
|
||||
nock('http://example.com')
|
||||
.get('/too-many-requests-429')
|
||||
.times(2)
|
||||
.reply(429, undefined)
|
||||
nock('http://example.com')
|
||||
.get('/too-many-requests-429')
|
||||
.reply(500, undefined)
|
||||
|
||||
try {
|
||||
const statusCodeUrl = 'http://example.com/too-many-requests-429'
|
||||
await tc.downloadTool(statusCodeUrl)
|
||||
} catch (err) {
|
||||
expect(err.toString()).toContain('500')
|
||||
}
|
||||
})
|
||||
|
||||
it("doesn't retry 404", async function() {
|
||||
nock('http://example.com')
|
||||
.get('/not-found-404')
|
||||
.reply(404, undefined)
|
||||
nock('http://example.com')
|
||||
.get('/not-found-404')
|
||||
.reply(500, undefined)
|
||||
|
||||
try {
|
||||
const statusCodeUrl = 'http://example.com/not-found-404'
|
||||
await tc.downloadTool(statusCodeUrl)
|
||||
} catch (err) {
|
||||
expect(err.toString()).toContain('404')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Sets up a mock response body for downloadTool. This function works around a limitation with
|
||||
* nock when the response stream emits an error.
|
||||
*/
|
||||
function setResponseMessageFactory(
|
||||
factory: (() => stream.Readable) | undefined
|
||||
): void {
|
||||
setGlobal('TEST_DOWNLOAD_TOOL_RESPONSE_MESSAGE_FACTORY', factory)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a global variable
|
||||
*/
|
||||
function setGlobal<T>(key: string, value: T | undefined): void {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const g = global as any
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
if (value === undefined) {
|
||||
delete g[key]
|
||||
} else {
|
||||
g[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+30
-18
@@ -1,9 +1,35 @@
|
||||
{
|
||||
"name": "@actions/tool-cache",
|
||||
"version": "1.1.1",
|
||||
"version": "1.3.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.3.tgz",
|
||||
"integrity": "sha512-Wp4xnyokakM45Uuj4WLUxdsa8fJjKVl1fDTsPbTEcTcuu0Nb26IPQbOtjmnfaCPGcaoPOOqId8H9NapZ8gii4w=="
|
||||
},
|
||||
"@actions/exec": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz",
|
||||
"integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==",
|
||||
"requires": {
|
||||
"@actions/io": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"@actions/http-client": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz",
|
||||
"integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==",
|
||||
"requires": {
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
},
|
||||
"@actions/io": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz",
|
||||
"integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg=="
|
||||
},
|
||||
"@types/nock": {
|
||||
"version": "10.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz",
|
||||
@@ -172,9 +198,9 @@
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
},
|
||||
"tunnel": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz",
|
||||
"integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM="
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
||||
},
|
||||
"type-detect": {
|
||||
"version": "4.0.8",
|
||||
@@ -182,20 +208,6 @@
|
||||
"integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
|
||||
"dev": true
|
||||
},
|
||||
"typed-rest-client": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.5.0.tgz",
|
||||
"integrity": "sha512-DVZRlmsfnTjp6ZJaatcdyvvwYwbWvR4YDNFDqb+qdTxpvaVP99YCpBkA8rxsLtAPjBVoDe4fNsnMIdZTiPuKWg==",
|
||||
"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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/tool-cache",
|
||||
"version": "1.1.2",
|
||||
"version": "1.3.4",
|
||||
"description": "Actions tool-cache lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
@@ -10,6 +10,7 @@
|
||||
"homepage": "https://github.com/actions/toolkit/tree/master/packages/exec",
|
||||
"license": "MIT",
|
||||
"main": "lib/tool-cache.js",
|
||||
"types": "lib/tool-cache.d.ts",
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "__tests__"
|
||||
@@ -27,6 +28,7 @@
|
||||
"directory": "packages/tool-cache"
|
||||
},
|
||||
"scripts": {
|
||||
"audit-moderate": "npm install && npm audit --audit-level=moderate",
|
||||
"test": "echo \"Error: run tests from root\" && exit 1",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
@@ -34,11 +36,11 @@
|
||||
"url": "https://github.com/actions/toolkit/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.1.0",
|
||||
"@actions/exec": "^1.0.1",
|
||||
"@actions/core": "^1.2.0",
|
||||
"@actions/exec": "^1.0.0",
|
||||
"@actions/http-client": "^1.0.8",
|
||||
"@actions/io": "^1.0.1",
|
||||
"semver": "^6.1.0",
|
||||
"typed-rest-client": "^1.4.0",
|
||||
"uuid": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as core from '@actions/core'
|
||||
|
||||
/**
|
||||
* Internal class for retries
|
||||
*/
|
||||
export class RetryHelper {
|
||||
private maxAttempts: number
|
||||
private minSeconds: number
|
||||
private maxSeconds: number
|
||||
|
||||
constructor(maxAttempts: number, minSeconds: number, maxSeconds: number) {
|
||||
if (maxAttempts < 1) {
|
||||
throw new Error('max attempts should be greater than or equal to 1')
|
||||
}
|
||||
|
||||
this.maxAttempts = maxAttempts
|
||||
this.minSeconds = Math.floor(minSeconds)
|
||||
this.maxSeconds = Math.floor(maxSeconds)
|
||||
if (this.minSeconds > this.maxSeconds) {
|
||||
throw new Error('min seconds should be less than or equal to max seconds')
|
||||
}
|
||||
}
|
||||
|
||||
async execute<T>(
|
||||
action: () => Promise<T>,
|
||||
isRetryable?: (e: Error) => boolean
|
||||
): Promise<T> {
|
||||
let attempt = 1
|
||||
while (attempt < this.maxAttempts) {
|
||||
// Try
|
||||
try {
|
||||
return await action()
|
||||
} catch (err) {
|
||||
if (isRetryable && !isRetryable(err)) {
|
||||
throw err
|
||||
}
|
||||
|
||||
core.info(err.message)
|
||||
}
|
||||
|
||||
// Sleep
|
||||
const seconds = this.getSleepAmount()
|
||||
core.info(`Waiting ${seconds} seconds before trying again`)
|
||||
await this.sleep(seconds)
|
||||
attempt++
|
||||
}
|
||||
|
||||
// Last attempt
|
||||
return await action()
|
||||
}
|
||||
|
||||
private getSleepAmount(): number {
|
||||
return (
|
||||
Math.floor(Math.random() * (this.maxSeconds - this.minSeconds + 1)) +
|
||||
this.minSeconds
|
||||
)
|
||||
}
|
||||
|
||||
private async sleep(seconds: number): Promise<void> {
|
||||
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,15 @@ 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 httpm from '@actions/http-client'
|
||||
import * as semver from 'semver'
|
||||
import * as uuidV4 from 'uuid/v4'
|
||||
import * as stream from 'stream'
|
||||
import * as util from 'util'
|
||||
import uuidV4 from 'uuid/v4'
|
||||
import {exec} from '@actions/exec/lib/exec'
|
||||
import {ExecOptions} from '@actions/exec/lib/interfaces'
|
||||
import {ok} from 'assert'
|
||||
import {RetryHelper} from './retry-helper'
|
||||
|
||||
export class HTTPError extends Error {
|
||||
constructor(readonly httpStatusCode: number | undefined) {
|
||||
@@ -21,91 +24,96 @@ const IS_WINDOWS = process.platform === 'win32'
|
||||
const IS_MAC = process.platform === 'darwin'
|
||||
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_TEMP'] || ''
|
||||
let cacheRoot: string = process.env['RUNNER_TOOL_CACHE'] || ''
|
||||
// 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
|
||||
* @param dest path to download tool
|
||||
* @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())
|
||||
export async function downloadTool(
|
||||
url: string,
|
||||
dest?: string
|
||||
): Promise<string> {
|
||||
dest = dest || path.join(_getTempDirectory(), uuidV4())
|
||||
await io.mkdirP(path.dirname(dest))
|
||||
core.debug(`Downloading ${url}`)
|
||||
core.debug(`Destination ${dest}`)
|
||||
|
||||
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)
|
||||
const maxAttempts = 3
|
||||
const minSeconds = _getGlobal<number>(
|
||||
'TEST_DOWNLOAD_TOOL_RETRY_MIN_SECONDS',
|
||||
10
|
||||
)
|
||||
const maxSeconds = _getGlobal<number>(
|
||||
'TEST_DOWNLOAD_TOOL_RETRY_MAX_SECONDS',
|
||||
20
|
||||
)
|
||||
const retryHelper = new RetryHelper(maxAttempts, minSeconds, maxSeconds)
|
||||
return await retryHelper.execute(
|
||||
async () => {
|
||||
return await downloadToolAttempt(url, dest || '')
|
||||
},
|
||||
(err: Error) => {
|
||||
if (err instanceof HTTPError && err.httpStatusCode) {
|
||||
// Don't retry anything less than 500, except 408 Request Timeout and 429 Too Many Requests
|
||||
if (
|
||||
err.httpStatusCode < 500 &&
|
||||
err.httpStatusCode !== 408 &&
|
||||
err.httpStatusCode !== 429
|
||||
) {
|
||||
return false
|
||||
}
|
||||
})
|
||||
file.on('error', err => {
|
||||
file.end()
|
||||
reject(err)
|
||||
})
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
|
||||
// Otherwise retry
|
||||
return true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function downloadToolAttempt(url: string, dest: string): Promise<string> {
|
||||
if (fs.existsSync(dest)) {
|
||||
throw new Error(`Destination file path ${dest} already exists`)
|
||||
}
|
||||
|
||||
// Get the response headers
|
||||
const http = new httpm.HttpClient(userAgent, [], {
|
||||
allowRetries: false
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
// Download the response body
|
||||
const pipeline = util.promisify(stream.pipeline)
|
||||
const responseMessageFactory = _getGlobal<() => stream.Readable>(
|
||||
'TEST_DOWNLOAD_TOOL_RESPONSE_MESSAGE_FACTORY',
|
||||
() => response.message
|
||||
)
|
||||
const readStream = responseMessageFactory()
|
||||
let succeeded = false
|
||||
try {
|
||||
await pipeline(readStream, fs.createWriteStream(dest))
|
||||
core.debug('download complete')
|
||||
succeeded = true
|
||||
return dest
|
||||
} finally {
|
||||
// Error, delete dest before retry
|
||||
if (!succeeded) {
|
||||
core.debug('download failed')
|
||||
try {
|
||||
await io.rmRF(dest)
|
||||
} catch (err) {
|
||||
core.debug(`Failed to delete '${dest}'. ${err.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,15 +139,16 @@ export async function extract7z(
|
||||
ok(IS_WINDOWS, 'extract7z() not supported on current OS')
|
||||
ok(file, 'parameter "file" is required')
|
||||
|
||||
dest = dest || (await _createExtractFolder(dest))
|
||||
dest = await _createExtractFolder(dest)
|
||||
|
||||
const originalCwd = process.cwd()
|
||||
process.chdir(dest)
|
||||
if (_7zPath) {
|
||||
try {
|
||||
const logLevel = core.isDebug() ? '-bb1' : '-bb0'
|
||||
const args: string[] = [
|
||||
'x', // eXtract files with full paths
|
||||
'-bb1', // -bb[0-3] : set output log level
|
||||
logLevel, // -bb[0-3] : set output log level
|
||||
'-bd', // disable progress indicator
|
||||
'-sccUTF-8', // set charset for for console input/output
|
||||
file
|
||||
@@ -184,11 +193,11 @@ export async function extract7z(
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a tar
|
||||
* Extract a compressed tar archive
|
||||
*
|
||||
* @param file path to the tar
|
||||
* @param dest destination directory. Optional.
|
||||
* @param flags flags for the tar. Optional.
|
||||
* @param flags flags for the tar command to use for extraction. Defaults to 'xz' (extracting gzipped tars). Optional.
|
||||
* @returns path to the destination directory
|
||||
*/
|
||||
export async function extractTar(
|
||||
@@ -200,9 +209,48 @@ export async function extractTar(
|
||||
throw new Error("parameter 'file' is required")
|
||||
}
|
||||
|
||||
dest = dest || (await _createExtractFolder(dest))
|
||||
const tarPath: string = await io.which('tar', true)
|
||||
await exec(`"${tarPath}"`, [flags, '-C', dest, '-f', file])
|
||||
// Create dest
|
||||
dest = await _createExtractFolder(dest)
|
||||
|
||||
// Determine whether GNU tar
|
||||
core.debug('Checking tar --version')
|
||||
let versionOutput = ''
|
||||
await exec('tar --version', [], {
|
||||
ignoreReturnCode: true,
|
||||
silent: true,
|
||||
listeners: {
|
||||
stdout: (data: Buffer) => (versionOutput += data.toString()),
|
||||
stderr: (data: Buffer) => (versionOutput += data.toString())
|
||||
}
|
||||
})
|
||||
core.debug(versionOutput.trim())
|
||||
const isGnuTar = versionOutput.toUpperCase().includes('GNU TAR')
|
||||
|
||||
// Initialize args
|
||||
const args = [flags]
|
||||
|
||||
if (core.isDebug() && !flags.includes('v')) {
|
||||
args.push('-v')
|
||||
}
|
||||
|
||||
let destArg = dest
|
||||
let fileArg = file
|
||||
if (IS_WINDOWS && isGnuTar) {
|
||||
args.push('--force-local')
|
||||
destArg = dest.replace(/\\/g, '/')
|
||||
|
||||
// Technically only the dest needs to have `/` but for aesthetic consistency
|
||||
// convert slashes in the file arg too.
|
||||
fileArg = file.replace(/\\/g, '/')
|
||||
}
|
||||
|
||||
if (isGnuTar) {
|
||||
// Suppress warnings when using GNU tar to extract archives created by BSD tar
|
||||
args.push('--warning=no-unknown-keyword')
|
||||
}
|
||||
|
||||
args.push('-C', destArg, '-f', fileArg)
|
||||
await exec(`tar`, args)
|
||||
|
||||
return dest
|
||||
}
|
||||
@@ -242,7 +290,7 @@ export async function extractZip(file: string, dest?: string): Promise<string> {
|
||||
throw new Error("parameter 'file' is required")
|
||||
}
|
||||
|
||||
dest = dest || (await _createExtractFolder(dest))
|
||||
dest = await _createExtractFolder(dest)
|
||||
|
||||
if (IS_WINDOWS) {
|
||||
await extractZipWin(file, dest)
|
||||
@@ -260,7 +308,7 @@ async function extractZipWin(file: string, dest: string): Promise<void> {
|
||||
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 powershellPath = await io.which('powershell', true)
|
||||
const args = [
|
||||
'-NoLogo',
|
||||
'-Sta',
|
||||
@@ -275,8 +323,12 @@ async function extractZipWin(file: string, dest: string): Promise<void> {
|
||||
}
|
||||
|
||||
async function extractZipNix(file: string, dest: string): Promise<void> {
|
||||
const unzipPath = await io.which('unzip')
|
||||
await exec(`"${unzipPath}"`, [file], {cwd: dest})
|
||||
const unzipPath = await io.which('unzip', true)
|
||||
const args = [file]
|
||||
if (!core.isDebug()) {
|
||||
args.unshift('-q')
|
||||
}
|
||||
await exec(`"${unzipPath}"`, args, {cwd: dest})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -391,7 +443,12 @@ export function find(
|
||||
let toolPath = ''
|
||||
if (versionSpec) {
|
||||
versionSpec = semver.clean(versionSpec) || ''
|
||||
const cachePath = path.join(cacheRoot, toolName, versionSpec, arch)
|
||||
const cachePath = path.join(
|
||||
_getCacheDirectory(),
|
||||
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}`)
|
||||
@@ -413,7 +470,7 @@ export function findAllVersions(toolName: string, arch?: string): string[] {
|
||||
const versions: string[] = []
|
||||
|
||||
arch = arch || os.arch()
|
||||
const toolPath = path.join(cacheRoot, toolName)
|
||||
const toolPath = path.join(_getCacheDirectory(), toolName)
|
||||
|
||||
if (fs.existsSync(toolPath)) {
|
||||
const children: string[] = fs.readdirSync(toolPath)
|
||||
@@ -433,7 +490,7 @@ export function findAllVersions(toolName: string, arch?: string): string[] {
|
||||
async function _createExtractFolder(dest?: string): Promise<string> {
|
||||
if (!dest) {
|
||||
// create a temp dir
|
||||
dest = path.join(tempDirectory, uuidV4())
|
||||
dest = path.join(_getTempDirectory(), uuidV4())
|
||||
}
|
||||
await io.mkdirP(dest)
|
||||
return dest
|
||||
@@ -445,7 +502,7 @@ async function _createToolPath(
|
||||
arch?: string
|
||||
): Promise<string> {
|
||||
const folderPath = path.join(
|
||||
cacheRoot,
|
||||
_getCacheDirectory(),
|
||||
tool,
|
||||
semver.clean(version) || version,
|
||||
arch || ''
|
||||
@@ -460,7 +517,7 @@ async function _createToolPath(
|
||||
|
||||
function _completeToolPath(tool: string, version: string, arch?: string): void {
|
||||
const folderPath = path.join(
|
||||
cacheRoot,
|
||||
_getCacheDirectory(),
|
||||
tool,
|
||||
semver.clean(version) || version,
|
||||
arch || ''
|
||||
@@ -506,3 +563,31 @@ function _evaluateVersions(versions: string[], versionSpec: string): string {
|
||||
|
||||
return version
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets RUNNER_TOOL_CACHE
|
||||
*/
|
||||
function _getCacheDirectory(): string {
|
||||
const cacheDirectory = process.env['RUNNER_TOOL_CACHE'] || ''
|
||||
ok(cacheDirectory, 'Expected RUNNER_TOOL_CACHE to be defined')
|
||||
return cacheDirectory
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets RUNNER_TEMP
|
||||
*/
|
||||
function _getTempDirectory(): string {
|
||||
const tempDirectory = process.env['RUNNER_TEMP'] || ''
|
||||
ok(tempDirectory, 'Expected RUNNER_TEMP to be defined')
|
||||
return tempDirectory
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a global variable
|
||||
*/
|
||||
function _getGlobal<T>(key: string, defaultValue: T): T {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const value = (global as any)[key] as T | undefined
|
||||
/* eslint-enable @typescript-eslint/no-explicit-any */
|
||||
return value !== undefined ? value : defaultValue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user