Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 123bccb3d9 |
Generated
+3
-3
@@ -5781,9 +5781,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/follow-redirects": {
|
"node_modules/follow-redirects": {
|
||||||
"version": "1.15.4",
|
"version": "1.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||||
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
|
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
"lint": "eslint packages/**/*.ts",
|
"lint": "eslint packages/**/*.ts",
|
||||||
"lint-fix": "eslint packages/**/*.ts --fix",
|
"lint-fix": "eslint packages/**/*.ts --fix",
|
||||||
"new-package": "scripts/create-package",
|
"new-package": "scripts/create-package",
|
||||||
"test": "jest --testTimeout 60000"
|
"test": "jest --testTimeout 10000"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.4",
|
"@types/jest": "^29.5.4",
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ This is the core library that powers the [`@actions/upload-artifact`](https://gi
|
|||||||
## v2 - What's New
|
## v2 - What's New
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> @actions/artifact v2+, upload-artifact@v4+, and download-artifact@v4+ are not currently supported on GHES yet. The previous version of this package can be found at [this tag](https://github.com/actions/toolkit/tree/@actions/artifact@1.1.2/packages/artifact) and [on npm](https://www.npmjs.com/package/@actions/artifact/v/1.1.2).
|
> @actions/artifact v2+, upload-artifact@v4+ download-artifact@v4+ are not currently supported on GHES yet. The previous version of this package can be found at [this tag](https://github.com/actions/toolkit/tree/@actions/artifact@1.1.2/packages/artifact) and [on npm](https://www.npmjs.com/package/@actions/artifact/v/1.1.2).
|
||||||
|
|
||||||
The release of `@actions/artifact@v2` (including `upload-artifact@v4` and `download-artifact@v4`) are major changes to the backend architecture of Artifacts. They have numerous performance and behavioral improvements.
|
The release of `@actions/artifact@v2` (including `upload-artifact@v4` and `download-artifact@v4`) are major changes to the backend architecture of Artifacts. They have numerous performance and behavioral improvements.
|
||||||
|
|
||||||
|
|||||||
@@ -97,11 +97,7 @@
|
|||||||
|
|
||||||
### 2.0.0
|
### 2.0.0
|
||||||
|
|
||||||
- Major release. Supports new Artifact backend for improved speed, reliability and behavior.
|
Major release. Supports new Artifact backend for improved speed, reliability and behavior.
|
||||||
- Numerous API changes, [some breaking](./README.md#breaking-changes).
|
Numerous API changes, [some breaking](./README.md#breaking-changes).
|
||||||
|
|
||||||
- Blog post with more info: TBD
|
Blog post with more info: TBD
|
||||||
|
|
||||||
### 2.0.1
|
|
||||||
|
|
||||||
- Patch to fix transient request timeouts https://github.com/actions/download-artifact/issues/249
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import archiver from 'archiver'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
downloadArtifactInternal,
|
downloadArtifactInternal,
|
||||||
downloadArtifactPublic,
|
downloadArtifactPublic
|
||||||
streamExtractExternal
|
|
||||||
} from '../src/internal/download/download-artifact'
|
} from '../src/internal/download/download-artifact'
|
||||||
import {getUserAgentString} from '../src/internal/shared/user-agent'
|
import {getUserAgentString} from '../src/internal/shared/user-agent'
|
||||||
import {noopLogs} from './common'
|
import {noopLogs} from './common'
|
||||||
@@ -249,44 +248,7 @@ describe('download-artifact', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should fail if blob storage storage chunk does not respond within 30s', async () => {
|
it('should fail if blob storage response is non-200', 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
|
const downloadArtifactMock = github.getOctokit(fixtures.token).rest
|
||||||
.actions.downloadArtifact as MockedDownloadArtifact
|
.actions.downloadArtifact as MockedDownloadArtifact
|
||||||
downloadArtifactMock.mockResolvedValueOnce({
|
downloadArtifactMock.mockResolvedValueOnce({
|
||||||
@@ -328,60 +290,7 @@ describe('download-artifact', () => {
|
|||||||
expect(mockGetArtifactFailure).toHaveBeenCalledWith(
|
expect(mockGetArtifactFailure).toHaveBeenCalledWith(
|
||||||
fixtures.blobStorageUrl
|
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', () => {
|
describe('internal', () => {
|
||||||
|
|||||||
Generated
+83
-16
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/artifact",
|
"name": "@actions/artifact",
|
||||||
"version": "2.0.1",
|
"version": "2.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@actions/artifact",
|
"name": "@actions/artifact",
|
||||||
"version": "2.0.1",
|
"version": "2.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
|
"nock": "^13.4.0",
|
||||||
"twirp-ts": "^2.5.0",
|
"twirp-ts": "^2.5.0",
|
||||||
"unzip-stream": "^0.3.1"
|
"unzip-stream": "^0.3.1"
|
||||||
},
|
},
|
||||||
@@ -108,6 +109,25 @@
|
|||||||
"node": ">=14.0.0"
|
"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": {
|
"node_modules/@azure/core-lro": {
|
||||||
"version": "2.5.4",
|
"version": "2.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.4.tgz",
|
||||||
@@ -332,6 +352,25 @@
|
|||||||
"once": "^1.4.0"
|
"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": {
|
"node_modules/@octokit/types": {
|
||||||
"version": "6.41.0",
|
"version": "6.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
|
||||||
@@ -756,6 +795,22 @@
|
|||||||
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
|
"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."
|
"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": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
@@ -911,6 +966,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
"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": {
|
"node_modules/jsonc-parser": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||||
@@ -1065,6 +1125,11 @@
|
|||||||
"mkdirp": "bin/cmd.js"
|
"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": {
|
"node_modules/neo-async": {
|
||||||
"version": "2.6.2",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
@@ -1080,23 +1145,17 @@
|
|||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/nock": {
|
||||||
"version": "2.6.12",
|
"version": "13.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
|
"resolved": "https://registry.npmjs.org/nock/-/nock-13.4.0.tgz",
|
||||||
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
|
"integrity": "sha512-W8NVHjO/LCTNA64yxAPHV/K47LpGYcVzgKd3Q0n6owhwvD0Dgoterc25R4rnZbckJEb6Loxz1f5QMuJpJnbSyQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"whatwg-url": "^5.0.0"
|
"debug": "^4.1.0",
|
||||||
|
"json-stringify-safe": "^5.0.1",
|
||||||
|
"propagate": "^2.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "4.x || >=6.0.0"
|
"node": ">= 10.13"
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"encoding": "^0.1.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"encoding": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
@@ -1164,6 +1223,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
"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": {
|
"node_modules/readable-stream": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/artifact",
|
"name": "@actions/artifact",
|
||||||
"version": "2.0.1",
|
"version": "2.0.0",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"description": "Actions artifact lib",
|
"description": "Actions artifact lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -52,6 +52,7 @@
|
|||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
|
"nock": "^13.4.0",
|
||||||
"twirp-ts": "^2.5.0",
|
"twirp-ts": "^2.5.0",
|
||||||
"unzip-stream": "^0.3.1"
|
"unzip-stream": "^0.3.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -38,65 +38,20 @@ async function exists(path: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function streamExtract(url: string, directory: string): Promise<void> {
|
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 client = new httpClient.HttpClient(getUserAgentString())
|
||||||
const response = await client.get(url)
|
const response = await client.get(url)
|
||||||
|
|
||||||
if (response.message.statusCode !== 200) {
|
if (response.message.statusCode !== 200) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unexpected HTTP response from blob storage: ${response.message.statusCode} ${response.message.statusMessage}`
|
`Unexpected HTTP response from blob storage: ${response.message.statusCode} ${response.message.statusMessage}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeout = 30 * 1000 // 30 seconds
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
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
|
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}))
|
.pipe(unzip.Extract({path: directory}))
|
||||||
.on('close', () => {
|
.on('close', resolve)
|
||||||
clearTimeout(timer)
|
.on('error', reject)
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.on('error', (error: Error) => {
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,21 @@ export interface UploadArtifactOptions {
|
|||||||
* For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads.
|
* For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads.
|
||||||
*/
|
*/
|
||||||
compressionLevel?: number
|
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,8 +5,17 @@ import {getUploadChunkSize, getConcurrency} from '../shared/config'
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import * as stream from 'stream'
|
import * as stream from 'stream'
|
||||||
|
import nock from 'nock'
|
||||||
import {NetworkError} from '../shared/errors'
|
import {NetworkError} from '../shared/errors'
|
||||||
|
|
||||||
|
export const DEFAULT_ERROR_NUMBER = 7
|
||||||
|
export const ERROR_TYPES = [
|
||||||
|
'fetchError',
|
||||||
|
'abortError',
|
||||||
|
'securityError',
|
||||||
|
'notAllowedError',
|
||||||
|
'quotaExceededError'
|
||||||
|
]
|
||||||
export interface BlobUploadResponse {
|
export interface BlobUploadResponse {
|
||||||
/**
|
/**
|
||||||
* The total reported upload size in bytes. Empty if the upload failed
|
* The total reported upload size in bytes. Empty if the upload failed
|
||||||
@@ -18,10 +27,58 @@ export interface BlobUploadResponse {
|
|||||||
*/
|
*/
|
||||||
sha256Hash?: string
|
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(
|
export async function uploadZipToBlobStorage(
|
||||||
authenticatedUploadURL: string,
|
authenticatedUploadURL: string,
|
||||||
zipUploadStream: ZipUploadStream
|
zipUploadStream: ZipUploadStream,
|
||||||
|
simulatedError: number = DEFAULT_ERROR_NUMBER
|
||||||
): Promise<BlobUploadResponse> {
|
): Promise<BlobUploadResponse> {
|
||||||
let uploadByteCount = 0
|
let uploadByteCount = 0
|
||||||
|
|
||||||
@@ -36,6 +93,9 @@ export async function uploadZipToBlobStorage(
|
|||||||
|
|
||||||
const uploadCallback = (progress: TransferProgressEvent): void => {
|
const uploadCallback = (progress: TransferProgressEvent): void => {
|
||||||
core.info(`Uploaded bytes ${progress.loadedBytes}`)
|
core.info(`Uploaded bytes ${progress.loadedBytes}`)
|
||||||
|
if (progress.loadedBytes > 1) {
|
||||||
|
sendSimulatedError(simulatedError, authenticatedUploadURL)
|
||||||
|
}
|
||||||
uploadByteCount = progress.loadedBytes
|
uploadByteCount = progress.loadedBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,8 @@ export async function uploadArtifact(
|
|||||||
// Upload zip to blob storage
|
// Upload zip to blob storage
|
||||||
const uploadResult = await uploadZipToBlobStorage(
|
const uploadResult = await uploadZipToBlobStorage(
|
||||||
createArtifactResp.signedUploadUrl,
|
createArtifactResp.signedUploadUrl,
|
||||||
zipUploadStream
|
zipUploadStream,
|
||||||
|
options?.simulateError
|
||||||
)
|
)
|
||||||
|
|
||||||
// finalize the artifact
|
// finalize the artifact
|
||||||
|
|||||||
@@ -358,129 +358,3 @@ const {
|
|||||||
version, // 10.0.22621
|
version, // 10.0.22621
|
||||||
} = await platform.getDetails()
|
} = await platform.getDetails()
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Populating job summary
|
|
||||||
|
|
||||||
These methods can be used to populate a [job summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). A job summary is a buffer that can be added to throughout your job via `core.summary` methods.
|
|
||||||
|
|
||||||
Job summaries when complete must be written to the summary buffer file via the `core.summary.write()` method.
|
|
||||||
|
|
||||||
All methods except `addRaw()` utilize the `addRaw()` method to append to the buffer, followed by an EOL using the `addEOL()` method.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
|
|
||||||
// Write raw text, optionally add an EOL after the content, defaults to false
|
|
||||||
core.summary.addRaw('Some content here :speech_balloon:', true)
|
|
||||||
// Output: Some content here :speech_balloon:\n
|
|
||||||
|
|
||||||
// Add an operating system-specific end-of-line marker
|
|
||||||
core.summary.addEOL()
|
|
||||||
// Output (POSIX): \n
|
|
||||||
// Output (Windows): \r\n
|
|
||||||
|
|
||||||
// Add a codeblock with an optional language for syntax highlighting
|
|
||||||
core.summary.addCodeBlock('console.log(\'hello world\')', 'javascript')
|
|
||||||
// Output: <pre lang="javascript"><code>console.log('hello world')</code></pre>
|
|
||||||
|
|
||||||
// Add a list, second parameter indicates if list is ordered, defaults to false
|
|
||||||
core.summary.addList(['item1','item2','item3'], true)
|
|
||||||
// Output: <ol><li>item1</li><li>item2</li><li>item3</li></ol>
|
|
||||||
|
|
||||||
// Add a collapsible HTML details element
|
|
||||||
core.summary.addDetails('Label', 'Some detail that will be collapsed')
|
|
||||||
// Output: <details><summary>Label</summary>Some detail that will be collapsed</details>
|
|
||||||
|
|
||||||
// Add an image, image options parameter is optional, you can supply one of or both width and height in pixels
|
|
||||||
core.summary.addImage('example.png', 'alt description of img', {width: '100', height: '100'})
|
|
||||||
// Output: <img src="example.png" alt="alt description of img" width="100" height="100">
|
|
||||||
|
|
||||||
// Add an HTML section heading element, optionally pass a level that translates to 'hX' ie. h2. Defaults to h1
|
|
||||||
core.summary.addHeading('My Heading', '2')
|
|
||||||
// Output: <h2>My Heading</h2>
|
|
||||||
|
|
||||||
// Add an HTML thematic break <hr>
|
|
||||||
core.summary.addSeparator()
|
|
||||||
// Output: <hr>
|
|
||||||
|
|
||||||
// Add an HTML line break <br>
|
|
||||||
core.summary.addBreak()
|
|
||||||
// Output: <br>
|
|
||||||
|
|
||||||
// Add an HTML blockquote with an optional citation
|
|
||||||
core.summary.addQuote('To be or not to be', 'Shakespeare')
|
|
||||||
// Output: <blockquote cite="Shakespeare">To be or not to be</blockquote>
|
|
||||||
|
|
||||||
// Add an HTML anchor tag
|
|
||||||
core.summary.addLink('click here', 'https://github.com')
|
|
||||||
// Output: <a href="https://github.com">click here</a>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Tables are added using the `addTable()` method, and an array of `SummaryTableRow`.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
|
|
||||||
export type SummaryTableRow = (SummaryTableCell | string)[]
|
|
||||||
|
|
||||||
export interface SummaryTableCell {
|
|
||||||
/**
|
|
||||||
* Cell content
|
|
||||||
*/
|
|
||||||
data: string
|
|
||||||
/**
|
|
||||||
* Render cell as header
|
|
||||||
* (optional) default: false
|
|
||||||
*/
|
|
||||||
header?: boolean
|
|
||||||
/**
|
|
||||||
* Number of columns the cell extends
|
|
||||||
* (optional) default: '1'
|
|
||||||
*/
|
|
||||||
colspan?: string
|
|
||||||
/**
|
|
||||||
* Number of rows the cell extends
|
|
||||||
* (optional) default: '1'
|
|
||||||
*/
|
|
||||||
rowspan?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
For example
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
|
|
||||||
const tableData = [
|
|
||||||
{data: 'Header1', header: true},
|
|
||||||
{data: 'Header2', header: true},
|
|
||||||
{data: 'Header3', header: true},
|
|
||||||
{data: 'MyData1'},
|
|
||||||
{data: 'MyData2'},
|
|
||||||
{data: 'MyData3'}
|
|
||||||
]
|
|
||||||
|
|
||||||
// Add an HTML table
|
|
||||||
core.summary.addTable([tableData])
|
|
||||||
// Output: <table><tr><th>Header1</th><th>Header2</th><th>Header3</th></tr><tr></tr><td>MyData1</td><td>MyData2</td><td>MyData3</td></tr></table>
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
In addition to job summary content, there are utility functions for interfacing with the buffer.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
|
|
||||||
// Empties the summary buffer AND wipes the summary file on disk
|
|
||||||
core.summary.clear()
|
|
||||||
|
|
||||||
// Returns the current summary buffer as a string
|
|
||||||
core.summary.stringify()
|
|
||||||
|
|
||||||
// If the summary buffer is empty
|
|
||||||
core.summary.isEmptyBuffer()
|
|
||||||
|
|
||||||
// Resets the summary buffer without writing to the summary file on disk
|
|
||||||
core.summary.emptyBuffer()
|
|
||||||
|
|
||||||
// Writes text in the buffer to the summary buffer file and empties the buffer, optionally overwriting all existing content in the summary file with buffer contents. Defaults to false.
|
|
||||||
core.summary.write({overwrite: true})
|
|
||||||
```
|
|
||||||
Reference in New Issue
Block a user