Compare commits

..
Author SHA1 Message Date
Luke Tomlinson 352aff7976 Update http-client to 2.2.1 2024-03-01 15:02:07 -05:00
89 changed files with 3882 additions and 11942 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ Note that before a PR will be accepted, you must ensure:
1. In a new branch, create a new Lerna package: 1. In a new branch, create a new Lerna package:
```console ```console
$ npm run new-package [name] $ npm run create-package new-package
``` ```
This will ask you some questions about the new package. Start with `0.0.0` as the first version (look generally at some of the other packages for how the package.json is structured). This will ask you some questions about the new package. Start with `0.0.0` as the first version (look generally at some of the other packages for how the package.json is structured).
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
run: npm run bootstrap run: npm run bootstrap
- name: audit tools (without allow-list) - name: audit tools (without allow-list)
run: npm audit --audit-level=moderate --omit dev run: npm audit --audit-level=moderate
- name: audit packages - name: audit packages
run: npm run audit-all run: npm run audit-all
+2 -4
View File
@@ -1,7 +1,5 @@
name: Publish NPM name: Publish NPM
run-name: Publish NPM - ${{ github.event.inputs.package }}
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
@@ -11,7 +9,7 @@ on:
jobs: jobs:
test: test:
runs-on: macos-latest-large runs-on: macos-latest
steps: steps:
- name: setup repo - name: setup repo
@@ -48,7 +46,7 @@ jobs:
path: packages/${{ github.event.inputs.package }}/*.tgz path: packages/${{ github.event.inputs.package }}/*.tgz
publish: publish:
runs-on: macos-latest-large runs-on: macos-latest
needs: test needs: test
environment: npm-publish environment: npm-publish
permissions: permissions:
+3 -7
View File
@@ -16,11 +16,7 @@ jobs:
strategy: strategy:
matrix: matrix:
runs-on: [ubuntu-latest, macos-latest-large, windows-latest] runs-on: [ubuntu-latest, macos-latest, windows-latest]
# Node 18 is the current default Node version in hosted runners, so users may still use the toolkit with it when running tests (see https://github.com/actions/toolkit/issues/1841)
# Node 20 is the currently support Node version for actions - https://docs.github.com/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#runsusing-for-javascript-actions
node-version: [18.x, 20.x]
fail-fast: false fail-fast: false
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }}
@@ -29,10 +25,10 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Node ${{ matrix.node-version }} - name: Set Node.js 20.x
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: ${{ matrix.node-version }} node-version: 20.x
- name: npm install - name: npm install
run: npm install run: npm install
+1858 -3470
View File
File diff suppressed because it is too large Load Diff
+2 -2
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 70000" "test": "jest --testTimeout 60000"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.4", "@types/jest": "^29.5.4",
@@ -27,7 +27,7 @@
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"flow-bin": "^0.115.0", "flow-bin": "^0.115.0",
"jest": "^29.6.4", "jest": "^29.6.4",
"lerna": "^6.4.1", "lerna": "^7.1.4",
"nx": "16.6.0", "nx": "16.6.0",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"ts-jest": "^29.1.1", "ts-jest": "^29.1.1",
-31
View File
@@ -1,36 +1,5 @@
# @actions/artifact Releases # @actions/artifact Releases
### 2.1.11
- Fixed a bug with relative symlinks resolution [#1844](https://github.com/actions/toolkit/pull/1844)
- Use native `crypto` [#1815](https://github.com/actions/toolkit/pull/1815)
### 2.1.10
- Fixed a regression with symlinks not being automatically resolved [#1830](https://github.com/actions/toolkit/pull/1830)
- Fixed a regression with chunk timeout [#1786](https://github.com/actions/toolkit/pull/1786)
### 2.1.9
- Fixed artifact upload chunk timeout logic [#1774](https://github.com/actions/toolkit/pull/1774)
- Use lazy stream to prevent issues with open file limits [#1771](https://github.com/actions/toolkit/pull/1771)
### 2.1.8
- Allows `*.localhost` domains for hostname checks for local development.
### 2.1.7
- Update unzip-stream dependency and reverted to using `unzip.Extract()`
### 2.1.6
- Will retry on invalid request responses.
### 2.1.5
- Bumped `archiver` dependency to 7.0.1
### 2.1.4 ### 2.1.4
- Adds info-level logging for zip extraction - Adds info-level logging for zip extraction
@@ -116,54 +116,6 @@ describe('artifact-http-client', () => {
expect(mockPost).toHaveBeenCalledTimes(2) expect(mockPost).toHaveBeenCalledTimes(2)
}) })
it('should retry if invalid body response', async () => {
const mockPost = jest
.fn(() => {
const msgSucceeded = new http.IncomingMessage(new net.Socket())
msgSucceeded.statusCode = 200
return {
message: msgSucceeded,
readBody: async () => {
return Promise.resolve(
`{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}`
)
}
}
})
.mockImplementationOnce(() => {
const msgFailed = new http.IncomingMessage(new net.Socket())
msgFailed.statusCode = 502
msgFailed.statusMessage = 'Bad Gateway'
return {
message: msgFailed,
readBody: async () => {
return Promise.resolve('💥')
}
}
})
const mockHttpClient = (
HttpClient as unknown as jest.Mock
).mockImplementation(() => {
return {
post: mockPost
}
})
const client = internalArtifactTwirpClient(clientOptions)
const artifact = await client.CreateArtifact({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678',
name: 'artifact',
version: 4
})
expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(artifact).toBeDefined()
expect(artifact.ok).toBe(true)
expect(artifact.signedUploadUrl).toBe('http://localhost:8080/upload')
expect(mockPost).toHaveBeenCalledTimes(2)
})
it('should fail if the request fails 5 times', async () => { it('should fail if the request fails 5 times', async () => {
const mockPost = jest.fn(() => { const mockPost = jest.fn(() => {
const msgFailed = new http.IncomingMessage(new net.Socket()) const msgFailed = new http.IncomingMessage(new net.Socket())
@@ -20,11 +20,6 @@ describe('isGhes', () => {
expect(config.isGhes()).toBe(false) expect(config.isGhes()).toBe(false)
}) })
it('should return false when the request domain ends with .localhost', () => {
process.env.GITHUB_SERVER_URL = 'https://github.localhost'
expect(config.isGhes()).toBe(false)
})
it('should return false when the request domain is specific to an enterprise', () => { it('should return false when the request domain is specific to an enterprise', () => {
process.env.GITHUB_SERVER_URL = 'https://my-enterprise.github.com' process.env.GITHUB_SERVER_URL = 'https://my-enterprise.github.com'
expect(config.isGhes()).toBe(true) expect(config.isGhes()).toBe(true)
@@ -200,12 +200,14 @@ describe('download-artifact', () => {
} }
) )
const response = await downloadArtifactPublic( await expect(
fixtures.artifactID, downloadArtifactPublic(
fixtures.repositoryOwner, fixtures.artifactID,
fixtures.repositoryName, fixtures.repositoryOwner,
fixtures.token fixtures.repositoryName,
) fixtures.token
)
).rejects.toBeInstanceOf(Error)
expect(downloadArtifactMock).toHaveBeenCalledWith({ expect(downloadArtifactMock).toHaveBeenCalledWith({
owner: fixtures.repositoryOwner, owner: fixtures.repositoryOwner,
@@ -221,16 +223,6 @@ describe('download-artifact', () => {
expect(mockGetArtifactMalicious).toHaveBeenCalledWith( expect(mockGetArtifactMalicious).toHaveBeenCalledWith(
fixtures.blobStorageUrl fixtures.blobStorageUrl
) )
// ensure path traversal was not possible
expect(
fs.existsSync(path.join(fixtures.workspaceDir, 'x/etc/hosts'))
).toBe(true)
expect(
fs.existsSync(path.join(fixtures.workspaceDir, 'y/etc/hosts'))
).toBe(true)
expect(response.downloadPath).toBe(fixtures.workspaceDir)
}) })
it('should successfully download an artifact to user defined path', async () => { it('should successfully download an artifact to user defined path', async () => {
@@ -1,173 +1,257 @@
import * as uploadZipSpecification from '../src/internal/upload/upload-zip-specification' import * as uploadZipSpecification from '../src/internal/upload/upload-zip-specification'
import * as zip from '../src/internal/upload/zip' import * as zip from '../src/internal/upload/zip'
import * as util from '../src/internal/shared/util' import * as util from '../src/internal/shared/util'
import * as retention from '../src/internal/upload/retention'
import * as config from '../src/internal/shared/config' import * as config from '../src/internal/shared/config'
import {ArtifactServiceClientJSON} from '../src/generated' import {Timestamp, ArtifactServiceClientJSON} from '../src/generated'
import * as blobUpload from '../src/internal/upload/blob-upload' import * as blobUpload from '../src/internal/upload/blob-upload'
import {uploadArtifact} from '../src/internal/upload/upload-artifact' import {uploadArtifact} from '../src/internal/upload/upload-artifact'
import {noopLogs} from './common' import {noopLogs} from './common'
import {FilesNotFoundError} from '../src/internal/shared/errors' import {FilesNotFoundError} from '../src/internal/shared/errors'
import {BlockBlobUploadStreamOptions} from '@azure/storage-blob'
import * as fs from 'fs'
import * as path from 'path'
import unzip from 'unzip-stream'
const uploadStreamMock = jest.fn()
const blockBlobClientMock = jest.fn().mockImplementation(() => ({
uploadStream: uploadStreamMock
}))
jest.mock('@azure/storage-blob', () => ({
BlobClient: jest.fn().mockImplementation(() => {
return {
getBlockBlobClient: blockBlobClientMock
}
})
}))
const fixtures = {
uploadDirectory: path.join(__dirname, '_temp', 'plz-upload'),
files: [
{name: 'file1.txt', content: 'test 1 file content'},
{name: 'file2.txt', content: 'test 2 file content'},
{name: 'file3.txt', content: 'test 3 file content'},
{
name: 'real.txt',
content: 'from a symlink'
},
{
name: 'relative.txt',
content: 'from a symlink',
symlink: 'real.txt',
relative: true
},
{
name: 'absolute.txt',
content: 'from a symlink',
symlink: 'real.txt',
relative: false
}
],
backendIDs: {
workflowRunBackendId: '67dbcc20-e851-4452-a7c3-2cc0d2e0ec67',
workflowJobRunBackendId: '5f49179d-3386-4c38-85f7-00f8138facd0'
},
runtimeToken: 'test-token',
resultsServiceURL: 'http://results.local',
inputs: {
artifactName: 'test-artifact',
files: [
'/home/user/files/plz-upload/file1.txt',
'/home/user/files/plz-upload/file2.txt',
'/home/user/files/plz-upload/dir/file3.txt'
],
rootDirectory: '/home/user/files/plz-upload'
}
}
describe('upload-artifact', () => { describe('upload-artifact', () => {
beforeAll(() => {
fs.mkdirSync(fixtures.uploadDirectory, {
recursive: true
})
for (const file of fixtures.files) {
if (file.symlink) {
let symlinkPath = file.symlink
if (!file.relative) {
symlinkPath = path.join(fixtures.uploadDirectory, file.symlink)
}
if (!fs.existsSync(path.join(fixtures.uploadDirectory, file.name))) {
fs.symlinkSync(
symlinkPath,
path.join(fixtures.uploadDirectory, file.name),
'file'
)
}
} else {
fs.writeFileSync(
path.join(fixtures.uploadDirectory, file.name),
file.content
)
}
}
})
beforeEach(() => { beforeEach(() => {
noopLogs() noopLogs()
jest
.spyOn(uploadZipSpecification, 'validateRootDirectory')
.mockReturnValue()
jest
.spyOn(util, 'getBackendIdsFromToken')
.mockReturnValue(fixtures.backendIDs)
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockReturnValue(
fixtures.files.map(file => ({
sourcePath: path.join(fixtures.uploadDirectory, file.name),
destinationPath: file.name,
stats: new fs.Stats()
}))
)
jest.spyOn(config, 'getRuntimeToken').mockReturnValue(fixtures.runtimeToken)
jest
.spyOn(config, 'getResultsServiceUrl')
.mockReturnValue(fixtures.resultsServiceURL)
}) })
afterEach(() => { afterEach(() => {
jest.restoreAllMocks() jest.restoreAllMocks()
}) })
it('should reject if there are no files to upload', async () => { it('should successfully upload an artifact', () => {
const mockDate = new Date('2020-01-01')
jest
.spyOn(uploadZipSpecification, 'validateRootDirectory')
.mockReturnValue()
jest jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification') .spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockClear() .mockReturnValue([
.mockReturnValue([]) {
sourcePath: '/home/user/files/plz-upload/file1.txt',
destinationPath: 'file1.txt'
},
{
sourcePath: '/home/user/files/plz-upload/file2.txt',
destinationPath: 'file2.txt'
},
{
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
destinationPath: 'dir/file3.txt'
}
])
const uploadResp = uploadArtifact(
fixtures.inputs.artifactName,
fixtures.inputs.files,
fixtures.inputs.rootDirectory
)
await expect(uploadResp).rejects.toThrowError(FilesNotFoundError)
})
it('should reject if no backend IDs are found', async () => {
jest.spyOn(util, 'getBackendIdsFromToken').mockRestore()
const uploadResp = uploadArtifact(
fixtures.inputs.artifactName,
fixtures.inputs.files,
fixtures.inputs.rootDirectory
)
await expect(uploadResp).rejects.toThrow()
})
it('should return false if the creation request fails', async () => {
jest jest
.spyOn(zip, 'createZipUploadStream') .spyOn(zip, 'createZipUploadStream')
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1))) .mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678'
})
jest
.spyOn(retention, 'getExpiration')
.mockReturnValue(Timestamp.fromDate(mockDate))
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
signedUploadUrl: 'https://signed-upload-url.com'
})
)
jest.spyOn(blobUpload, 'uploadZipToBlobStorage').mockReturnValue(
Promise.resolve({
uploadSize: 1234,
sha256Hash: 'test-sha256-hash'
})
)
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
.mockReturnValue(Promise.resolve({ok: true, artifactId: '1'}))
// ArtifactHttpClient mocks
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
jest
.spyOn(config, 'getResultsServiceUrl')
.mockReturnValue('https://test-url.com')
const uploadResp = uploadArtifact(
'test-artifact',
[
'/home/user/files/plz-upload/file1.txt',
'/home/user/files/plz-upload/file2.txt',
'/home/user/files/plz-upload/dir/file3.txt'
],
'/home/user/files/plz-upload'
)
expect(uploadResp).resolves.toEqual({size: 1234, id: 1})
})
it('should throw an error if the root directory is invalid', () => {
jest
.spyOn(uploadZipSpecification, 'validateRootDirectory')
.mockImplementation(() => {
throw new Error('Invalid root directory')
})
const uploadResp = uploadArtifact(
'test-artifact',
[
'/home/user/files/plz-upload/file1.txt',
'/home/user/files/plz-upload/file2.txt',
'/home/user/files/plz-upload/dir/file3.txt'
],
'/home/user/files/plz-upload'
)
expect(uploadResp).rejects.toThrow('Invalid root directory')
})
it('should reject if there are no files to upload', () => {
jest
.spyOn(uploadZipSpecification, 'validateRootDirectory')
.mockReturnValue()
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockReturnValue([])
const uploadResp = uploadArtifact(
'test-artifact',
[
'/home/user/files/plz-upload/file1.txt',
'/home/user/files/plz-upload/file2.txt',
'/home/user/files/plz-upload/dir/file3.txt'
],
'/home/user/files/plz-upload'
)
expect(uploadResp).rejects.toThrowError(FilesNotFoundError)
})
it('should reject if no backend IDs are found', () => {
jest
.spyOn(uploadZipSpecification, 'validateRootDirectory')
.mockReturnValue()
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockReturnValue([
{
sourcePath: '/home/user/files/plz-upload/file1.txt',
destinationPath: 'file1.txt'
},
{
sourcePath: '/home/user/files/plz-upload/file2.txt',
destinationPath: 'file2.txt'
},
{
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
destinationPath: 'dir/file3.txt'
}
])
jest
.spyOn(zip, 'createZipUploadStream')
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
const uploadResp = uploadArtifact(
'test-artifact',
[
'/home/user/files/plz-upload/file1.txt',
'/home/user/files/plz-upload/file2.txt',
'/home/user/files/plz-upload/dir/file3.txt'
],
'/home/user/files/plz-upload'
)
expect(uploadResp).rejects.toThrow()
})
it('should return false if the creation request fails', () => {
const mockDate = new Date('2020-01-01')
jest
.spyOn(uploadZipSpecification, 'validateRootDirectory')
.mockReturnValue()
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockReturnValue([
{
sourcePath: '/home/user/files/plz-upload/file1.txt',
destinationPath: 'file1.txt'
},
{
sourcePath: '/home/user/files/plz-upload/file2.txt',
destinationPath: 'file2.txt'
},
{
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
destinationPath: 'dir/file3.txt'
}
])
jest
.spyOn(zip, 'createZipUploadStream')
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678'
})
jest
.spyOn(retention, 'getExpiration')
.mockReturnValue(Timestamp.fromDate(mockDate))
jest jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact') .spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(Promise.resolve({ok: false, signedUploadUrl: ''})) .mockReturnValue(Promise.resolve({ok: false, signedUploadUrl: ''}))
// ArtifactHttpClient mocks
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
jest
.spyOn(config, 'getResultsServiceUrl')
.mockReturnValue('https://test-url.com')
const uploadResp = uploadArtifact( const uploadResp = uploadArtifact(
fixtures.inputs.artifactName, 'test-artifact',
fixtures.inputs.files, [
fixtures.inputs.rootDirectory '/home/user/files/plz-upload/file1.txt',
'/home/user/files/plz-upload/file2.txt',
'/home/user/files/plz-upload/dir/file3.txt'
],
'/home/user/files/plz-upload'
) )
await expect(uploadResp).rejects.toThrow() expect(uploadResp).rejects.toThrow()
}) })
it('should return false if blob storage upload is unsuccessful', async () => { it('should return false if blob storage upload is unsuccessful', () => {
const mockDate = new Date('2020-01-01')
jest
.spyOn(uploadZipSpecification, 'validateRootDirectory')
.mockReturnValue()
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockReturnValue([
{
sourcePath: '/home/user/files/plz-upload/file1.txt',
destinationPath: 'file1.txt'
},
{
sourcePath: '/home/user/files/plz-upload/file2.txt',
destinationPath: 'file2.txt'
},
{
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
destinationPath: 'dir/file3.txt'
}
])
jest jest
.spyOn(zip, 'createZipUploadStream') .spyOn(zip, 'createZipUploadStream')
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1))) .mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678'
})
jest
.spyOn(retention, 'getExpiration')
.mockReturnValue(Timestamp.fromDate(mockDate))
jest jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact') .spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue( .mockReturnValue(
@@ -180,19 +264,57 @@ describe('upload-artifact', () => {
.spyOn(blobUpload, 'uploadZipToBlobStorage') .spyOn(blobUpload, 'uploadZipToBlobStorage')
.mockReturnValue(Promise.reject(new Error('boom'))) .mockReturnValue(Promise.reject(new Error('boom')))
// ArtifactHttpClient mocks
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
jest
.spyOn(config, 'getResultsServiceUrl')
.mockReturnValue('https://test-url.com')
const uploadResp = uploadArtifact( const uploadResp = uploadArtifact(
fixtures.inputs.artifactName, 'test-artifact',
fixtures.inputs.files, [
fixtures.inputs.rootDirectory '/home/user/files/plz-upload/file1.txt',
'/home/user/files/plz-upload/file2.txt',
'/home/user/files/plz-upload/dir/file3.txt'
],
'/home/user/files/plz-upload'
) )
await expect(uploadResp).rejects.toThrow() expect(uploadResp).rejects.toThrow()
}) })
it('should reject if finalize artifact fails', async () => { it('should reject if finalize artifact fails', () => {
const mockDate = new Date('2020-01-01')
jest
.spyOn(uploadZipSpecification, 'validateRootDirectory')
.mockReturnValue()
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockReturnValue([
{
sourcePath: '/home/user/files/plz-upload/file1.txt',
destinationPath: 'file1.txt'
},
{
sourcePath: '/home/user/files/plz-upload/file2.txt',
destinationPath: 'file2.txt'
},
{
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
destinationPath: 'dir/file3.txt'
}
])
jest jest
.spyOn(zip, 'createZipUploadStream') .spyOn(zip, 'createZipUploadStream')
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1))) .mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
workflowRunBackendId: '1234',
workflowJobRunBackendId: '5678'
})
jest
.spyOn(retention, 'getExpiration')
.mockReturnValue(Timestamp.fromDate(mockDate))
jest jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact') .spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue( .mockReturnValue(
@@ -211,161 +333,22 @@ describe('upload-artifact', () => {
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact') .spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
.mockReturnValue(Promise.resolve({ok: false, artifactId: ''})) .mockReturnValue(Promise.resolve({ok: false, artifactId: ''}))
const uploadResp = uploadArtifact( // ArtifactHttpClient mocks
fixtures.inputs.artifactName, jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
fixtures.inputs.files,
fixtures.inputs.rootDirectory
)
await expect(uploadResp).rejects.toThrow()
})
it('should successfully upload an artifact', async () => {
jest
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
.mockRestore()
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
signedUploadUrl: 'https://signed-upload-url.local'
})
)
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
artifactId: '1'
})
)
let loadedBytes = 0
const uploadedZip = path.join(
fixtures.uploadDirectory,
'..',
'uploaded.zip'
)
uploadStreamMock.mockImplementation(
async (
stream: NodeJS.ReadableStream,
bufferSize?: number,
maxConcurrency?: number,
options?: BlockBlobUploadStreamOptions
) => {
const {onProgress} = options || {}
if (fs.existsSync(uploadedZip)) {
fs.unlinkSync(uploadedZip)
}
const uploadedZipStream = fs.createWriteStream(uploadedZip)
onProgress?.({loadedBytes: 0})
return new Promise((resolve, reject) => {
stream.on('data', chunk => {
loadedBytes += chunk.length
uploadedZipStream.write(chunk)
onProgress?.({loadedBytes})
})
stream.on('end', () => {
onProgress?.({loadedBytes})
uploadedZipStream.end()
resolve({})
})
stream.on('error', err => {
reject(err)
})
})
}
)
const {id, size} = await uploadArtifact(
fixtures.inputs.artifactName,
fixtures.files.map(file =>
path.join(fixtures.uploadDirectory, file.name)
),
fixtures.uploadDirectory
)
expect(id).toBe(1)
expect(size).toBe(loadedBytes)
const extractedDirectory = path.join(
fixtures.uploadDirectory,
'..',
'extracted'
)
if (fs.existsSync(extractedDirectory)) {
fs.rmdirSync(extractedDirectory, {recursive: true})
}
const extract = new Promise((resolve, reject) => {
fs.createReadStream(uploadedZip)
.pipe(unzip.Extract({path: extractedDirectory}))
.on('close', () => {
resolve(true)
})
.on('error', err => {
reject(err)
})
})
await expect(extract).resolves.toBe(true)
for (const file of fixtures.files) {
const filePath = path.join(extractedDirectory, file.name)
expect(fs.existsSync(filePath)).toBe(true)
expect(fs.readFileSync(filePath, 'utf8')).toBe(file.content)
}
})
it('should throw an error uploading blob chunks get delayed', async () => {
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
signedUploadUrl: 'https://signed-upload-url.local'
})
)
jest
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
.mockReturnValue(
Promise.resolve({
ok: true,
artifactId: '1'
})
)
jest jest
.spyOn(config, 'getResultsServiceUrl') .spyOn(config, 'getResultsServiceUrl')
.mockReturnValue('https://results.local') .mockReturnValue('https://test-url.com')
jest.spyOn(config, 'getUploadChunkTimeout').mockReturnValue(2_000)
uploadStreamMock.mockImplementation(
async (
stream: NodeJS.ReadableStream,
bufferSize?: number,
maxConcurrency?: number,
options?: BlockBlobUploadStreamOptions
) => {
const {onProgress, abortSignal} = options || {}
onProgress?.({loadedBytes: 0})
return new Promise(resolve => {
abortSignal?.addEventListener('abort', () => {
resolve({})
})
})
}
)
const uploadResp = uploadArtifact( const uploadResp = uploadArtifact(
fixtures.inputs.artifactName, 'test-artifact',
fixtures.inputs.files, [
fixtures.inputs.rootDirectory '/home/user/files/plz-upload/file1.txt',
'/home/user/files/plz-upload/file2.txt',
'/home/user/files/plz-upload/dir/file3.txt'
],
'/home/user/files/plz-upload'
) )
await expect(uploadResp).rejects.toThrow('Upload progress stalled.') expect(uploadResp).rejects.toThrow()
}) })
}) })
@@ -305,22 +305,4 @@ describe('Search', () => {
} }
} }
}) })
it('Upload Specification - Includes symlinks', async () => {
const targetPath = path.join(root, 'link-dir', 'symlink-me.txt')
await fs.mkdir(path.dirname(targetPath), {recursive: true})
await fs.writeFile(targetPath, 'symlink file content')
const uploadPath = path.join(root, 'upload-dir', 'symlink.txt')
await fs.mkdir(path.dirname(uploadPath), {recursive: true})
await fs.symlink(targetPath, uploadPath, 'file')
const specifications = getUploadZipSpecification([uploadPath], root)
expect(specifications.length).toEqual(1)
expect(specifications[0].sourcePath).toEqual(uploadPath)
expect(specifications[0].destinationPath).toEqual(
path.join('/upload-dir', 'symlink.txt')
)
expect(specifications[0].stats.isSymbolicLink()).toBe(true)
})
}) })
+148 -554
View File
@@ -1,12 +1,12 @@
{ {
"name": "@actions/artifact", "name": "@actions/artifact",
"version": "2.1.11", "version": "2.1.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@actions/artifact", "name": "@actions/artifact",
"version": "2.1.11", "version": "2.1.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.10.0", "@actions/core": "^1.10.0",
@@ -18,7 +18,8 @@
"@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",
"archiver": "^7.0.1", "archiver": "^5.3.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" "unzip-stream": "^0.3.1"
@@ -185,22 +186,6 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
"strip-ansi": "^7.0.1",
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
"wrap-ansi": "^8.1.0",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@octokit/auth-token": { "node_modules/@octokit/auth-token": {
"version": "2.5.0", "version": "2.5.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz",
@@ -363,15 +348,6 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/@pkgjs/parseargs": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
"optional": true,
"engines": {
"node": ">=14"
}
},
"node_modules/@protobuf-ts/plugin": { "node_modules/@protobuf-ts/plugin": {
"version": "2.9.1", "version": "2.9.1",
"resolved": "https://registry.npmjs.org/@protobuf-ts/plugin/-/plugin-2.9.1.tgz", "resolved": "https://registry.npmjs.org/@protobuf-ts/plugin/-/plugin-2.9.1.tgz",
@@ -504,120 +480,74 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/ansi-sequence-parser": { "node_modules/ansi-sequence-parser": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz",
"integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==",
"dev": true "dev": true
}, },
"node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/archiver": { "node_modules/archiver": {
"version": "7.0.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.1.tgz",
"integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", "integrity": "sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w==",
"dependencies": { "dependencies": {
"archiver-utils": "^5.0.2", "archiver-utils": "^2.1.0",
"async": "^3.2.4", "async": "^3.2.3",
"buffer-crc32": "^1.0.0", "buffer-crc32": "^0.2.1",
"readable-stream": "^4.0.0", "readable-stream": "^3.6.0",
"readdir-glob": "^1.1.2", "readdir-glob": "^1.0.0",
"tar-stream": "^3.0.0", "tar-stream": "^2.2.0",
"zip-stream": "^6.0.1" "zip-stream": "^4.1.0"
}, },
"engines": { "engines": {
"node": ">= 14" "node": ">= 10"
} }
}, },
"node_modules/archiver-utils": { "node_modules/archiver-utils": {
"version": "5.0.2", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz",
"integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==",
"dependencies": { "dependencies": {
"glob": "^10.0.0", "glob": "^7.1.4",
"graceful-fs": "^4.2.0", "graceful-fs": "^4.2.0",
"is-stream": "^2.0.1",
"lazystream": "^1.0.0", "lazystream": "^1.0.0",
"lodash": "^4.17.15", "lodash.defaults": "^4.2.0",
"lodash.difference": "^4.5.0",
"lodash.flatten": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.union": "^4.6.0",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"readable-stream": "^4.0.0" "readable-stream": "^2.0.0"
}, },
"engines": { "engines": {
"node": ">= 14" "node": ">= 6"
} }
}, },
"node_modules/archiver-utils/node_modules/brace-expansion": { "node_modules/archiver-utils/node_modules/readable-stream": {
"version": "2.0.1", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "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/archiver-utils/node_modules/glob": { "node_modules/archiver-utils/node_modules/safe-buffer": {
"version": "10.3.12", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.6",
"minimatch": "^9.0.1",
"minipass": "^7.0.4",
"path-scurry": "^1.10.2"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
}, },
"node_modules/archiver-utils/node_modules/minimatch": { "node_modules/archiver-utils/node_modules/string_decoder": {
"version": "9.0.4", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "safe-buffer": "~5.1.0"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/async": { "node_modules/async": {
@@ -630,22 +560,11 @@
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
}, },
"node_modules/b4a": {
"version": "1.6.6",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz",
"integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg=="
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
}, },
"node_modules/bare-events": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.2.tgz",
"integrity": "sha512-h7z00dWdG0PYOQEvChhOSWvOfkIKsdZGkWr083FgN/HyoQuebSew/cgirYqh9SCuy/hRvxc5Vy6Fw8xAmYHLkQ==",
"optional": true
},
"node_modules/base64-js": { "node_modules/base64-js": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -682,6 +601,16 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"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",
@@ -697,9 +626,9 @@
} }
}, },
"node_modules/buffer": { "node_modules/buffer": {
"version": "6.0.3", "version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -716,15 +645,15 @@
], ],
"dependencies": { "dependencies": {
"base64-js": "^1.3.1", "base64-js": "^1.3.1",
"ieee754": "^1.2.1" "ieee754": "^1.1.13"
} }
}, },
"node_modules/buffer-crc32": { "node_modules/buffer-crc32": {
"version": "1.0.0", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"engines": { "engines": {
"node": ">=8.0.0" "node": "*"
} }
}, },
"node_modules/buffers": { "node_modules/buffers": {
@@ -755,22 +684,6 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -791,18 +704,17 @@
} }
}, },
"node_modules/compress-commons": { "node_modules/compress-commons": {
"version": "6.0.2", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz",
"integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", "integrity": "sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==",
"dependencies": { "dependencies": {
"crc-32": "^1.2.0", "buffer-crc32": "^0.2.13",
"crc32-stream": "^6.0.0", "crc32-stream": "^4.0.2",
"is-stream": "^2.0.1",
"normalize-path": "^3.0.0", "normalize-path": "^3.0.0",
"readable-stream": "^4.0.0" "readable-stream": "^3.6.0"
}, },
"engines": { "engines": {
"node": ">= 14" "node": ">= 10"
} }
}, },
"node_modules/concat-map": { "node_modules/concat-map": {
@@ -827,30 +739,22 @@
} }
}, },
"node_modules/crc32-stream": { "node_modules/crc32-stream": {
"version": "6.0.0", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.2.tgz",
"integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "integrity": "sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==",
"dependencies": { "dependencies": {
"crc-32": "^1.2.0", "crc-32": "^1.2.0",
"readable-stream": "^4.0.0" "readable-stream": "^3.4.0"
}, },
"engines": { "engines": {
"node": ">= 14" "node": ">= 10"
} }
}, },
"node_modules/cross-spawn": { "node_modules/crypto": {
"version": "7.0.6", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"license": "MIT", "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."
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
}, },
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
@@ -877,22 +781,12 @@
"dot-object": "bin/dot-object" "dot-object": "bin/dot-object"
} }
}, },
"node_modules/eastasianwidth": { "node_modules/end-of-stream": {
"version": "0.2.0", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
}, "dependencies": {
"node_modules/emoji-regex": { "once": "^1.4.0"
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"engines": {
"node": ">=6"
} }
}, },
"node_modules/events": { "node_modules/events": {
@@ -903,26 +797,6 @@
"node": ">=0.8.x" "node": ">=0.8.x"
} }
}, },
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
},
"node_modules/foreground-child": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
"dependencies": {
"cross-spawn": "^7.0.0",
"signal-exit": "^4.0.1"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
@@ -936,6 +810,11 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"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",
@@ -1019,14 +898,6 @@
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
}, },
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"engines": {
"node": ">=8"
}
},
"node_modules/is-plain-object": { "node_modules/is-plain-object": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
@@ -1035,44 +906,11 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
}, },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
},
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jsonc-parser": { "node_modules/jsonc-parser": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
@@ -1127,6 +965,31 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}, },
"node_modules/lodash.defaults": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="
},
"node_modules/lodash.difference": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz",
"integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="
},
"node_modules/lodash.flatten": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz",
"integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
},
"node_modules/lodash.union": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz",
"integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="
},
"node_modules/lower-case": { "node_modules/lower-case": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
@@ -1135,14 +998,6 @@
"tslib": "^2.0.3" "tslib": "^2.0.3"
} }
}, },
"node_modules/lru-cache": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz",
"integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==",
"engines": {
"node": "14 || >=16.14"
}
},
"node_modules/lunr": { "node_modules/lunr": {
"version": "2.3.9", "version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
@@ -1199,14 +1054,6 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/minipass": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz",
"integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==",
"engines": {
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mkdirp": { "node_modules/mkdirp": {
"version": "0.5.6", "version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
@@ -1285,33 +1132,10 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz",
"integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==",
"dependencies": {
"lru-cache": "^10.2.0",
"minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "6.3.0", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
}, },
"node_modules/prettier": { "node_modules/prettier": {
"version": "2.8.8", "version": "2.8.8",
@@ -1340,24 +1164,17 @@
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
}, },
"node_modules/queue-tick": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
},
"node_modules/readable-stream": { "node_modules/readable-stream": {
"version": "4.5.2", "version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"dependencies": { "dependencies": {
"abort-controller": "^3.0.0", "inherits": "^2.0.3",
"buffer": "^6.0.3", "string_decoder": "^1.1.1",
"events": "^3.3.0", "util-deprecate": "^1.0.1"
"process": "^0.11.10",
"string_decoder": "^1.3.0"
}, },
"engines": { "engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0" "node": ">= 6"
} }
}, },
"node_modules/readdir-glob": { "node_modules/readdir-glob": {
@@ -1411,25 +1228,6 @@
"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/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"engines": {
"node": ">=8"
}
},
"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",
@@ -1442,17 +1240,6 @@
"vscode-textmate": "^8.0.0" "vscode-textmate": "^8.0.0"
} }
}, },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -1462,18 +1249,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/streamx": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.16.1.tgz",
"integrity": "sha512-m9QYj6WygWyWa3H1YY69amr4nVgy61xfjys7xO7kviL5rfIEc2naf+ewFiOA+aEJD7y0JO3h2GoiUv4TDwEGzQ==",
"dependencies": {
"fast-fifo": "^1.1.0",
"queue-tick": "^1.0.1"
},
"optionalDependencies": {
"bare-events": "^2.2.0"
}
},
"node_modules/string_decoder": { "node_modules/string_decoder": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -1482,102 +1257,19 @@
"safe-buffer": "~5.2.0" "safe-buffer": "~5.2.0"
} }
}, },
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/string-width-cjs": {
"name": "string-width",
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/string-width-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/strip-ansi-cjs": {
"name": "strip-ansi",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/tar-stream": { "node_modules/tar-stream": {
"version": "3.1.7", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dependencies": { "dependencies": {
"b4a": "^1.6.4", "bl": "^4.0.3",
"fast-fifo": "^1.2.0", "end-of-stream": "^1.4.1",
"streamx": "^2.15.0" "fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
} }
}, },
"node_modules/tr46": { "node_modules/tr46": {
@@ -1732,9 +1424,9 @@
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
}, },
"node_modules/unzip-stream": { "node_modules/unzip-stream": {
"version": "0.3.4", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.4.tgz", "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz",
"integrity": "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw==", "integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==",
"dependencies": { "dependencies": {
"binary": "^0.3.0", "binary": "^0.3.0",
"mkdirp": "^0.5.1" "mkdirp": "^0.5.1"
@@ -1779,110 +1471,12 @@
"webidl-conversions": "^3.0.0" "webidl-conversions": "^3.0.0"
} }
}, },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wordwrap": { "node_modules/wordwrap": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"dev": true "dev": true
}, },
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs": {
"name": "wrap-ansi",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -1917,16 +1511,16 @@
} }
}, },
"node_modules/zip-stream": { "node_modules/zip-stream": {
"version": "6.0.1", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz",
"integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", "integrity": "sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==",
"dependencies": { "dependencies": {
"archiver-utils": "^5.0.0", "archiver-utils": "^2.1.0",
"compress-commons": "^6.0.2", "compress-commons": "^4.1.0",
"readable-stream": "^4.0.0" "readable-stream": "^3.6.0"
}, },
"engines": { "engines": {
"node": ">= 14" "node": ">= 10"
} }
} }
} }
+3 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/artifact", "name": "@actions/artifact",
"version": "2.1.11", "version": "2.1.4",
"preview": true, "preview": true,
"description": "Actions artifact lib", "description": "Actions artifact lib",
"keywords": [ "keywords": [
@@ -49,7 +49,8 @@
"@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",
"archiver": "^7.0.1", "archiver": "^5.3.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" "unzip-stream": "^0.3.1"
@@ -1,4 +1,7 @@
import fs from 'fs/promises' import fs from 'fs/promises'
import * as stream from 'stream'
import {createWriteStream} from 'fs'
import * as path from 'path'
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'
@@ -44,6 +47,11 @@ async function streamExtract(url: string, directory: string): Promise<void> {
await streamExtractExternal(url, directory) await streamExtractExternal(url, directory)
return return
} catch (error) { } catch (error) {
if (error.message.includes('Malformed extraction path')) {
throw new Error(
`Artifact download failed with unretryable error: ${error.message}`
)
}
retryCount++ retryCount++
core.debug( core.debug(
`Failed to download artifact after ${retryCount} retries due to ${error.message}. Retrying in 5 seconds...` `Failed to download artifact after ${retryCount} retries due to ${error.message}. Retrying in 5 seconds...`
@@ -78,6 +86,8 @@ export async function streamExtractExternal(
} }
const timer = setTimeout(timerFn, timeout) const timer = setTimeout(timerFn, timeout)
const createdDirectories = new Set<string>()
createdDirectories.add(directory)
response.message response.message
.on('data', () => { .on('data', () => {
timer.refresh() timer.refresh()
@@ -89,8 +99,46 @@ export async function streamExtractExternal(
clearTimeout(timer) clearTimeout(timer)
reject(error) reject(error)
}) })
.pipe(unzip.Extract({path: directory})) .pipe(unzip.Parse())
.on('close', () => { .pipe(
new stream.Transform({
objectMode: true,
transform: async (entry, _, callback) => {
const fullPath = path.normalize(path.join(directory, entry.path))
if (!directory.endsWith(path.sep)) {
directory += path.sep
}
if (!fullPath.startsWith(directory)) {
reject(new Error(`Malformed extraction path: ${fullPath}`))
}
if (entry.type === 'Directory') {
if (!createdDirectories.has(fullPath)) {
createdDirectories.add(fullPath)
await resolveOrCreateDirectory(fullPath).then(() => {
entry.autodrain()
callback()
})
} else {
entry.autodrain()
callback()
}
} else {
core.info(`Extracting artifact entry: ${fullPath}`)
if (!createdDirectories.has(path.dirname(fullPath))) {
createdDirectories.add(path.dirname(fullPath))
await resolveOrCreateDirectory(path.dirname(fullPath))
}
const writeStream = createWriteStream(fullPath)
writeStream.on('finish', callback)
writeStream.on('error', reject)
entry.pipe(writeStream)
}
}
})
)
.on('finish', async () => {
clearTimeout(timer) clearTimeout(timer)
resolve() resolve()
}) })
@@ -102,6 +102,7 @@ class ArtifactHttpClient implements Rpc {
} catch (error) { } catch (error) {
if (error instanceof SyntaxError) { if (error instanceof SyntaxError) {
debug(`Raw Body: ${rawBody}`) debug(`Raw Body: ${rawBody}`)
throw error
} }
if (error instanceof UsageError) { if (error instanceof UsageError) {
@@ -30,10 +30,10 @@ export function isGhes(): boolean {
const hostname = ghUrl.hostname.trimEnd().toUpperCase() const hostname = ghUrl.hostname.trimEnd().toUpperCase()
const isGitHubHost = hostname === 'GITHUB.COM' const isGitHubHost = hostname === 'GITHUB.COM'
const isGheHost = hostname.endsWith('.GHE.COM') const isGheHost =
const isLocalHost = hostname.endsWith('.LOCALHOST') hostname.endsWith('.GHE.COM') || hostname.endsWith('.GHE.LOCALHOST')
return !isGitHubHost && !isGheHost && !isLocalHost return !isGitHubHost && !isGheHost
} }
export function getGitHubWorkspaceDir(): string { export function getGitHubWorkspaceDir(): string {
@@ -57,7 +57,3 @@ export function getConcurrency(): number {
const concurrency = 16 * numCPUs const concurrency = 16 * numCPUs
return concurrency > 300 ? 300 : concurrency return concurrency > 300 ? 300 : concurrency
} }
export function getUploadChunkTimeout(): number {
return 300_000 // 5 minutes
}
@@ -1,11 +1,7 @@
import {BlobClient, BlockBlobUploadStreamOptions} from '@azure/storage-blob' import {BlobClient, BlockBlobUploadStreamOptions} from '@azure/storage-blob'
import {TransferProgressEvent} from '@azure/core-http' import {TransferProgressEvent} from '@azure/core-http'
import {ZipUploadStream} from './zip' import {ZipUploadStream} from './zip'
import { import {getUploadChunkSize, getConcurrency} from '../shared/config'
getUploadChunkSize,
getConcurrency,
getUploadChunkTimeout
} 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'
@@ -28,22 +24,6 @@ export async function uploadZipToBlobStorage(
zipUploadStream: ZipUploadStream zipUploadStream: ZipUploadStream
): Promise<BlobUploadResponse> { ): Promise<BlobUploadResponse> {
let uploadByteCount = 0 let uploadByteCount = 0
let lastProgressTime = Date.now()
const abortController = new AbortController()
const chunkTimer = async (interval: number): Promise<void> =>
new Promise((resolve, reject) => {
const timer = setInterval(() => {
if (Date.now() - lastProgressTime > interval) {
reject(new Error('Upload progress stalled.'))
}
}, interval)
abortController.signal.addEventListener('abort', () => {
clearInterval(timer)
resolve()
})
})
const maxConcurrency = getConcurrency() const maxConcurrency = getConcurrency()
const bufferSize = getUploadChunkSize() const bufferSize = getUploadChunkSize()
@@ -57,13 +37,11 @@ export async function uploadZipToBlobStorage(
const uploadCallback = (progress: TransferProgressEvent): void => { const uploadCallback = (progress: TransferProgressEvent): void => {
core.info(`Uploaded bytes ${progress.loadedBytes}`) core.info(`Uploaded bytes ${progress.loadedBytes}`)
uploadByteCount = progress.loadedBytes uploadByteCount = progress.loadedBytes
lastProgressTime = Date.now()
} }
const options: BlockBlobUploadStreamOptions = { const options: BlockBlobUploadStreamOptions = {
blobHTTPHeaders: {blobContentType: 'zip'}, blobHTTPHeaders: {blobContentType: 'zip'},
onProgress: uploadCallback, onProgress: uploadCallback
abortSignal: abortController.signal
} }
let sha256Hash: string | undefined = undefined let sha256Hash: string | undefined = undefined
@@ -76,22 +54,18 @@ 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 { try {
await Promise.race([ await blockBlobClient.uploadStream(
blockBlobClient.uploadStream( uploadStream,
uploadStream, bufferSize,
bufferSize, maxConcurrency,
maxConcurrency, options
options )
),
chunkTimer(getUploadChunkTimeout())
])
} catch (error) { } catch (error) {
if (NetworkError.isNetworkErrorCode(error?.code)) { if (NetworkError.isNetworkErrorCode(error?.code)) {
throw new NetworkError(error?.code) throw new NetworkError(error?.code)
} }
throw error throw error
} finally {
abortController.abort()
} }
core.info('Finished uploading artifact content to blob storage!') core.info('Finished uploading artifact content to blob storage!')
@@ -105,6 +79,7 @@ export async function uploadZipToBlobStorage(
`No data was uploaded to blob storage. Reported upload byte count is 0.` `No data was uploaded to blob storage. Reported upload byte count is 0.`
) )
} }
return { return {
uploadSize: uploadByteCount, uploadSize: uploadByteCount,
sha256Hash sha256Hash
@@ -13,12 +13,6 @@ export interface UploadZipSpecification {
* The destination path in a zip for a file * The destination path in a zip for a file
*/ */
destinationPath: string destinationPath: string
/**
* Information about the file
* https://nodejs.org/api/fs.html#class-fsstats
*/
stats: fs.Stats
} }
/** /**
@@ -81,11 +75,10 @@ export function getUploadZipSpecification(
- file3.txt - file3.txt
*/ */
for (let file of filesToZip) { for (let file of filesToZip) {
const stats = fs.lstatSync(file, {throwIfNoEntry: false}) if (!fs.existsSync(file)) {
if (!stats) {
throw new Error(`File ${file} does not exist`) throw new Error(`File ${file} does not exist`)
} }
if (!stats.isDirectory()) { if (!fs.statSync(file).isDirectory()) {
// Normalize and resolve, this allows for either absolute or relative paths to be used // Normalize and resolve, this allows for either absolute or relative paths to be used
file = normalize(file) file = normalize(file)
file = resolve(file) file = resolve(file)
@@ -101,8 +94,7 @@ export function getUploadZipSpecification(
specification.push({ specification.push({
sourcePath: file, sourcePath: file,
destinationPath: uploadPath, destinationPath: uploadPath
stats
}) })
} else { } else {
// Empty directory // Empty directory
@@ -111,8 +103,7 @@ export function getUploadZipSpecification(
specification.push({ specification.push({
sourcePath: null, sourcePath: null,
destinationPath: directoryPath, destinationPath: directoryPath
stats
}) })
} }
} }
+3 -9
View File
@@ -1,7 +1,7 @@
import * as stream from 'stream' import * as stream from 'stream'
import {realpath} from 'fs/promises'
import * as archiver from 'archiver' import * as archiver from 'archiver'
import * as core from '@actions/core' import * as core from '@actions/core'
import {createReadStream} from 'fs'
import {UploadZipSpecification} from './upload-zip-specification' import {UploadZipSpecification} from './upload-zip-specification'
import {getUploadChunkSize} from '../shared/config' import {getUploadChunkSize} from '../shared/config'
@@ -43,14 +43,8 @@ export async function createZipUploadStream(
for (const file of uploadSpecification) { for (const file of uploadSpecification) {
if (file.sourcePath !== null) { if (file.sourcePath !== null) {
// Check if symlink and resolve the source path // Add a normal file to the zip
let sourcePath = file.sourcePath zip.append(createReadStream(file.sourcePath), {
if (file.stats.isSymbolicLink()) {
sourcePath = await realpath(file.sourcePath)
}
// Add the file to the zip
zip.file(sourcePath, {
name: file.destinationPath name: file.destinationPath
}) })
} else { } else {
+13 -32
View File
@@ -12,9 +12,6 @@ Once the attestation has been created and signed, it will be uploaded to the GH
attestations API and associated with the repository from which the workflow was attestations API and associated with the repository from which the workflow was
initiated. initiated.
See [Using artifact attestations to establish provenance for builds](https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)
for more information on artifact attestations.
## Usage ## Usage
### `attest` ### `attest`
@@ -32,7 +29,8 @@ async function run() {
const ghToken = core.getInput('gh-token'); const ghToken = core.getInput('gh-token');
const attestation = await attest({ const attestation = await attest({
subjects: [{name: 'my-artifact-name', digest: { 'sha256': '36ab4667...'}}], subjectName: 'my-artifact-name',
subjectDigest: { 'sha256': '36ab4667...'},
predicateType: 'https://in-toto.io/attestation/release', predicateType: 'https://in-toto.io/attestation/release',
predicate: { . . . }, predicate: { . . . },
token: ghToken token: ghToken
@@ -48,12 +46,11 @@ The `attest` function supports the following options:
```typescript ```typescript
export type AttestOptions = { export type AttestOptions = {
// Deprecated. Use 'subjects' instead. // The name of the subject to be attested.
subjectName?: string subjectName: string
// Deprecated. Use 'subjects' instead. // The digest of the subject to be attested. Should be a map of digest
subjectDigest?: Record<string, string> // algorithms to their hex-encoded values.
// Collection of subjects to be attested subjectDigest: Record<string, string>
subjects?: Subject[]
// URI identifying the content type of the predicate being attested. // URI identifying the content type of the predicate being attested.
predicateType: string predicateType: string
// Predicate to be attested. // Predicate to be attested.
@@ -63,18 +60,9 @@ export type AttestOptions = {
// Sigstore instance to use for signing. Must be one of "public-good" or // Sigstore instance to use for signing. Must be one of "public-good" or
// "github". // "github".
sigstore?: 'public-good' | 'github' sigstore?: 'public-good' | 'github'
// HTTP headers to include in request to attestations API.
headers?: {[header: string]: string | number | undefined}
// Whether to skip writing the attestation to the GH attestations API. // Whether to skip writing the attestation to the GH attestations API.
skipWrite?: boolean skipWrite?: boolean
} }
export type Subject = {
// Name of the subject.
name: string
// Digests of the subject. Should be a map of digest algorithms to their hex-encoded values.
digest: Record<string, string>
}
``` ```
### `attestProvenance` ### `attestProvenance`
@@ -112,25 +100,18 @@ The `attestProvenance` function supports the following options:
```typescript ```typescript
export type AttestProvenanceOptions = { export type AttestProvenanceOptions = {
// Deprecated. Use 'subjects' instead. // The name of the subject to be attested.
subjectName?: string subjectName: string
// Deprecated. Use 'subjects' instead. // The digest of the subject to be attested. Should be a map of digest
subjectDigest?: Record<string, string> // algorithms to their hex-encoded values.
// Collection of subjects to be attested subjectDigest: Record<string, string>
subjects?: Subject[] // GitHub token for writing attestations.
// URI identifying the content type of the predicate being attested.
token: string token: string
// Sigstore instance to use for signing. Must be one of "public-good" or // Sigstore instance to use for signing. Must be one of "public-good" or
// "github". // "github".
sigstore?: 'public-good' | 'github' sigstore?: 'public-good' | 'github'
// HTTP headers to include in request to attestations API.
headers?: {[header: string]: string | number | undefined}
// Whether to skip writing the attestation to the GH attestations API. // Whether to skip writing the attestation to the GH attestations API.
skipWrite?: boolean skipWrite?: boolean
// Issuer URL responsible for minting the OIDC token from which the
// provenance data is read. Defaults to
// 'https://token.actions.githubusercontent.com".
issuer?: string
} }
``` ```
-47
View File
@@ -1,52 +1,5 @@
# @actions/attest Releases # @actions/attest Releases
### 1.5.0
- Bump @actions/core from 1.10.1 to 1.11.1 [#1847](https://github.com/actions/toolkit/pull/1847)
- Bump @sigstore/bundle from 2.3.2 to 3.0.0 [#1846](https://github.com/actions/toolkit/pull/1846)
- Bump @sigstore/sign from 2.3.2 to 3.0.0 [#1846](https://github.com/actions/toolkit/pull/1846)
- Support for generating multi-subject attestations [#1864](https://github.com/actions/toolkit/pull/1865)
- Fix bug in `buildSLSAProvenancePredicate` related to `workflow_ref` OIDC token claims containing the "@" symbol in the tag name [#1863](https://github.com/actions/toolkit/pull/1863)
### 1.4.2
- Fix bug in `buildSLSAProvenancePredicate`/`attestProvenance` when generating provenance statement for enterprise account using customized OIDC issuer value [#1823](https://github.com/actions/toolkit/pull/1823)
### 1.4.1
- Bump @actions/http-client from 2.2.1 to 2.2.3 [#1805](https://github.com/actions/toolkit/pull/1805)
### 1.4.0
- Add new `headers` parameter to the `attest` and `attestProvenance` functions [#1790](https://github.com/actions/toolkit/pull/1790)
- Update `buildSLSAProvenancePredicate`/`attestProvenance` to automatically derive default OIDC issuer URL from current execution context [#1796](https://github.com/actions/toolkit/pull/1796)
### 1.3.1
- Fix bug with proxy support when retrieving JWKS for OIDC issuer [#1776](https://github.com/actions/toolkit/pull/1776)
### 1.3.0
- Dynamic construction of Sigstore API URLs [#1735](https://github.com/actions/toolkit/pull/1735)
- Switch to new GH provenance build type [#1745](https://github.com/actions/toolkit/pull/1745)
- Fetch existing Rekor entry on 409 conflict error [#1759](https://github.com/actions/toolkit/pull/1759)
- Bump @sigstore/bundle from 2.3.0 to 2.3.2 [#1738](https://github.com/actions/toolkit/pull/1738)
- Bump @sigstore/sign from 2.3.0 to 2.3.2 [#1738](https://github.com/actions/toolkit/pull/1738)
### 1.2.1
- Retry request on attestation persistence failure [#1725](https://github.com/actions/toolkit/pull/1725)
### 1.2.0
- Generate attestations using the v0.3 Sigstore bundle format [#1701](https://github.com/actions/toolkit/pull/1701)
- Bump @sigstore/bundle from 2.2.0 to 2.3.0 [#1701](https://github.com/actions/toolkit/pull/1701)
- Bump @sigstore/sign from 2.2.3 to 2.3.0 [#1701](https://github.com/actions/toolkit/pull/1701)
- Remove dependency on make-fetch-happen [#1714](https://github.com/actions/toolkit/pull/1714)
### 1.1.0
- Updates the `attestProvenance` function to retrieve a token from the GitHub OIDC provider and use the token claims to populate the provenance statement [#1693](https://github.com/actions/toolkit/pull/1693)
### 1.0.0 ### 1.0.0
- Initial release - Initial release
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`buildIntotoStatement returns an intoto statement 1`] = ` exports[`buildIntotoStatement returns a provenance hydrated from env vars 1`] = `
{ {
"_type": "https://in-toto.io/Statement/v1", "_type": "https://in-toto.io/Statement/v1",
"predicate": { "predicate": {
@@ -1,57 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`provenance functions buildSLSAProvenancePredicate handle tags including "@" character 1`] = ` exports[`buildSLSAProvenancePredicate returns a provenance hydrated from env vars 1`] = `
{ {
"params": { "params": {
"buildDefinition": { "buildDefinition": {
"buildType": "https://actions.github.io/buildtypes/workflow/v1", "buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"path": ".github/workflows/main.yml",
"ref": "[email protected]",
"repository": "https://foo.ghe.com/owner/repo",
},
},
"internalParameters": {
"github": {
"event_name": "push",
"repository_id": "repo-id",
"repository_owner_id": "owner-id",
"runner_environment": "github-hosted",
},
},
"resolvedDependencies": [
{
"digest": {
"gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b",
},
"uri": "git+https://foo.ghe.com/owner/repo@refs/heads/main",
},
],
},
"runDetails": {
"builder": {
"id": "https://foo.ghe.com/owner/workflows/.github/workflows/publish.yml@main",
},
"metadata": {
"invocationId": "https://foo.ghe.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
},
},
},
"type": "https://slsa.dev/provenance/v1",
}
`;
exports[`provenance functions buildSLSAProvenancePredicate returns a provenance hydrated from an OIDC token 1`] = `
{
"params": {
"buildDefinition": {
"buildType": "https://actions.github.io/buildtypes/workflow/v1",
"externalParameters": { "externalParameters": {
"workflow": { "workflow": {
"path": ".github/workflows/main.yml", "path": ".github/workflows/main.yml",
"ref": "main", "ref": "main",
"repository": "https://foo.ghe.com/owner/repo", "repository": "https://github.com/owner/repo",
}, },
}, },
"internalParameters": { "internalParameters": {
@@ -59,7 +17,6 @@ exports[`provenance functions buildSLSAProvenancePredicate returns a provenance
"event_name": "push", "event_name": "push",
"repository_id": "repo-id", "repository_id": "repo-id",
"repository_owner_id": "owner-id", "repository_owner_id": "owner-id",
"runner_environment": "github-hosted",
}, },
}, },
"resolvedDependencies": [ "resolvedDependencies": [
@@ -67,16 +24,16 @@ exports[`provenance functions buildSLSAProvenancePredicate returns a provenance
"digest": { "digest": {
"gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b", "gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b",
}, },
"uri": "git+https://foo.ghe.com/owner/repo@refs/heads/main", "uri": "git+https://github.com/owner/repo@refs/heads/main",
}, },
], ],
}, },
"runDetails": { "runDetails": {
"builder": { "builder": {
"id": "https://foo.ghe.com/owner/workflows/.github/workflows/publish.yml@main", "id": "https://github.com/actions/runner/github-hosted",
}, },
"metadata": { "metadata": {
"invocationId": "https://foo.ghe.com/owner/repo/actions/runs/run-id/attempts/run-attempt", "invocationId": "https://github.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
}, },
}, },
}, },
-16
View File
@@ -1,16 +0,0 @@
import {attest} from '../src/attest'
describe('attest', () => {
describe('when no subject information is provided', () => {
it('throws an error', async () => {
const options = {
predicateType: 'foo',
predicate: {bar: 'baz'},
token: 'token'
}
expect(attest(options)).rejects.toThrowError(
'Must provide either subjectName and subjectDigest or subjects'
)
})
})
})
@@ -1,41 +0,0 @@
import {signingEndpoints} from '../src/endpoints'
describe('signingEndpoints', () => {
const originalEnv = process.env
afterEach(() => {
process.env = originalEnv
})
describe('when using github.com', () => {
beforeEach(async () => {
process.env = {
...originalEnv,
GITHUB_SERVER_URL: 'https://github.com'
}
})
it('returns expected endpoints', async () => {
const endpoints = signingEndpoints('github')
expect(endpoints.fulcioURL).toEqual('https://fulcio.githubapp.com')
expect(endpoints.tsaServerURL).toEqual('https://timestamp.githubapp.com')
})
})
describe('when using custom domain', () => {
beforeEach(async () => {
process.env = {
...originalEnv,
GITHUB_SERVER_URL: 'https://foo.bar.com'
}
})
it('returns a expected endpoints', async () => {
const endpoints = signingEndpoints('github')
expect(endpoints.fulcioURL).toEqual('https://fulcio.foo.bar.com')
expect(endpoints.tsaServerURL).toEqual('https://timestamp.foo.bar.com')
})
})
})
+2 -2
View File
@@ -16,8 +16,8 @@ describe('buildIntotoStatement', () => {
} }
} }
it('returns an intoto statement', () => { it('returns a provenance hydrated from env vars', () => {
const statement = buildIntotoStatement([subject], predicate) const statement = buildIntotoStatement(subject, predicate)
expect(statement).toMatchSnapshot() expect(statement).toMatchSnapshot()
}) })
}) })
-199
View File
@@ -1,199 +0,0 @@
import * as jose from 'jose'
import nock from 'nock'
import {getIDTokenClaims} from '../src/oidc'
describe('getIDTokenClaims', () => {
const originalEnv = process.env
const issuer = 'https://example.com'
const audience = 'nobody'
const requestToken = 'token'
const openidConfigPath = '/.well-known/openid-configuration'
const jwksPath = '/.well-known/jwks.json'
const tokenPath = '/token'
const openIDConfig = {jwks_uri: `${issuer}${jwksPath}`}
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
let key: any
beforeEach(async () => {
process.env = {
...originalEnv,
ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`,
ACTIONS_ID_TOKEN_REQUEST_TOKEN: requestToken
}
// Generate JWT signing key
key = await jose.generateKeyPair('PS256')
// Create JWK and JWKS
const jwk = await jose.exportJWK(key.publicKey)
const jwks = {keys: [jwk]}
nock(issuer).get(openidConfigPath).reply(200, openIDConfig)
nock(issuer).get(jwksPath).reply(200, jwks)
})
afterEach(() => {
process.env = originalEnv
})
describe('when ID token is valid', () => {
const claims = {
iss: issuer,
aud: audience,
ref: 'ref',
sha: 'sha',
repository: 'repo',
event_name: 'push',
job_workflow_ref: 'job_workflow_ref',
workflow_ref: 'workflow',
repository_id: '1',
repository_owner_id: '1',
runner_environment: 'github-hosted',
run_id: '1',
run_attempt: '1'
}
beforeEach(async () => {
const jwt = await new jose.SignJWT(claims)
.setProtectedHeader({alg: 'PS256'})
.sign(key.privateKey)
nock(issuer).get(tokenPath).query({audience}).reply(200, {value: jwt})
})
it('returns the ID token claims', async () => {
const result = await getIDTokenClaims(issuer)
expect(result).toEqual(claims)
})
})
describe('when ID token is valid (w/ enterprise slug)', () => {
const claims = {
iss: `${issuer}/foo-bar`,
aud: audience,
ref: 'ref',
sha: 'sha',
repository: 'repo',
event_name: 'push',
job_workflow_ref: 'job_workflow_ref',
workflow_ref: 'workflow',
repository_id: '1',
repository_owner_id: '1',
runner_environment: 'github-hosted',
run_id: '1',
run_attempt: '1'
}
beforeEach(async () => {
const jwt = await new jose.SignJWT(claims)
.setProtectedHeader({alg: 'PS256'})
.sign(key.privateKey)
nock(issuer).get(tokenPath).query({audience}).reply(200, {value: jwt})
})
it('returns the ID token claims', async () => {
const result = await getIDTokenClaims(issuer)
expect(result).toEqual(claims)
})
})
describe('when ID token is missing the "iss" claim', () => {
const claims = {
aud: audience
}
beforeEach(async () => {
const jwt = await new jose.SignJWT(claims)
.setProtectedHeader({alg: 'PS256'})
.sign(key.privateKey)
nock(issuer).get(tokenPath).query({audience}).reply(200, {value: jwt})
})
it('throws an error', async () => {
await expect(getIDTokenClaims(issuer)).rejects.toThrow(/missing "iss"/i)
})
})
describe('when ID token is missing required claims', () => {
const claims = {
iss: issuer,
aud: audience
}
beforeEach(async () => {
const jwt = await new jose.SignJWT(claims)
.setProtectedHeader({alg: 'PS256'})
.sign(key.privateKey)
nock(issuer).get(tokenPath).query({audience}).reply(200, {value: jwt})
})
it('throws an error', async () => {
await expect(getIDTokenClaims(issuer)).rejects.toThrow(/missing claims/i)
})
})
describe('when ID has the wrong issuer', () => {
const claims = {foo: 'bar', iss: 'foo', aud: 'nobody'}
beforeEach(async () => {
const jwt = await new jose.SignJWT(claims)
.setProtectedHeader({alg: 'PS256'})
.sign(key.privateKey)
nock(issuer).get(tokenPath).query({audience}).reply(200, {value: jwt})
})
it('throws an error', async () => {
await expect(getIDTokenClaims(issuer)).rejects.toThrow(
/unexpected "iss"/i
)
})
})
describe('when ID has the wrong audience', () => {
const claims = {foo: 'bar', iss: issuer, aud: 'bar'}
beforeEach(async () => {
const jwt = await new jose.SignJWT(claims)
.setProtectedHeader({alg: 'PS256'})
.sign(key.privateKey)
nock(issuer).get(tokenPath).query({audience}).reply(200, {value: jwt})
})
it('throw an error', async () => {
await expect(getIDTokenClaims(issuer)).rejects.toThrow(/unexpected "aud"/)
})
})
describe('when openid config cannot be retrieved', () => {
const claims = {foo: 'bar', iss: issuer, aud: 'nobody'}
beforeEach(async () => {
const jwt = await new jose.SignJWT(claims)
.setProtectedHeader({alg: 'PS256'})
.sign(key.privateKey)
nock(issuer).get(tokenPath).query({audience}).reply(200, {value: jwt})
// Disable the openid config endpoint
nock.removeInterceptor({
proto: 'https',
hostname: 'example.com',
port: '443',
method: 'GET',
path: openidConfigPath
})
})
it('throws an error', async () => {
await expect(getIDTokenClaims(issuer)).rejects.toThrow(
/failed to get id/i
)
})
})
})
+163 -208
View File
@@ -1,258 +1,213 @@
import * as github from '@actions/github' import * as github from '@actions/github'
import {mockFulcio, mockRekor, mockTSA} from '@sigstore/mock' import {mockFulcio, mockRekor, mockTSA} from '@sigstore/mock'
import * as jose from 'jose'
import nock from 'nock' import nock from 'nock'
import {MockAgent, setGlobalDispatcher} from 'undici' import {SIGSTORE_GITHUB, SIGSTORE_PUBLIC_GOOD} from '../src/endpoints'
import {SIGSTORE_PUBLIC_GOOD, signingEndpoints} from '../src/endpoints'
import {attestProvenance, buildSLSAProvenancePredicate} from '../src/provenance' import {attestProvenance, buildSLSAProvenancePredicate} from '../src/provenance'
describe('provenance functions', () => { // Dummy workflow environment
const env = {
GITHUB_REPOSITORY: 'owner/repo',
GITHUB_REF: 'refs/heads/main',
GITHUB_SHA: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
GITHUB_WORKFLOW_REF: 'owner/repo/.github/workflows/main.yml@main',
GITHUB_SERVER_URL: 'https://github.com',
GITHUB_EVENT_NAME: 'push',
GITHUB_REPOSITORY_ID: 'repo-id',
GITHUB_REPOSITORY_OWNER_ID: 'owner-id',
GITHUB_RUN_ID: 'run-id',
GITHUB_RUN_ATTEMPT: 'run-attempt',
RUNNER_ENVIRONMENT: 'github-hosted'
}
describe('buildSLSAProvenancePredicate', () => {
it('returns a provenance hydrated from env vars', () => {
const predicate = buildSLSAProvenancePredicate(env)
expect(predicate).toMatchSnapshot()
})
})
describe('attestProvenance', () => {
// Capture original environment variables so we can restore them after each
// test
const originalEnv = process.env const originalEnv = process.env
const issuer = 'https://token.actions.foo.ghe.com'
const audience = 'nobody'
const jwksPath = '/.well-known/jwks.json'
const tokenPath = '/token'
// MockAgent for mocking @actions/github // Subject to attest
const mockAgent = new MockAgent() const subjectName = 'subjective'
setGlobalDispatcher(mockAgent) const subjectDigest = {
sha256: '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
const claims = {
iss: issuer,
aud: 'nobody',
repository: 'owner/repo',
ref: 'refs/heads/main',
sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
job_workflow_ref: 'owner/workflows/.github/workflows/publish.yml@main',
workflow_ref: 'owner/repo/.github/workflows/main.yml@main',
event_name: 'push',
repository_id: 'repo-id',
repository_owner_id: 'owner-id',
run_id: 'run-id',
run_attempt: 'run-attempt',
runner_environment: 'github-hosted'
} }
const mockIssuer = async (claims: jose.JWTPayload): Promise<void> => { // Fake an OIDC token
// Generate JWT signing key const oidcPayload = {sub: '[email protected]', iss: ''}
const key = await jose.generateKeyPair('PS256') const oidcToken = `.${Buffer.from(JSON.stringify(oidcPayload)).toString(
'base64'
)}.}`
// Create JWK, JWKS, and JWT const tokenURL = 'https://token.url'
const jwk = await jose.exportJWK(key.publicKey) const attestationID = '1234567890'
const jwks = {keys: [jwk]}
const jwt = await new jose.SignJWT(claims)
.setProtectedHeader({alg: 'PS256'})
.sign(key.privateKey)
// Mock OpenID configuration and JWKS endpoints
nock(issuer)
.get('/.well-known/openid-configuration')
.reply(200, {jwks_uri: `${issuer}${jwksPath}`})
nock(issuer).get(jwksPath).reply(200, jwks)
// Mock OIDC token endpoint for populating the provenance
nock(issuer).get(tokenPath).query({audience}).reply(200, {value: jwt})
}
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks()
nock(tokenURL)
.get('/')
.query({audience: 'sigstore'})
.reply(200, {value: oidcToken})
// Set-up GHA environment variables
process.env = { process.env = {
...originalEnv, ...originalEnv,
ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`, ...env,
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token', ACTIONS_ID_TOKEN_REQUEST_URL: tokenURL,
GITHUB_SERVER_URL: 'https://foo.ghe.com', ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token'
GITHUB_REPOSITORY: claims.repository
} }
await mockIssuer(claims)
}) })
afterEach(() => { afterEach(() => {
// Restore the original environment
process.env = originalEnv process.env = originalEnv
}) })
describe('buildSLSAProvenancePredicate', () => { describe('when using the github Sigstore instance', () => {
it('returns a provenance hydrated from an OIDC token', async () => { const {fulcioURL, tsaServerURL} = SIGSTORE_GITHUB
const predicate = await buildSLSAProvenancePredicate()
expect(predicate).toMatchSnapshot()
})
it('handle tags including "@" character', async () => {
nock.cleanAll()
await mockIssuer({
...claims,
workflow_ref: 'owner/repo/.github/workflows/main.yml@[email protected]'
})
const predicate = await buildSLSAProvenancePredicate()
expect(predicate).toMatchSnapshot()
})
})
describe('attestProvenance', () => {
// Subject to attest
const subjectName = 'subjective'
const subjectDigest = {
sha256: '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
}
// Fake an OIDC token
const oidcPayload = {sub: '[email protected]', iss: ''}
const oidcToken = `.${Buffer.from(JSON.stringify(oidcPayload)).toString(
'base64'
)}.}`
const attestationID = '1234567890'
beforeEach(async () => { beforeEach(async () => {
nock(issuer) // Mock Sigstore
.get(tokenPath) await mockFulcio({baseURL: fulcioURL, strict: false})
.query({audience: 'sigstore'}) await mockTSA({baseURL: tsaServerURL})
.reply(200, {value: oidcToken})
// Mock GH attestations API
nock('https://api.github.com')
.post(/^\/repos\/.*\/.*\/attestations$/)
.reply(201, {id: attestationID})
}) })
describe('when using the github Sigstore instance', () => { describe('when the sigstore instance is explicitly set', () => {
beforeEach(async () => { it('attests provenance', async () => {
const {fulcioURL, tsaServerURL} = signingEndpoints('github') const attestation = await attestProvenance({
subjectName,
// Mock Sigstore subjectDigest,
await mockFulcio({baseURL: fulcioURL, strict: false}) token: 'token',
await mockTSA({baseURL: tsaServerURL}) sigstore: 'github'
mockAgent
.get('https://api.github.com')
.intercept({
path: /^\/repos\/.*\/.*\/attestations$/,
method: 'post'
})
.reply(201, {id: attestationID})
})
describe('when the sigstore instance is explicitly set', () => {
it('attests provenance', async () => {
const attestation = await attestProvenance({
subjects: [{name: subjectName, digest: subjectDigest}],
token: 'token',
sigstore: 'github'
})
expect(attestation).toBeDefined()
expect(attestation.bundle).toBeDefined()
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeUndefined()
expect(attestation.attestationID).toBe(attestationID)
})
})
describe('when the sigstore instance is inferred from the repo visibility', () => {
const savedRepository = github.context.payload.repository
beforeEach(() => {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
github.context.payload.repository = {visibility: 'private'} as any
}) })
afterEach(() => { expect(attestation).toBeDefined()
github.context.payload.repository = savedRepository expect(attestation.bundle).toBeDefined()
}) expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeUndefined()
it('attests provenance', async () => { expect(attestation.attestationID).toBe(attestationID)
const attestation = await attestProvenance({
subjects: [{name: subjectName, digest: subjectDigest}],
token: 'token'
})
expect(attestation).toBeDefined()
expect(attestation.bundle).toBeDefined()
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeUndefined()
expect(attestation.attestationID).toBe(attestationID)
})
}) })
}) })
describe('when using the public-good Sigstore instance', () => { describe('when the sigstore instance is inferred from the repo visibility', () => {
const {fulcioURL, rekorURL} = SIGSTORE_PUBLIC_GOOD const savedRepository = github.context.payload.repository
beforeEach(async () => { beforeEach(() => {
// Mock Sigstore /* eslint-disable-next-line @typescript-eslint/no-explicit-any */
await mockFulcio({baseURL: fulcioURL, strict: false}) github.context.payload.repository = {visibility: 'private'} as any
await mockRekor({baseURL: rekorURL})
// Mock GH attestations API
mockAgent
.get('https://api.github.com')
.intercept({
path: /^\/repos\/.*\/.*\/attestations$/,
method: 'post'
})
.reply(201, {id: attestationID})
}) })
describe('when the sigstore instance is explicitly set', () => { afterEach(() => {
it('attests provenance', async () => { github.context.payload.repository = savedRepository
const attestation = await attestProvenance({
subjects: [{name: subjectName, digest: subjectDigest}],
token: 'token',
sigstore: 'public-good'
})
expect(attestation).toBeDefined()
expect(attestation.bundle).toBeDefined()
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeDefined()
expect(attestation.attestationID).toBe(attestationID)
})
})
describe('when the sigstore instance is inferred from the repo visibility', () => {
const savedRepository = github.context.payload.repository
beforeEach(() => {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
github.context.payload.repository = {visibility: 'public'} as any
})
afterEach(() => {
github.context.payload.repository = savedRepository
})
it('attests provenance', async () => {
const attestation = await attestProvenance({
subjects: [{name: subjectName, digest: subjectDigest}],
token: 'token'
})
expect(attestation).toBeDefined()
expect(attestation.bundle).toBeDefined()
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeDefined()
expect(attestation.attestationID).toBe(attestationID)
})
})
})
describe('when skipWrite is set to true', () => {
const {fulcioURL, rekorURL} = SIGSTORE_PUBLIC_GOOD
beforeEach(async () => {
// Mock Sigstore
await mockFulcio({baseURL: fulcioURL, strict: false})
await mockRekor({baseURL: rekorURL})
}) })
it('attests provenance', async () => {
const attestation = await attestProvenance({
subjectName,
subjectDigest,
token: 'token'
})
expect(attestation).toBeDefined()
expect(attestation.bundle).toBeDefined()
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeUndefined()
expect(attestation.attestationID).toBe(attestationID)
})
})
})
describe('when using the public-good Sigstore instance', () => {
const {fulcioURL, rekorURL} = SIGSTORE_PUBLIC_GOOD
beforeEach(async () => {
// Mock Sigstore
await mockFulcio({baseURL: fulcioURL, strict: false})
await mockRekor({baseURL: rekorURL})
// Mock GH attestations API
nock('https://api.github.com')
.post(/^\/repos\/.*\/.*\/attestations$/)
.reply(201, {id: attestationID})
})
describe('when the sigstore instance is explicitly set', () => {
it('attests provenance', async () => { it('attests provenance', async () => {
const attestation = await attestProvenance({ const attestation = await attestProvenance({
subjectName, subjectName,
subjectDigest, subjectDigest,
token: 'token', token: 'token',
sigstore: 'public-good', sigstore: 'public-good'
skipWrite: true
}) })
expect(attestation).toBeDefined() expect(attestation).toBeDefined()
expect(attestation.bundle).toBeDefined() expect(attestation.bundle).toBeDefined()
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/) expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeDefined() expect(attestation.tlogID).toBeDefined()
expect(attestation.attestationID).toBeUndefined() expect(attestation.attestationID).toBe(attestationID)
})
})
describe('when the sigstore instance is inferred from the repo visibility', () => {
const savedRepository = github.context.payload.repository
beforeEach(() => {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
github.context.payload.repository = {visibility: 'public'} as any
})
afterEach(() => {
github.context.payload.repository = savedRepository
})
it('attests provenance', async () => {
const attestation = await attestProvenance({
subjectName,
subjectDigest,
token: 'token'
})
expect(attestation).toBeDefined()
expect(attestation.bundle).toBeDefined()
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeDefined()
expect(attestation.attestationID).toBe(attestationID)
}) })
}) })
}) })
describe('when skipWrite is set to true', () => {
const {fulcioURL, rekorURL} = SIGSTORE_PUBLIC_GOOD
beforeEach(async () => {
// Mock Sigstore
await mockFulcio({baseURL: fulcioURL, strict: false})
await mockRekor({baseURL: rekorURL})
})
it('attests provenance', async () => {
const attestation = await attestProvenance({
subjectName,
subjectDigest,
token: 'token',
sigstore: 'public-good',
skipWrite: true
})
expect(attestation).toBeDefined()
expect(attestation.bundle).toBeDefined()
expect(attestation.certificate).toMatch(/-----BEGIN CERTIFICATE-----/)
expect(attestation.tlogID).toBeDefined()
expect(attestation.attestationID).toBeUndefined()
})
})
}) })
+8 -4
View File
@@ -64,11 +64,13 @@ describe('signProvenance', () => {
expect(att).toBeDefined() expect(att).toBeDefined()
expect(att.mediaType).toEqual( expect(att.mediaType).toEqual(
'application/vnd.dev.sigstore.bundle.v0.3+json' 'application/vnd.dev.sigstore.bundle+json;version=0.2'
) )
expect(att.content.$case).toEqual('dsseEnvelope') expect(att.content.$case).toEqual('dsseEnvelope')
expect(att.verificationMaterial.content.$case).toEqual('certificate') expect(att.verificationMaterial.content.$case).toEqual(
'x509CertificateChain'
)
expect(att.verificationMaterial.tlogEntries).toHaveLength(1) expect(att.verificationMaterial.tlogEntries).toHaveLength(1)
expect( expect(
att.verificationMaterial.timestampVerificationData?.rfc3161Timestamps att.verificationMaterial.timestampVerificationData?.rfc3161Timestamps
@@ -87,11 +89,13 @@ describe('signProvenance', () => {
expect(att).toBeDefined() expect(att).toBeDefined()
expect(att.mediaType).toEqual( expect(att.mediaType).toEqual(
'application/vnd.dev.sigstore.bundle.v0.3+json' 'application/vnd.dev.sigstore.bundle+json;version=0.2'
) )
expect(att.content.$case).toEqual('dsseEnvelope') expect(att.content.$case).toEqual('dsseEnvelope')
expect(att.verificationMaterial.content.$case).toEqual('certificate') expect(att.verificationMaterial.content.$case).toEqual(
'x509CertificateChain'
)
expect(att.verificationMaterial.tlogEntries).toHaveLength(0) expect(att.verificationMaterial.tlogEntries).toHaveLength(0)
expect( expect(
att.verificationMaterial.timestampVerificationData?.rfc3161Timestamps att.verificationMaterial.timestampVerificationData?.rfc3161Timestamps
+17 -65
View File
@@ -1,14 +1,10 @@
import {MockAgent, setGlobalDispatcher} from 'undici' import nock from 'nock'
import {writeAttestation} from '../src/store' import {writeAttestation} from '../src/store'
describe('writeAttestation', () => { describe('writeAttestation', () => {
const originalEnv = process.env const originalEnv = process.env
const attestation = {foo: 'bar '} const attestation = {foo: 'bar '}
const token = 'token' const token = 'token'
const headers = {'X-GitHub-Foo': 'true'}
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
beforeEach(() => { beforeEach(() => {
process.env = { process.env = {
@@ -23,71 +19,27 @@ describe('writeAttestation', () => {
describe('when the api call is successful', () => { describe('when the api call is successful', () => {
beforeEach(() => { beforeEach(() => {
mockAgent nock('https://api.github.com')
.get('https://api.github.com') .matchHeader('authorization', `token ${token}`)
.intercept({ .post('/repos/foo/bar/attestations', {bundle: attestation})
path: '/repos/foo/bar/attestations',
method: 'POST',
headers: {authorization: `token ${token}`, ...headers},
body: JSON.stringify({bundle: attestation})
})
.reply(201, {id: '123'}) .reply(201, {id: '123'})
}) })
it('persists the attestation', async () => {
await expect(
writeAttestation(attestation, token, {headers})
).resolves.toEqual('123')
})
})
describe('when the api call fails', () => {
beforeEach(() => {
mockAgent
.get('https://api.github.com')
.intercept({
path: '/repos/foo/bar/attestations',
method: 'POST',
headers: {authorization: `token ${token}`},
body: JSON.stringify({bundle: attestation})
})
.reply(500, 'oops')
})
it('throws an error', async () => {
await expect(
writeAttestation(attestation, token, {retry: 0})
).rejects.toThrow(/oops/)
})
})
describe('when the api call fails but succeeds on retry', () => {
beforeEach(() => {
const pool = mockAgent.get('https://api.github.com')
pool
.intercept({
path: '/repos/foo/bar/attestations',
method: 'POST',
headers: {authorization: `token ${token}`},
body: JSON.stringify({bundle: attestation})
})
.reply(500, 'oops')
.times(1)
pool
.intercept({
path: '/repos/foo/bar/attestations',
method: 'POST',
headers: {authorization: `token ${token}`},
body: JSON.stringify({bundle: attestation})
})
.reply(201, {id: '123'})
.times(1)
})
it('persists the attestation', async () => { it('persists the attestation', async () => {
await expect(writeAttestation(attestation, token)).resolves.toEqual('123') await expect(writeAttestation(attestation, token)).resolves.toEqual('123')
}) })
}) })
describe('when the api call fails', () => {
beforeEach(() => {
nock('https://api.github.com')
.matchHeader('authorization', `token ${token}`)
.post('/repos/foo/bar/attestations', {bundle: attestation})
.reply(500, 'oops')
})
it('persists the attestation', async () => {
await expect(writeAttestation(attestation, token)).rejects.toThrow(/oops/)
})
})
}) })
+941 -1074
View File
File diff suppressed because it is too large Load Diff
+8 -17
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/attest", "name": "@actions/attest",
"version": "1.5.0", "version": "1.0.0",
"description": "Actions attestation lib", "description": "Actions attestation lib",
"keywords": [ "keywords": [
"github", "github",
@@ -35,24 +35,15 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"devDependencies": { "devDependencies": {
"@sigstore/mock": "^0.8.0", "@sigstore/mock": "^0.6.5",
"@sigstore/rekor-types": "^3.0.0", "@sigstore/rekor-types": "^2.0.0",
"@types/jsonwebtoken": "^9.0.6", "@types/make-fetch-happen": "^10.0.4",
"nock": "^13.5.1", "nock": "^13.5.1"
"undici": "^5.28.4"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1",
"@actions/github": "^6.0.0", "@actions/github": "^6.0.0",
"@actions/http-client": "^2.2.3", "@sigstore/bundle": "^2.2.0",
"@octokit/plugin-retry": "^6.0.1", "@sigstore/sign": "^2.2.3",
"@sigstore/bundle": "^3.0.0", "make-fetch-happen": "^13.0.0"
"@sigstore/sign": "^3.0.0",
"jose": "^5.2.3"
},
"overrides": {
"@octokit/plugin-retry": {
"@octokit/core": "^5.2.0"
}
} }
} }
+11 -32
View File
@@ -1,11 +1,10 @@
import {bundleToJSON} from '@sigstore/bundle' import {Bundle, bundleToJSON} from '@sigstore/bundle'
import {X509Certificate} from 'crypto' import {X509Certificate} from 'crypto'
import {SigstoreInstance, signingEndpoints} from './endpoints' import {SigstoreInstance, signingEndpoints} from './endpoints'
import {buildIntotoStatement} from './intoto' import {buildIntotoStatement} from './intoto'
import {Payload, signPayload} from './sign' import {Payload, signPayload} from './sign'
import {writeAttestation} from './store' import {writeAttestation} from './store'
import type {Bundle} from '@sigstore/sign'
import type {Attestation, Predicate, Subject} from './shared.types' import type {Attestation, Predicate, Subject} from './shared.types'
const INTOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json' const INTOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json'
@@ -14,16 +13,11 @@ const INTOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json'
* Options for attesting a subject / predicate. * Options for attesting a subject / predicate.
*/ */
export type AttestOptions = { export type AttestOptions = {