Compare commits

..
Author SHA1 Message Date
Rob Herley 0db2ef8ead clean up comments for better autogen docs 2023-12-06 20:24:33 +00:00
Rob Herley e3764a56e8 autogenerate artifact documentation 2023-12-06 20:06:58 +00:00
21 changed files with 235 additions and 628 deletions
+33 -69
View File
@@ -1,3 +1,5 @@
# Temporarily disabled while v2.0.0 of @actions/artifact is under development
name: artifact-unit-tests name: artifact-unit-tests
on: on:
push: push:
@@ -10,8 +12,8 @@ on:
- '**.md' - '**.md'
jobs: jobs:
upload: build:
name: Upload name: Build
strategy: strategy:
matrix: matrix:
@@ -40,13 +42,19 @@ jobs:
npm run tsc npm run tsc
working-directory: packages/artifact working-directory: packages/artifact
- name: Set artifact file contents
shell: bash
run: |
echo "file1=hello from file 1" >> $GITHUB_ENV
echo "file2=hello from file 2" >> $GITHUB_ENV
- name: Create files that will be uploaded - name: Create files that will be uploaded
run: | run: |
mkdir artifact-path mkdir artifact-path
echo -n 'hello from file 1' > artifact-path/first.txt echo '${{ env.file1 }}' > artifact-path/first.txt
echo -n 'hello from file 2' > artifact-path/second.txt echo '${{ env.file2 }}' > artifact-path/second.txt
- name: Upload Artifacts - name: Upload Artifacts using actions/github-script@v7
uses: actions/github-script@v7 uses: actions/github-script@v7
with: with:
script: | script: |
@@ -65,16 +73,9 @@ jobs:
console.log(`Successfully uploaded artifact ${id}`) console.log(`Successfully uploaded artifact ${id}`)
try {
await artifact.uploadArtifact(artifactName, fileContents, './')
throw new Error('should have failed second upload')
} catch (err) {
console.log('Successfully blocked second artifact upload')
}
verify: verify:
name: Verify
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [upload] needs: [build]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -95,72 +96,35 @@ jobs:
npm run tsc npm run tsc
working-directory: packages/artifact working-directory: packages/artifact
- name: List and Download Artifacts - name: List artifacts using actions/github-script@v7
uses: actions/github-script@v7 uses: actions/github-script@v7
with: with:
script: | script: |
const {default: artifactClient} = require('./packages/artifact/lib/artifact') const {default: artifact} = require('./packages/artifact/lib/artifact')
const {readFile} = require('fs/promises') const workflowRunId = process.env.GITHUB_RUN_ID
const path = require('path') const repository = process.env.GITHUB_REPOSITORY
const repositoryOwner = repository.split('/')[0]
const repositoryName = repository.split('/')[1]
const findBy = { const listResult = await artifact.listArtifacts(workflowRunId, repositoryOwner, repositoryName, '${{ secrets.GITHUB_TOKEN }}')
repositoryOwner: process.env.GITHUB_REPOSITORY.split('/')[0],
repositoryName: process.env.GITHUB_REPOSITORY.split('/')[1],
token: '${{ secrets.GITHUB_TOKEN }}',
workflowRunId: process.env.GITHUB_RUN_ID
}
const listResult = await artifactClient.listArtifacts({latest: true, findBy})
console.log(listResult) console.log(listResult)
const artifacts = listResult.artifacts const artifacts = listResult.artifacts
const expected = [
'my-artifact-ubuntu-latest',
'my-artifact-windows-latest',
'my-artifact-macos-latest'
]
const foundArtifacts = artifacts.filter(artifact => if (artifacts.length !== 3) {
expected.includes(artifact.name) throw new Error('Expected 3 artifacts but only found ' + artifacts.length + ' artifacts')
) }
if (foundArtifacts.length !== 3) { const artifactNames = artifacts.map(artifact => artifact.name)
console.log('Unexpected length of found artifacts', foundArtifacts) if (!artifactNames.includes('my-artifact-ubuntu-latest')){
throw new Error( throw new Error("Expected artifact list to contain an artifact named my-artifact-ubuntu-latest but it's missing")
`Expected 3 artifacts but found ${foundArtifacts.length} artifacts.` }
) if (!artifactNames.includes('my-artifact-windows-latest')){
throw new Error("Expected artifact list to contain an artifact named my-artifact-windows-latest but it's missing")
}
if (!artifactNames.includes('my-artifact-macos-latest')){
throw new Error("Expected artifact list to contain an artifact named my-artifact-macos-latest but it's missing")
} }
console.log('Successfully listed artifacts that were uploaded') console.log('Successfully listed artifacts that were uploaded')
const files = [
{name: 'artifact-path/first.txt', content: 'hello from file 1'},
{name: 'artifact-path/second.txt', content: 'hello from file 2'}
]
for (const artifact of foundArtifacts) {
const {downloadPath} = await artifactClient.downloadArtifact(artifact.id, {
path: artifact.name,
findBy
})
console.log('Downloaded artifact to:', downloadPath)
for (const file of files) {
const filepath = path.join(
process.env.GITHUB_WORKSPACE,
downloadPath,
file.name
)
console.log('Checking file:', filepath)
const content = await readFile(filepath, 'utf8')
if (content.trim() !== file.content.trim()) {
throw new Error(
`Expected file '${file.name}' to contain '${file.content}' but found '${content}'`
)
}
}
}
+3 -3
View File
@@ -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
View File
@@ -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",
+3 -9
View File
@@ -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.
@@ -63,16 +63,10 @@ Import the module:
```js ```js
// ES6 module // ES6 module
import {DefaultArtifactClient} from '@actions/artifact' import artifact from '@actions/artifact'
// CommonJS // CommonJS
const {DefaultArtifactClient} = require('@actions/artifact') const {default: artifact} = require('@actions/artifact')
```
Then instantiate:
```js
const artifact = new DefaultArtifactClient()
``` ```
️ For a comprehensive list of classes, interfaces, functions and more, see the [generated documentation](./docs/generated/README.md). ️ For a comprehensive list of classes, interfaces, functions and more, see the [generated documentation](./docs/generated/README.md).
+3 -7
View File
@@ -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
@@ -4,16 +4,9 @@ import {HttpClient} from '@actions/http-client'
import * as config from '../src/internal/shared/config' import * as config from '../src/internal/shared/config'
import {internalArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client' import {internalArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client'
import {noopLogs} from './common' import {noopLogs} from './common'
import {NetworkError, UsageError} from '../src/internal/shared/errors'
jest.mock('@actions/http-client') jest.mock('@actions/http-client')
const clientOptions = {
maxAttempts: 5,
retryIntervalMs: 1,
retryMultiplier: 1.5
}
describe('artifact-http-client', () => { describe('artifact-http-client', () => {
beforeAll(() => { beforeAll(() => {
noopLogs() noopLogs()
@@ -101,7 +94,11 @@ describe('artifact-http-client', () => {
} }
}) })
const client = internalArtifactTwirpClient(clientOptions) const client = internalArtifactTwirpClient({
maxAttempts: 5,
retryIntervalMs: 1,
retryMultiplier: 1.5
})
const artifact = await client.CreateArtifact({ const artifact = await client.CreateArtifact({
workflowRunBackendId: '1234', workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678', workflowJobRunBackendId: '5678',
@@ -136,7 +133,11 @@ describe('artifact-http-client', () => {
post: mockPost post: mockPost
} }
}) })
const client = internalArtifactTwirpClient(clientOptions) const client = internalArtifactTwirpClient({
maxAttempts: 5,
retryIntervalMs: 1,
retryMultiplier: 1.5
})
await expect(async () => { await expect(async () => {
await client.CreateArtifact({ await client.CreateArtifact({
workflowRunBackendId: '1234', workflowRunBackendId: '1234',
@@ -171,7 +172,11 @@ describe('artifact-http-client', () => {
post: mockPost post: mockPost
} }
}) })
const client = internalArtifactTwirpClient(clientOptions) const client = internalArtifactTwirpClient({
maxAttempts: 5,
retryIntervalMs: 1,
retryMultiplier: 1.5
})
await expect(async () => { await expect(async () => {
await client.CreateArtifact({ await client.CreateArtifact({
workflowRunBackendId: '1234', workflowRunBackendId: '1234',
@@ -185,116 +190,4 @@ describe('artifact-http-client', () => {
expect(mockHttpClient).toHaveBeenCalledTimes(1) expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledTimes(1) expect(mockPost).toHaveBeenCalledTimes(1)
}) })
it('should fail with a descriptive error', async () => {
// 409 duplicate error
const mockPost = jest.fn(() => {
const msgFailed = new http.IncomingMessage(new net.Socket())
msgFailed.statusCode = 409
msgFailed.statusMessage = 'Conflict'
return {
message: msgFailed,
readBody: async () => {
return Promise.resolve(
`{"msg": "an artifact with this name already exists on the workflow run"}`
)
}
}
})
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalArtifactTwirpClient(clientOptions)
await expect(async () => {
await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
}).rejects.toThrowError(
'Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run'
)
expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledTimes(1)
})
it('should properly describe a network failure', async () => {
class FakeNodeError extends Error {
code: string
constructor(code: string) {
super()
this.code = code
}
}
const mockPost = jest.fn(() => {
throw new FakeNodeError('ENOTFOUND')
})
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalArtifactTwirpClient()
await expect(async () => {
await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
}).rejects.toThrowError(new NetworkError('ENOTFOUND').message)
expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledTimes(1)
})
it('should properly describe a usage error', async () => {
const mockPost = jest.fn(() => {
const msgFailed = new http.IncomingMessage(new net.Socket())
msgFailed.statusCode = 403
msgFailed.statusMessage = 'Forbidden'
return {
message: msgFailed,
readBody: async () => {
return Promise.resolve(
`{"msg": "insufficient usage to create artifact"}`
)
}
}
})
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalArtifactTwirpClient()
await expect(async () => {
await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
}).rejects.toThrowError(new UsageError().message)
expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledTimes(1)
})
}) })
@@ -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'
@@ -105,7 +104,6 @@ const mockGetArtifactSuccess = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket()) const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 200 message.statusCode = 200
message.push(fs.readFileSync(fixtures.exampleArtifact.path)) message.push(fs.readFileSync(fixtures.exampleArtifact.path))
message.push(null)
return { return {
message message
} }
@@ -115,7 +113,6 @@ const mockGetArtifactFailure = jest.fn(() => {
const message = new http.IncomingMessage(new net.Socket()) const message = new http.IncomingMessage(new net.Socket())
message.statusCode = 500 message.statusCode = 500
message.push('Internal Server Error') message.push('Internal Server Error')
message.push(null)
return { return {
message message
} }
@@ -249,44 +246,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 +288,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', () => {
-21
View File
@@ -33,24 +33,3 @@ The value can range from 0 to 9:
Higher levels will result in better compression, but will take longer to complete. Higher levels will result in better compression, but will take longer to complete.
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.
## Which versions of the artifacts packages are compatible?
[actions/upload-artifact](https://github.com/actions/upload-artifact) and [actions/download-artifact](https://github.com/actions/download-artifact), are part of the [GitHub Actions toolkit](https://github.com/actions/toolkit) and are typically used together to upload and download artifacts in your workflows.
1. **Matching Versions:**
- Use matching versions of `actions/upload-artifact` and `actions/download-artifact` to ensure compatibility.
2. **Workflow YAML File:**
- In your GitHub Actions workflow YAML file, you specify the version of the actions you want to use. For example:
```yaml
uses: actions/upload-artifact@v4
# ...
uses: actions/download-artifact@v4
# ...
```
3. **Release Notes:**
- Check the release notes for each repository to see if there are any specific notes about compatibility or changes in behavior.
## How long will my artifact be available?
The default retention period is 90 days. For more information, visit: https://github.com/actions/upload-artifact?tab=readme-ov-file#retention-period
+140 -15
View File
@@ -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",
@@ -18,15 +18,15 @@
"@octokit/plugin-retry": "^3.0.9", "@octokit/plugin-retry": "^3.0.9",
"@octokit/request-error": "^5.0.0", "@octokit/request-error": "^5.0.0",
"@protobuf-ts/plugin": "^2.2.3-alpha.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1",
"@types/unzipper": "^0.10.6",
"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",
"twirp-ts": "^2.5.0", "twirp-ts": "^2.5.0",
"unzip-stream": "^0.3.1" "unzipper": "^0.10.14"
}, },
"devDependencies": { "devDependencies": {
"@types/archiver": "^5.3.2", "@types/archiver": "^5.3.2",
"@types/unzip-stream": "^0.3.4",
"typedoc": "^0.25.4", "typedoc": "^0.25.4",
"typedoc-plugin-markdown": "^3.17.1", "typedoc-plugin-markdown": "^3.17.1",
"typescript": "^5.2.2" "typescript": "^5.2.2"
@@ -471,11 +471,10 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/unzip-stream": { "node_modules/@types/unzipper": {
"version": "0.3.4", "version": "0.10.6",
"resolved": "https://registry.npmjs.org/@types/unzip-stream/-/unzip-stream-0.3.4.tgz", "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.6.tgz",
"integrity": "sha512-ud0vtsNRF+joUCyvNMyo0j5DKX2Lh/im+xVgRzBEsfHhQYZ+i4fKTveova9XxLzt6Jl6G0e/0mM4aC0gqZYSnA==", "integrity": "sha512-zcBj329AHgKLQyz209N/S9R0GZqXSkUQO4tJSYE3x02qg4JuDFpgKMj50r82Erk1natCWQDIvSccDddt7jPzjA==",
"dev": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
@@ -589,6 +588,14 @@
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="
}, },
"node_modules/big-integer": {
"version": "1.6.51",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/binary": { "node_modules/binary": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
@@ -611,6 +618,11 @@
"readable-stream": "^3.4.0" "readable-stream": "^3.4.0"
} }
}, },
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
},
"node_modules/bottleneck": { "node_modules/bottleneck": {
"version": "2.19.5", "version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
@@ -656,6 +668,14 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/buffer-indexof-polyfill": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/buffers": { "node_modules/buffers": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
@@ -781,6 +801,41 @@
"dot-object": "bin/dot-object" "dot-object": "bin/dot-object"
} }
}, },
"node_modules/duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
"dependencies": {
"readable-stream": "^2.0.2"
}
},
"node_modules/duplexer2/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/duplexer2/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/duplexer2/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/end-of-stream": { "node_modules/end-of-stream": {
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -820,6 +875,20 @@
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
}, },
"node_modules/fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"dependencies": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
},
"engines": {
"node": ">=0.6"
}
},
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -960,6 +1029,11 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"node_modules/listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
"integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -1204,6 +1278,17 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1228,6 +1313,11 @@
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
}, },
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/shiki": { "node_modules/shiki": {
"version": "0.14.5", "version": "0.14.5",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz", "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.5.tgz",
@@ -1423,13 +1513,48 @@
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz",
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
}, },
"node_modules/unzip-stream": { "node_modules/unzipper": {
"version": "0.3.1", "version": "0.10.14",
"resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz", "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz",
"integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==", "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==",
"dependencies": { "dependencies": {
"binary": "^0.3.0", "big-integer": "^1.6.17",
"mkdirp": "^0.5.1" "binary": "~0.3.0",
"bluebird": "~3.4.1",
"buffer-indexof-polyfill": "~1.0.0",
"duplexer2": "~0.1.4",
"fstream": "^1.0.12",
"graceful-fs": "^4.2.2",
"listenercount": "~1.0.1",
"readable-stream": "~2.3.6",
"setimmediate": "~1.0.4"
}
},
"node_modules/unzipper/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/unzipper/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/unzipper/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": {
"safe-buffer": "~5.1.0"
} }
}, },
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
+3 -3
View File
@@ -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": [
@@ -49,15 +49,15 @@
"@octokit/plugin-retry": "^3.0.9", "@octokit/plugin-retry": "^3.0.9",
"@octokit/request-error": "^5.0.0", "@octokit/request-error": "^5.0.0",
"@protobuf-ts/plugin": "^2.2.3-alpha.1", "@protobuf-ts/plugin": "^2.2.3-alpha.1",
"@types/unzipper": "^0.10.6",
"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",
"twirp-ts": "^2.5.0", "twirp-ts": "^2.5.0",
"unzip-stream": "^0.3.1" "unzipper": "^0.10.14"
}, },
"devDependencies": { "devDependencies": {
"@types/archiver": "^5.3.2", "@types/archiver": "^5.3.2",
"@types/unzip-stream": "^0.3.4",
"typedoc": "^0.25.4", "typedoc": "^0.25.4",
"typedoc-plugin-markdown": "^3.17.1", "typedoc-plugin-markdown": "^3.17.1",
"typescript": "^5.2.2" "typescript": "^5.2.2"
@@ -2,7 +2,7 @@ import fs from 'fs/promises'
import * as github from '@actions/github' import * as github from '@actions/github'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as httpClient from '@actions/http-client' import * as httpClient from '@actions/http-client'
import unzip from 'unzip-stream' import unzipper from 'unzipper'
import { import {
DownloadArtifactOptions, DownloadArtifactOptions,
DownloadArtifactResponse DownloadArtifactResponse
@@ -38,66 +38,16 @@ 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 response.message.pipe(unzipper.Extract({path: directory})).promise()
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', () => {
clearTimeout(timer)
resolve()
})
.on('error', (error: Error) => {
reject(error)
})
})
} }
export async function downloadArtifactPublic( export async function downloadArtifactPublic(
@@ -49,9 +49,7 @@ export async function getArtifactPublic(
if (getArtifactResp.data.artifacts.length === 0) { if (getArtifactResp.data.artifacts.length === 0) {
throw new ArtifactNotFoundError( throw new ArtifactNotFoundError(
`Artifact not found for name: ${artifactName} `Artifact not found for name: ${artifactName}`
Please ensure that your artifact is not expired and the artifact was uploaded using a compatible version of toolkit/upload-artifact.
For more information, visit the GitHub Artifacts FAQ: https://github.com/actions/toolkit/blob/main/packages/artifact/docs/faq.md`
) )
} }
@@ -91,9 +89,7 @@ export async function getArtifactInternal(
if (res.artifacts.length === 0) { if (res.artifacts.length === 0) {
throw new ArtifactNotFoundError( throw new ArtifactNotFoundError(
`Artifact not found for name: ${artifactName} `Artifact not found for name: ${artifactName}`
Please ensure that your artifact is not expired and the artifact was uploaded using a compatible version of toolkit/upload-artifact.
For more information, visit the GitHub Artifacts FAQ: https://github.com/actions/toolkit/blob/main/packages/artifact/docs/faq.md`
) )
} }
@@ -4,7 +4,6 @@ import {info, debug} from '@actions/core'
import {ArtifactServiceClientJSON} from '../../generated' import {ArtifactServiceClientJSON} from '../../generated'
import {getResultsServiceUrl, getRuntimeToken} from './config' import {getResultsServiceUrl, getRuntimeToken} from './config'
import {getUserAgentString} from './user-agent' import {getUserAgentString} from './user-agent'
import {NetworkError, UsageError} from './errors'
// The twirp http client must implement this interface // The twirp http client must implement this interface
interface Rpc { interface Rpc {
@@ -60,11 +59,11 @@ class ArtifactHttpClient implements Rpc {
'Content-Type': contentType 'Content-Type': contentType
} }
try { try {
const {body} = await this.retryableRequest(async () => const response = await this.retryableRequest(async () =>
this.httpClient.post(url, JSON.stringify(data), headers) this.httpClient.post(url, JSON.stringify(data), headers)
) )
const body = await response.readBody()
return body return JSON.parse(body)
} catch (error) { } catch (error) {
throw new Error(`Failed to ${method}: ${error.message}`) throw new Error(`Failed to ${method}: ${error.message}`)
} }
@@ -72,47 +71,25 @@ class ArtifactHttpClient implements Rpc {
async retryableRequest( async retryableRequest(
operation: () => Promise<HttpClientResponse> operation: () => Promise<HttpClientResponse>
): Promise<{response: HttpClientResponse; body: object}> { ): Promise<HttpClientResponse> {
let attempt = 0 let attempt = 0
let errorMessage = '' let errorMessage = ''
let rawBody = ''
while (attempt < this.maxAttempts) { while (attempt < this.maxAttempts) {
let isRetryable = false let isRetryable = false
try { try {
const response = await operation() const response = await operation()
const statusCode = response.message.statusCode const statusCode = response.message.statusCode
rawBody = await response.readBody() debug(`[Response] ${response.message.statusCode}`)
debug(`[Response] - ${response.message.statusCode}`) debug(JSON.stringify(response.message.headers, null, 2))
debug(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`)
const body = JSON.parse(rawBody)
debug(`Body: ${JSON.stringify(body, null, 2)}`)
if (this.isSuccessStatusCode(statusCode)) { if (this.isSuccessStatusCode(statusCode)) {
return {response, body} return response
} }
isRetryable = this.isRetryableHttpStatusCode(statusCode) isRetryable = this.isRetryableHttpStatusCode(statusCode)
errorMessage = `Failed request: (${statusCode}) ${response.message.statusMessage}` errorMessage = `Failed request: (${statusCode}) ${response.message.statusMessage}`
if (body.msg) {
if (UsageError.isUsageErrorMessage(body.msg)) {
throw new UsageError()
}
errorMessage = `${errorMessage}: ${body.msg}`
}
} catch (error) { } catch (error) {
if (error instanceof SyntaxError) {
debug(`Raw Body: ${rawBody}`)
throw error
}
if (error instanceof UsageError) {
throw error
}
if (NetworkError.isNetworkErrorCode(error?.code)) {
throw new NetworkError(error?.code)
}
isRetryable = true isRetryable = true
errorMessage = error.message errorMessage = error.message
} }
@@ -154,7 +131,8 @@ class ArtifactHttpClient implements Rpc {
HttpCodes.GatewayTimeout, HttpCodes.GatewayTimeout,
HttpCodes.InternalServerError, HttpCodes.InternalServerError,
HttpCodes.ServiceUnavailable, HttpCodes.ServiceUnavailable,
HttpCodes.TooManyRequests HttpCodes.TooManyRequests,
413 // Payload Too Large
] ]
return retryableStatusCodes.includes(statusCode) return retryableStatusCodes.includes(statusCode)
@@ -35,38 +35,3 @@ export class GHESNotSupportedError extends Error {
this.name = 'GHESNotSupportedError' this.name = 'GHESNotSupportedError'
} }
} }
export class NetworkError extends Error {
code: string
constructor(code: string) {
const message = `Unable to make request: ${code}\nIf you are using self-hosted runners, please make sure your runner has access to all GitHub endpoints: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#communication-between-self-hosted-runners-and-github`
super(message)
this.code = code
this.name = 'NetworkError'
}
static isNetworkErrorCode = (code?: string): boolean => {
if (!code) return false
return [
'ECONNRESET',
'ENOTFOUND',
'ETIMEDOUT',
'ECONNREFUSED',
'EHOSTUNREACH'
].includes(code)
}
}
export class UsageError extends Error {
constructor() {
const message = `Artifact storage quota has been hit. Unable to upload any new artifacts. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`
super(message)
this.name = 'UsageError'
}
static isUsageErrorMessage = (msg?: string): boolean => {
if (!msg) return false
return msg.includes('insufficient usage')
}
}
@@ -5,7 +5,6 @@ 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 {NetworkError} from '../shared/errors'
export interface BlobUploadResponse { export interface BlobUploadResponse {
/** /**
@@ -53,20 +52,12 @@ export async function uploadZipToBlobStorage(
core.info('Beginning upload of artifact content to blob storage') core.info('Beginning upload of artifact content to blob storage')
try { await blockBlobClient.uploadStream(
await blockBlobClient.uploadStream( uploadStream,
uploadStream, bufferSize,
bufferSize, maxConcurrency,
maxConcurrency, options
options )
)
} catch (error) {
if (NetworkError.isNetworkErrorCode(error?.code)) {
throw new NetworkError(error?.code)
}
throw error
}
core.info('Finished uploading artifact content to blob storage!') core.info('Finished uploading artifact content to blob storage!')
@@ -40,6 +40,11 @@ export async function uploadArtifact(
) )
} }
const zipUploadStream = await createZipUploadStream(
zipSpecification,
options?.compressionLevel
)
// get the IDs needed for the artifact creation // get the IDs needed for the artifact creation
const backendIds = getBackendIdsFromToken() const backendIds = getBackendIdsFromToken()
@@ -68,11 +73,6 @@ export async function uploadArtifact(
) )
} }
const zipUploadStream = await createZipUploadStream(
zipSpecification,
options?.compressionLevel
)
// Upload zip to blob storage // Upload zip to blob storage
const uploadResult = await uploadZipToBlobStorage( const uploadResult = await uploadZipToBlobStorage(
createArtifactResp.signedUploadUrl, createArtifactResp.signedUploadUrl,
-4
View File
@@ -164,7 +164,3 @@
### 3.2.2 ### 3.2.2
- Add new default cache download method to improve performance and reduce hangs [#1484](https://github.com/actions/toolkit/pull/1484) - Add new default cache download method to improve performance and reduce hangs [#1484](https://github.com/actions/toolkit/pull/1484)
### 3.2.3
- Fixed a bug that mutated path arguments to `getCacheVersion` [#1378](https://github.com/actions/toolkit/pull/1378)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/cache", "name": "@actions/cache",
"version": "3.2.3", "version": "3.2.2",
"preview": true, "preview": true,
"description": "Actions cache lib", "description": "Actions cache lib",
"keywords": [ "keywords": [
+1 -2
View File
@@ -80,8 +80,7 @@ export function getCacheVersion(
compressionMethod?: CompressionMethod, compressionMethod?: CompressionMethod,
enableCrossOsArchive = false enableCrossOsArchive = false
): string { ): string {
// don't pass changes upstream const components = paths
const components = paths.slice()
// Add compression method to cache version to restore // Add compression method to cache version to restore
// compressed cache as per compression method // compressed cache as per compression method
-126
View File
@@ -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})
```