Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
439cd9b37e | ||
|
|
c1ded1dc4d | ||
|
|
f37c445bc5 | ||
|
|
e95bcfe359 | ||
|
|
7549d1b218 | ||
|
|
2124ef2413 | ||
|
|
d617670abc | ||
|
|
47157e5ade | ||
|
|
8a6aae0a16 | ||
|
|
58ec2bdcc9 | ||
|
|
e19b629130 | ||
|
|
d63a8c4d3f | ||
|
|
67d2d582dc | ||
|
|
9d70b8a9fb | ||
|
|
7f47ffaee2 | ||
|
|
98e1a813db | ||
|
|
0d39975814 | ||
|
|
f482643a6e | ||
|
|
ff2c524611 | ||
|
|
ecb4df89bf | ||
|
|
03319fcffa | ||
|
|
c33724abbd | ||
|
|
d6f3ee93b8 | ||
|
|
34a411f3c0 | ||
|
|
2d6ba67518 | ||
|
|
78ed49ff88 | ||
|
|
c119fcd773 | ||
|
|
73babeabef | ||
|
|
bf93b54558 | ||
|
|
0c0770ce57 | ||
|
|
571bf222ee |
+1
-1
@@ -8,4 +8,4 @@ module.exports = {
|
||||
'^.+\\.ts$': 'ts-jest'
|
||||
},
|
||||
verbose: true
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3
-3
@@ -5781,9 +5781,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"version": "1.15.4",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
||||
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"lint": "eslint packages/**/*.ts",
|
||||
"lint-fix": "eslint packages/**/*.ts --fix",
|
||||
"new-package": "scripts/create-package",
|
||||
"test": "jest --testTimeout 10000"
|
||||
"test": "jest --testTimeout 60000"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.4",
|
||||
|
||||
@@ -9,7 +9,8 @@ import archiver from 'archiver'
|
||||
|
||||
import {
|
||||
downloadArtifactInternal,
|
||||
downloadArtifactPublic
|
||||
downloadArtifactPublic,
|
||||
streamExtractExternal
|
||||
} from '../src/internal/download/download-artifact'
|
||||
import {getUserAgentString} from '../src/internal/shared/user-agent'
|
||||
import {noopLogs} from './common'
|
||||
@@ -248,7 +249,44 @@ describe('download-artifact', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should fail if blob storage response is non-200', async () => {
|
||||
it('should fail if blob storage storage chunk does not respond within 30s', async () => {
|
||||
// mock http client to delay response data by 30s
|
||||
const msg = new http.IncomingMessage(new net.Socket())
|
||||
msg.statusCode = 200
|
||||
|
||||
const mockGet = jest.fn(async () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Resolve with a 200 status code immediately
|
||||
resolve({
|
||||
message: msg,
|
||||
readBody: async () => {
|
||||
return Promise.resolve(`{"ok": true}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Reject with an error after 31 seconds
|
||||
setTimeout(() => {
|
||||
reject(new Error('Request timeout'))
|
||||
}, 31000) // Timeout after 31 seconds
|
||||
})
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGet
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await expect(
|
||||
streamExtractExternal(fixtures.blobStorageUrl, fixtures.workspaceDir)
|
||||
).rejects.toBeInstanceOf(Error)
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
}, 35000) // add longer timeout to allow for timer to run out
|
||||
|
||||
it('should fail if blob storage response is non-200 after 5 retries', async () => {
|
||||
const downloadArtifactMock = github.getOctokit(fixtures.token).rest
|
||||
.actions.downloadArtifact as MockedDownloadArtifact
|
||||
downloadArtifactMock.mockResolvedValueOnce({
|
||||
@@ -290,7 +328,60 @@ describe('download-artifact', () => {
|
||||
expect(mockGetArtifactFailure).toHaveBeenCalledWith(
|
||||
fixtures.blobStorageUrl
|
||||
)
|
||||
})
|
||||
expect(mockGetArtifactFailure).toHaveBeenCalledTimes(5)
|
||||
}, 38000)
|
||||
|
||||
it('should retry if blob storage response is non-200 and then succeed with a 200', async () => {
|
||||
const downloadArtifactMock = github.getOctokit(fixtures.token).rest
|
||||
.actions.downloadArtifact as MockedDownloadArtifact
|
||||
downloadArtifactMock.mockResolvedValueOnce({
|
||||
headers: {
|
||||
location: fixtures.blobStorageUrl
|
||||
},
|
||||
status: 302,
|
||||
url: '',
|
||||
data: Buffer.from('')
|
||||
})
|
||||
|
||||
const mockGetArtifact = jest
|
||||
.fn(mockGetArtifactSuccess)
|
||||
.mockImplementationOnce(mockGetArtifactFailure)
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetArtifact
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const response = await downloadArtifactPublic(
|
||||
fixtures.artifactID,
|
||||
fixtures.repositoryOwner,
|
||||
fixtures.repositoryName,
|
||||
fixtures.token
|
||||
)
|
||||
|
||||
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
||||
owner: fixtures.repositoryOwner,
|
||||
repo: fixtures.repositoryName,
|
||||
artifact_id: fixtures.artifactID,
|
||||
archive_format: 'zip',
|
||||
request: {
|
||||
redirect: 'manual'
|
||||
}
|
||||
})
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
expect(mockGetArtifactFailure).toHaveBeenCalledWith(
|
||||
fixtures.blobStorageUrl
|
||||
)
|
||||
expect(mockGetArtifactFailure).toHaveBeenCalledTimes(1)
|
||||
expect(mockGetArtifactSuccess).toHaveBeenCalledWith(
|
||||
fixtures.blobStorageUrl
|
||||
)
|
||||
expect(mockGetArtifactSuccess).toHaveBeenCalledTimes(1)
|
||||
expect(response.downloadPath).toBe(fixtures.workspaceDir)
|
||||
}, 28000)
|
||||
})
|
||||
|
||||
describe('internal', () => {
|
||||
|
||||
Generated
+14
-81
@@ -21,7 +21,6 @@
|
||||
"archiver": "^5.3.1",
|
||||
"crypto": "^1.0.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"nock": "^13.4.0",
|
||||
"twirp-ts": "^2.5.0",
|
||||
"unzip-stream": "^0.3.1"
|
||||
},
|
||||
@@ -109,25 +108,6 @@
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-http/node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/core-lro": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.4.tgz",
|
||||
@@ -352,25 +332,6 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/request/node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/types": {
|
||||
"version": "6.41.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
|
||||
@@ -795,22 +756,6 @@
|
||||
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
|
||||
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -966,11 +911,6 @@
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
|
||||
},
|
||||
"node_modules/jsonc-parser": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||
@@ -1125,11 +1065,6 @@
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
@@ -1145,17 +1080,23 @@
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/nock": {
|
||||
"version": "13.4.0",
|
||||
"resolved": "https://registry.npmjs.org/nock/-/nock-13.4.0.tgz",
|
||||
"integrity": "sha512-W8NVHjO/LCTNA64yxAPHV/K47LpGYcVzgKd3Q0n6owhwvD0Dgoterc25R4rnZbckJEb6Loxz1f5QMuJpJnbSyQ==",
|
||||
"node_modules/node-fetch": {
|
||||
"version": "2.6.12",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
|
||||
"dependencies": {
|
||||
"debug": "^4.1.0",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"propagate": "^2.0.0"
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13"
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
@@ -1223,14 +1164,6 @@
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||
},
|
||||
"node_modules/propagate": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
|
||||
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
|
||||
@@ -52,7 +52,6 @@
|
||||
"archiver": "^5.3.1",
|
||||
"crypto": "^1.0.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"nock": "^13.4.0",
|
||||
"twirp-ts": "^2.5.0",
|
||||
"unzip-stream": "^0.3.1"
|
||||
},
|
||||
|
||||
@@ -38,20 +38,65 @@ async function exists(path: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
async function streamExtract(url: string, directory: string): Promise<void> {
|
||||
let retryCount = 0
|
||||
while (retryCount < 5) {
|
||||
try {
|
||||
await streamExtractExternal(url, directory)
|
||||
return
|
||||
} catch (error) {
|
||||
retryCount++
|
||||
core.debug(
|
||||
`Failed to download artifact after ${retryCount} retries due to ${error.message}. Retrying in 5 seconds...`
|
||||
)
|
||||
// wait 5 seconds before retrying
|
||||
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Artifact download failed after ${retryCount} retries.`)
|
||||
}
|
||||
|
||||
export async function streamExtractExternal(
|
||||
url: string,
|
||||
directory: string
|
||||
): Promise<void> {
|
||||
const client = new httpClient.HttpClient(getUserAgentString())
|
||||
const response = await client.get(url)
|
||||
|
||||
if (response.message.statusCode !== 200) {
|
||||
throw new Error(
|
||||
`Unexpected HTTP response from blob storage: ${response.message.statusCode} ${response.message.statusMessage}`
|
||||
)
|
||||
}
|
||||
|
||||
const timeout = 30 * 1000 // 30 seconds
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timerFn = (): void => {
|
||||
response.message.destroy(
|
||||
new Error(`Blob storage chunk did not respond in ${timeout}ms`)
|
||||
)
|
||||
}
|
||||
const timer = setTimeout(timerFn, timeout)
|
||||
|
||||
response.message
|
||||
.on('data', () => {
|
||||
timer.refresh()
|
||||
})
|
||||
.on('error', (error: Error) => {
|
||||
core.debug(
|
||||
`response.message: Artifact download failed: ${error.message}`
|
||||
)
|
||||
clearTimeout(timer)
|
||||
reject(error)
|
||||
})
|
||||
.pipe(unzip.Extract({path: directory}))
|
||||
.on('close', resolve)
|
||||
.on('error', reject)
|
||||
.on('close', () => {
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
})
|
||||
.on('error', (error: Error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -45,21 +45,6 @@ export interface UploadArtifactOptions {
|
||||
* For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads.
|
||||
*/
|
||||
compressionLevel?: number
|
||||
/**
|
||||
* The simulated network error we'll temporarily use to test the azure blob
|
||||
* client behavior.
|
||||
* The value can range from 0 to 6
|
||||
* 0 - fetchError
|
||||
* 1 - abortError
|
||||
* 2 - networkError
|
||||
* 3 - securityError
|
||||
* 4 - notAllowedError
|
||||
* 5 - quotaExceededError
|
||||
* 6 - random
|
||||
* 7 - none
|
||||
*
|
||||
*/
|
||||
simulateError?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,17 +5,8 @@ import {getUploadChunkSize, getConcurrency} from '../shared/config'
|
||||
import * as core from '@actions/core'
|
||||
import * as crypto from 'crypto'
|
||||
import * as stream from 'stream'
|
||||
import nock from 'nock'
|
||||
import {NetworkError} from '../shared/errors'
|
||||
|
||||
export const DEFAULT_ERROR_NUMBER = 7
|
||||
export const ERROR_TYPES = [
|
||||
'fetchError',
|
||||
'abortError',
|
||||
'securityError',
|
||||
'notAllowedError',
|
||||
'quotaExceededError'
|
||||
]
|
||||
export interface BlobUploadResponse {
|
||||
/**
|
||||
* The total reported upload size in bytes. Empty if the upload failed
|
||||
@@ -27,58 +18,10 @@ export interface BlobUploadResponse {
|
||||
*/
|
||||
sha256Hash?: string
|
||||
}
|
||||
export async function sendSimulatedError(
|
||||
simulatedError: number,
|
||||
authenticatedUploadURL: string
|
||||
): Promise<void> {
|
||||
switch (simulatedError) {
|
||||
case 0: {
|
||||
nock(authenticatedUploadURL).get('/').replyWithError({
|
||||
code: 'ECONNRESET',
|
||||
message: 'socket hang up'
|
||||
})
|
||||
break
|
||||
}
|
||||
case 1: {
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
break
|
||||
}
|
||||
case 2: {
|
||||
nock(authenticatedUploadURL).get('/').replyWithError({
|
||||
code: 'ETIMEDOUT'
|
||||
})
|
||||
break
|
||||
}
|
||||
case 3: {
|
||||
nock(authenticatedUploadURL).get('/').reply(403)
|
||||
break
|
||||
}
|
||||
case 4: {
|
||||
nock(authenticatedUploadURL).get('/').reply(405)
|
||||
break
|
||||
}
|
||||
case 5: {
|
||||
nock(authenticatedUploadURL).get('/').reply(429)
|
||||
break
|
||||
}
|
||||
case 6: {
|
||||
const rand = Math.floor(Math.random() * ERROR_TYPES.length)
|
||||
sendSimulatedError(rand, authenticatedUploadURL)
|
||||
break
|
||||
}
|
||||
case 7: {
|
||||
core.info('no error selected')
|
||||
break
|
||||
}
|
||||
default:
|
||||
core.error('something went wrong')
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadZipToBlobStorage(
|
||||
authenticatedUploadURL: string,
|
||||
zipUploadStream: ZipUploadStream,
|
||||
simulatedError: number = DEFAULT_ERROR_NUMBER
|
||||
zipUploadStream: ZipUploadStream
|
||||
): Promise<BlobUploadResponse> {
|
||||
let uploadByteCount = 0
|
||||
|
||||
@@ -93,9 +36,6 @@ export async function uploadZipToBlobStorage(
|
||||
|
||||
const uploadCallback = (progress: TransferProgressEvent): void => {
|
||||
core.info(`Uploaded bytes ${progress.loadedBytes}`)
|
||||
if (progress.loadedBytes > 1) {
|
||||
sendSimulatedError(simulatedError, authenticatedUploadURL)
|
||||
}
|
||||
uploadByteCount = progress.loadedBytes
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +76,7 @@ export async function uploadArtifact(
|
||||
// Upload zip to blob storage
|
||||
const uploadResult = await uploadZipToBlobStorage(
|
||||
createArtifactResp.signedUploadUrl,
|
||||
zipUploadStream,
|
||||
options?.simulateError
|
||||
zipUploadStream
|
||||
)
|
||||
|
||||
// finalize the artifact
|
||||
|
||||
Reference in New Issue
Block a user