Compare commits

..

1 Commits

Author SHA1 Message Date
Vallie Joseph 123bccb3d9 adding network tests 2023-12-19 06:40:01 +00:00
22 changed files with 186 additions and 1067 deletions
+1 -29
View File
@@ -72,7 +72,7 @@ jobs:
console.log('Successfully blocked second artifact upload')
}
verify:
name: Verify and Delete
name: Verify
runs-on: ubuntu-latest
needs: [upload]
steps:
@@ -164,31 +164,3 @@ jobs:
}
}
}
- name: Delete Artifacts
uses: actions/github-script@v7
with:
script: |
const {default: artifactClient} = require('./packages/artifact/lib/artifact')
const artifactsToDelete = [
'my-artifact-ubuntu-latest',
'my-artifact-windows-latest',
'my-artifact-macos-latest'
]
for (const artifactName of artifactsToDelete) {
const {id} = await artifactClient.deleteArtifact(artifactName)
}
const {artifacts} = await artifactClient.listArtifacts({latest: true})
const foundArtifacts = artifacts.filter(artifact =>
artifactsToDelete.includes(artifact.name)
)
if (foundArtifacts.length !== 0) {
console.log('Unexpected length of found artifacts:', foundArtifacts)
throw new Error(
`Expected 0 artifacts but found ${foundArtifacts.length} artifacts.`
)
}
+1 -1
View File
@@ -8,4 +8,4 @@ module.exports = {
'^.+\\.ts$': 'ts-jest'
},
verbose: true
}
}
+3 -3
View File
@@ -5781,9 +5781,9 @@
}
},
"node_modules/follow-redirects": {
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"dev": true,
"funding": [
{
+1 -1
View File
@@ -13,7 +13,7 @@
"lint": "eslint packages/**/*.ts",
"lint-fix": "eslint packages/**/*.ts --fix",
"new-package": "scripts/create-package",
"test": "jest --testTimeout 60000"
"test": "jest --testTimeout 10000"
},
"devDependencies": {
"@types/jest": "^29.5.4",
+1 -37
View File
@@ -12,7 +12,6 @@ This is the core library that powers the [`@actions/upload-artifact`](https://gi
- [Quick Start](#quick-start)
- [Examples](#examples)
- [Upload and Download](#upload-and-download)
- [Delete an Artifact](#delete-an-artifact)
- [Downloading from other workflow runs or repos](#downloading-from-other-workflow-runs-or-repos)
- [Speeding up large uploads](#speeding-up-large-uploads)
- [Additional Resources](#additional-resources)
@@ -20,7 +19,7 @@ This is the core library that powers the [`@actions/upload-artifact`](https://gi
## v2 - What's New
> [!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.
@@ -107,41 +106,6 @@ const {downloadPath} = await artifact.downloadArtifact(id, {
console.log(`Downloaded artifact ${id} to: ${downloadPath}`)
```
### Delete an Artifact
To delete an artifact, all you need is the name.
```js
const {id} = await artifact.deleteArtifact(
// name of the artifact
'my-artifact'
)
console.log(`Deleted Artifact ID '${id}'`)
```
It also supports options to delete from other repos/runs given a github token with `actions:write` permissions on the target repository is supplied.
```js
const findBy = {
// must have actions:write permission on target repository
token: process.env['GITHUB_TOKEN'],
workflowRunId: 123,
repositoryOwner: 'actions',
repositoryName: 'toolkit'
}
const {id} = await artifact.deleteArtifact(
// name of the artifact
'my-artifact',
// options to find by other repo/owner
{ findBy }
)
console.log(`Deleted Artifact ID '${id}' from ${findBy.repositoryOwner}/ ${findBy.repositoryName}`)
```
### Downloading from other workflow runs or repos
It may be useful to download Artifacts from other workflow runs, or even other repositories. By default, the permissions are scoped so they can only download Artifacts within the current workflow run. To elevate permissions for this scenario, you must specify `options.findBy` to `downloadArtifact`.
+3 -7
View File
@@ -97,11 +97,7 @@
### 2.0.0
- Major release. Supports new Artifact backend for improved speed, reliability and behavior.
- Numerous API changes, [some breaking](./README.md#breaking-changes).
Major release. Supports new Artifact backend for improved speed, reliability and behavior.
Numerous API changes, [some breaking](./README.md#breaking-changes).
- Blog post with more info: TBD
### 2.0.1
- Patch to fix transient request timeouts https://github.com/actions/download-artifact/issues/249
Blog post with more info: TBD
@@ -1,192 +0,0 @@
import * as github from '@actions/github'
import type {RestEndpointMethods} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/method-types'
import type {RequestInterface} from '@octokit/types'
import {
deleteArtifactInternal,
deleteArtifactPublic
} from '../src/internal/delete/delete-artifact'
import * as config from '../src/internal/shared/config'
import {ArtifactServiceClientJSON, Timestamp} from '../src/generated'
import * as util from '../src/internal/shared/util'
import {noopLogs} from './common'
type MockedRequest = jest.MockedFunction<RequestInterface<object>>
type MockedDeleteArtifact = jest.MockedFunction<
RestEndpointMethods['actions']['deleteArtifact']
>
jest.mock('@actions/github', () => ({
getOctokit: jest.fn().mockReturnValue({
request: jest.fn(),
rest: {
actions: {
deleteArtifact: jest.fn()
}
}
})
}))
const fixtures = {
repo: 'toolkit',
owner: 'actions',
token: 'ghp_1234567890',
runId: 123,
backendIds: {
workflowRunBackendId: 'c4d7c21f-ba3f-4ddc-a8c8-6f2f626f8422',
workflowJobRunBackendId: '760803a1-f890-4d25-9a6e-a3fc01a0c7cf'
},
artifacts: [
{
id: 1,
name: 'my-artifact',
size: 456,
createdAt: new Date('2023-12-01')
},
{
id: 2,
name: 'my-artifact',
size: 456,
createdAt: new Date('2023-12-02')
}
]
}
describe('delete-artifact', () => {
beforeAll(() => {
noopLogs()
})
describe('public', () => {
it('should delete an artifact', async () => {
const mockRequest = github.getOctokit(fixtures.token)
.request as MockedRequest
mockRequest.mockResolvedValueOnce({
status: 200,
headers: {},
url: '',
data: {
artifacts: [
{
name: fixtures.artifacts[0].name,
id: fixtures.artifacts[0].id,
size_in_bytes: fixtures.artifacts[0].size,
created_at: fixtures.artifacts[0].createdAt.toISOString()
}
]
}
})
const mockDeleteArtifact = github.getOctokit(fixtures.token).rest.actions
.deleteArtifact as MockedDeleteArtifact
mockDeleteArtifact.mockResolvedValueOnce({
status: 204,
headers: {},
url: '',
data: null as never
})
const response = await deleteArtifactPublic(
fixtures.artifacts[0].name,
fixtures.runId,
fixtures.owner,
fixtures.repo,
fixtures.token
)
expect(response).toEqual({
id: fixtures.artifacts[0].id
})
})
it('should fail if non-200 response', async () => {
const mockRequest = github.getOctokit(fixtures.token)
.request as MockedRequest
mockRequest.mockResolvedValueOnce({
status: 200,
headers: {},
url: '',
data: {
artifacts: [
{
name: fixtures.artifacts[0].name,
id: fixtures.artifacts[0].id,
size_in_bytes: fixtures.artifacts[0].size,
created_at: fixtures.artifacts[0].createdAt.toISOString()
}
]
}
})
const mockDeleteArtifact = github.getOctokit(fixtures.token).rest.actions
.deleteArtifact as MockedDeleteArtifact
mockDeleteArtifact.mockRejectedValue(new Error('boom'))
await expect(
deleteArtifactPublic(
fixtures.artifacts[0].name,
fixtures.runId,
fixtures.owner,
fixtures.repo,
fixtures.token
)
).rejects.toThrow('boom')
})
})
describe('internal', () => {
beforeEach(() => {
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
jest
.spyOn(util, 'getBackendIdsFromToken')
.mockReturnValue(fixtures.backendIds)
jest
.spyOn(config, 'getResultsServiceUrl')
.mockReturnValue('https://results.local')
})
it('should delete an artifact', async () => {
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'ListArtifacts')
.mockResolvedValue({
artifacts: fixtures.artifacts.map(artifact => ({
...fixtures.backendIds,
databaseId: artifact.id.toString(),
name: artifact.name,
size: artifact.size.toString(),
createdAt: Timestamp.fromDate(artifact.createdAt)
}))
})
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'DeleteArtifact')
.mockResolvedValue({
ok: true,
artifactId: fixtures.artifacts[0].id.toString()
})
const response = await deleteArtifactInternal(fixtures.artifacts[0].name)
expect(response).toEqual({
id: fixtures.artifacts[0].id
})
})
it('should fail if non-200 response', async () => {
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'ListArtifacts')
.mockResolvedValue({
artifacts: fixtures.artifacts.map(artifact => ({
...fixtures.backendIds,
databaseId: artifact.id.toString(),
name: artifact.name,
size: artifact.size.toString(),
createdAt: Timestamp.fromDate(artifact.createdAt)
}))
})
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'DeleteArtifact')
.mockRejectedValue(new Error('boom'))
await expect(
deleteArtifactInternal(fixtures.artifacts[0].id)
).rejects.toThrow('boom')
})
})
})
@@ -9,8 +9,7 @@ import archiver from 'archiver'
import {
downloadArtifactInternal,
downloadArtifactPublic,
streamExtractExternal
downloadArtifactPublic
} from '../src/internal/download/download-artifact'
import {getUserAgentString} from '../src/internal/shared/user-agent'
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 () => {
// 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 () => {
it('should fail if blob storage response is non-200', async () => {
const downloadArtifactMock = github.getOctokit(fixtures.token).rest
.actions.downloadArtifact as MockedDownloadArtifact
downloadArtifactMock.mockResolvedValueOnce({
@@ -328,60 +290,7 @@ describe('download-artifact', () => {
expect(mockGetArtifactFailure).toHaveBeenCalledWith(
fixtures.blobStorageUrl
)
expect(mockGetArtifactFailure).toHaveBeenCalledTimes(5)
}, 38000)
it('should retry if blob storage response is non-200 and then succeed with a 200', async () => {
const downloadArtifactMock = github.getOctokit(fixtures.token).rest
.actions.downloadArtifact as MockedDownloadArtifact
downloadArtifactMock.mockResolvedValueOnce({
headers: {
location: fixtures.blobStorageUrl
},
status: 302,
url: '',
data: Buffer.from('')
})
const mockGetArtifact = jest
.fn(mockGetArtifactSuccess)
.mockImplementationOnce(mockGetArtifactFailure)
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetArtifact
}
}
)
const response = await downloadArtifactPublic(
fixtures.artifactID,
fixtures.repositoryOwner,
fixtures.repositoryName,
fixtures.token
)
expect(downloadArtifactMock).toHaveBeenCalledWith({
owner: fixtures.repositoryOwner,
repo: fixtures.repositoryName,
artifact_id: fixtures.artifactID,
archive_format: 'zip',
request: {
redirect: 'manual'
}
})
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
expect(mockGetArtifactFailure).toHaveBeenCalledWith(
fixtures.blobStorageUrl
)
expect(mockGetArtifactFailure).toHaveBeenCalledTimes(1)
expect(mockGetArtifactSuccess).toHaveBeenCalledWith(
fixtures.blobStorageUrl
)
expect(mockGetArtifactSuccess).toHaveBeenCalledTimes(1)
expect(response.downloadPath).toBe(fixtures.workspaceDir)
}, 28000)
})
})
describe('internal', () => {
+83 -16
View File
@@ -1,12 +1,12 @@
{
"name": "@actions/artifact",
"version": "2.0.1",
"version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@actions/artifact",
"version": "2.0.1",
"version": "2.0.0",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.0",
@@ -21,6 +21,7 @@
"archiver": "^5.3.1",
"crypto": "^1.0.1",
"jwt-decode": "^3.1.2",
"nock": "^13.4.0",
"twirp-ts": "^2.5.0",
"unzip-stream": "^0.3.1"
},
@@ -108,6 +109,25 @@
"node": ">=14.0.0"
}
},
"node_modules/@azure/core-http/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/@azure/core-lro": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.4.tgz",
@@ -332,6 +352,25 @@
"once": "^1.4.0"
}
},
"node_modules/@octokit/request/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/@octokit/types": {
"version": "6.41.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
@@ -756,6 +795,22 @@
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -911,6 +966,11 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
},
"node_modules/jsonc-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
@@ -1065,6 +1125,11 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
@@ -1080,23 +1145,17 @@
"tslib": "^2.0.3"
}
},
"node_modules/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
"node_modules/nock": {
"version": "13.4.0",
"resolved": "https://registry.npmjs.org/nock/-/nock-13.4.0.tgz",
"integrity": "sha512-W8NVHjO/LCTNA64yxAPHV/K47LpGYcVzgKd3Q0n6owhwvD0Dgoterc25R4rnZbckJEb6Loxz1f5QMuJpJnbSyQ==",
"dependencies": {
"whatwg-url": "^5.0.0"
"debug": "^4.1.0",
"json-stringify-safe": "^5.0.1",
"propagate": "^2.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
"node": ">= 10.13"
}
},
"node_modules/normalize-path": {
@@ -1164,6 +1223,14 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/propagate": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
"engines": {
"node": ">= 8"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/artifact",
"version": "2.0.1",
"version": "2.0.0",
"preview": true,
"description": "Actions artifact lib",
"keywords": [
@@ -52,6 +52,7 @@
"archiver": "^5.3.1",
"crypto": "^1.0.1",
"jwt-decode": "^3.1.2",
"nock": "^13.4.0",
"twirp-ts": "^2.5.0",
"unzip-stream": "^0.3.1"
},
@@ -62,4 +63,4 @@
"typedoc-plugin-markdown": "^3.17.1",
"typescript": "^5.2.2"
}
}
}
@@ -196,36 +196,6 @@ export interface GetSignedArtifactURLResponse {
*/
signedUrl: string;
}
/**
* @generated from protobuf message github.actions.results.api.v1.DeleteArtifactRequest
*/
export interface DeleteArtifactRequest {
/**
* @generated from protobuf field: string workflow_run_backend_id = 1;
*/
workflowRunBackendId: string;
/**
* @generated from protobuf field: string workflow_job_run_backend_id = 2;
*/
workflowJobRunBackendId: string;
/**
* @generated from protobuf field: string name = 3;
*/
name: string;
}
/**
* @generated from protobuf message github.actions.results.api.v1.DeleteArtifactResponse
*/
export interface DeleteArtifactResponse {
/**
* @generated from protobuf field: bool ok = 1;
*/
ok: boolean;
/**
* @generated from protobuf field: int64 artifact_id = 2;
*/
artifactId: string;
}
// @generated message type with reflection information, may provide speed optimized methods
class CreateArtifactRequest$Type extends MessageType<CreateArtifactRequest> {
constructor() {
@@ -789,121 +759,6 @@ class GetSignedArtifactURLResponse$Type extends MessageType<GetSignedArtifactURL
* @generated MessageType for protobuf message github.actions.results.api.v1.GetSignedArtifactURLResponse
*/
export const GetSignedArtifactURLResponse = new GetSignedArtifactURLResponse$Type();
// @generated message type with reflection information, may provide speed optimized methods
class DeleteArtifactRequest$Type extends MessageType<DeleteArtifactRequest> {
constructor() {
super("github.actions.results.api.v1.DeleteArtifactRequest", [
{ no: 1, name: "workflow_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "workflow_job_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
]);
}
create(value?: PartialMessage<DeleteArtifactRequest>): DeleteArtifactRequest {
const message = { workflowRunBackendId: "", workflowJobRunBackendId: "", name: "" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<DeleteArtifactRequest>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeleteArtifactRequest): DeleteArtifactRequest {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string workflow_run_backend_id */ 1:
message.workflowRunBackendId = reader.string();
break;
case /* string workflow_job_run_backend_id */ 2:
message.workflowJobRunBackendId = reader.string();
break;
case /* string name */ 3:
message.name = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: DeleteArtifactRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* string workflow_run_backend_id = 1; */
if (message.workflowRunBackendId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.workflowRunBackendId);
/* string workflow_job_run_backend_id = 2; */
if (message.workflowJobRunBackendId !== "")
writer.tag(2, WireType.LengthDelimited).string(message.workflowJobRunBackendId);
/* string name = 3; */
if (message.name !== "")
writer.tag(3, WireType.LengthDelimited).string(message.name);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message github.actions.results.api.v1.DeleteArtifactRequest
*/
export const DeleteArtifactRequest = new DeleteArtifactRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class DeleteArtifactResponse$Type extends MessageType<DeleteArtifactResponse> {
constructor() {
super("github.actions.results.api.v1.DeleteArtifactResponse", [
{ no: 1, name: "ok", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
{ no: 2, name: "artifact_id", kind: "scalar", T: 3 /*ScalarType.INT64*/ }
]);
}
create(value?: PartialMessage<DeleteArtifactResponse>): DeleteArtifactResponse {
const message = { ok: false, artifactId: "0" };
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
if (value !== undefined)
reflectionMergePartial<DeleteArtifactResponse>(this, message, value);
return message;
}
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DeleteArtifactResponse): DeleteArtifactResponse {
let message = target ?? this.create(), end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* bool ok */ 1:
message.ok = reader.bool();
break;
case /* int64 artifact_id */ 2:
message.artifactId = reader.int64().toString();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
}
}
return message;
}
internalBinaryWrite(message: DeleteArtifactResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
/* bool ok = 1; */
if (message.ok !== false)
writer.tag(1, WireType.Varint).bool(message.ok);
/* int64 artifact_id = 2; */
if (message.artifactId !== "0")
writer.tag(2, WireType.Varint).int64(message.artifactId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
return writer;
}
}
/**
* @generated MessageType for protobuf message github.actions.results.api.v1.DeleteArtifactResponse
*/
export const DeleteArtifactResponse = new DeleteArtifactResponse$Type();
/**
* @generated ServiceType for protobuf service github.actions.results.api.v1.ArtifactService
*/
@@ -911,6 +766,5 @@ export const ArtifactService = new ServiceType("github.actions.results.api.v1.Ar
{ name: "CreateArtifact", options: {}, I: CreateArtifactRequest, O: CreateArtifactResponse },
{ name: "FinalizeArtifact", options: {}, I: FinalizeArtifactRequest, O: FinalizeArtifactResponse },
{ name: "ListArtifacts", options: {}, I: ListArtifactsRequest, O: ListArtifactsResponse },
{ name: "GetSignedArtifactURL", options: {}, I: GetSignedArtifactURLRequest, O: GetSignedArtifactURLResponse },
{ name: "DeleteArtifact", options: {}, I: DeleteArtifactRequest, O: DeleteArtifactResponse }
{ name: "GetSignedArtifactURL", options: {}, I: GetSignedArtifactURLRequest, O: GetSignedArtifactURLResponse }
]);
@@ -17,8 +17,6 @@ import {
ListArtifactsResponse,
GetSignedArtifactURLRequest,
GetSignedArtifactURLResponse,
DeleteArtifactRequest,
DeleteArtifactResponse,
} from "./artifact";
//==================================//
@@ -45,9 +43,6 @@ export interface ArtifactServiceClient {
GetSignedArtifactURL(
request: GetSignedArtifactURLRequest
): Promise<GetSignedArtifactURLResponse>;
DeleteArtifact(
request: DeleteArtifactRequest
): Promise<DeleteArtifactResponse>;
}
export class ArtifactServiceClientJSON implements ArtifactServiceClient {
@@ -58,7 +53,6 @@ export class ArtifactServiceClientJSON implements ArtifactServiceClient {
this.FinalizeArtifact.bind(this);
this.ListArtifacts.bind(this);
this.GetSignedArtifactURL.bind(this);
this.DeleteArtifact.bind(this);
}
CreateArtifact(
request: CreateArtifactRequest
@@ -135,26 +129,6 @@ export class ArtifactServiceClientJSON implements ArtifactServiceClient {
})
);
}
DeleteArtifact(
request: DeleteArtifactRequest
): Promise<DeleteArtifactResponse> {
const data = DeleteArtifactRequest.toJson(request, {
useProtoFieldName: true,
emitDefaultValues: false,
});
const promise = this.rpc.request(
"github.actions.results.api.v1.ArtifactService",
"DeleteArtifact",
"application/json",
data as object
);
return promise.then((data) =>
DeleteArtifactResponse.fromJson(data as any, {
ignoreUnknownFields: true,
})
);
}
}
export class ArtifactServiceClientProtobuf implements ArtifactServiceClient {
@@ -165,7 +139,6 @@ export class ArtifactServiceClientProtobuf implements ArtifactServiceClient {
this.FinalizeArtifact.bind(this);
this.ListArtifacts.bind(this);
this.GetSignedArtifactURL.bind(this);
this.DeleteArtifact.bind(this);
}
CreateArtifact(
request: CreateArtifactRequest
@@ -224,21 +197,6 @@ export class ArtifactServiceClientProtobuf implements ArtifactServiceClient {
GetSignedArtifactURLResponse.fromBinary(data as Uint8Array)
);
}
DeleteArtifact(
request: DeleteArtifactRequest
): Promise<DeleteArtifactResponse> {
const data = DeleteArtifactRequest.toBinary(request);
const promise = this.rpc.request(
"github.actions.results.api.v1.ArtifactService",
"DeleteArtifact",
"application/protobuf",
data
);
return promise.then((data) =>
DeleteArtifactResponse.fromBinary(data as Uint8Array)
);
}
}
//==================================//
@@ -262,10 +220,6 @@ export interface ArtifactServiceTwirp<T extends TwirpContext = TwirpContext> {
ctx: T,
request: GetSignedArtifactURLRequest
): Promise<GetSignedArtifactURLResponse>;
DeleteArtifact(
ctx: T,
request: DeleteArtifactRequest
): Promise<DeleteArtifactResponse>;
}
export enum ArtifactServiceMethod {
@@ -273,7 +227,6 @@ export enum ArtifactServiceMethod {
FinalizeArtifact = "FinalizeArtifact",
ListArtifacts = "ListArtifacts",
GetSignedArtifactURL = "GetSignedArtifactURL",
DeleteArtifact = "DeleteArtifact",
}
export const ArtifactServiceMethodList = [
@@ -281,7 +234,6 @@ export const ArtifactServiceMethodList = [
ArtifactServiceMethod.FinalizeArtifact,
ArtifactServiceMethod.ListArtifacts,
ArtifactServiceMethod.GetSignedArtifactURL,
ArtifactServiceMethod.DeleteArtifact,
];
export function createArtifactServiceServer<
@@ -381,26 +333,6 @@ function matchArtifactServiceRoute<T extends TwirpContext = TwirpContext>(
interceptors
);
};
case "DeleteArtifact":
return async (
ctx: T,
service: ArtifactServiceTwirp,
data: Buffer,
interceptors?: Interceptor<
T,
DeleteArtifactRequest,
DeleteArtifactResponse
>[]
) => {
ctx = { ...ctx, methodName: "DeleteArtifact" };
await events.onMatch(ctx);
return handleArtifactServiceDeleteArtifactRequest(
ctx,
service,
data,
interceptors
);
};
default:
events.onNotFound();
const msg = `no handler found`;
@@ -531,35 +463,6 @@ function handleArtifactServiceGetSignedArtifactURLRequest<
throw new TwirpError(TwirpErrorCode.BadRoute, msg);
}
}
function handleArtifactServiceDeleteArtifactRequest<
T extends TwirpContext = TwirpContext
>(
ctx: T,
service: ArtifactServiceTwirp,
data: Buffer,
interceptors?: Interceptor<T, DeleteArtifactRequest, DeleteArtifactResponse>[]
): Promise<string | Uint8Array> {
switch (ctx.contentType) {
case TwirpContentType.JSON:
return handleArtifactServiceDeleteArtifactJSON<T>(
ctx,
service,
data,
interceptors
);
case TwirpContentType.Protobuf:
return handleArtifactServiceDeleteArtifactProtobuf<T>(
ctx,
service,
data,
interceptors
);
default:
const msg = "unexpected Content-Type";
throw new TwirpError(TwirpErrorCode.BadRoute, msg);
}
}
async function handleArtifactServiceCreateArtifactJSON<
T extends TwirpContext = TwirpContext
>(
@@ -743,50 +646,6 @@ async function handleArtifactServiceGetSignedArtifactURLJSON<
}) as string
);
}
async function handleArtifactServiceDeleteArtifactJSON<
T extends TwirpContext = TwirpContext
>(
ctx: T,
service: ArtifactServiceTwirp,
data: Buffer,
interceptors?: Interceptor<T, DeleteArtifactRequest, DeleteArtifactResponse>[]
) {
let request: DeleteArtifactRequest;
let response: DeleteArtifactResponse;
try {
const body = JSON.parse(data.toString() || "{}");
request = DeleteArtifactRequest.fromJson(body, {
ignoreUnknownFields: true,
});
} catch (e) {
if (e instanceof Error) {
const msg = "the json request could not be decoded";
throw new TwirpError(TwirpErrorCode.Malformed, msg).withCause(e, true);
}
}
if (interceptors && interceptors.length > 0) {
const interceptor = chainInterceptors(...interceptors) as Interceptor<
T,
DeleteArtifactRequest,
DeleteArtifactResponse
>;
response = await interceptor(ctx, request!, (ctx, inputReq) => {
return service.DeleteArtifact(ctx, inputReq);
});
} else {
response = await service.DeleteArtifact(ctx, request!);
}
return JSON.stringify(
DeleteArtifactResponse.toJson(response, {
useProtoFieldName: true,
emitDefaultValues: false,
}) as string
);
}
async function handleArtifactServiceCreateArtifactProtobuf<
T extends TwirpContext = TwirpContext
>(
@@ -938,39 +797,3 @@ async function handleArtifactServiceGetSignedArtifactURLProtobuf<
return Buffer.from(GetSignedArtifactURLResponse.toBinary(response));
}
async function handleArtifactServiceDeleteArtifactProtobuf<
T extends TwirpContext = TwirpContext
>(
ctx: T,
service: ArtifactServiceTwirp,
data: Buffer,
interceptors?: Interceptor<T, DeleteArtifactRequest, DeleteArtifactResponse>[]
) {
let request: DeleteArtifactRequest;
let response: DeleteArtifactResponse;
try {
request = DeleteArtifactRequest.fromBinary(data);
} catch (e) {
if (e instanceof Error) {
const msg = "the protobuf request could not be decoded";
throw new TwirpError(TwirpErrorCode.Malformed, msg).withCause(e, true);
}
}
if (interceptors && interceptors.length > 0) {
const interceptor = chainInterceptors(...interceptors) as Interceptor<
T,
DeleteArtifactRequest,
DeleteArtifactResponse
>;
response = await interceptor(ctx, request!, (ctx, inputReq) => {
return service.DeleteArtifact(ctx, inputReq);
});
} else {
response = await service.DeleteArtifact(ctx, request!);
}
return Buffer.from(DeleteArtifactResponse.toBinary(response));
}
+2 -58
View File
@@ -8,18 +8,13 @@ import {
ListArtifactsOptions,
ListArtifactsResponse,
DownloadArtifactResponse,
FindOptions,
DeleteArtifactResponse
FindOptions
} from './shared/interfaces'
import {uploadArtifact} from './upload/upload-artifact'
import {
downloadArtifactPublic,
downloadArtifactInternal
} from './download/download-artifact'
import {
deleteArtifactPublic,
deleteArtifactInternal
} from './delete/delete-artifact'
import {getArtifactPublic, getArtifactInternal} from './find/get-artifact'
import {listArtifactsPublic, listArtifactsInternal} from './find/list-artifacts'
import {GHESNotSupportedError} from './shared/errors'
@@ -82,7 +77,7 @@ export interface ArtifactClient {
*
* If `options.findBy` is specified, this will use the public Download Artifact API https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28#download-an-artifact
*
* @param artifactId The id of the artifact to download
* @param artifactId The name of the artifact to download
* @param options Extra options that allow for the customization of the download behavior
* @returns single DownloadArtifactResponse object
*/
@@ -90,20 +85,6 @@ export interface ArtifactClient {
artifactId: number,
options?: DownloadArtifactOptions & FindOptions
): Promise<DownloadArtifactResponse>
/**
* Delete an Artifact
*
* If `options.findBy` is specified, this will use the public Delete Artifact API https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28#delete-an-artifact
*
* @param artifactName The name of the artifact to delete
* @param options Extra options that allow for the customization of the delete behavior
* @returns single DeleteArtifactResponse object
*/
deleteArtifact(
artifactName: string,
options?: FindOptions
): Promise<DeleteArtifactResponse>
}
/**
@@ -244,41 +225,4 @@ If the error persists, please check whether Actions and API requests are operati
throw error
}
}
async deleteArtifact(
artifactName: string,
options?: FindOptions
): Promise<DeleteArtifactResponse> {
try {
if (isGhes()) {
throw new GHESNotSupportedError()
}
if (options?.findBy) {
const {
findBy: {repositoryOwner, repositoryName, workflowRunId, token}
} = options
return deleteArtifactPublic(
artifactName,
workflowRunId,
repositoryOwner,
repositoryName,
token
)
}
return deleteArtifactInternal(artifactName)
} catch (error) {
warning(
`Delete Artifact failed with error: ${error}.
Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information.
If the error persists, please check whether Actions and API requests are operating normally at [https://githubstatus.com](https://www.githubstatus.com).`
)
throw error
}
}
}
@@ -1,109 +0,0 @@
import {info, debug} from '@actions/core'
import {getOctokit} from '@actions/github'
import {DeleteArtifactResponse} from '../shared/interfaces'
import {getUserAgentString} from '../shared/user-agent'
import {getRetryOptions} from '../find/retry-options'
import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils'
import {requestLog} from '@octokit/plugin-request-log'
import {retry} from '@octokit/plugin-retry'
import {OctokitOptions} from '@octokit/core/dist-types/types'
import {internalArtifactTwirpClient} from '../shared/artifact-twirp-client'
import {getBackendIdsFromToken} from '../shared/util'
import {
DeleteArtifactRequest,
ListArtifactsRequest,
StringValue
} from '../../generated'
import {getArtifactPublic} from '../find/get-artifact'
import {ArtifactNotFoundError, InvalidResponseError} from '../shared/errors'
export async function deleteArtifactPublic(
artifactName: string,
workflowRunId: number,
repositoryOwner: string,
repositoryName: string,
token: string
): Promise<DeleteArtifactResponse> {
const [retryOpts, requestOpts] = getRetryOptions(defaultGitHubOptions)
const opts: OctokitOptions = {
log: undefined,
userAgent: getUserAgentString(),
previews: undefined,
retry: retryOpts,
request: requestOpts
}
const github = getOctokit(token, opts, retry, requestLog)
const getArtifactResp = await getArtifactPublic(
artifactName,
workflowRunId,
repositoryOwner,
repositoryName,
token
)
const deleteArtifactResp = await github.rest.actions.deleteArtifact({
owner: repositoryOwner,
repo: repositoryName,
artifact_id: getArtifactResp.artifact.id
})
if (deleteArtifactResp.status !== 204) {
throw new InvalidResponseError(
`Invalid response from GitHub API: ${deleteArtifactResp.status} (${deleteArtifactResp?.headers?.['x-github-request-id']})`
)
}
return {
id: getArtifactResp.artifact.id
}
}
export async function deleteArtifactInternal(
artifactName
): Promise<DeleteArtifactResponse> {
const artifactClient = internalArtifactTwirpClient()
const {workflowRunBackendId, workflowJobRunBackendId} =
getBackendIdsFromToken()
const listReq: ListArtifactsRequest = {
workflowRunBackendId,
workflowJobRunBackendId,
nameFilter: StringValue.create({value: artifactName})
}
const listRes = await artifactClient.ListArtifacts(listReq)
if (listRes.artifacts.length === 0) {
throw new ArtifactNotFoundError(
`Artifact not found for name: ${artifactName}`
)
}
let artifact = listRes.artifacts[0]
if (listRes.artifacts.length > 1) {
artifact = listRes.artifacts.sort(
(a, b) => Number(b.databaseId) - Number(a.databaseId)
)[0]
debug(
`More than one artifact found for a single name, returning newest (id: ${artifact.databaseId})`
)
}
const req: DeleteArtifactRequest = {
workflowRunBackendId: artifact.workflowRunBackendId,
workflowJobRunBackendId: artifact.workflowJobRunBackendId,
name: artifact.name
}
const res = await artifactClient.DeleteArtifact(req)
info(`Artifact '${artifactName}' (ID: ${res.artifactId}) deleted`)
return {
id: Number(res.artifactId)
}
}
@@ -38,65 +38,20 @@ async function exists(path: string): Promise<boolean> {
}
async function streamExtract(url: string, directory: string): Promise<void> {
let retryCount = 0
while (retryCount < 5) {
try {
await streamExtractExternal(url, directory)
return
} catch (error) {
retryCount++
core.debug(
`Failed to download artifact after ${retryCount} retries due to ${error.message}. Retrying in 5 seconds...`
)
// wait 5 seconds before retrying
await new Promise(resolve => setTimeout(resolve, 5000))
}
}
throw new Error(`Artifact download failed after ${retryCount} retries.`)
}
export async function streamExtractExternal(
url: string,
directory: string
): Promise<void> {
const client = new httpClient.HttpClient(getUserAgentString())
const response = await client.get(url)
if (response.message.statusCode !== 200) {
throw new Error(
`Unexpected HTTP response from blob storage: ${response.message.statusCode} ${response.message.statusMessage}`
)
}
const timeout = 30 * 1000 // 30 seconds
return new Promise((resolve, reject) => {
const timerFn = (): void => {
response.message.destroy(
new Error(`Blob storage chunk did not respond in ${timeout}ms`)
)
}
const timer = setTimeout(timerFn, timeout)
response.message
.on('data', () => {
timer.refresh()
})
.on('error', (error: Error) => {
core.debug(
`response.message: Artifact download failed: ${error.message}`
)
clearTimeout(timer)
reject(error)
})
.pipe(unzip.Extract({path: directory}))
.on('close', () => {
clearTimeout(timer)
resolve()
})
.on('error', (error: Error) => {
reject(error)
})
.on('close', resolve)
.on('error', reject)
})
}
@@ -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.
*/
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
}
/**
@@ -147,13 +162,3 @@ export interface FindOptions {
repositoryName: string
}
}
/**
* Response from the server when deleting an artifact
*/
export interface DeleteArtifactResponse {
/**
* The id of the artifact that was deleted
*/
id: number
}
@@ -5,8 +5,17 @@ import {getUploadChunkSize, getConcurrency} from '../shared/config'
import * as core from '@actions/core'
import * as crypto from 'crypto'
import * as stream from 'stream'
import nock from 'nock'
import {NetworkError} from '../shared/errors'
export const DEFAULT_ERROR_NUMBER = 7
export const ERROR_TYPES = [
'fetchError',
'abortError',
'securityError',
'notAllowedError',
'quotaExceededError'
]
export interface BlobUploadResponse {
/**
* The total reported upload size in bytes. Empty if the upload failed
@@ -18,10 +27,58 @@ export interface BlobUploadResponse {
*/
sha256Hash?: string
}
export async function sendSimulatedError(
simulatedError: number,
authenticatedUploadURL: string
): Promise<void> {
switch (simulatedError) {
case 0: {
nock(authenticatedUploadURL).get('/').replyWithError({
code: 'ECONNRESET',
message: 'socket hang up'
})
break
}
case 1: {
const controller = new AbortController()
controller.abort()
break
}
case 2: {
nock(authenticatedUploadURL).get('/').replyWithError({
code: 'ETIMEDOUT'
})
break
}
case 3: {
nock(authenticatedUploadURL).get('/').reply(403)
break
}
case 4: {
nock(authenticatedUploadURL).get('/').reply(405)
break
}
case 5: {
nock(authenticatedUploadURL).get('/').reply(429)
break
}
case 6: {
const rand = Math.floor(Math.random() * ERROR_TYPES.length)
sendSimulatedError(rand, authenticatedUploadURL)
break
}
case 7: {
core.info('no error selected')
break
}
default:
core.error('something went wrong')
}
}
export async function uploadZipToBlobStorage(
authenticatedUploadURL: string,
zipUploadStream: ZipUploadStream
zipUploadStream: ZipUploadStream,
simulatedError: number = DEFAULT_ERROR_NUMBER
): Promise<BlobUploadResponse> {
let uploadByteCount = 0
@@ -36,6 +93,9 @@ export async function uploadZipToBlobStorage(
const uploadCallback = (progress: TransferProgressEvent): void => {
core.info(`Uploaded bytes ${progress.loadedBytes}`)
if (progress.loadedBytes > 1) {
sendSimulatedError(simulatedError, authenticatedUploadURL)
}
uploadByteCount = progress.loadedBytes
}
@@ -76,7 +76,8 @@ export async function uploadArtifact(
// Upload zip to blob storage
const uploadResult = await uploadZipToBlobStorage(
createArtifactResp.signedUploadUrl,
zipUploadStream
zipUploadStream,
options?.simulateError
)
// finalize the artifact
-4
View File
@@ -164,7 +164,3 @@
### 3.2.2
- 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",
"version": "3.2.3",
"version": "3.2.2",
"preview": true,
"description": "Actions cache lib",
"keywords": [
+1 -2
View File
@@ -80,8 +80,7 @@ export function getCacheVersion(
compressionMethod?: CompressionMethod,
enableCrossOsArchive = false
): string {
// don't pass changes upstream
const components = paths.slice()
const components = paths
// Add compression method to cache version to restore
// compressed cache as per compression method
-126
View File
@@ -358,129 +358,3 @@ const {
version, // 10.0.22621
} = 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})
```