Compare commits

..
71 changed files with 3962 additions and 6701 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
+1846 -3457
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",
+2 -33
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
@@ -42,9 +11,9 @@
### 2.1.2 ### 2.1.2
- Updated the stream extract functionality to use `unzip.Parse()` instead of `unzip.Extract()` for greater control of unzipping artifacts - Updated the stream extract functionality to use `unzip.Parse()` instead of `unzip.Extract()` for greater control of unzipping artifacts
### 2.1.1 ### 2.1.1
- Updated `isGhes` check to include `.ghe.com` and `.ghe.localhost` as accepted hosts - Updated `isGhes` check to include `.ghe.com` and `.ghe.localhost` as accepted hosts
### 2.1.0 ### 2.1.0
@@ -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 -553
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,29 +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.3", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"dependencies": { "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."
"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",
@@ -876,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": {
@@ -902,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",
@@ -935,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",
@@ -1018,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",
@@ -1034,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",
@@ -1126,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",
@@ -1134,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",
@@ -1198,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",
@@ -1284,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",
@@ -1339,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": {
@@ -1410,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",
@@ -1441,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",
@@ -1461,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",
@@ -1481,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": {
@@ -1731,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"
@@ -1778,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",
@@ -1916,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 = {
/** // The name of the subject to be attested.
* @deprecated Use `subjects` instead. subjectName: string
**/ // The digest of the subject to be attested. Should be a map of digest
subjectName?: string // algorithms to their hex-encoded values.
/** subjectDigest: Record<string, string>
* @deprecated Use `subjects` instead.
**/
subjectDigest?: Record<string, string>
// Subjects to be attested.
subjects?: Subject[]
// Content type of the predicate being attested. // Content type of the predicate being attested.
predicateType: string predicateType: string
// Predicate to be attested. // Predicate to be attested.
@@ -33,8 +27,6 @@ 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?: SigstoreInstance sigstore?: SigstoreInstance
// 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
} }
@@ -47,24 +39,15 @@ export type AttestOptions = {
* @returns A promise that resolves to the attestation. * @returns A promise that resolves to the attestation.
*/ */
export async function attest(options: AttestOptions): Promise<Attestation> { export async function attest(options: AttestOptions): Promise<Attestation> {
let subjects: Subject[] const subject: Subject = {
name: options.subjectName,
if (options.subjects) { digest: options.subjectDigest
subjects = options.subjects
} else if (options.subjectName && options.subjectDigest) {
subjects = [{name: options.subjectName, digest: options.subjectDigest}]
} else {
throw new Error(
'Must provide either subjectName and subjectDigest or subjects'
)
} }
const predicate: Predicate = { const predicate: Predicate = {
type: options.predicateType, type: options.predicateType,
params: options.predicate params: options.predicate
} }
const statement = buildIntotoStatement(subject, predicate)
const statement = buildIntotoStatement(subjects, predicate)
// Sign the provenance statement // Sign the provenance statement
const payload: Payload = { const payload: Payload = {
@@ -77,11 +60,7 @@ export async function attest(options: AttestOptions): Promise<Attestation> {
// Store the attestation // Store the attestation
let attestationID: string | undefined let attestationID: string | undefined
if (options.skipWrite !== true) { if (options.skipWrite !== true) {
attestationID = await writeAttestation( attestationID = await writeAttestation(bundleToJSON(bundle), options.token)
bundleToJSON(bundle),
options.token,
{headers: options.headers}
)
} }
return toAttestation(bundle, attestationID) return toAttestation(bundle, attestationID)
+9 -14
View File
@@ -6,6 +6,9 @@ const GITHUB_ID = 'github'
const FULCIO_PUBLIC_GOOD_URL = 'https://fulcio.sigstore.dev' const FULCIO_PUBLIC_GOOD_URL = 'https://fulcio.sigstore.dev'
const REKOR_PUBLIC_GOOD_URL = 'https://rekor.sigstore.dev' const REKOR_PUBLIC_GOOD_URL = 'https://rekor.sigstore.dev'
const FULCIO_INTERNAL_URL = 'https://fulcio.githubapp.com'
const TSA_INTERNAL_URL = 'https://timestamp.githubapp.com'
export type SigstoreInstance = typeof PUBLIC_GOOD_ID | typeof GITHUB_ID export type SigstoreInstance = typeof PUBLIC_GOOD_ID | typeof GITHUB_ID
export type Endpoints = { export type Endpoints = {
@@ -19,6 +22,11 @@ export const SIGSTORE_PUBLIC_GOOD: Endpoints = {
rekorURL: REKOR_PUBLIC_GOOD_URL rekorURL: REKOR_PUBLIC_GOOD_URL
} }
export const SIGSTORE_GITHUB: Endpoints = {
fulcioURL: FULCIO_INTERNAL_URL,
tsaServerURL: TSA_INTERNAL_URL
}
export const signingEndpoints = (sigstore?: SigstoreInstance): Endpoints => { export const signingEndpoints = (sigstore?: SigstoreInstance): Endpoints => {
let instance: SigstoreInstance let instance: SigstoreInstance
@@ -37,19 +45,6 @@ export const signingEndpoints = (sigstore?: SigstoreInstance): Endpoints => {
case PUBLIC_GOOD_ID: case PUBLIC_GOOD_ID:
return SIGSTORE_PUBLIC_GOOD return SIGSTORE_PUBLIC_GOOD
case GITHUB_ID: case GITHUB_ID:
return buildGitHubEndpoints() return SIGSTORE_GITHUB
}
}
function buildGitHubEndpoints(): Endpoints {
const serverURL = process.env.GITHUB_SERVER_URL || 'https://github.com'
let host = new URL(serverURL).hostname
if (host === 'github.com') {
host = 'githubapp.com'
}
return {
fulcioURL: `https://fulcio.${host}`,
tsaServerURL: `https://timestamp.${host}`
} }
} }
+2 -2
View File
@@ -20,12 +20,12 @@ export type InTotoStatement = {
* @returns The constructed in-toto statement. * @returns The constructed in-toto statement.
*/ */
export const buildIntotoStatement = ( export const buildIntotoStatement = (
subjects: Subject[], subject: Subject,
predicate: Predicate predicate: Predicate
): InTotoStatement => { ): InTotoStatement => {
return { return {
_type: INTOTO_STATEMENT_V1_TYPE, _type: INTOTO_STATEMENT_V1_TYPE,
subject: subjects, subject: [subject],
predicateType: predicate.type, predicateType: predicate.type,
predicate: predicate.params predicate: predicate.params
} }
-117
View File
@@ -1,117 +0,0 @@
import {getIDToken} from '@actions/core'
import {HttpClient} from '@actions/http-client'
import * as jose from 'jose'
const OIDC_AUDIENCE = 'nobody'
const VALID_SERVER_URLS = [
'https://github.com',
new RegExp('^https://[a-z0-9-]+\\.ghe\\.com$')
] as const
const REQUIRED_CLAIMS = [
'iss',
'ref',
'sha',
'repository',
'event_name',
'job_workflow_ref',
'workflow_ref',
'repository_id',
'repository_owner_id',
'runner_environment',
'run_id',
'run_attempt'
] as const
export type ClaimSet = {[K in (typeof REQUIRED_CLAIMS)[number]]: string}
type OIDCConfig = {
jwks_uri: string
}
export const getIDTokenClaims = async (issuer?: string): Promise<ClaimSet> => {
issuer = issuer || getIssuer()
try {
const token = await getIDToken(OIDC_AUDIENCE)
const claims = await decodeOIDCToken(token, issuer)
assertClaimSet(claims)
return claims
} catch (error) {
throw new Error(`Failed to get ID token: ${error.message}`)
}
}
const decodeOIDCToken = async (
token: string,
issuer: string
): Promise<jose.JWTPayload> => {
// Verify and decode token
const jwks = jose.createLocalJWKSet(await getJWKS(issuer))
const {payload} = await jose.jwtVerify(token, jwks, {
audience: OIDC_AUDIENCE
})
if (!payload.iss) {
throw new Error('Missing "iss" claim')
}
// Check that the issuer STARTS WITH the expected issuer URL to account for
// the fact that the value may include an enterprise-specific slug
if (!payload.iss.startsWith(issuer)) {
throw new Error(`Unexpected "iss" claim: ${payload.iss}`)
}
return payload
}
const getJWKS = async (issuer: string): Promise<jose.JSONWebKeySet> => {
const client = new HttpClient('@actions/attest')
const config = await client.getJson<OIDCConfig>(
`${issuer}/.well-known/openid-configuration`
)
if (!config.result) {
throw new Error('No OpenID configuration found')
}
const jwks = await client.getJson<jose.JSONWebKeySet>(config.result.jwks_uri)
if (!jwks.result) {
throw new Error('No JWKS found for issuer')
}
return jwks.result
}
function assertClaimSet(claims: jose.JWTPayload): asserts claims is ClaimSet {
const missingClaims: string[] = []
for (const claim of REQUIRED_CLAIMS) {
if (!(claim in claims)) {
missingClaims.push(claim)
}
}
if (missingClaims.length > 0) {
throw new Error(`Missing claims: ${missingClaims.join(', ')}`)
}
}
// Derive the current OIDC issuer based on the server URL
function getIssuer(): string {
const serverURL = process.env.GITHUB_SERVER_URL || 'https://github.com'
// Ensure the server URL is a valid GitHub server URL
if (!VALID_SERVER_URLS.some(valid_url => serverURL.match(valid_url))) {
throw new Error(`Invalid server URL: ${serverURL}`)
}
let host = new URL(serverURL).hostname
if (host === 'github.com') {
host = 'githubusercontent.com'
}
return `https://token.actions.${host}`
}
+22 -26
View File
@@ -1,40 +1,37 @@
import {attest, AttestOptions} from './attest' import {attest, AttestOptions} from './attest'
import {getIDTokenClaims} from './oidc'
import type {Attestation, Predicate} from './shared.types' import type {Attestation, Predicate} from './shared.types'
const SLSA_PREDICATE_V1_TYPE = 'https://slsa.dev/provenance/v1' const SLSA_PREDICATE_V1_TYPE = 'https://slsa.dev/provenance/v1'
const GITHUB_BUILD_TYPE = 'https://actions.github.io/buildtypes/workflow/v1'
const GITHUB_BUILDER_ID_PREFIX = 'https://github.com/actions/runner'
const GITHUB_BUILD_TYPE =
'https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1'
export type AttestProvenanceOptions = Omit< export type AttestProvenanceOptions = Omit<
AttestOptions, AttestOptions,
'predicate' | 'predicateType' 'predicate' | 'predicateType'
> & { >
issuer?: string
}
/** /**
* Builds an SLSA (Supply Chain Levels for Software Artifacts) provenance * Builds an SLSA (Supply Chain Levels for Software Artifacts) provenance
* predicate using the GitHub Actions Workflow build type. * predicate using the GitHub Actions Workflow build type.
* https://slsa.dev/spec/v1.0/provenance * https://slsa.dev/spec/v1.0/provenance
* https://github.com/slsa-framework/github-actions-buildtypes/tree/main/workflow/v1 * https://github.com/slsa-framework/github-actions-buildtypes/tree/main/workflow/v1
* @param issuer - URL for the OIDC issuer. Defaults to the GitHub Actions token * @param env - The Node.js process environment variables. Defaults to
* issuer. * `process.env`.
* @returns The SLSA provenance predicate. * @returns The SLSA provenance predicate.
*/ */
export const buildSLSAProvenancePredicate = async ( export const buildSLSAProvenancePredicate = (
issuer?: string env: NodeJS.ProcessEnv = process.env
): Promise<Predicate> => { ): Predicate => {
const serverURL = process.env.GITHUB_SERVER_URL const workflow = env.GITHUB_WORKFLOW_REF || ''
const claims = await getIDTokenClaims(issuer)
// Split just the path and ref from the workflow string. // Split just the path and ref from the workflow string.
// owner/repo/.github/workflows/main.yml@main => // owner/repo/.github/workflows/main.yml@main =>
// .github/workflows/main.yml, main // .github/workflows/main.yml, main
const [workflowPath, ...workflowRefChunks] = claims.workflow_ref const [workflowPath, workflowRef] = workflow
.replace(`${claims.repository}/`, '') .replace(`${env.GITHUB_REPOSITORY}/`, '')
.split('@') .split('@')
// Handle case where tag contains `@` (e.g: when using changesets in a monorepo context),
const workflowRef = workflowRefChunks.join('@')
return { return {
type: SLSA_PREDICATE_V1_TYPE, type: SLSA_PREDICATE_V1_TYPE,
@@ -44,33 +41,32 @@ export const buildSLSAProvenancePredicate = async (
externalParameters: { externalParameters: {
workflow: { workflow: {
ref: workflowRef, ref: workflowRef,
repository: `${serverURL}/${claims.repository}`, repository: `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}`,
path: workflowPath path: workflowPath
} }
}, },
internalParameters: { internalParameters: {
github: { github: {
event_name: claims.event_name, event_name: env.GITHUB_EVENT_NAME,
repository_id: claims.repository_id, repository_id: env.GITHUB_REPOSITORY_ID,
repository_owner_id: claims.repository_owner_id, repository_owner_id: env.GITHUB_REPOSITORY_OWNER_ID
runner_environment: claims.runner_environment
} }
}, },
resolvedDependencies: [ resolvedDependencies: [
{ {
uri: `git+${serverURL}/${claims.repository}@${claims.ref}`, uri: `git+${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}@${env.GITHUB_REF}`,
digest: { digest: {
gitCommit: claims.sha gitCommit: env.GITHUB_SHA
} }
} }
] ]
}, },
runDetails: { runDetails: {
builder: { builder: {
id: `${serverURL}/${claims.job_workflow_ref}` id: `${GITHUB_BUILDER_ID_PREFIX}/${env.RUNNER_ENVIRONMENT}`
}, },
metadata: { metadata: {
invocationId: `${serverURL}/${claims.repository}/actions/runs/${claims.run_id}/attempts/${claims.run_attempt}` invocationId: `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}/attempts/${env.GITHUB_RUN_ATTEMPT}`
} }
} }
} }
@@ -88,7 +84,7 @@ export const buildSLSAProvenancePredicate = async (
export async function attestProvenance( export async function attestProvenance(
options: AttestProvenanceOptions options: AttestProvenanceOptions
): Promise<Attestation> { ): Promise<Attestation> {
const predicate = await buildSLSAProvenancePredicate(options.issuer) const predicate = buildSLSAProvenancePredicate(process.env)
return attest({ return attest({
...options, ...options,
predicateType: predicate.type, predicateType: predicate.type,
+2 -4
View File
@@ -1,5 +1,5 @@
import {Bundle} from '@sigstore/bundle'
import { import {
Bundle,
BundleBuilder, BundleBuilder,
CIContextProvider, CIContextProvider,
DSSEBundleBuilder, DSSEBundleBuilder,
@@ -86,7 +86,7 @@ const initBundleBuilder = (opts: SignOptions): BundleBuilder => {
witnesses.push( witnesses.push(
new RekorWitness({ new RekorWitness({
rekorBaseURL: opts.rekorURL, rekorBaseURL: opts.rekorURL,
fetchOnConflict: true, entryType: 'dsse',
timeout, timeout,
retry retry
}) })
@@ -103,7 +103,5 @@ const initBundleBuilder = (opts: SignOptions): BundleBuilder => {
) )
} }
// Build the bundle with the singleCertificate option which will
// trigger the creation of v0.3 DSSE bundles
return new DSSEBundleBuilder({signer, witnesses}) return new DSSEBundleBuilder({signer, witnesses})
} }
+4 -17
View File
@@ -1,14 +1,8 @@
import * as github from '@actions/github' import * as github from '@actions/github'
import {retry} from '@octokit/plugin-retry' import fetch from 'make-fetch-happen'
import {RequestHeaders} from '@octokit/types'
const CREATE_ATTESTATION_REQUEST = 'POST /repos/{owner}/{repo}/attestations' const CREATE_ATTESTATION_REQUEST = 'POST /repos/{owner}/{repo}/attestations'
const DEFAULT_RETRY_COUNT = 5
export type WriteOptions = {
retry?: number
headers?: RequestHeaders
}
/** /**
* Writes an attestation to the repository's attestations endpoint. * Writes an attestation to the repository's attestations endpoint.
* @param attestation - The attestation to write. * @param attestation - The attestation to write.
@@ -18,25 +12,18 @@ export type WriteOptions = {
*/ */
export const writeAttestation = async ( export const writeAttestation = async (
attestation: unknown, attestation: unknown,
token: string, token: string
options: WriteOptions = {}
): Promise<string> => { ): Promise<string> => {
const retries = options.retry ?? DEFAULT_RETRY_COUNT const octokit = github.getOctokit(token, {request: {fetch}})
const octokit = github.getOctokit(token, {retry: {retries}}, retry)
try { try {
const response = await octokit.request(CREATE_ATTESTATION_REQUEST, { const response = await octokit.request(CREATE_ATTESTATION_REQUEST, {
owner: github.context.repo.owner, owner: github.context.repo.owner,
repo: github.context.repo.repo, repo: github.context.repo.repo,
headers: options.headers,
data: {bundle: attestation} data: {bundle: attestation}
}) })
const data = return response.data?.id
typeof response.data == 'string'
? JSON.parse(response.data)
: response.data
return data?.id
} catch (err) { } catch (err) {
const message = err instanceof Error ? err.message : err const message = err instanceof Error ? err.message : err
throw new Error(`Failed to persist attestation: ${message}`) throw new Error(`Failed to persist attestation: ${message}`)
+1 -5
View File
@@ -1,13 +1,9 @@
# @actions/cache Releases # @actions/cache Releases
### 3.3.0
- Update `@actions/core` to `1.11.1`
- Remove dependency on `uuid` package [#1824](https://github.com/actions/toolkit/pull/1824), [#1842](https://github.com/actions/toolkit/pull/1842)
### 3.2.4 ### 3.2.4
- Updated `isGhes` check to include `.ghe.com` and `.ghe.localhost` as accepted hosts - Updated `isGhes` check to include `.ghe.com` and `.ghe.localhost` as accepted hosts
### 3.2.3 ### 3.2.3
- Fixed a bug that mutated path arguments to `getCacheVersion` [#1378](https://github.com/actions/toolkit/pull/1378) - Fixed a bug that mutated path arguments to `getCacheVersion` [#1378](https://github.com/actions/toolkit/pull/1378)
+57 -14
View File
@@ -1,15 +1,15 @@
{ {
"name": "@actions/cache", "name": "@actions/cache",
"version": "3.3.0", "version": "3.2.4",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@actions/cache", "name": "@actions/cache",
"version": "3.3.0", "version": "3.2.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@actions/core": "^1.10.0",
"@actions/exec": "^1.0.1", "@actions/exec": "^1.0.1",
"@actions/glob": "^0.1.0", "@actions/glob": "^0.1.0",
"@actions/http-client": "^2.1.1", "@actions/http-client": "^2.1.1",
@@ -17,20 +17,30 @@
"@azure/abort-controller": "^1.1.0", "@azure/abort-controller": "^1.1.0",
"@azure/ms-rest-js": "^2.6.0", "@azure/ms-rest-js": "^2.6.0",
"@azure/storage-blob": "^12.13.0", "@azure/storage-blob": "^12.13.0",
"semver": "^6.3.1" "semver": "^6.3.1",
"uuid": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"@types/semver": "^6.0.0", "@types/semver": "^6.0.0",
"@types/uuid": "^3.4.5",
"typescript": "^5.2.2" "typescript": "^5.2.2"
} }
}, },
"node_modules/@actions/core": { "node_modules/@actions/core": {
"version": "1.11.1", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
"dependencies": { "dependencies": {
"@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1",
"@actions/http-client": "^2.0.1" "uuid": "^8.3.2"
}
},
"node_modules/@actions/core/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
} }
}, },
"node_modules/@actions/exec": { "node_modules/@actions/exec": {
@@ -286,6 +296,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/uuid": {
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.10.tgz",
"integrity": "sha512-BgeaZuElf7DEYZhWYDTc/XcLZXdVgFkVSTa13BqKvbnmUrxr3TJFKofUxCtDO9UQOdhnV+HPOESdHiHKZOJV1A==",
"dev": true
},
"node_modules/abort-controller": { "node_modules/abort-controller": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -470,6 +486,15 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
@@ -507,12 +532,19 @@
}, },
"dependencies": { "dependencies": {
"@actions/core": { "@actions/core": {
"version": "1.11.1", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
"requires": { "requires": {
"@actions/exec": "^1.1.1", "@actions/http-client": "^2.0.1",
"@actions/http-client": "^2.0.1" "uuid": "^8.3.2"
},
"dependencies": {
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
}
} }
}, },
"@actions/exec": { "@actions/exec": {
@@ -732,6 +764,12 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/uuid": {
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.10.tgz",
"integrity": "sha512-BgeaZuElf7DEYZhWYDTc/XcLZXdVgFkVSTa13BqKvbnmUrxr3TJFKofUxCtDO9UQOdhnV+HPOESdHiHKZOJV1A==",
"dev": true
},
"abort-controller": { "abort-controller": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -862,6 +900,11 @@
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"dev": true "dev": true
}, },
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
},
"webidl-conversions": { "webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+5 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/cache", "name": "@actions/cache",
"version": "3.3.0", "version": "3.2.4",
"preview": true, "preview": true,
"description": "Actions cache lib", "description": "Actions cache lib",
"keywords": [ "keywords": [
@@ -37,7 +37,7 @@
"url": "https://github.com/actions/toolkit/issues" "url": "https://github.com/actions/toolkit/issues"
}, },
"dependencies": { "dependencies": {
"@actions/core": "^1.11.1", "@actions/core": "^1.10.0",
"@actions/exec": "^1.0.1", "@actions/exec": "^1.0.1",
"@actions/glob": "^0.1.0", "@actions/glob": "^0.1.0",
"@actions/http-client": "^2.1.1", "@actions/http-client": "^2.1.1",
@@ -45,10 +45,12 @@
"@azure/abort-controller": "^1.1.0", "@azure/abort-controller": "^1.1.0",
"@azure/ms-rest-js": "^2.6.0", "@azure/ms-rest-js": "^2.6.0",
"@azure/storage-blob": "^12.13.0", "@azure/storage-blob": "^12.13.0",
"semver": "^6.3.1" "semver": "^6.3.1",
"uuid": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"@types/semver": "^6.0.0", "@types/semver": "^6.0.0",
"@types/uuid": "^3.4.5",
"typescript": "^5.2.2" "typescript": "^5.2.2"
} }
} }
+2 -2
View File
@@ -2,11 +2,11 @@ import * as core from '@actions/core'
import * as exec from '@actions/exec' import * as exec from '@actions/exec'
import * as glob from '@actions/glob' import * as glob from '@actions/glob'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as crypto from 'crypto'
import * as fs from 'fs' import * as fs from 'fs'
import * as path from 'path' import * as path from 'path'
import * as semver from 'semver' import * as semver from 'semver'
import * as util from 'util' import * as util from 'util'
import {v4 as uuidV4} from 'uuid'
import { import {
CacheFilename, CacheFilename,
CompressionMethod, CompressionMethod,
@@ -34,7 +34,7 @@ export async function createTempDirectory(): Promise<string> {
tempDirectory = path.join(baseLocation, 'actions', 'temp') tempDirectory = path.join(baseLocation, 'actions', 'temp')
} }
const dest = path.join(tempDirectory, crypto.randomUUID()) const dest = path.join(tempDirectory, uuidV4())
await io.mkdirP(dest) await io.mkdirP(dest)
return dest return dest
} }
-7
View File
@@ -1,12 +1,5 @@
# @actions/core Releases # @actions/core Releases
### 1.11.1
- Fix uses of `crypto.randomUUID` on Node 18 and earlier [#1842](https://github.com/actions/toolkit/pull/1842)
### 1.11.0
- Add platform info utilities [#1551](https://github.com/actions/toolkit/pull/1551)
- Remove dependency on `uuid` package [#1824](https://github.com/actions/toolkit/pull/1824)
### 1.10.1 ### 1.10.1
- Fix error message reference in oidc utils [#1511](https://github.com/actions/toolkit/pull/1511) - Fix error message reference in oidc utils [#1511](https://github.com/actions/toolkit/pull/1511)
+74 -28
View File
@@ -4,6 +4,9 @@ import * as path from 'path'
import * as core from '../src/core' import * as core from '../src/core'
import {HttpClient} from '@actions/http-client' import {HttpClient} from '@actions/http-client'
import {toCommandProperties} from '../src/utils' import {toCommandProperties} from '../src/utils'
import * as uuid from 'uuid'
jest.mock('uuid')
/* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/unbound-method */
@@ -46,23 +49,27 @@ const testEnvVars = {
const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d' const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
const DELIMITER = `ghadelimiter_${UUID}` const DELIMITER = `ghadelimiter_${UUID}`
jest.mock('crypto', () => ({ function extractErrorMetadata(error: Error): {
...jest.requireActual('crypto'), file: string | undefined
randomUUID: jest.fn(() => UUID) line: string | undefined
})) column: string | undefined
} {
const TEMP_DIR = path.join(__dirname, '_temp') const stackLines = error.stack?.split(os.EOL) || []
const firstTraceLine = stackLines[1]
const match = firstTraceLine.match(/at (?:.*) \((.*):(\d+):(\d+)\)/) || []
const [, file, line, column] = match
return {
file,
line,
column
}
}
describe('@actions/core', () => { describe('@actions/core', () => {
beforeAll(() => { beforeAll(() => {
const filePath = TEMP_DIR const filePath = path.join(__dirname, `test`)
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
fs.mkdirSync(filePath) fs.mkdirSync(filePath)
} else {
// Clear out the temp directory
for (const file of fs.readdirSync(filePath)) {
fs.unlinkSync(path.join(filePath, file))
}
} }
}) })
@@ -71,6 +78,10 @@ describe('@actions/core', () => {
process.env[key] = testEnvVars[key as keyof typeof testEnvVars] process.env[key] = testEnvVars[key as keyof typeof testEnvVars]
} }
process.stdout.write = jest.fn() process.stdout.write = jest.fn()
jest.spyOn(uuid, 'v4').mockImplementation(() => {
return UUID
})
}) })
afterEach(() => { afterEach(() => {
@@ -146,7 +157,7 @@ describe('@actions/core', () => {
`Unexpected input: value should not contain the delimiter "${DELIMITER}"` `Unexpected input: value should not contain the delimiter "${DELIMITER}"`
) )
const filePath = path.join(TEMP_DIR, command) const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath) fs.unlinkSync(filePath)
}) })
@@ -160,7 +171,7 @@ describe('@actions/core', () => {
`Unexpected input: name should not contain the delimiter "${DELIMITER}"` `Unexpected input: name should not contain the delimiter "${DELIMITER}"`
) )
const filePath = path.join(TEMP_DIR, command) const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath) fs.unlinkSync(filePath)
}) })
@@ -352,7 +363,7 @@ describe('@actions/core', () => {
`Unexpected input: value should not contain the delimiter "${DELIMITER}"` `Unexpected input: value should not contain the delimiter "${DELIMITER}"`
) )
const filePath = path.join(TEMP_DIR, command) const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath) fs.unlinkSync(filePath)
}) })
@@ -366,7 +377,7 @@ describe('@actions/core', () => {
`Unexpected input: name should not contain the delimiter "${DELIMITER}"` `Unexpected input: name should not contain the delimiter "${DELIMITER}"`
) )
const filePath = path.join(TEMP_DIR, command) const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath) fs.unlinkSync(filePath)
}) })
@@ -384,9 +395,14 @@ describe('@actions/core', () => {
it('setFailed handles Error', () => { it('setFailed handles Error', () => {
const message = 'this is my error message' const message = 'this is my error message'
core.setFailed(new Error(message)) const error = new Error(message)
core.setFailed(error)
expect(process.exitCode).toBe(core.ExitCode.Failure) expect(process.exitCode).toBe(core.ExitCode.Failure)
assertWriteCalls([`::error::Error: ${message}${os.EOL}`]) const {file, line, column} = extractErrorMetadata(error)
assertWriteCalls([
`::error title=Error,file=${file},line=${line},col=${column}::Error: ${message}${os.EOL}`
])
}) })
it('error sets the correct error message', () => { it('error sets the correct error message', () => {
@@ -401,11 +417,21 @@ describe('@actions/core', () => {
it('error handles an error object', () => { it('error handles an error object', () => {
const message = 'this is my error message' const message = 'this is my error message'
core.error(new Error(message)) const error = new Error(message)
core.error(error)
const {file, line, column} = extractErrorMetadata(error)
assertWriteCalls([
`::error title=Error,file=${file},line=${line},col=${column}::Error: ${message}${os.EOL}`
])
})
it('error handles an error object and an empty properties', () => {
const message = 'this is my error message'
core.error(new Error(message), {})
assertWriteCalls([`::error::Error: ${message}${os.EOL}`]) assertWriteCalls([`::error::Error: ${message}${os.EOL}`])
}) })
it('error handles parameters correctly', () => { it('error handles custom properties correctly', () => {
const message = 'this is my error message' const message = 'this is my error message'
core.error(new Error(message), { core.error(new Error(message), {
title: 'A title', title: 'A title',
@@ -432,11 +458,21 @@ describe('@actions/core', () => {
it('warning handles an error object', () => { it('warning handles an error object', () => {
const message = 'this is my error message' const message = 'this is my error message'
core.warning(new Error(message)) const error = new Error(message)
core.warning(error)
const {file, line, column} = extractErrorMetadata(error)
assertWriteCalls([
`::warning title=Error,file=${file},line=${line},col=${column}::Error: ${message}${os.EOL}`
])
})
it('warning handles an error object and an empty properties', () => {
const message = 'this is my error message'
core.warning(new Error(message), {})
assertWriteCalls([`::warning::Error: ${message}${os.EOL}`]) assertWriteCalls([`::warning::Error: ${message}${os.EOL}`])
}) })
it('warning handles parameters correctly', () => { it('warning handles custom properties correctly', () => {
const message = 'this is my error message' const message = 'this is my error message'
core.warning(new Error(message), { core.warning(new Error(message), {
title: 'A title', title: 'A title',
@@ -463,11 +499,21 @@ describe('@actions/core', () => {
it('notice handles an error object', () => { it('notice handles an error object', () => {
const message = 'this is my error message' const message = 'this is my error message'
core.notice(new Error(message)) const error = new Error(message)
core.notice(error)
const {file, line, column} = extractErrorMetadata(error)
assertWriteCalls([
`::notice title=Error,file=${file},line=${line},col=${column}::Error: ${message}${os.EOL}`
])
})
it('notice handles an error object and an empty properties', () => {
const message = 'this is my error message'
core.notice(new Error(message), {})
assertWriteCalls([`::notice::Error: ${message}${os.EOL}`]) assertWriteCalls([`::notice::Error: ${message}${os.EOL}`])
}) })
it('notice handles parameters correctly', () => { it('notice handles custom properties correctly', () => {
const message = 'this is my error message' const message = 'this is my error message'
core.notice(new Error(message), { core.notice(new Error(message), {
title: 'A title', title: 'A title',
@@ -590,7 +636,7 @@ describe('@actions/core', () => {
`Unexpected input: value should not contain the delimiter "${DELIMITER}"` `Unexpected input: value should not contain the delimiter "${DELIMITER}"`
) )
const filePath = path.join(TEMP_DIR, command) const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath) fs.unlinkSync(filePath)
}) })
@@ -604,7 +650,7 @@ describe('@actions/core', () => {
`Unexpected input: name should not contain the delimiter "${DELIMITER}"` `Unexpected input: name should not contain the delimiter "${DELIMITER}"`
) )
const filePath = path.join(TEMP_DIR, command) const filePath = path.join(__dirname, `test/${command}`)
fs.unlinkSync(filePath) fs.unlinkSync(filePath)
}) })
@@ -646,7 +692,7 @@ function assertWriteCalls(calls: string[]): void {
} }
function createFileCommandFile(command: string): void { function createFileCommandFile(command: string): void {
const filePath = path.join(__dirname, `_temp/${command}`) const filePath = path.join(__dirname, `test/${command}`)
process.env[`GITHUB_${command}`] = filePath process.env[`GITHUB_${command}`] = filePath
fs.appendFileSync(filePath, '', { fs.appendFileSync(filePath, '', {
encoding: 'utf8' encoding: 'utf8'
@@ -654,7 +700,7 @@ function createFileCommandFile(command: string): void {
} }
function verifyFileCommand(command: string, expectedContents: string): void { function verifyFileCommand(command: string, expectedContents: string): void {
const filePath = path.join(__dirname, `_temp/${command}`) const filePath = path.join(__dirname, `test/${command}`)
const contents = fs.readFileSync(filePath, 'utf8') const contents = fs.readFileSync(filePath, 'utf8')
try { try {
expect(contents).toEqual(expectedContents) expect(contents).toEqual(expectedContents)
+26
View File
@@ -0,0 +1,26 @@
import {toAnnotationProperties} from '../src/utils'
describe('@actions/core/src/utils', () => {
describe('.toAnnotationProperties', () => {
it('extracts title only from Error instance without a parseable stack', () => {
const error = new TypeError('Test error')
error.stack = ''
expect(toAnnotationProperties(error)).toEqual({
title: 'TypeError',
file: undefined,
startLine: undefined,
startColumn: undefined
})
})
it('extracts AnnotationProperties from Error instance', () => {
const error = new ReferenceError('Test error')
expect(toAnnotationProperties(error)).toEqual({
title: 'ReferenceError',
file: expect.stringMatching(/utils\.test\.ts$/),
startLine: expect.any(Number),
startColumn: expect.any(Number)
})
})
})
})
+64 -10
View File
@@ -1,19 +1,22 @@
{ {
"name": "@actions/core", "name": "@actions/core",
"version": "1.11.1", "version": "1.10.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@actions/core", "name": "@actions/core",
"version": "1.11.1", "version": "1.10.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1" "@actions/http-client": "^2.0.1",
"error-stack-parser": "^2.1.4",
"uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^16.18.112" "@types/node": "^12.0.2",
"@types/uuid": "^8.3.4"
} }
}, },
"node_modules/@actions/exec": { "node_modules/@actions/exec": {
@@ -38,11 +41,30 @@
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "16.18.112", "version": "12.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.112.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
"integrity": "sha512-EKrbKUGJROm17+dY/gMi31aJlGLJ75e1IkTojt9n6u+hnaTBDs+M1bIdOawpk2m6YUAXq/R2W0SxCng1tndHCg==", "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
"dev": true "dev": true
}, },
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"node_modules/error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"dependencies": {
"stackframe": "^1.3.4"
}
},
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
},
"node_modules/tunnel": { "node_modules/tunnel": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
@@ -50,6 +72,14 @@
"engines": { "engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3" "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
} }
},
"node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
} }
}, },
"dependencies": { "dependencies": {
@@ -75,15 +105,39 @@
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="
}, },
"@types/node": { "@types/node": {
"version": "16.18.112", "version": "12.0.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.112.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
"integrity": "sha512-EKrbKUGJROm17+dY/gMi31aJlGLJ75e1IkTojt9n6u+hnaTBDs+M1bIdOawpk2m6YUAXq/R2W0SxCng1tndHCg==", "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
"dev": true "dev": true
}, },
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"error-stack-parser": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
"integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==",
"requires": {
"stackframe": "^1.3.4"
}
},
"stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
"integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="
},
"tunnel": { "tunnel": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
},
"uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
} }
} }
} }
+7 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/core", "name": "@actions/core",
"version": "1.11.1", "version": "1.10.1",
"description": "Actions core lib", "description": "Actions core lib",
"keywords": [ "keywords": [
"github", "github",
@@ -37,9 +37,12 @@
}, },
"dependencies": { "dependencies": {
"@actions/exec": "^1.1.1", "@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1" "@actions/http-client": "^2.0.1",
"error-stack-parser": "^2.1.4",
"uuid": "^8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^16.18.112" "@types/node": "^12.0.2",
"@types/uuid": "^8.3.4"
} }
} }
+29 -5
View File
@@ -1,7 +1,10 @@
import {issue, issueCommand} from './command' import {issue, issueCommand} from './command'
import {issueFileCommand, prepareKeyValueMessage} from './file-command' import {issueFileCommand, prepareKeyValueMessage} from './file-command'
import {toCommandProperties, toCommandValue} from './utils' import {
toAnnotationProperties,
toCommandProperties,
toCommandValue
} from './utils'
import * as os from 'os' import * as os from 'os'
import * as path from 'path' import * as path from 'path'
@@ -242,6 +245,21 @@ export function debug(message: string): void {
issueCommand('debug', {}, message) issueCommand('debug', {}, message)
} }
function defaultAnnotationPropertes(
message: string | Error,
properties: AnnotationProperties | undefined = undefined
): AnnotationProperties {
// If no properties are provided, try to extract them from the Error instance
if (properties === undefined) {
if (message instanceof Error) {
properties = toAnnotationProperties(message)
} else {
properties = {}
}
}
return properties
}
/** /**
* Adds an error issue * Adds an error issue
* @param message error issue message. Errors will be converted to string via toString() * @param message error issue message. Errors will be converted to string via toString()
@@ -249,8 +267,10 @@ export function debug(message: string): void {
*/ */
export function error( export function error(
message: string | Error, message: string | Error,
properties: AnnotationProperties = {} properties: AnnotationProperties | undefined = undefined
): void { ): void {
properties = defaultAnnotationPropertes(message, properties)
issueCommand( issueCommand(
'error', 'error',
toCommandProperties(properties), toCommandProperties(properties),
@@ -265,8 +285,10 @@ export function error(
*/ */
export function warning( export function warning(
message: string | Error, message: string | Error,
properties: AnnotationProperties = {} properties: AnnotationProperties | undefined = undefined
): void { ): void {
properties = defaultAnnotationPropertes(message, properties)
issueCommand( issueCommand(
'warning', 'warning',
toCommandProperties(properties), toCommandProperties(properties),
@@ -281,8 +303,10 @@ export function warning(
*/ */
export function notice( export function notice(
message: string | Error, message: string | Error,
properties: AnnotationProperties = {} properties: AnnotationProperties | undefined = undefined
): void { ): void {
properties = defaultAnnotationPropertes(message, properties)
issueCommand( issueCommand(
'notice', 'notice',
toCommandProperties(properties), toCommandProperties(properties),
+2 -2
View File
@@ -3,9 +3,9 @@
// We use any as a valid input type // We use any as a valid input type
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import * as crypto from 'crypto'
import * as fs from 'fs' import * as fs from 'fs'
import * as os from 'os' import * as os from 'os'
import {v4 as uuidv4} from 'uuid'
import {toCommandValue} from './utils' import {toCommandValue} from './utils'
export function issueFileCommand(command: string, message: any): void { export function issueFileCommand(command: string, message: any): void {
@@ -25,7 +25,7 @@ export function issueFileCommand(command: string, message: any): void {
} }
export function prepareKeyValueMessage(key: string, value: any): string { export function prepareKeyValueMessage(key: string, value: any): string {
const delimiter = `ghadelimiter_${crypto.randomUUID()}` const delimiter = `ghadelimiter_${uuidv4()}`
const convertedValue = toCommandValue(value) const convertedValue = toCommandValue(value)
// These should realistically never happen, but just in case someone finds a // These should realistically never happen, but just in case someone finds a
+19
View File
@@ -3,6 +3,7 @@
import {AnnotationProperties} from './core' import {AnnotationProperties} from './core'
import {CommandProperties} from './command' import {CommandProperties} from './command'
import ErrorStackParser from 'error-stack-parser'
/** /**
* Sanitizes an input into a string so it can be passed into issueCommand safely * Sanitizes an input into a string so it can be passed into issueCommand safely
@@ -39,3 +40,21 @@ export function toCommandProperties(
endColumn: annotationProperties.endColumn endColumn: annotationProperties.endColumn
} }
} }
export function toAnnotationProperties(error: Error): AnnotationProperties {
let firstFrame
try {
const stack = ErrorStackParser.parse(error)
firstFrame = stack?.[0]
} catch (parseError) {
// If we can't parse the stack, we'll just skip it
}
return {
title: error.name,
file: firstFrame?.fileName,
startLine: firstFrame?.lineNumber,
startColumn: firstFrame?.columnNumber
}
}
-3
View File
@@ -1,8 +1,5 @@
# @actions/glob Releases # @actions/glob Releases
### 0.5.0
- Added `excludeHiddenFiles` option, which is disabled by default to preserve existing behavior [#1791: Add glob option to ignore hidden files](https://github.com/actions/toolkit/pull/1791)
### 0.4.0 ### 0.4.0
- Pass in the current workspace as a parameter to HashFiles [#1318](https://github.com/actions/toolkit/pull/1318) - Pass in the current workspace as a parameter to HashFiles [#1318](https://github.com/actions/toolkit/pull/1318)
@@ -708,7 +708,7 @@ describe('globber', () => {
expect(itemPaths).toEqual([]) expect(itemPaths).toEqual([])
}) })
it('returns hidden files by default', async () => { it('returns hidden files', async () => {
// Create the following layout: // Create the following layout:
// <root> // <root>
// <root>/.emptyFolder // <root>/.emptyFolder
@@ -734,26 +734,6 @@ describe('globber', () => {
]) ])
}) })
it('ignores hidden files when excludeHiddenFiles is set', async () => {
// Create the following layout:
// <root>
// <root>/.emptyFolder
// <root>/.file
// <root>/.folder
// <root>/.folder/file
const root = path.join(getTestTemp(), 'ignores-hidden-files')
await createHiddenDirectory(path.join(root, '.emptyFolder'))
await createHiddenDirectory(path.join(root, '.folder'))
await createHiddenFile(path.join(root, '.file'), 'test .file content')
await fs.writeFile(
path.join(root, '.folder', 'file'),
'test .folder/file content'
)
const itemPaths = await glob(root, {excludeHiddenFiles: true})
expect(itemPaths).toEqual([root])
})
it('returns normalized paths', async () => { it('returns normalized paths', async () => {
// Create the following layout: // Create the following layout:
// <root>/hello/world.txt // <root>/hello/world.txt
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/glob", "name": "@actions/glob",
"version": "0.5.0", "version": "0.4.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"description": "Actions glob lib", "description": "Actions glob lib",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/glob", "name": "@actions/glob",
"version": "0.5.0", "version": "0.4.0",
"preview": true, "preview": true,
"description": "Actions glob lib", "description": "Actions glob lib",
"keywords": [ "keywords": [
@@ -9,8 +9,7 @@ export function getOptions(copy?: GlobOptions): GlobOptions {
followSymbolicLinks: true, followSymbolicLinks: true,
implicitDescendants: true, implicitDescendants: true,
matchDirectories: true, matchDirectories: true,
omitBrokenSymbolicLinks: true, omitBrokenSymbolicLinks: true
excludeHiddenFiles: false
} }
if (copy) { if (copy) {
@@ -33,11 +32,6 @@ export function getOptions(copy?: GlobOptions): GlobOptions {
result.omitBrokenSymbolicLinks = copy.omitBrokenSymbolicLinks result.omitBrokenSymbolicLinks = copy.omitBrokenSymbolicLinks
core.debug(`omitBrokenSymbolicLinks '${result.omitBrokenSymbolicLinks}'`) core.debug(`omitBrokenSymbolicLinks '${result.omitBrokenSymbolicLinks}'`)
} }
if (typeof copy.excludeHiddenFiles === 'boolean') {
result.excludeHiddenFiles = copy.excludeHiddenFiles
core.debug(`excludeHiddenFiles '${result.excludeHiddenFiles}'`)
}
} }
return result return result
@@ -36,13 +36,4 @@ export interface GlobOptions {
* @default true * @default true
*/ */
omitBrokenSymbolicLinks?: boolean omitBrokenSymbolicLinks?: boolean
/**
* Indicates whether to exclude hidden files (files and directories starting with a `.`).
* This does not apply to Windows files and directories with the hidden attribute unless
* they are also prefixed with a `.`.
*
* @default false
*/
excludeHiddenFiles?: boolean
} }
-5
View File
@@ -128,11 +128,6 @@ export class DefaultGlobber implements Globber {
continue continue
} }
// Hidden file or directory?
if (options.excludeHiddenFiles && path.basename(item.path).match(/^\./)) {
continue
}
// Directory // Directory
if (stats.isDirectory()) { if (stats.isDirectory()) {
// Matched // Matched
-9
View File
@@ -1,14 +1,5 @@
## Releases ## Releases
## 2.2.3
- Fixed an issue where proxy username and password were not handled correctly [#1799](https://github.com/actions/toolkit/pull/1799)
## 2.2.2
- Better handling of url encoded usernames and passwords in proxy config [#1782](https://github.com/actions/toolkit/pull/1782)
## 2.2.1
- Make sure RequestOptions.keepAlive is applied properly on node20 runtime [#1572](https://github.com/actions/toolkit/pull/1572)
## 2.2.0 ## 2.2.0
- Add function to return proxy agent dispatcher for compatibility with latest octokit packages [#1547](https://github.com/actions/toolkit/pull/1547) - Add function to return proxy agent dispatcher for compatibility with latest octokit packages [#1547](https://github.com/actions/toolkit/pull/1547)
+31 -32
View File
@@ -37,7 +37,7 @@ describe('basics', () => {
// "user-agent": "typed-test-client-tests" // "user-agent": "typed-test-client-tests"
// }, // },
// "origin": "173.95.152.44", // "origin": "173.95.152.44",
// "url": "http://postman-echo.com/get" // "url": "https://postman-echo.com/get"
// } // }
it('does basic http get request', async () => { it('does basic http get request', async () => {
@@ -63,17 +63,16 @@ describe('basics', () => {
expect(obj.headers['user-agent']).toBeFalsy() expect(obj.headers['user-agent']).toBeFalsy()
}) })
/* TODO write a mock rather then relying on a third party
it('does basic https get request', async () => { it('does basic https get request', async () => {
const res: httpm.HttpClientResponse = await _http.get( const res: httpm.HttpClientResponse = await _http.get(
'http://postman-echo.com/get' 'https://postman-echo.com/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() const body: string = await res.readBody()
const obj = JSON.parse(body) const obj = JSON.parse(body)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('https://postman-echo.com/get')
}) })
*/
it('does basic http get request with default headers', async () => { it('does basic http get request with default headers', async () => {
const http: httpm.HttpClient = new httpm.HttpClient( const http: httpm.HttpClient = new httpm.HttpClient(
'http-client-tests', 'http-client-tests',
@@ -126,12 +125,12 @@ describe('basics', () => {
it('pipes a get request', async () => { it('pipes a get request', async () => {
return new Promise<void>(async resolve => { return new Promise<void>(async resolve => {
const file = fs.createWriteStream(sampleFilePath) const file = fs.createWriteStream(sampleFilePath)
;(await _http.get('http://postman-echo.com/get')).message ;(await _http.get('https://postman-echo.com/get')).message
.pipe(file) .pipe(file)
.on('close', () => { .on('close', () => {
const body: string = fs.readFileSync(sampleFilePath).toString() const body: string = fs.readFileSync(sampleFilePath).toString()
const obj = JSON.parse(body) const obj = JSON.parse(body)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('https://postman-echo.com/get')
resolve() resolve()
}) })
}) })
@@ -139,32 +138,32 @@ describe('basics', () => {
it('does basic get request with redirects', async () => { it('does basic get request with redirects', async () => {
const res: httpm.HttpClientResponse = await _http.get( const res: httpm.HttpClientResponse = await _http.get(
`http://postman-echo.com/redirect-to?url=${encodeURIComponent( `https://postman-echo.com/redirect-to?url=${encodeURIComponent(
'http://postman-echo.com/get' 'https://postman-echo.com/get'
)}` )}`
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() const body: string = await res.readBody()
const obj = JSON.parse(body) const obj = JSON.parse(body)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('https://postman-echo.com/get')
}) })
it('does basic get request with redirects (303)', async () => { it('does basic get request with redirects (303)', async () => {
const res: httpm.HttpClientResponse = await _http.get( const res: httpm.HttpClientResponse = await _http.get(
`http://postman-echo.com/redirect-to?url=${encodeURIComponent( `https://postman-echo.com/redirect-to?url=${encodeURIComponent(
'http://postman-echo.com/get' 'https://postman-echo.com/get'
)}&status_code=303` )}&status_code=303`
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() const body: string = await res.readBody()
const obj = JSON.parse(body) const obj = JSON.parse(body)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('https://postman-echo.com/get')
}) })
it('returns 404 for not found get request on redirect', async () => { it('returns 404 for not found get request on redirect', async () => {
const res: httpm.HttpClientResponse = await _http.get( const res: httpm.HttpClientResponse = await _http.get(
`http://postman-echo.com/redirect-to?url=${encodeURIComponent( `https://postman-echo.com/redirect-to?url=${encodeURIComponent(
'http://postman-echo.com/status/404' 'https://postman-echo.com/status/404'
)}&status_code=303` )}&status_code=303`
) )
expect(res.message.statusCode).toBe(404) expect(res.message.statusCode).toBe(404)
@@ -178,8 +177,8 @@ describe('basics', () => {
{allowRedirects: false} {allowRedirects: false}
) )
const res: httpm.HttpClientResponse = await http.get( const res: httpm.HttpClientResponse = await http.get(
`http://postman-echo.com/redirect-to?url=${encodeURIComponent( `https://postman-echo.com/redirect-to?url=${encodeURIComponent(
'http://postman-echo.com/get' 'https://postman-echo.com/get'
)}` )}`
) )
expect(res.message.statusCode).toBe(302) expect(res.message.statusCode).toBe(302)
@@ -192,8 +191,8 @@ describe('basics', () => {
authorization: 'shhh' authorization: 'shhh'
} }
const res: httpm.HttpClientResponse = await _http.get( const res: httpm.HttpClientResponse = await _http.get(
`http://postman-echo.com/redirect-to?url=${encodeURIComponent( `https://postman-echo.com/redirect-to?url=${encodeURIComponent(
'http://www.postman-echo.com/get' 'https://www.postman-echo.com/get'
)}`, )}`,
headers headers
) )
@@ -205,7 +204,7 @@ describe('basics', () => {
expect(obj.headers[httpm.Headers.Accept]).toBe('application/json') expect(obj.headers[httpm.Headers.Accept]).toBe('application/json')
expect(obj.headers['Authorization']).toBeUndefined() expect(obj.headers['Authorization']).toBeUndefined()
expect(obj.headers['authorization']).toBeUndefined() expect(obj.headers['authorization']).toBeUndefined()
expect(obj.url).toBe('http://www.postman-echo.com/get') expect(obj.url).toBe('https://www.postman-echo.com/get')
}) })
it('does not pass Auth with diff hostname redirects', async () => { it('does not pass Auth with diff hostname redirects', async () => {
@@ -214,8 +213,8 @@ describe('basics', () => {
Authorization: 'shhh' Authorization: 'shhh'
} }
const res: httpm.HttpClientResponse = await _http.get( const res: httpm.HttpClientResponse = await _http.get(
`http://postman-echo.com/redirect-to?url=${encodeURIComponent( `https://postman-echo.com/redirect-to?url=${encodeURIComponent(
'http://www.postman-echo.com/get' 'https://www.postman-echo.com/get'
)}`, )}`,
headers headers
) )
@@ -227,7 +226,7 @@ describe('basics', () => {
expect(obj.headers[httpm.Headers.Accept]).toBe('application/json') expect(obj.headers[httpm.Headers.Accept]).toBe('application/json')
expect(obj.headers['Authorization']).toBeUndefined() expect(obj.headers['Authorization']).toBeUndefined()
expect(obj.headers['authorization']).toBeUndefined() expect(obj.headers['authorization']).toBeUndefined()
expect(obj.url).toBe('http://www.postman-echo.com/get') expect(obj.url).toBe('https://www.postman-echo.com/get')
}) })
it('does basic head request', async () => { it('does basic head request', async () => {
@@ -290,11 +289,11 @@ describe('basics', () => {
it('gets a json object', async () => { it('gets a json object', async () => {
const jsonObj = await _http.getJson<HttpBinData>( const jsonObj = await _http.getJson<HttpBinData>(
'http://postman-echo.com/get' 'https://postman-echo.com/get'
) )
expect(jsonObj.statusCode).toBe(200) expect(jsonObj.statusCode).toBe(200)
expect(jsonObj.result).toBeDefined() expect(jsonObj.result).toBeDefined()
expect(jsonObj.result?.url).toBe('http://postman-echo.com/get') expect(jsonObj.result?.url).toBe('https://postman-echo.com/get')
expect(jsonObj.result?.headers[httpm.Headers.Accept]).toBe( expect(jsonObj.result?.headers[httpm.Headers.Accept]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
) )
@@ -305,7 +304,7 @@ describe('basics', () => {
it('getting a non existent json object returns null', async () => { it('getting a non existent json object returns null', async () => {
const jsonObj = await _http.getJson<HttpBinData>( const jsonObj = await _http.getJson<HttpBinData>(
'http://postman-echo.com/status/404' 'https://postman-echo.com/status/404'
) )
expect(jsonObj.statusCode).toBe(404) expect(jsonObj.statusCode).toBe(404)
expect(jsonObj.result).toBeNull() expect(jsonObj.result).toBeNull()
@@ -314,12 +313,12 @@ describe('basics', () => {
it('posts a json object', async () => { it('posts a json object', async () => {
const res = {name: 'foo'} const res = {name: 'foo'}
const restRes = await _http.postJson<HttpBinData>( const restRes = await _http.postJson<HttpBinData>(
'http://postman-echo.com/post', 'https://postman-echo.com/post',
res res
) )
expect(restRes.statusCode).toBe(200) expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined() expect(restRes.result).toBeDefined()
expect(restRes.result?.url).toBe('http://postman-echo.com/post') expect(restRes.result?.url).toBe('https://postman-echo.com/post')
expect(restRes.result?.json.name).toBe('foo') expect(restRes.result?.json.name).toBe('foo')
expect(restRes.result?.headers[httpm.Headers.Accept]).toBe( expect(restRes.result?.headers[httpm.Headers.Accept]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
@@ -335,12 +334,12 @@ describe('basics', () => {
it('puts a json object', async () => { it('puts a json object', async () => {
const res = {name: 'foo'} const res = {name: 'foo'}
const restRes = await _http.putJson<HttpBinData>( const restRes = await _http.putJson<HttpBinData>(
'http://postman-echo.com/put', 'https://postman-echo.com/put',
res res
) )
expect(restRes.statusCode).toBe(200) expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined() expect(restRes.result).toBeDefined()
expect(restRes.result?.url).toBe('http://postman-echo.com/put') expect(restRes.result?.url).toBe('https://postman-echo.com/put')
expect(restRes.result?.json.name).toBe('foo') expect(restRes.result?.json.name).toBe('foo')
expect(restRes.result?.headers[httpm.Headers.Accept]).toBe( expect(restRes.result?.headers[httpm.Headers.Accept]).toBe(
@@ -357,12 +356,12 @@ describe('basics', () => {
it('patch a json object', async () => { it('patch a json object', async () => {
const res = {name: 'foo'} const res = {name: 'foo'}
const restRes = await _http.patchJson<HttpBinData>( const restRes = await _http.patchJson<HttpBinData>(
'http://postman-echo.com/patch', 'https://postman-echo.com/patch',
res res
) )
expect(restRes.statusCode).toBe(200) expect(restRes.statusCode).toBe(200)
expect(restRes.result).toBeDefined() expect(restRes.result).toBeDefined()
expect(restRes.result?.url).toBe('http://postman-echo.com/patch') expect(restRes.result?.url).toBe('https://postman-echo.com/patch')
expect(restRes.result?.json.name).toBe('foo') expect(restRes.result?.json.name).toBe('foo')
expect(restRes.result?.headers[httpm.Headers.Accept]).toBe( expect(restRes.result?.headers[httpm.Headers.Accept]).toBe(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
@@ -12,7 +12,7 @@ describe('headers', () => {
it('preserves existing headers on getJson', async () => { it('preserves existing headers on getJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} const additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.getJson<any>( let jsonObj = await _http.getJson<any>(
'http://postman-echo.com/get', 'https://postman-echo.com/get',
additionalHeaders additionalHeaders
) )
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('foo') expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('foo')
@@ -26,7 +26,7 @@ describe('headers', () => {
[httpm.Headers.Accept]: 'baz' [httpm.Headers.Accept]: 'baz'
} }
} }
jsonObj = await httpWithHeaders.getJson<any>('http://postman-echo.com/get') jsonObj = await httpWithHeaders.getJson<any>('https://postman-echo.com/get')
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz') expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz')
expect(jsonObj.headers[httpm.Headers.ContentType]).toContain( expect(jsonObj.headers[httpm.Headers.ContentType]).toContain(
httpm.MediaTypes.ApplicationJson httpm.MediaTypes.ApplicationJson
@@ -36,7 +36,7 @@ describe('headers', () => {
it('preserves existing headers on postJson', async () => { it('preserves existing headers on postJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} const additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.postJson<any>( let jsonObj = await _http.postJson<any>(
'http://postman-echo.com/post', 'https://postman-echo.com/post',
{}, {},
additionalHeaders additionalHeaders
) )
@@ -52,7 +52,7 @@ describe('headers', () => {
} }
} }
jsonObj = await httpWithHeaders.postJson<any>( jsonObj = await httpWithHeaders.postJson<any>(
'http://postman-echo.com/post', 'https://postman-echo.com/post',
{} {}
) )
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz') expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz')
@@ -64,7 +64,7 @@ describe('headers', () => {
it('preserves existing headers on putJson', async () => { it('preserves existing headers on putJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} const additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.putJson<any>( let jsonObj = await _http.putJson<any>(
'http://postman-echo.com/put', 'https://postman-echo.com/put',
{}, {},
additionalHeaders additionalHeaders
) )
@@ -80,7 +80,7 @@ describe('headers', () => {
} }
} }
jsonObj = await httpWithHeaders.putJson<any>( jsonObj = await httpWithHeaders.putJson<any>(
'http://postman-echo.com/put', 'https://postman-echo.com/put',
{} {}
) )
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz') expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz')
@@ -92,7 +92,7 @@ describe('headers', () => {
it('preserves existing headers on patchJson', async () => { it('preserves existing headers on patchJson', async () => {
const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} const additionalHeaders = {[httpm.Headers.Accept]: 'foo'}
let jsonObj = await _http.patchJson<any>( let jsonObj = await _http.patchJson<any>(
'http://postman-echo.com/patch', 'https://postman-echo.com/patch',
{}, {},
additionalHeaders additionalHeaders
) )
@@ -108,7 +108,7 @@ describe('headers', () => {
} }
} }
jsonObj = await httpWithHeaders.patchJson<any>( jsonObj = await httpWithHeaders.patchJson<any>(
'http://postman-echo.com/patch', 'https://postman-echo.com/patch',
{} {}
) )
expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz') expect(jsonObj.result.headers[httpm.Headers.Accept]).toBe('baz')
+6 -21
View File
@@ -222,33 +222,30 @@ describe('proxy', () => {
expect(_proxyConnects).toHaveLength(0) expect(_proxyConnects).toHaveLength(0)
}) })
// TODO mock this out so we don't rely on a third party
/*
it('HttpClient does basic https get request through proxy', async () => { it('HttpClient does basic https get request through proxy', async () => {
process.env['https_proxy'] = _proxyUrl process.env['https_proxy'] = _proxyUrl
const httpClient = new httpm.HttpClient() const httpClient = new httpm.HttpClient()
const res: httpm.HttpClientResponse = await httpClient.get( const res: httpm.HttpClientResponse = await httpClient.get(
'http://postman-echo.com/get' 'https://postman-echo.com/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() const body: string = await res.readBody()
const obj = JSON.parse(body) const obj = JSON.parse(body)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('https://postman-echo.com/get')
expect(_proxyConnects).toEqual(['postman-echo.com:443']) expect(_proxyConnects).toEqual(['postman-echo.com:443'])
}) })
*/
it('HttpClient does basic http get request when bypass proxy', async () => { it('HttpClient does basic https get request when bypass proxy', async () => {
process.env['http_proxy'] = _proxyUrl process.env['https_proxy'] = _proxyUrl
process.env['no_proxy'] = 'postman-echo.com' process.env['no_proxy'] = 'postman-echo.com'
const httpClient = new httpm.HttpClient() const httpClient = new httpm.HttpClient()
const res: httpm.HttpClientResponse = await httpClient.get( const res: httpm.HttpClientResponse = await httpClient.get(
'http://postman-echo.com/get' 'https://postman-echo.com/get'
) )
expect(res.message.statusCode).toBe(200) expect(res.message.statusCode).toBe(200)
const body: string = await res.readBody() const body: string = await res.readBody()
const obj = JSON.parse(body) const obj = JSON.parse(body)
expect(obj.url).toBe('http://postman-echo.com/get') expect(obj.url).toBe('https://postman-echo.com/get')
expect(_proxyConnects).toHaveLength(0) expect(_proxyConnects).toHaveLength(0)
}) })
@@ -307,18 +304,6 @@ describe('proxy', () => {
console.log(agent) console.log(agent)
expect(agent instanceof ProxyAgent).toBe(true) expect(agent instanceof ProxyAgent).toBe(true)
}) })
it('proxyAuth is set in tunnel agent when authentication is provided with URIencoding', async () => {
process.env['https_proxy'] =
'http://user%40github.com:p%[email protected]:8080'
const httpClient = new httpm.HttpClient()
const agent: any = httpClient.getAgent('https://some-url')
// eslint-disable-next-line no-console
console.log(agent)
expect(agent.proxyOptions.host).toBe('127.0.0.1')
expect(agent.proxyOptions.port).toBe('8080')
expect(agent.proxyOptions.proxyAuth).toBe('[email protected]:p@ssword')
})
}) })
function _clearVars(): void { function _clearVars(): void {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@actions/http-client", "name": "@actions/http-client",
"version": "2.2.3", "version": "2.2.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@actions/http-client", "name": "@actions/http-client",
"version": "2.2.1", "version": "2.2.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tunnel": "^0.0.6", "tunnel": "^0.0.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@actions/http-client", "name": "@actions/http-client",
"version": "2.2.3", "version": "2.2.1",
"description": "Actions Http Client", "description": "Actions Http Client",
"keywords": [ "keywords": [
"github", "github",
+1 -3
View File
@@ -726,9 +726,7 @@ export class HttpClient {
uri: proxyUrl.href, uri: proxyUrl.href,
pipelining: !this._keepAlive ? 0 : 1, pipelining: !this._keepAlive ? 0 : 1,
...((proxyUrl.username || proxyUrl.password) && { ...((proxyUrl.username || proxyUrl.password) && {
token: `Basic ${Buffer.from( token: `${proxyUrl.username}:${proxyUrl.password}`
`${proxyUrl.username}:${proxyUrl.password}`
).toString('base64')}`
}) })
}) })
this._proxyAgentDispatcher = proxyAgent this._proxyAgentDispatcher = proxyAgent
+2 -21
View File
@@ -15,10 +15,10 @@ export function getProxyUrl(reqUrl: URL): URL | undefined {
if (proxyVar) { if (proxyVar) {
try { try {
return new DecodedURL(proxyVar) return new URL(proxyVar)
} catch { } catch {
if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://')) if (!proxyVar.startsWith('http://') && !proxyVar.startsWith('https://'))
return new DecodedURL(`http://${proxyVar}`) return new URL(`http://${proxyVar}`)
} }
} else { } else {
return undefined return undefined
@@ -87,22 +87,3 @@ function isLoopbackAddress(host: string): boolean {
hostLower.startsWith('[0:0:0:0:0:0:0:1]') hostLower.startsWith('[0:0:0:0:0:0:0:1]')
) )
} }
class DecodedURL extends URL {
private _decodedUsername: string
private _decodedPassword: string
constructor(url: string | URL, base?: string | URL) {
super(url, base)
this._decodedUsername = decodeURIComponent(super.username)
this._decodedPassword = decodeURIComponent(super.password)
}
get username(): string {
return this._decodedUsername
}
get password(): string {
return this._decodedPassword
}
}
-3
View File
@@ -1,8 +1,5 @@
# @actions/tool-cache Releases # @actions/tool-cache Releases
### Unreleased
- Remove dependency on `uuid` package [#1824](https://github.com/actions/toolkit/pull/1824), [#1842](https://github.com/actions/toolkit/pull/1842)
### 2.0.1 ### 2.0.1
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087) - Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
+47 -1
View File
@@ -13,11 +13,13 @@
"@actions/exec": "^1.0.0", "@actions/exec": "^1.0.0",
"@actions/http-client": "^2.0.1", "@actions/http-client": "^2.0.1",
"@actions/io": "^1.1.1", "@actions/io": "^1.1.1",
"semver": "^6.1.0" "semver": "^6.1.0",
"uuid": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/nock": "^11.1.0", "@types/nock": "^11.1.0",
"@types/semver": "^6.0.0", "@types/semver": "^6.0.0",
"@types/uuid": "^3.4.4",
"nock": "^13.2.9" "nock": "^13.2.9"
} }
}, },
@@ -69,12 +71,27 @@
"nock": "*" "nock": "*"
} }
}, },
"node_modules/@types/node": {
"version": "12.7.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.0.tgz",
"integrity": "sha512-vqcj1MVm2Sla4PpMfYKh1MyDN4D2f/mPIZD7RdAGqEsbE+JxfeqQHHVbRDQ0Nqn8i73gJa1HQ1Pu3+nH4Q0Yiw==",
"dev": true
},
"node_modules/@types/semver": { "node_modules/@types/semver": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.1.tgz",
"integrity": "sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg==", "integrity": "sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg==",
"dev": true "dev": true
}, },
"node_modules/@types/uuid": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.5.tgz",
"integrity": "sha512-MNL15wC3EKyw1VLF+RoVO4hJJdk9t/Hlv3rt1OL65Qvuadm4BYo6g9ZJQqoq7X8NBFSsQXgAujWciovh2lpVjA==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -149,6 +166,15 @@
"engines": { "engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3" "node": ">=0.6.11 <=0.7.0 || >=0.7.3"
} }
},
"node_modules/uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"bin": {
"uuid": "bin/uuid"
}
} }
}, },
"dependencies": { "dependencies": {
@@ -198,12 +224,27 @@
"nock": "*" "nock": "*"
} }
}, },
"@types/node": {
"version": "12.7.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.0.tgz",
"integrity": "sha512-vqcj1MVm2Sla4PpMfYKh1MyDN4D2f/mPIZD7RdAGqEsbE+JxfeqQHHVbRDQ0Nqn8i73gJa1HQ1Pu3+nH4Q0Yiw==",
"dev": true
},
"@types/semver": { "@types/semver": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.0.1.tgz",
"integrity": "sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg==", "integrity": "sha512-ffCdcrEE5h8DqVxinQjo+2d1q+FV5z7iNtPofw3JsrltSoSVlOGaW0rY8XxtO9XukdTn8TaCGWmk2VFGhI70mg==",
"dev": true "dev": true
}, },
"@types/uuid": {
"version": "3.4.5",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.5.tgz",
"integrity": "sha512-MNL15wC3EKyw1VLF+RoVO4hJJdk9t/Hlv3rt1OL65Qvuadm4BYo6g9ZJQqoq7X8NBFSsQXgAujWciovh2lpVjA==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"debug": { "debug": {
"version": "4.3.4", "version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -258,6 +299,11 @@
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
} }
} }
} }
+3 -1
View File
@@ -40,11 +40,13 @@
"@actions/exec": "^1.0.0", "@actions/exec": "^1.0.0",
"@actions/http-client": "^2.0.1", "@actions/http-client": "^2.0.1",
"@actions/io": "^1.1.1", "@actions/io": "^1.1.1",
"semver": "^6.1.0" "semver": "^6.1.0",
"uuid": "^3.3.2"
}, },
"devDependencies": { "devDependencies": {
"@types/nock": "^11.1.0", "@types/nock": "^11.1.0",
"@types/semver": "^6.0.0", "@types/semver": "^6.0.0",
"@types/uuid": "^3.4.4",
"nock": "^13.2.9" "nock": "^13.2.9"
} }
} }
+3 -3
View File
@@ -1,6 +1,5 @@
import * as core from '@actions/core' import * as core from '@actions/core'
import * as io from '@actions/io' import * as io from '@actions/io'
import * as crypto from 'crypto'
import * as fs from 'fs' import * as fs from 'fs'
import * as mm from './manifest' import * as mm from './manifest'
import * as os from 'os' import * as os from 'os'
@@ -11,6 +10,7 @@ import * as stream from 'stream'
import * as util from 'util' import * as util from 'util'
import {ok} from 'assert' import {ok} from 'assert'
import {OutgoingHttpHeaders} from 'http' import {OutgoingHttpHeaders} from 'http'
import uuidV4 from 'uuid/v4'
import {exec} from '@actions/exec/lib/exec' import {exec} from '@actions/exec/lib/exec'
import {ExecOptions} from '@actions/exec/lib/interfaces' import {ExecOptions} from '@actions/exec/lib/interfaces'
import {RetryHelper} from './retry-helper' import {RetryHelper} from './retry-helper'
@@ -41,7 +41,7 @@ export async function downloadTool(
auth?: string, auth?: string,
headers?: OutgoingHttpHeaders headers?: OutgoingHttpHeaders
): Promise<string> { ): Promise<string> {
dest = dest || path.join(_getTempDirectory(), crypto.randomUUID()) dest = dest || path.join(_getTempDirectory(), uuidV4())
await io.mkdirP(path.dirname(dest)) await io.mkdirP(path.dirname(dest))
core.debug(`Downloading ${url}`) core.debug(`Downloading ${url}`)
core.debug(`Destination ${dest}`) core.debug(`Destination ${dest}`)
@@ -651,7 +651,7 @@ export async function findFromManifest(
async function _createExtractFolder(dest?: string): Promise<string> { async function _createExtractFolder(dest?: string): Promise<string> {
if (!dest) { if (!dest) {
// create a temp dir // create a temp dir
dest = path.join(_getTempDirectory(), crypto.randomUUID()) dest = path.join(_getTempDirectory(), uuidV4())
} }
await io.mkdirP(dest) await io.mkdirP(dest)
return dest return dest