Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b54065b5da |
+1
-1
@@ -8,4 +8,4 @@ module.exports = {
|
|||||||
'^.+\\.ts$': 'ts-jest'
|
'^.+\\.ts$': 'ts-jest'
|
||||||
},
|
},
|
||||||
verbose: true
|
verbose: true
|
||||||
}
|
}
|
||||||
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/[email protected]/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/[email protected]/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
+2
-2
@@ -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",
|
||||||
|
|||||||
@@ -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": [
|
||||||
@@ -62,4 +62,4 @@
|
|||||||
"typedoc-plugin-markdown": "^3.17.1",
|
"typedoc-plugin-markdown": "^3.17.1",
|
||||||
"typescript": "^5.2.2"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,64 +39,73 @@ 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
|
let retryCount = 0
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const promises: Promise<any>[] = []
|
||||||
while (retryCount < 5) {
|
while (retryCount < 5) {
|
||||||
try {
|
const promise = new Promise(async () => {
|
||||||
await streamExtractExternal(url, directory)
|
try {
|
||||||
return
|
await streamExtractInternal(url, directory)
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
retryCount++
|
retryCount++
|
||||||
core.debug(
|
core.warning(`Failed to download artifact. Retrying in 5 seconds...`)
|
||||||
`Failed to download artifact after ${retryCount} retries due to ${error.message}. Retrying in 5 seconds...`
|
await new Promise(resolve => setTimeout(resolve, 5000))
|
||||||
)
|
}
|
||||||
// wait 5 seconds before retrying
|
})
|
||||||
await new Promise(resolve => setTimeout(resolve, 5000))
|
promises.push(promise)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Artifact download failed after ${retryCount} retries.`)
|
try {
|
||||||
|
await Promise.all(promises)
|
||||||
|
core.info('All Promises Returned')
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Artifact download failed after ${retryCount} retries.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// throw new Error(`Artifact download failed after ${retryCount} retries.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function streamExtractExternal(
|
async function streamExtractInternal(
|
||||||
url: string,
|
url: string,
|
||||||
directory: string
|
directory: string
|
||||||
): Promise<void> {
|
): 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 => {
|
const zipStream = unzip.Extract({path: directory})
|
||||||
response.message.destroy(
|
|
||||||
new Error(`Blob storage chunk did not respond in ${timeout}ms`)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const timer = setTimeout(timerFn, timeout)
|
|
||||||
|
|
||||||
response.message
|
const timeout = 30 * 1000
|
||||||
.on('data', () => {
|
const timerFn = (): void => {
|
||||||
timer.refresh()
|
zipStream.end()
|
||||||
})
|
reject(new Error(`Blob storage chunk did not respond in ${timeout}ms `))
|
||||||
.on('error', (error: Error) => {
|
}
|
||||||
core.debug(
|
let timer = setTimeout(timerFn, timeout)
|
||||||
`response.message: Artifact download failed: ${error.message}`
|
|
||||||
)
|
try {
|
||||||
clearTimeout(timer)
|
response.message
|
||||||
reject(error)
|
.on('data', () => {
|
||||||
})
|
clearTimeout(timer)
|
||||||
.pipe(unzip.Extract({path: directory}))
|
timer = setTimeout(timerFn, timeout)
|
||||||
.on('close', () => {
|
})
|
||||||
clearTimeout(timer)
|
.pipe(zipStream)
|
||||||
resolve()
|
.on('close', () => {
|
||||||
})
|
core.debug(`zip stream: Artifact downloaded to: ${directory}`)
|
||||||
.on('error', (error: Error) => {
|
clearTimeout(timer)
|
||||||
reject(error)
|
resolve()
|
||||||
})
|
})
|
||||||
|
.on('error', reject)
|
||||||
|
} catch (error) {
|
||||||
|
zipStream.end()
|
||||||
|
reject(error)
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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