Compare commits

..

40 Commits

Author SHA1 Message Date
Rob Herley 7c27528ab4 Update RELEASES.md
Updates release notes for @actions/cache v3.2.3
2024-01-10 17:32:52 -05:00
Rob Herley 82e8bc69b8 Merge pull request #1622 from actions/robherley/bump-cache-version
Update cache npm package version
2024-01-10 17:29:16 -05:00
Rob Herley b9079670eb Update cache npm package version 2024-01-10 17:05:13 -05:00
Rob Herley cab491a426 Merge pull request #1378 from MSP-Greg/00-cache-paths-dup
cache - getCacheVersion - dup paths array
2024-01-10 17:01:51 -05:00
Vallie Joseph 0389dcd5e4 updating release notes (#1620) 2024-01-10 10:43:38 -05:00
Ryan Troost 64b2775394 Merge pull request #1613 from actions/srryan/download-v4-client-blob
Update `http.client` to retry transient network hang ups
2024-01-09 16:01:39 -05:00
Vallie Joseph 439cd9b37e appeasing linter 2024-01-09 19:47:25 +00:00
Vallie Joseph c1ded1dc4d appeasing linter 2024-01-09 19:47:02 +00:00
Vallie Joseph f37c445bc5 reverting jest 2024-01-09 19:46:17 +00:00
Vallie Joseph e95bcfe359 Update jest.config.js 2024-01-09 14:44:29 -05:00
Vallie Joseph 7549d1b218 removing info logs 2024-01-09 19:42:04 +00:00
Vallie Joseph 2124ef2413 cleaning up logs 2024-01-09 19:36:26 +00:00
Vallie Joseph d617670abc updating timer; removing logs 2024-01-09 19:23:57 +00:00
Vallie Joseph 47157e5ade fixing true 2024-01-09 19:05:11 +00:00
Vallie Joseph 8a6aae0a16 updating global timeout 2024-01-09 19:03:41 +00:00
Vallie Joseph 58ec2bdcc9 increase timeout 2024-01-09 18:55:50 +00:00
Vallie Joseph e19b629130 increasing timeout 2024-01-09 18:45:26 +00:00
Vallie Joseph d63a8c4d3f updating package-json 2024-01-09 17:13:35 +00:00
Vallie Joseph 67d2d582dc adding delayed response to message body http mock 2024-01-09 16:44:12 +00:00
Vallie Joseph 9d70b8a9fb testing reject after timeout 2024-01-08 15:20:05 +00:00
Vallie Joseph 7f47ffaee2 committing v1 2023-12-22 03:51:47 +00:00
Vallie Joseph 98e1a813db testing ci 2023-12-21 20:22:20 +00:00
Vallie Joseph 0d39975814 updating test with blob timeouts 2023-12-21 18:31:01 +00:00
Vallie Joseph f482643a6e updating timeout for retries 2023-12-21 15:10:01 +00:00
bethanyj28 ff2c524611 lint and format 2023-12-21 09:25:34 -05:00
srryan ecb4df89bf remove the exit 2023-12-20 18:23:47 -05:00
srryan 03319fcffa client fixes for retries + logging 2023-12-20 18:08:00 -05:00
srryan c33724abbd update to http client 2023-12-20 15:45:19 -05:00
Rob Herley d6f3ee93b8 reject don't throw 2023-12-20 14:37:13 -05:00
Rob Herley 34a411f3c0 add timeout in between data chunks 2023-12-20 13:59:31 -05:00
Rob Herley 2d6ba67518 retry the promise 2023-12-20 13:11:04 -05:00
Yukai Chou 5430c5d848 fix typo (#1611) 2023-12-20 03:16:52 -05:00
James Renaud bc68ce94ea chore(docs): add missing job summary documentation (#1574)
Co-authored-by: Konrad Pabjan <konradpabjan@github.com>
2023-12-20 03:12:17 -05:00
srryan 78ed49ff88 update error handling abort 2023-12-19 12:46:58 -05:00
srryan c119fcd773 update optional settings for blob client 2023-12-19 12:02:10 -05:00
srryan 73babeabef add explicit options 2023-12-19 11:49:39 -05:00
Vallie Joseph bf93b54558 adding logger for blob client and response 2023-12-18 23:09:10 +00:00
srryan 0c0770ce57 cleanup 2023-12-18 17:52:55 -05:00
srryan 571bf222ee update to use blob client over http client 2023-12-18 17:11:14 -05:00
MSP-Greg 0747ab3577 cache - getCacheVersion - dup paths array 2023-03-20 18:29:46 -05:00
16 changed files with 309 additions and 182 deletions
+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.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"version": "1.15.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
"integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
"dev": true,
"funding": [
{
+1 -1
View File
@@ -13,7 +13,7 @@
"lint": "eslint packages/**/*.ts",
"lint-fix": "eslint packages/**/*.ts --fix",
"new-package": "scripts/create-package",
"test": "jest --testTimeout 10000"
"test": "jest --testTimeout 60000"
},
"devDependencies": {
"@types/jest": "^29.5.4",
+1 -1
View File
@@ -19,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+ 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+, 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).
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.
+7 -3
View File
@@ -97,7 +97,11 @@
### 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
- Blog post with more info: TBD
### 2.0.1
- Patch to fix transient request timeouts https://github.com/actions/download-artifact/issues/249
@@ -9,7 +9,8 @@ import archiver from 'archiver'
import {
downloadArtifactInternal,
downloadArtifactPublic
downloadArtifactPublic,
streamExtractExternal
} from '../src/internal/download/download-artifact'
import {getUserAgentString} from '../src/internal/shared/user-agent'
import {noopLogs} from './common'
@@ -248,7 +249,44 @@ describe('download-artifact', () => {
})
})
it('should fail if blob storage response is non-200', async () => {
it('should fail if blob storage storage chunk does not respond within 30s', async () => {
// mock http client to delay response data by 30s
const msg = new http.IncomingMessage(new net.Socket())
msg.statusCode = 200
const mockGet = jest.fn(async () => {
return new Promise((resolve, reject) => {
// Resolve with a 200 status code immediately
resolve({
message: msg,
readBody: async () => {
return Promise.resolve(`{"ok": true}`)
}
})
// Reject with an error after 31 seconds
setTimeout(() => {
reject(new Error('Request timeout'))
}, 31000) // Timeout after 31 seconds
})
})
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGet
}
}
)
await expect(
streamExtractExternal(fixtures.blobStorageUrl, fixtures.workspaceDir)
).rejects.toBeInstanceOf(Error)
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
}, 35000) // add longer timeout to allow for timer to run out
it('should fail if blob storage response is non-200 after 5 retries', async () => {
const downloadArtifactMock = github.getOctokit(fixtures.token).rest
.actions.downloadArtifact as MockedDownloadArtifact
downloadArtifactMock.mockResolvedValueOnce({
@@ -290,7 +328,60 @@ describe('download-artifact', () => {
expect(mockGetArtifactFailure).toHaveBeenCalledWith(
fixtures.blobStorageUrl
)
})
expect(mockGetArtifactFailure).toHaveBeenCalledTimes(5)
}, 38000)
it('should retry if blob storage response is non-200 and then succeed with a 200', async () => {
const downloadArtifactMock = github.getOctokit(fixtures.token).rest
.actions.downloadArtifact as MockedDownloadArtifact
downloadArtifactMock.mockResolvedValueOnce({
headers: {
location: fixtures.blobStorageUrl
},
status: 302,
url: '',
data: Buffer.from('')
})
const mockGetArtifact = jest
.fn(mockGetArtifactSuccess)
.mockImplementationOnce(mockGetArtifactFailure)
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
() => {
return {
get: mockGetArtifact
}
}
)
const response = await downloadArtifactPublic(
fixtures.artifactID,
fixtures.repositoryOwner,
fixtures.repositoryName,
fixtures.token
)
expect(downloadArtifactMock).toHaveBeenCalledWith({
owner: fixtures.repositoryOwner,
repo: fixtures.repositoryName,
artifact_id: fixtures.artifactID,
archive_format: 'zip',
request: {
redirect: 'manual'
}
})
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
expect(mockGetArtifactFailure).toHaveBeenCalledWith(
fixtures.blobStorageUrl
)
expect(mockGetArtifactFailure).toHaveBeenCalledTimes(1)
expect(mockGetArtifactSuccess).toHaveBeenCalledWith(
fixtures.blobStorageUrl
)
expect(mockGetArtifactSuccess).toHaveBeenCalledTimes(1)
expect(response.downloadPath).toBe(fixtures.workspaceDir)
}, 28000)
})
describe('internal', () => {
+16 -83
View File
@@ -1,12 +1,12 @@
{
"name": "@actions/artifact",
"version": "2.0.0",
"version": "2.0.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@actions/artifact",
"version": "2.0.0",
"version": "2.0.1",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.0",
@@ -21,7 +21,6 @@
"archiver": "^5.3.1",
"crypto": "^1.0.1",
"jwt-decode": "^3.1.2",
"nock": "^13.4.0",
"twirp-ts": "^2.5.0",
"unzip-stream": "^0.3.1"
},
@@ -109,25 +108,6 @@
"node": ">=14.0.0"
}
},
"node_modules/@azure/core-http/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/@azure/core-lro": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.4.tgz",
@@ -352,25 +332,6 @@
"once": "^1.4.0"
}
},
"node_modules/@octokit/request/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/@octokit/types": {
"version": "6.41.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
@@ -795,22 +756,6 @@
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -966,11 +911,6 @@
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
},
"node_modules/jsonc-parser": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
@@ -1125,11 +1065,6 @@
"mkdirp": "bin/cmd.js"
}
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
@@ -1145,17 +1080,23 @@
"tslib": "^2.0.3"
}
},
"node_modules/nock": {
"version": "13.4.0",
"resolved": "https://registry.npmjs.org/nock/-/nock-13.4.0.tgz",
"integrity": "sha512-W8NVHjO/LCTNA64yxAPHV/K47LpGYcVzgKd3Q0n6owhwvD0Dgoterc25R4rnZbckJEb6Loxz1f5QMuJpJnbSyQ==",
"node_modules/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
"dependencies": {
"debug": "^4.1.0",
"json-stringify-safe": "^5.0.1",
"propagate": "^2.0.0"
"whatwg-url": "^5.0.0"
},
"engines": {
"node": ">= 10.13"
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/normalize-path": {
@@ -1223,14 +1164,6 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/propagate": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
"engines": {
"node": ">= 8"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/artifact",
"version": "2.0.0",
"version": "2.0.1",
"preview": true,
"description": "Actions artifact lib",
"keywords": [
@@ -52,7 +52,6 @@
"archiver": "^5.3.1",
"crypto": "^1.0.1",
"jwt-decode": "^3.1.2",
"nock": "^13.4.0",
"twirp-ts": "^2.5.0",
"unzip-stream": "^0.3.1"
},
@@ -63,4 +62,4 @@
"typedoc-plugin-markdown": "^3.17.1",
"typescript": "^5.2.2"
}
}
}
@@ -38,20 +38,65 @@ async function exists(path: string): Promise<boolean> {
}
async function streamExtract(url: string, directory: string): Promise<void> {
let retryCount = 0
while (retryCount < 5) {
try {
await streamExtractExternal(url, directory)
return
} catch (error) {
retryCount++
core.debug(
`Failed to download artifact after ${retryCount} retries due to ${error.message}. Retrying in 5 seconds...`
)
// wait 5 seconds before retrying
await new Promise(resolve => setTimeout(resolve, 5000))
}
}
throw new Error(`Artifact download failed after ${retryCount} retries.`)
}
export async function streamExtractExternal(
url: string,
directory: string
): Promise<void> {
const client = new httpClient.HttpClient(getUserAgentString())
const response = await client.get(url)
if (response.message.statusCode !== 200) {
throw new Error(
`Unexpected HTTP response from blob storage: ${response.message.statusCode} ${response.message.statusMessage}`
)
}
const timeout = 30 * 1000 // 30 seconds
return new Promise((resolve, reject) => {
const timerFn = (): void => {
response.message.destroy(
new Error(`Blob storage chunk did not respond in ${timeout}ms`)
)
}
const timer = setTimeout(timerFn, timeout)
response.message
.on('data', () => {
timer.refresh()
})
.on('error', (error: Error) => {
core.debug(
`response.message: Artifact download failed: ${error.message}`
)
clearTimeout(timer)
reject(error)
})
.pipe(unzip.Extract({path: directory}))
.on('close', resolve)
.on('error', reject)
.on('close', () => {
clearTimeout(timer)
resolve()
})
.on('error', (error: Error) => {
reject(error)
})
})
}
@@ -45,21 +45,6 @@ export interface UploadArtifactOptions {
* For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads.
*/
compressionLevel?: number
/**
* The simulated network error we'll temporarily use to test the azure blob
* client behavior.
* The value can range from 0 to 6
* 0 - fetchError
* 1 - abortError
* 2 - networkError
* 3 - securityError
* 4 - notAllowedError
* 5 - quotaExceededError
* 6 - random
* 7 - none
*
*/
simulateError?: number
}
/**
@@ -5,17 +5,8 @@ import {getUploadChunkSize, getConcurrency} from '../shared/config'
import * as core from '@actions/core'
import * as crypto from 'crypto'
import * as stream from 'stream'
import nock from 'nock'
import {NetworkError} from '../shared/errors'
export const DEFAULT_ERROR_NUMBER = 7
export const ERROR_TYPES = [
'fetchError',
'abortError',
'securityError',
'notAllowedError',
'quotaExceededError'
]
export interface BlobUploadResponse {
/**
* The total reported upload size in bytes. Empty if the upload failed
@@ -27,58 +18,10 @@ export interface BlobUploadResponse {
*/
sha256Hash?: string
}
export async function sendSimulatedError(
simulatedError: number,
authenticatedUploadURL: string
): Promise<void> {
switch (simulatedError) {
case 0: {
nock(authenticatedUploadURL).get('/').replyWithError({
code: 'ECONNRESET',
message: 'socket hang up'
})
break
}
case 1: {
const controller = new AbortController()
controller.abort()
break
}
case 2: {
nock(authenticatedUploadURL).get('/').replyWithError({
code: 'ETIMEDOUT'
})
break
}
case 3: {
nock(authenticatedUploadURL).get('/').reply(403)
break
}
case 4: {
nock(authenticatedUploadURL).get('/').reply(405)
break
}
case 5: {
nock(authenticatedUploadURL).get('/').reply(429)
break
}
case 6: {
const rand = Math.floor(Math.random() * ERROR_TYPES.length)
sendSimulatedError(rand, authenticatedUploadURL)
break
}
case 7: {
core.info('no error selected')
break
}
default:
core.error('something went wrong')
}
}
export async function uploadZipToBlobStorage(
authenticatedUploadURL: string,
zipUploadStream: ZipUploadStream,
simulatedError: number = DEFAULT_ERROR_NUMBER
zipUploadStream: ZipUploadStream
): Promise<BlobUploadResponse> {
let uploadByteCount = 0
@@ -93,9 +36,6 @@ export async function uploadZipToBlobStorage(
const uploadCallback = (progress: TransferProgressEvent): void => {
core.info(`Uploaded bytes ${progress.loadedBytes}`)
if (progress.loadedBytes > 1) {
sendSimulatedError(simulatedError, authenticatedUploadURL)
}
uploadByteCount = progress.loadedBytes
}
@@ -76,8 +76,7 @@ export async function uploadArtifact(
// Upload zip to blob storage
const uploadResult = await uploadZipToBlobStorage(
createArtifactResp.signedUploadUrl,
zipUploadStream,
options?.simulateError
zipUploadStream
)
// finalize the artifact
+4
View File
@@ -164,3 +164,7 @@
### 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.2",
"version": "3.2.3",
"preview": true,
"description": "Actions cache lib",
"keywords": [
+2 -1
View File
@@ -80,7 +80,8 @@ export function getCacheVersion(
compressionMethod?: CompressionMethod,
enableCrossOsArchive = false
): string {
const components = paths
// don't pass changes upstream
const components = paths.slice()
// Add compression method to cache version to restore
// compressed cache as per compression method
+126
View File
@@ -358,3 +358,129 @@ 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})
```