Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c5b20a89f | |||
| bd74802644 | |||
| dfc588096e | |||
| 4379135168 | |||
| ef77c9d60b | |||
| 8fee77b04b | |||
| b807fc9c54 | |||
| 55c7a1e03d | |||
| 4799020e28 | |||
| bb420e4681 | |||
| 0c735ba79d | |||
| e918bf24ae | |||
| eea6b7f517 | |||
| ff435e591d | |||
| df3315bbea | |||
| b7770574c2 | |||
| 29bf378d97 | |||
| 68b042febd | |||
| c366a07d62 | |||
| 9e5eb95517 | |||
| 7f96bd610d | |||
| 8f53a1d37f | |||
| 37a562b194 | |||
| ad1f156c7c | |||
| 6079dea4c4 | |||
| 437f2be56d | |||
| 97c606b612 | |||
| 5a7faf0eb5 | |||
| dcc55dfd04 | |||
| 902046e4d8 | |||
| 88f7a7bc65 | |||
| 6cf4fbcef8 | |||
| 7fa864a4f4 | |||
| f77cbc9ef7 | |||
| 8a1800c5da | |||
| 90894a8853 | |||
| 614f27a4fb | |||
| ac84a9bee3 | |||
| 4256ea99c5 | |||
| 76489f433b | |||
| e9005f7727 | |||
| 8d03fb4787 | |||
| d3301c9bc2 | |||
| 1e326de474 | |||
| 83731e6528 | |||
| a24b9c0184 | |||
| 31c555afda | |||
| 9dea373bba | |||
| b956d8a4dd | |||
| 81d5e48db0 | |||
| bc5b3a85ae | |||
| 415c42d27c | |||
| e6c1cd0d8c | |||
| 39621898ff | |||
| c500de6dea | |||
| c4f4f5ae07 | |||
| f1d9b4b985 | |||
| d134334a38 | |||
| 3b02a6fdc5 |
@@ -18,10 +18,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
|
||||
@@ -22,10 +22,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
inputs:
|
||||
package:
|
||||
required: true
|
||||
description: 'core, artifact, cache, exec, github, glob, http-client, io, tool-cache'
|
||||
description: 'core, artifact, cache, exec, github, glob, http-client, io, tool-cache, attest'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@@ -13,13 +13,13 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: setup repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: verify package exists
|
||||
run: ls packages/${{ github.event.inputs.package }}
|
||||
|
||||
- name: Set Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
working-directory: packages/${{ github.event.inputs.package }}
|
||||
|
||||
- name: upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ github.event.inputs.package }}
|
||||
path: packages/${{ github.event.inputs.package }}/*.tgz
|
||||
@@ -49,10 +49,13 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
needs: test
|
||||
environment: npm-publish
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
|
||||
- name: download artifact
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: ${{ github.event.inputs.package }}
|
||||
|
||||
@@ -62,7 +65,7 @@ jobs:
|
||||
NPM_TOKEN: ${{ secrets.TOKEN }}
|
||||
|
||||
- name: publish
|
||||
run: npm publish *.tgz
|
||||
run: npm publish --provenance *.tgz
|
||||
|
||||
- name: notify slack on failure
|
||||
if: failure()
|
||||
|
||||
@@ -23,10 +23,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ jobs:
|
||||
if: ${{ github.repository_owner == 'actions' }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Update Octokit
|
||||
working-directory: packages/github
|
||||
run: |
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
/packages/artifact/ @actions/artifacts-actions
|
||||
/packages/cache/ @actions/actions-cache
|
||||
/packages/attest/ @actions/package-security
|
||||
|
||||
@@ -102,6 +102,15 @@ $ npm install @actions/cache
|
||||
```
|
||||
<br/>
|
||||
|
||||
:lock_with_ink_pen: [@actions/attest](packages/attest)
|
||||
|
||||
Provides functions to write attestations for workflow artifacts. Read more [here](packages/attest)
|
||||
|
||||
```bash
|
||||
$ npm install @actions/attest
|
||||
```
|
||||
<br/>
|
||||
|
||||
## Creating an Action with the Toolkit
|
||||
|
||||
:question: [Choosing an action type](docs/action-types.md)
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
os: [ubuntu-16.04, windows-2019]
|
||||
runs-on: ${{matrix.os}}
|
||||
actions:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
version: ${{matrix.node}}
|
||||
- run: |
|
||||
|
||||
@@ -18,7 +18,7 @@ e.g. To use https://github.com/actions/setup-node, users will author:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
using: actions/setup-node@v3
|
||||
using: actions/setup-node@v4
|
||||
```
|
||||
|
||||
# Define Metadata
|
||||
|
||||
Generated
+3
-3
@@ -6664,9 +6664,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
|
||||
"integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/is-array-buffer": {
|
||||
|
||||
+110
-94
@@ -1,15 +1,111 @@
|
||||
# @actions/artifact Releases
|
||||
|
||||
### 0.1.0
|
||||
### 2.1.4
|
||||
|
||||
- Initial release
|
||||
- Adds info-level logging for zip extraction
|
||||
|
||||
### 0.2.0
|
||||
### 2.1.3
|
||||
|
||||
- Fixes to TCP connections not closing
|
||||
- GZip file compression to speed up downloads
|
||||
- Improved logging and output
|
||||
- Extra documentation
|
||||
- Fixes a bug in the extract logic updated in 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
|
||||
|
||||
### 2.1.1
|
||||
|
||||
- Updated `isGhes` check to include `.ghe.com` and `.ghe.localhost` as accepted hosts
|
||||
|
||||
### 2.1.0
|
||||
|
||||
- Added `ArtifactClient#deleteArtifact` to delete artifacts by name [#1626](https://github.com/actions/toolkit/pull/1626)
|
||||
- Update error messaging to be more useful [#1628](https://github.com/actions/toolkit/pull/1628)
|
||||
|
||||
### 2.0.1
|
||||
|
||||
- Patch to fix transient request timeouts https://github.com/actions/download-artifact/issues/249
|
||||
|
||||
### 2.0.0
|
||||
|
||||
- Major release. Supports new Artifact backend for improved speed, reliability and behavior.
|
||||
- Numerous API changes, [some breaking](./README.md#breaking-changes).
|
||||
|
||||
- [Blog post with more info](https://github.blog/2024-02-12-get-started-with-v4-of-github-actions-artifacts/)
|
||||
|
||||
### 1.1.1
|
||||
|
||||
- Fixed a bug in Node16 where if an HTTP download finished too quickly (<1ms, e.g. when it's mocked) we attempt to delete a temp file that has not been created yet [#1278](https://github.com/actions/toolkit/pull/1278/commits/b9de68a590daf37c6747e38d3cb4f1dd2cfb791c)
|
||||
|
||||
### 1.1.0
|
||||
|
||||
- Add `x-actions-results-crc64` and `x-actions-results-md5` checksum headers on upload [#1063](https://github.com/actions/toolkit/pull/1063)
|
||||
|
||||
### 1.0.2
|
||||
|
||||
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
|
||||
|
||||
### 1.0.1
|
||||
|
||||
- Update to v2.0.0 of `@actions/http-client`
|
||||
|
||||
### 1.0.0
|
||||
|
||||
- Update `lockfileVersion` to `v2` in `package-lock.json` [#1009](https://github.com/actions/toolkit/pull/1009)
|
||||
|
||||
### 0.6.1
|
||||
|
||||
- Fix for failing 0 byte file uploads on Windows [#962](https://github.com/actions/toolkit/pull/962)
|
||||
|
||||
### 0.6.0
|
||||
|
||||
- Support upload from named pipes [#748](https://github.com/actions/toolkit/pull/748)
|
||||
- Fixes to percentage values being greater than 100% when downloading all artifacts [#889](https://github.com/actions/toolkit/pull/889)
|
||||
- Improved logging and output during artifact upload [#949](https://github.com/actions/toolkit/pull/949)
|
||||
- Improvements to client-side validation for certain invalid characters not allowed during upload: [#951](https://github.com/actions/toolkit/pull/951)
|
||||
- Faster upload speeds for certain types of large files by exempting gzip compression [#956](https://github.com/actions/toolkit/pull/956)
|
||||
- More detailed logging when dealing with chunked uploads [#957](https://github.com/actions/toolkit/pull/957)
|
||||
|
||||
### 0.5.2
|
||||
|
||||
- Add HTTP 500 as a retryable status code for artifact upload and download.
|
||||
|
||||
### 0.5.1
|
||||
|
||||
- Bump @actions/http-client to version 1.0.11 to fix proxy related issues during artifact upload and download
|
||||
|
||||
### 0.5.0
|
||||
|
||||
- Improved retry-ability for all http calls during artifact upload and download if an error is encountered
|
||||
|
||||
### 0.4.2
|
||||
|
||||
- Improved retry-ability when a partial artifact download is encountered
|
||||
|
||||
### 0.4.1
|
||||
|
||||
- Update to latest @actions/core version
|
||||
|
||||
### 0.4.0
|
||||
|
||||
- Add option to specify custom retentions on artifacts
|
||||
-
|
||||
### 0.3.5
|
||||
|
||||
- Retry in the event of a 413 response
|
||||
|
||||
### 0.3.3
|
||||
|
||||
- Increase chunk size during upload from 4MB to 8MB
|
||||
- Improve user-agent strings during API calls to help internally diagnose issues
|
||||
|
||||
### 0.3.2
|
||||
|
||||
- Fix to ensure readstreams get correctly reset in the event of a retry
|
||||
|
||||
### 0.3.1
|
||||
|
||||
- Fix to ensure temporary gzip files get correctly deleted during artifact upload
|
||||
- Remove spaces as a forbidden character during upload
|
||||
|
||||
### 0.3.0
|
||||
|
||||
@@ -20,93 +116,13 @@
|
||||
- Clearer error message if storage quota has been reached
|
||||
- Improved logging and output during artifact download
|
||||
|
||||
### 0.3.1
|
||||
### 0.2.0
|
||||
|
||||
- Fix to ensure temporary gzip files get correctly deleted during artifact upload
|
||||
- Remove spaces as a forbidden character during upload
|
||||
- Fixes to TCP connections not closing
|
||||
- GZip file compression to speed up downloads
|
||||
- Improved logging and output
|
||||
- Extra documentation
|
||||
|
||||
### 0.3.2
|
||||
### 0.1.0
|
||||
|
||||
- Fix to ensure readstreams get correctly reset in the event of a retry
|
||||
|
||||
### 0.3.3
|
||||
|
||||
- Increase chunk size during upload from 4MB to 8MB
|
||||
- Improve user-agent strings during API calls to help internally diagnose issues
|
||||
|
||||
### 0.3.5
|
||||
|
||||
- Retry in the event of a 413 response
|
||||
|
||||
### 0.4.0
|
||||
|
||||
- Add option to specify custom retentions on artifacts
|
||||
|
||||
### 0.4.1
|
||||
|
||||
- Update to latest @actions/core version
|
||||
|
||||
### 0.4.2
|
||||
|
||||
- Improved retry-ability when a partial artifact download is encountered
|
||||
|
||||
### 0.5.0
|
||||
|
||||
- Improved retry-ability for all http calls during artifact upload and download if an error is encountered
|
||||
|
||||
### 0.5.1
|
||||
|
||||
- Bump @actions/http-client to version 1.0.11 to fix proxy related issues during artifact upload and download
|
||||
|
||||
### 0.5.2
|
||||
|
||||
- Add HTTP 500 as a retryable status code for artifact upload and download.
|
||||
|
||||
### 0.6.0
|
||||
|
||||
- Support upload from named pipes [#748](https://github.com/actions/toolkit/pull/748)
|
||||
- Fixes to percentage values being greater than 100% when downloading all artifacts [#889](https://github.com/actions/toolkit/pull/889)
|
||||
- Improved logging and output during artifact upload [#949](https://github.com/actions/toolkit/pull/949)
|
||||
- Improvements to client-side validation for certain invalid characters not allowed during upload: [#951](https://github.com/actions/toolkit/pull/951)
|
||||
- Faster upload speeds for certain types of large files by exempting gzip compression [#956](https://github.com/actions/toolkit/pull/956)
|
||||
- More detailed logging when dealing with chunked uploads [#957](https://github.com/actions/toolkit/pull/957)
|
||||
|
||||
### 0.6.1
|
||||
|
||||
- Fix for failing 0 byte file uploads on Windows [#962](https://github.com/actions/toolkit/pull/962)
|
||||
|
||||
### 1.0.0
|
||||
|
||||
- Update `lockfileVersion` to `v2` in `package-lock.json` [#1009](https://github.com/actions/toolkit/pull/1009)
|
||||
|
||||
### 1.0.1
|
||||
|
||||
- Update to v2.0.0 of `@actions/http-client`
|
||||
|
||||
### 1.0.2
|
||||
|
||||
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
|
||||
|
||||
### 1.1.0
|
||||
|
||||
- Add `x-actions-results-crc64` and `x-actions-results-md5` checksum headers on upload [#1063](https://github.com/actions/toolkit/pull/1063)
|
||||
|
||||
### 1.1.1
|
||||
|
||||
- Fixed a bug in Node16 where if an HTTP download finished too quickly (<1ms, e.g. when it's mocked) we attempt to delete a temp file that has not been created yet [#1278](https://github.com/actions/toolkit/pull/1278/commits/b9de68a590daf37c6747e38d3cb4f1dd2cfb791c)
|
||||
|
||||
### 2.0.0
|
||||
|
||||
- Major release. Supports new Artifact backend for improved speed, reliability and behavior.
|
||||
- Numerous API changes, [some breaking](./README.md#breaking-changes).
|
||||
|
||||
- Blog post with more info: TBD
|
||||
|
||||
### 2.0.1
|
||||
|
||||
- Patch to fix transient request timeouts https://github.com/actions/download-artifact/issues/249
|
||||
|
||||
### 2.1.0
|
||||
|
||||
- Added `ArtifactClient#deleteArtifact` to delete artifacts by name [#1626](https://github.com/actions/toolkit/pull/1626)
|
||||
- Update error messaging to be more useful [#1628](https://github.com/actions/toolkit/pull/1628)
|
||||
- Initial release
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as config from '../src/internal/shared/config'
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
describe('isGhes', () => {
|
||||
it('should return false when the request domain is github.com', () => {
|
||||
process.env.GITHUB_SERVER_URL = 'https://github.com'
|
||||
expect(config.isGhes()).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when the request domain ends with ghe.com', () => {
|
||||
process.env.GITHUB_SERVER_URL = 'https://my.domain.ghe.com'
|
||||
expect(config.isGhes()).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when the request domain ends with ghe.localhost', () => {
|
||||
process.env.GITHUB_SERVER_URL = 'https://my.domain.ghe.localhost'
|
||||
expect(config.isGhes()).toBe(false)
|
||||
})
|
||||
|
||||
it('should return false when the request domain is specific to an enterprise', () => {
|
||||
process.env.GITHUB_SERVER_URL = 'https://my-enterprise.github.com'
|
||||
expect(config.isGhes()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -121,6 +121,16 @@ const mockGetArtifactFailure = jest.fn(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const mockGetArtifactMalicious = jest.fn(() => {
|
||||
const message = new http.IncomingMessage(new net.Socket())
|
||||
message.statusCode = 200
|
||||
message.push(fs.readFileSync(path.join(__dirname, 'fixtures', 'evil.zip'))) // evil.zip contains files that are formatted x/../../etc/hosts
|
||||
message.push(null)
|
||||
return {
|
||||
message
|
||||
}
|
||||
})
|
||||
|
||||
describe('download-artifact', () => {
|
||||
describe('public', () => {
|
||||
beforeEach(setup)
|
||||
@@ -170,6 +180,51 @@ describe('download-artifact', () => {
|
||||
expect(response.downloadPath).toBe(fixtures.workspaceDir)
|
||||
})
|
||||
|
||||
it('should not allow path traversal from malicious artifacts', async () => {
|
||||
const downloadArtifactMock = github.getOctokit(fixtures.token).rest
|
||||
.actions.downloadArtifact as MockedDownloadArtifact
|
||||
downloadArtifactMock.mockResolvedValueOnce({
|
||||
headers: {
|
||||
location: fixtures.blobStorageUrl
|
||||
},
|
||||
status: 302,
|
||||
url: '',
|
||||
data: Buffer.from('')
|
||||
})
|
||||
|
||||
const mockHttpClient = (HttpClient as jest.Mock).mockImplementation(
|
||||
() => {
|
||||
return {
|
||||
get: mockGetArtifactMalicious
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
await expect(
|
||||
downloadArtifactPublic(
|
||||
fixtures.artifactID,
|
||||
fixtures.repositoryOwner,
|
||||
fixtures.repositoryName,
|
||||
fixtures.token
|
||||
)
|
||||
).rejects.toBeInstanceOf(Error)
|
||||
|
||||
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
||||
owner: fixtures.repositoryOwner,
|
||||
repo: fixtures.repositoryName,
|
||||
artifact_id: fixtures.artifactID,
|
||||
archive_format: 'zip',
|
||||
request: {
|
||||
redirect: 'manual'
|
||||
}
|
||||
})
|
||||
|
||||
expect(mockHttpClient).toHaveBeenCalledWith(getUserAgentString())
|
||||
expect(mockGetArtifactMalicious).toHaveBeenCalledWith(
|
||||
fixtures.blobStorageUrl
|
||||
)
|
||||
})
|
||||
|
||||
it('should successfully download an artifact to user defined path', async () => {
|
||||
const customPath = path.join(testDir, 'custom')
|
||||
|
||||
|
||||
Binary file not shown.
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@actions/artifact",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/artifact",
|
||||
"version": "2.0.1",
|
||||
"version": "2.1.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/artifact",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.4",
|
||||
"preview": true,
|
||||
"description": "Actions artifact lib",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
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 core from '@actions/core'
|
||||
import * as httpClient from '@actions/http-client'
|
||||
@@ -44,6 +47,11 @@ async function streamExtract(url: string, directory: string): Promise<void> {
|
||||
await streamExtractExternal(url, directory)
|
||||
return
|
||||
} catch (error) {
|
||||
if (error.message.includes('Malformed extraction path')) {
|
||||
throw new Error(
|
||||
`Artifact download failed with unretryable error: ${error.message}`
|
||||
)
|
||||
}
|
||||
retryCount++
|
||||
core.debug(
|
||||
`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 createdDirectories = new Set<string>()
|
||||
createdDirectories.add(directory)
|
||||
response.message
|
||||
.on('data', () => {
|
||||
timer.refresh()
|
||||
@@ -89,8 +99,46 @@ export async function streamExtractExternal(
|
||||
clearTimeout(timer)
|
||||
reject(error)
|
||||
})
|
||||
.pipe(unzip.Extract({path: directory}))
|
||||
.on('close', () => {
|
||||
.pipe(unzip.Parse())
|
||||
.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)
|
||||
resolve()
|
||||
})
|
||||
|
||||
@@ -27,7 +27,13 @@ export function isGhes(): boolean {
|
||||
const ghUrl = new URL(
|
||||
process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
)
|
||||
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
|
||||
|
||||
const hostname = ghUrl.hostname.trimEnd().toUpperCase()
|
||||
const isGitHubHost = hostname === 'GITHUB.COM'
|
||||
const isGheHost =
|
||||
hostname.endsWith('.GHE.COM') || hostname.endsWith('.GHE.LOCALHOST')
|
||||
|
||||
return !isGitHubHost && !isGheHost
|
||||
}
|
||||
|
||||
export function getGitHubWorkspaceDir(): string {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright 2024 GitHub
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -0,0 +1,172 @@
|
||||
# `@actions/attest`
|
||||
|
||||
Functions for generating signed attestations for workflow artifacts.
|
||||
|
||||
Attestations bind some subject (a named artifact along with its digest) to a
|
||||
predicate (some assertion about that subject) using the [in-toto
|
||||
statement](https://github.com/in-toto/attestation/tree/main/spec/v1) format. A
|
||||
signature is generated for the attestation using a
|
||||
[Sigstore](https://www.sigstore.dev/)-issued signing certificate.
|
||||
|
||||
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
|
||||
initiated.
|
||||
|
||||
## Usage
|
||||
|
||||
### `attest`
|
||||
|
||||
The `attest` function takes the supplied subject/predicate pair and generates a
|
||||
signed attestation.
|
||||
|
||||
```js
|
||||
const { attest } = require('@actions/attest');
|
||||
const core = require('@actions/core');
|
||||
|
||||
async function run() {
|
||||
// In order to persist attestations to the repo, this should be a token with
|
||||
// repository write permissions.
|
||||
const ghToken = core.getInput('gh-token');
|
||||
|
||||
const attestation = await attest({
|
||||
subjectName: 'my-artifact-name',
|
||||
subjectDigest: { 'sha256': '36ab4667...'},
|
||||
predicateType: 'https://in-toto.io/attestation/release',
|
||||
predicate: { . . . },
|
||||
token: ghToken
|
||||
});
|
||||
|
||||
console.log(attestation);
|
||||
}
|
||||
|
||||
run();
|
||||
```
|
||||
|
||||
The `attest` function supports the following options:
|
||||
|
||||
```typescript
|
||||
export type AttestOptions = {
|
||||
// The name of the subject to be attested.
|
||||
subjectName: string
|
||||
// The digest of the subject to be attested. Should be a map of digest
|
||||
// algorithms to their hex-encoded values.
|
||||
subjectDigest: Record<string, string>
|
||||
// URI identifying the content type of the predicate being attested.
|
||||
predicateType: string
|
||||
// Predicate to be attested.
|
||||
predicate: object
|
||||
// GitHub token for writing attestations.
|
||||
token: string
|
||||
// Sigstore instance to use for signing. Must be one of "public-good" or
|
||||
// "github".
|
||||
sigstore?: 'public-good' | 'github'
|
||||
// Whether to skip writing the attestation to the GH attestations API.
|
||||
skipWrite?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### `attestProvenance`
|
||||
|
||||
The `attestProvenance` function accepts the name and digest of some artifact and
|
||||
generates a build provenance attestation over those values.
|
||||
|
||||
The attestation is formed by first generating a [SLSA provenance
|
||||
predicate](https://slsa.dev/spec/v1.0/provenance) populated with
|
||||
[metadata](https://github.com/slsa-framework/github-actions-buildtypes/tree/main/workflow/v1)
|
||||
pulled from the GitHub Actions run.
|
||||
|
||||
```js
|
||||
const { attestProvenance } = require('@actions/attest');
|
||||
const core = require('@actions/core');
|
||||
|
||||
async function run() {
|
||||
// In order to persist attestations to the repo, this should be a token with
|
||||
// repository write permissions.
|
||||
const ghToken = core.getInput('gh-token');
|
||||
|
||||
const attestation = await attestProvenance({
|
||||
subjectName: 'my-artifact-name',
|
||||
subjectDigest: { 'sha256': '36ab4667...'},
|
||||
token: ghToken
|
||||
});
|
||||
|
||||
console.log(attestation);
|
||||
}
|
||||
|
||||
run();
|
||||
```
|
||||
|
||||
The `attestProvenance` function supports the following options:
|
||||
|
||||
```typescript
|
||||
export type AttestProvenanceOptions = {
|
||||
// The name of the subject to be attested.
|
||||
subjectName: string
|
||||
// The digest of the subject to be attested. Should be a map of digest
|
||||
// algorithms to their hex-encoded values.
|
||||
subjectDigest: Record<string, string>
|
||||
// GitHub token for writing attestations.
|
||||
token: string
|
||||
// Sigstore instance to use for signing. Must be one of "public-good" or
|
||||
// "github".
|
||||
sigstore?: 'public-good' | 'github'
|
||||
// Whether to skip writing the attestation to the GH attestations API.
|
||||
skipWrite?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### `Attestation`
|
||||
|
||||
The `Attestation` returned by `attest`/`attestProvenance` has the following
|
||||
fields:
|
||||
|
||||
```typescript
|
||||
export type Attestation = {
|
||||
/*
|
||||
* JSON-serialized Sigstore bundle containing the provenance attestation,
|
||||
* signature, signing certificate and witnessed timestamp.
|
||||
*/
|
||||
bundle: SerializedBundle
|
||||
/*
|
||||
* PEM-encoded signing certificate used to sign the attestation.
|
||||
*/
|
||||
certificate: string
|
||||
/*
|
||||
* ID of Rekor transparency log entry created for the attestation (if
|
||||
* applicable).
|
||||
*/
|
||||
tlogID?: string
|
||||
/*
|
||||
* ID of the persisted attestation (accessible via the GH API).
|
||||
*/
|
||||
attestationID?: string
|
||||
}
|
||||
```
|
||||
|
||||
For details about the Sigstore bundle format, see the [Bundle protobuf
|
||||
specification](https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto).
|
||||
|
||||
## Sigstore Instance
|
||||
|
||||
When generating the signed attestation there are two different Sigstore
|
||||
instances which can be used to issue the signing certificate. By default,
|
||||
workflows initiated from public repositories will use the Sigstore public-good
|
||||
instance and persist the attestation signature to the public [Rekor transparency
|
||||
log](https://docs.sigstore.dev/logging/overview/). Workflows initiated from
|
||||
private/internal repositories will use the GitHub-internal Sigstore instance
|
||||
which uses a signed timestamp issued by GitHub's timestamp authority in place of
|
||||
the public transparency log.
|
||||
|
||||
The default Sigstore instance selection can be overridden by passing an explicit
|
||||
value of either "public-good" or "github" for the `sigstore` option when calling
|
||||
either `attest` or `attestProvenance`.
|
||||
|
||||
## Storage
|
||||
|
||||
Attestations created by `attest`/`attestProvenance` will be uploaded to the GH
|
||||
attestations API and associated with the appropriate repository. Attestation
|
||||
storage is only supported for public repositories or repositories which belong
|
||||
to a GitHub Enterprise Cloud account.
|
||||
|
||||
In order to generate attestations for private, non-Enterprise repositories, the
|
||||
`skipWrite` option should be set to `true`.
|
||||
@@ -0,0 +1,5 @@
|
||||
# @actions/attest Releases
|
||||
|
||||
### 1.0.0
|
||||
|
||||
- Initial release
|
||||
@@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`buildIntotoStatement returns a provenance hydrated from env vars 1`] = `
|
||||
{
|
||||
"_type": "https://in-toto.io/Statement/v1",
|
||||
"predicate": {
|
||||
"key": "value",
|
||||
},
|
||||
"predicateType": "predicatey",
|
||||
"subject": [
|
||||
{
|
||||
"digest": {
|
||||
"sha256": "7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32",
|
||||
},
|
||||
"name": "subjecty",
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,42 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`buildSLSAProvenancePredicate returns a provenance hydrated from env vars 1`] = `
|
||||
{
|
||||
"params": {
|
||||
"buildDefinition": {
|
||||
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
|
||||
"externalParameters": {
|
||||
"workflow": {
|
||||
"path": ".github/workflows/main.yml",
|
||||
"ref": "main",
|
||||
"repository": "https://github.com/owner/repo",
|
||||
},
|
||||
},
|
||||
"internalParameters": {
|
||||
"github": {
|
||||
"event_name": "push",
|
||||
"repository_id": "repo-id",
|
||||
"repository_owner_id": "owner-id",
|
||||
},
|
||||
},
|
||||
"resolvedDependencies": [
|
||||
{
|
||||
"digest": {
|
||||
"gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b",
|
||||
},
|
||||
"uri": "git+https://github.com/owner/repo@refs/heads/main",
|
||||
},
|
||||
],
|
||||
},
|
||||
"runDetails": {
|
||||
"builder": {
|
||||
"id": "https://github.com/actions/runner/github-hosted",
|
||||
},
|
||||
"metadata": {
|
||||
"invocationId": "https://github.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
|
||||
},
|
||||
},
|
||||
},
|
||||
"type": "https://slsa.dev/provenance/v1",
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,6 @@
|
||||
import {attest, attestProvenance} from '../src'
|
||||
|
||||
it('exports functions', () => {
|
||||
expect(attestProvenance).toBeInstanceOf(Function)
|
||||
expect(attest).toBeInstanceOf(Function)
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import {buildIntotoStatement} from '../src/intoto'
|
||||
import type {Predicate, Subject} from '../src/shared.types'
|
||||
|
||||
describe('buildIntotoStatement', () => {
|
||||
const subject: Subject = {
|
||||
name: 'subjecty',
|
||||
digest: {
|
||||
sha256: '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
|
||||
}
|
||||
}
|
||||
|
||||
const predicate: Predicate = {
|
||||
type: 'predicatey',
|
||||
params: {
|
||||
key: 'value'
|
||||
}
|
||||
}
|
||||
|
||||
it('returns a provenance hydrated from env vars', () => {
|
||||
const statement = buildIntotoStatement(subject, predicate)
|
||||
expect(statement).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,213 @@
|
||||
import * as github from '@actions/github'
|
||||
import {mockFulcio, mockRekor, mockTSA} from '@sigstore/mock'
|
||||
import nock from 'nock'
|
||||
import {SIGSTORE_GITHUB, SIGSTORE_PUBLIC_GOOD} from '../src/endpoints'
|
||||
import {attestProvenance, buildSLSAProvenancePredicate} from '../src/provenance'
|
||||
|
||||
// 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
|
||||
|
||||
// Subject to attest
|
||||
const subjectName = 'subjective'
|
||||
const subjectDigest = {
|
||||
sha256: '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
|
||||
}
|
||||
|
||||
// Fake an OIDC token
|
||||
const oidcPayload = {sub: 'foo@bar.com', iss: ''}
|
||||
const oidcToken = `.${Buffer.from(JSON.stringify(oidcPayload)).toString(
|
||||
'base64'
|
||||
)}.}`
|
||||
|
||||
const tokenURL = 'https://token.url'
|
||||
const attestationID = '1234567890'
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
|
||||
nock(tokenURL)
|
||||
.get('/')
|
||||
.query({audience: 'sigstore'})
|
||||
.reply(200, {value: oidcToken})
|
||||
|
||||
// Set-up GHA environment variables
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
...env,
|
||||
ACTIONS_ID_TOKEN_REQUEST_URL: tokenURL,
|
||||
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token'
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Restore the original environment
|
||||
process.env = originalEnv
|
||||
})
|
||||
|
||||
describe('when using the github Sigstore instance', () => {
|
||||
const {fulcioURL, tsaServerURL} = SIGSTORE_GITHUB
|
||||
|
||||
beforeEach(async () => {
|
||||
// Mock Sigstore
|
||||
await mockFulcio({baseURL: fulcioURL, strict: false})
|
||||
await mockTSA({baseURL: tsaServerURL})
|
||||
|
||||
// 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 () => {
|
||||
const attestation = await attestProvenance({
|
||||
subjectName,
|
||||
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(() => {
|
||||
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).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 () => {
|
||||
const attestation = await attestProvenance({
|
||||
subjectName,
|
||||
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({
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,105 @@
|
||||
import {mockFulcio, mockRekor, mockTSA} from '@sigstore/mock'
|
||||
import nock from 'nock'
|
||||
import {Payload, signPayload} from '../src/sign'
|
||||
|
||||
describe('signProvenance', () => {
|
||||
const originalEnv = process.env
|
||||
|
||||
// Fake an OIDC token
|
||||
const subject = 'foo@bar.com'
|
||||
const oidcPayload = {sub: subject, iss: ''}
|
||||
const oidcToken = `.${Buffer.from(JSON.stringify(oidcPayload)).toString(
|
||||
'base64'
|
||||
)}.}`
|
||||
|
||||
// Dummy provenance to be signed
|
||||
const provenance = {
|
||||
_type: 'https://in-toto.io/Statement/v1',
|
||||
subject: {
|
||||
name: 'subjective',
|
||||
digest: {
|
||||
sha256:
|
||||
'7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const payload: Payload = {
|
||||
body: Buffer.from(JSON.stringify(provenance)),
|
||||
type: 'application/vnd.in-toto+json'
|
||||
}
|
||||
|
||||
const fulcioURL = 'https://fulcio.url'
|
||||
const rekorURL = 'https://rekor.url'
|
||||
const tsaServerURL = 'https://tsa.url'
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock OIDC token endpoint
|
||||
const tokenURL = 'https://token.url'
|
||||
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
ACTIONS_ID_TOKEN_REQUEST_URL: tokenURL,
|
||||
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token'
|
||||
}
|
||||
|
||||
nock(tokenURL)
|
||||
.get('/')
|
||||
.query({audience: 'sigstore'})
|
||||
.reply(200, {value: oidcToken})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv
|
||||
})
|
||||
|
||||
describe('when visibility is public', () => {
|
||||
beforeEach(async () => {
|
||||
await mockFulcio({baseURL: fulcioURL, strict: false})
|
||||
await mockRekor({baseURL: rekorURL})
|
||||
})
|
||||
|
||||
it('returns a bundle', async () => {
|
||||
const att = await signPayload(payload, {fulcioURL, rekorURL})
|
||||
|
||||
expect(att).toBeDefined()
|
||||
expect(att.mediaType).toEqual(
|
||||
'application/vnd.dev.sigstore.bundle+json;version=0.2'
|
||||
)
|
||||
|
||||
expect(att.content.$case).toEqual('dsseEnvelope')
|
||||
expect(att.verificationMaterial.content.$case).toEqual(
|
||||
'x509CertificateChain'
|
||||
)
|
||||
expect(att.verificationMaterial.tlogEntries).toHaveLength(1)
|
||||
expect(
|
||||
att.verificationMaterial.timestampVerificationData?.rfc3161Timestamps
|
||||
).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when visibility is private', () => {
|
||||
beforeEach(async () => {
|
||||
await mockFulcio({baseURL: fulcioURL, strict: false})
|
||||
await mockTSA({baseURL: tsaServerURL})
|
||||
})
|
||||
|
||||
it('returns a bundle', async () => {
|
||||
const att = await signPayload(payload, {fulcioURL, tsaServerURL})
|
||||
|
||||
expect(att).toBeDefined()
|
||||
expect(att.mediaType).toEqual(
|
||||
'application/vnd.dev.sigstore.bundle+json;version=0.2'
|
||||
)
|
||||
|
||||
expect(att.content.$case).toEqual('dsseEnvelope')
|
||||
expect(att.verificationMaterial.content.$case).toEqual(
|
||||
'x509CertificateChain'
|
||||
)
|
||||
expect(att.verificationMaterial.tlogEntries).toHaveLength(0)
|
||||
expect(
|
||||
att.verificationMaterial.timestampVerificationData?.rfc3161Timestamps
|
||||
).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,45 @@
|
||||
import nock from 'nock'
|
||||
import {writeAttestation} from '../src/store'
|
||||
|
||||
describe('writeAttestation', () => {
|
||||
const originalEnv = process.env
|
||||
const attestation = {foo: 'bar '}
|
||||
const token = 'token'
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = {
|
||||
...originalEnv,
|
||||
GITHUB_REPOSITORY: 'foo/bar'
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv
|
||||
})
|
||||
|
||||
describe('when the api call is successful', () => {
|
||||
beforeEach(() => {
|
||||
nock('https://api.github.com')
|
||||
.matchHeader('authorization', `token ${token}`)
|
||||
.post('/repos/foo/bar/attestations', {bundle: attestation})
|
||||
.reply(201, {id: '123'})
|
||||
})
|
||||
|
||||
it('persists the attestation', async () => {
|
||||
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/)
|
||||
})
|
||||
})
|
||||
})
|
||||
Generated
+3041
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@actions/attest",
|
||||
"version": "1.0.0",
|
||||
"description": "Actions attestation lib",
|
||||
"keywords": [
|
||||
"github",
|
||||
"actions",
|
||||
"attestation"
|
||||
],
|
||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/attest",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"directories": {
|
||||
"lib": "lib",
|
||||
"test": "__tests__"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"provenance": true
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/actions/toolkit.git",
|
||||
"directory": "packages/attest"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: run tests from root\" && exit 1",
|
||||
"tsc": "tsc"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/actions/toolkit/issues"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sigstore/mock": "^0.6.5",
|
||||
"@sigstore/rekor-types": "^2.0.0",
|
||||
"@types/make-fetch-happen": "^10.0.4",
|
||||
"nock": "^13.5.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/github": "^6.0.0",
|
||||
"@sigstore/bundle": "^2.2.0",
|
||||
"@sigstore/sign": "^2.2.3",
|
||||
"make-fetch-happen": "^13.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import {Bundle, bundleToJSON} from '@sigstore/bundle'
|
||||
import {X509Certificate} from 'crypto'
|
||||
import {SigstoreInstance, signingEndpoints} from './endpoints'
|
||||
import {buildIntotoStatement} from './intoto'
|
||||
import {Payload, signPayload} from './sign'
|
||||
import {writeAttestation} from './store'
|
||||
|
||||
import type {Attestation, Predicate, Subject} from './shared.types'
|
||||
|
||||
const INTOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json'
|
||||
|
||||
/**
|
||||
* Options for attesting a subject / predicate.
|
||||
*/
|
||||
export type AttestOptions = {
|
||||
// The name of the subject to be attested.
|
||||
subjectName: string
|
||||
// The digest of the subject to be attested. Should be a map of digest
|
||||
// algorithms to their hex-encoded values.
|
||||
subjectDigest: Record<string, string>
|
||||
// Content type of the predicate being attested.
|
||||
predicateType: string
|
||||
// Predicate to be attested.
|
||||
predicate: object
|
||||
// GitHub token for writing attestations.
|
||||
token: string
|
||||
// Sigstore instance to use for signing. Must be one of "public-good" or
|
||||
// "github".
|
||||
sigstore?: SigstoreInstance
|
||||
// Whether to skip writing the attestation to the GH attestations API.
|
||||
skipWrite?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an attestation for the given subject and predicate. The subject and
|
||||
* predicate are combined into an in-toto statement, which is then signed using
|
||||
* the identified Sigstore instance and stored as an attestation.
|
||||
* @param options - The options for attestation.
|
||||
* @returns A promise that resolves to the attestation.
|
||||
*/
|
||||
export async function attest(options: AttestOptions): Promise<Attestation> {
|
||||
const subject: Subject = {
|
||||
name: options.subjectName,
|
||||
digest: options.subjectDigest
|
||||
}
|
||||
const predicate: Predicate = {
|
||||
type: options.predicateType,
|
||||
params: options.predicate
|
||||
}
|
||||
const statement = buildIntotoStatement(subject, predicate)
|
||||
|
||||
// Sign the provenance statement
|
||||
const payload: Payload = {
|
||||
body: Buffer.from(JSON.stringify(statement)),
|
||||
type: INTOTO_PAYLOAD_TYPE
|
||||
}
|
||||
const endpoints = signingEndpoints(options.sigstore)
|
||||
const bundle = await signPayload(payload, endpoints)
|
||||
|
||||
// Store the attestation
|
||||
let attestationID: string | undefined
|
||||
if (options.skipWrite !== true) {
|
||||
attestationID = await writeAttestation(bundleToJSON(bundle), options.token)
|
||||
}
|
||||
|
||||
return toAttestation(bundle, attestationID)
|
||||
}
|
||||
|
||||
function toAttestation(bundle: Bundle, attestationID?: string): Attestation {
|
||||
let certBytes: Buffer
|
||||
switch (bundle.verificationMaterial.content.$case) {
|
||||
case 'x509CertificateChain':
|
||||
certBytes =
|
||||
bundle.verificationMaterial.content.x509CertificateChain.certificates[0]
|
||||
.rawBytes
|
||||
break
|
||||
case 'certificate':
|
||||
certBytes = bundle.verificationMaterial.content.certificate.rawBytes
|
||||
break
|
||||
default:
|
||||
throw new Error('Bundle must contain an x509 certificate')
|
||||
}
|
||||
|
||||
const signingCert = new X509Certificate(certBytes)
|
||||
|
||||
// Collect transparency log ID if available
|
||||
const tlogEntries = bundle.verificationMaterial.tlogEntries
|
||||
const tlogID = tlogEntries.length > 0 ? tlogEntries[0].logIndex : undefined
|
||||
|
||||
return {
|
||||
bundle: bundleToJSON(bundle),
|
||||
certificate: signingCert.toString(),
|
||||
tlogID,
|
||||
attestationID
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import * as github from '@actions/github'
|
||||
|
||||
const PUBLIC_GOOD_ID = 'public-good'
|
||||
const GITHUB_ID = 'github'
|
||||
|
||||
const FULCIO_PUBLIC_GOOD_URL = 'https://fulcio.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 Endpoints = {
|
||||
fulcioURL: string
|
||||
rekorURL?: string
|
||||
tsaServerURL?: string
|
||||
}
|
||||
|
||||
export const SIGSTORE_PUBLIC_GOOD: Endpoints = {
|
||||
fulcioURL: FULCIO_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 => {
|
||||
let instance: SigstoreInstance
|
||||
|
||||
// An explicitly set instance type takes precedence, but if not set, use the
|
||||
// repository's visibility to determine the instance type.
|
||||
if (sigstore && [PUBLIC_GOOD_ID, GITHUB_ID].includes(sigstore)) {
|
||||
instance = sigstore
|
||||
} else {
|
||||
instance =
|
||||
github.context.payload.repository?.visibility === 'public'
|
||||
? PUBLIC_GOOD_ID
|
||||
: GITHUB_ID
|
||||
}
|
||||
|
||||
switch (instance) {
|
||||
case PUBLIC_GOOD_ID:
|
||||
return SIGSTORE_PUBLIC_GOOD
|
||||
case GITHUB_ID:
|
||||
return SIGSTORE_GITHUB
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export {AttestOptions, attest} from './attest'
|
||||
export {
|
||||
AttestProvenanceOptions,
|
||||
attestProvenance,
|
||||
buildSLSAProvenancePredicate
|
||||
} from './provenance'
|
||||
|
||||
export type {SerializedBundle} from '@sigstore/bundle'
|
||||
export type {Attestation, Predicate, Subject} from './shared.types'
|
||||
@@ -0,0 +1,32 @@
|
||||
import {Predicate, Subject} from './shared.types'
|
||||
|
||||
const INTOTO_STATEMENT_V1_TYPE = 'https://in-toto.io/Statement/v1'
|
||||
|
||||
/**
|
||||
* An in-toto statement.
|
||||
* https://github.com/in-toto/attestation/blob/main/spec/v1/statement.md
|
||||
*/
|
||||
export type InTotoStatement = {
|
||||
_type: string
|
||||
subject: Subject[]
|
||||
predicateType: string
|
||||
predicate: object
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles the given subject and predicate into an in-toto statement.
|
||||
* @param subject - The subject of the statement.
|
||||
* @param predicate - The predicate of the statement.
|
||||
* @returns The constructed in-toto statement.
|
||||
*/
|
||||
export const buildIntotoStatement = (
|
||||
subject: Subject,
|
||||
predicate: Predicate
|
||||
): InTotoStatement => {
|
||||
return {
|
||||
_type: INTOTO_STATEMENT_V1_TYPE,
|
||||
subject: [subject],
|
||||
predicateType: predicate.type,
|
||||
predicate: predicate.params
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import {attest, AttestOptions} from './attest'
|
||||
import type {Attestation, Predicate} from './shared.types'
|
||||
|
||||
const SLSA_PREDICATE_V1_TYPE = 'https://slsa.dev/provenance/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<
|
||||
AttestOptions,
|
||||
'predicate' | 'predicateType'
|
||||
>
|
||||
|
||||
/**
|
||||
* Builds an SLSA (Supply Chain Levels for Software Artifacts) provenance
|
||||
* predicate using the GitHub Actions Workflow build type.
|
||||
* https://slsa.dev/spec/v1.0/provenance
|
||||
* https://github.com/slsa-framework/github-actions-buildtypes/tree/main/workflow/v1
|
||||
* @param env - The Node.js process environment variables. Defaults to
|
||||
* `process.env`.
|
||||
* @returns The SLSA provenance predicate.
|
||||
*/
|
||||
export const buildSLSAProvenancePredicate = (
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): Predicate => {
|
||||
const workflow = env.GITHUB_WORKFLOW_REF || ''
|
||||
|
||||
// Split just the path and ref from the workflow string.
|
||||
// owner/repo/.github/workflows/main.yml@main =>
|
||||
// .github/workflows/main.yml, main
|
||||
const [workflowPath, workflowRef] = workflow
|
||||
.replace(`${env.GITHUB_REPOSITORY}/`, '')
|
||||
.split('@')
|
||||
|
||||
return {
|
||||
type: SLSA_PREDICATE_V1_TYPE,
|
||||
params: {
|
||||
buildDefinition: {
|
||||
buildType: GITHUB_BUILD_TYPE,
|
||||
externalParameters: {
|
||||
workflow: {
|
||||
ref: workflowRef,
|
||||
repository: `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}`,
|
||||
path: workflowPath
|
||||
}
|
||||
},
|
||||
internalParameters: {
|
||||
github: {
|
||||
event_name: env.GITHUB_EVENT_NAME,
|
||||
repository_id: env.GITHUB_REPOSITORY_ID,
|
||||
repository_owner_id: env.GITHUB_REPOSITORY_OWNER_ID
|
||||
}
|
||||
},
|
||||
resolvedDependencies: [
|
||||
{
|
||||
uri: `git+${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}@${env.GITHUB_REF}`,
|
||||
digest: {
|
||||
gitCommit: env.GITHUB_SHA
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
runDetails: {
|
||||
builder: {
|
||||
id: `${GITHUB_BUILDER_ID_PREFIX}/${env.RUNNER_ENVIRONMENT}`
|
||||
},
|
||||
metadata: {
|
||||
invocationId: `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}/attempts/${env.GITHUB_RUN_ATTEMPT}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attests the build provenance of the provided subject. Generates the SLSA
|
||||
* build provenance predicate, assembles it into an in-toto statement, and
|
||||
* attests it.
|
||||
*
|
||||
* @param options - The options for attesting the provenance.
|
||||
* @returns A promise that resolves to the attestation.
|
||||
*/
|
||||
export async function attestProvenance(
|
||||
options: AttestProvenanceOptions
|
||||
): Promise<Attestation> {
|
||||
const predicate = buildSLSAProvenancePredicate(process.env)
|
||||
return attest({
|
||||
...options,
|
||||
predicateType: predicate.type,
|
||||
predicate: predicate.params
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type {SerializedBundle} from '@sigstore/bundle'
|
||||
|
||||
/*
|
||||
* The subject of an attestation.
|
||||
*/
|
||||
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>
|
||||
}
|
||||
|
||||
/*
|
||||
* The predicate of an attestation.
|
||||
*/
|
||||
export type Predicate = {
|
||||
/*
|
||||
* URI identifying the content type of the predicate.
|
||||
*/
|
||||
type: string
|
||||
/*
|
||||
* Predicate parameters.
|
||||
*/
|
||||
params: object
|
||||
}
|
||||
|
||||
/*
|
||||
* Artifact attestation.
|
||||
*/
|
||||
export type Attestation = {
|
||||
/*
|
||||
* Serialized Sigstore bundle containing the provenance attestation,
|
||||
* signature, signing certificate and witnessed timestamp.
|
||||
*/
|
||||
bundle: SerializedBundle
|
||||
/*
|
||||
* PEM-encoded signing certificate used to sign the attestation.
|
||||
*/
|
||||
certificate: string
|
||||
/*
|
||||
* ID of Rekor transparency log entry created for the attestation.
|
||||
*/
|
||||
tlogID?: string
|
||||
/*
|
||||
* ID of the persisted attestation (accessible via the GH API).
|
||||
*/
|
||||
attestationID?: string
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import {Bundle} from '@sigstore/bundle'
|
||||
import {
|
||||
BundleBuilder,
|
||||
CIContextProvider,
|
||||
DSSEBundleBuilder,
|
||||
FulcioSigner,
|
||||
RekorWitness,
|
||||
TSAWitness,
|
||||
Witness
|
||||
} from '@sigstore/sign'
|
||||
|
||||
const OIDC_AUDIENCE = 'sigstore'
|
||||
const DEFAULT_TIMEOUT = 10000
|
||||
const DEFAULT_RETRIES = 3
|
||||
|
||||
/**
|
||||
* The payload to be signed (body) and its media type (type).
|
||||
*/
|
||||
export type Payload = {
|
||||
body: Buffer
|
||||
type: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for signing a document.
|
||||
*/
|
||||
export type SignOptions = {
|
||||
/**
|
||||
* The URL of the Fulcio service.
|
||||
*/
|
||||
fulcioURL: string
|
||||
/**
|
||||
* The URL of the Rekor service.
|
||||
*/
|
||||
rekorURL?: string
|
||||
/**
|
||||
* The URL of the TSA (Time Stamping Authority) server.
|
||||
*/
|
||||
tsaServerURL?: string
|
||||
/**
|
||||
* The timeout duration in milliseconds when communicating with Sigstore
|
||||
* services.
|
||||
*/
|
||||
timeout?: number
|
||||
/**
|
||||
* The number of retry attempts.
|
||||
*/
|
||||
retry?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs the provided payload with a Sigstore-issued certificate and returns the
|
||||
* signature bundle.
|
||||
* @param payload Payload to be signed.
|
||||
* @param options Signing options.
|
||||
* @returns A promise that resolves to the Sigstore signature bundle.
|
||||
*/
|
||||
export const signPayload = async (
|
||||
payload: Payload,
|
||||
options: SignOptions
|
||||
): Promise<Bundle> => {
|
||||
const artifact = {
|
||||
data: payload.body,
|
||||
type: payload.type
|
||||
}
|
||||
|
||||
// Sign the artifact and build the bundle
|
||||
return initBundleBuilder(options).create(artifact)
|
||||
}
|
||||
|
||||
// Assembles the Sigstore bundle builder with the appropriate options
|
||||
const initBundleBuilder = (opts: SignOptions): BundleBuilder => {
|
||||
const identityProvider = new CIContextProvider(OIDC_AUDIENCE)
|
||||
const timeout = opts.timeout || DEFAULT_TIMEOUT
|
||||
const retry = opts.retry || DEFAULT_RETRIES
|
||||
const witnesses: Witness[] = []
|
||||
|
||||
const signer = new FulcioSigner({
|
||||
identityProvider,
|
||||
fulcioBaseURL: opts.fulcioURL,
|
||||
timeout,
|
||||
retry
|
||||
})
|
||||
|
||||
if (opts.rekorURL) {
|
||||
witnesses.push(
|
||||
new RekorWitness({
|
||||
rekorBaseURL: opts.rekorURL,
|
||||
entryType: 'dsse',
|
||||
timeout,
|
||||
retry
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if (opts.tsaServerURL) {
|
||||
witnesses.push(
|
||||
new TSAWitness({
|
||||
tsaBaseURL: opts.tsaServerURL,
|
||||
timeout,
|
||||
retry
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return new DSSEBundleBuilder({signer, witnesses})
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as github from '@actions/github'
|
||||
import fetch from 'make-fetch-happen'
|
||||
|
||||
const CREATE_ATTESTATION_REQUEST = 'POST /repos/{owner}/{repo}/attestations'
|
||||
|
||||
/**
|
||||
* Writes an attestation to the repository's attestations endpoint.
|
||||
* @param attestation - The attestation to write.
|
||||
* @param token - The GitHub token for authentication.
|
||||
* @returns The ID of the attestation.
|
||||
* @throws Error if the attestation fails to persist.
|
||||
*/
|
||||
export const writeAttestation = async (
|
||||
attestation: unknown,
|
||||
token: string
|
||||
): Promise<string> => {
|
||||
const octokit = github.getOctokit(token, {request: {fetch}})
|
||||
|
||||
try {
|
||||
const response = await octokit.request(CREATE_ATTESTATION_REQUEST, {
|
||||
owner: github.context.repo.owner,
|
||||
repo: github.context.repo.repo,
|
||||
data: {bundle: attestation}
|
||||
})
|
||||
|
||||
return response.data?.id
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : err
|
||||
throw new Error(`Failed to persist attestation: ${message}`)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"outDir": "./lib",
|
||||
"declaration": true,
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"./src"
|
||||
]
|
||||
}
|
||||
Vendored
+152
-152
@@ -1,16 +1,154 @@
|
||||
# @actions/cache Releases
|
||||
|
||||
### 0.1.0
|
||||
### 3.2.4
|
||||
|
||||
- Initial release
|
||||
- Updated `isGhes` check to include `.ghe.com` and `.ghe.localhost` as accepted hosts
|
||||
|
||||
### 3.2.3
|
||||
|
||||
### 0.2.0
|
||||
- Fixed a bug that mutated path arguments to `getCacheVersion` [#1378](https://github.com/actions/toolkit/pull/1378)
|
||||
|
||||
- Fixes issues with the zstd compression algorithm on Windows and Ubuntu 16.04 [#469](https://github.com/actions/toolkit/pull/469)
|
||||
### 3.2.2
|
||||
|
||||
### 0.2.1
|
||||
- Add new default cache download method to improve performance and reduce hangs [#1484](https://github.com/actions/toolkit/pull/1484)
|
||||
|
||||
- Fix to await async function getCompressionMethod
|
||||
### 3.2.1
|
||||
|
||||
- Updated @azure/storage-blob to `v12.13.0`
|
||||
|
||||
### 3.2.0
|
||||
|
||||
- Add `lookupOnly` to cache restore `DownloadOptions`.
|
||||
|
||||
### 3.1.4
|
||||
|
||||
- Fix zstd not being used due to `zstd --version` output change in zstd 1.5.4 release. See [#1353](https://github.com/actions/toolkit/pull/1353).
|
||||
|
||||
### 3.1.3
|
||||
|
||||
- Fix to prevent from setting MYSYS environement variable globally [#1329](https://github.com/actions/toolkit/pull/1329).
|
||||
|
||||
### 3.1.2
|
||||
|
||||
- Fix issue with symlink restoration on windows.
|
||||
|
||||
### 3.1.1
|
||||
|
||||
- Reverted changes in 3.1.0 to fix issue with symlink restoration on windows.
|
||||
- Added support for verbose logging about cache version during cache miss.
|
||||
|
||||
### 3.1.0
|
||||
|
||||
- Update actions/cache on windows to use gnu tar and zstd by default
|
||||
- Update actions/cache on windows to fallback to bsdtar and zstd if gnu tar is not available.
|
||||
- Added support for fallback to gzip to restore old caches on windows.
|
||||
|
||||
### 3.1.0-beta.3
|
||||
|
||||
- Bug Fixes for fallback to gzip to restore old caches on windows and bsdtar if gnutar is not available.
|
||||
|
||||
### 3.1.0-beta.2
|
||||
|
||||
- Added support for fallback to gzip to restore old caches on windows.
|
||||
|
||||
### 3.0.6
|
||||
|
||||
- Added `@azure/abort-controller` to dependencies to fix compatibility issue with ESM [#1208](https://github.com/actions/toolkit/issues/1208)
|
||||
|
||||
### 3.0.5
|
||||
|
||||
- Update `@actions/cache` to use `@actions/core@^1.10.0`
|
||||
|
||||
### 3.0.4
|
||||
|
||||
- Fix zstd not working for windows on gnu tar in issues [#888](https://github.com/actions/cache/issues/888) and [#891](https://github.com/actions/cache/issues/891).
|
||||
- Allowing users to provide a custom timeout as input for aborting download of a cache segment using an environment variable `SEGMENT_DOWNLOAD_TIMEOUT_MINS`. Default is 60 minutes.
|
||||
|
||||
### 3.0.3
|
||||
|
||||
- Bug fixes for download stuck issue [#810](https://github.com/actions/cache/issues/810).
|
||||
|
||||
### 3.0.2
|
||||
|
||||
- Added 1 hour timeout for the download stuck issue [#810](https://github.com/actions/cache/issues/810).
|
||||
|
||||
### 3.0.1
|
||||
|
||||
- Fix [#833](https://github.com/actions/cache/issues/833) - cache doesn't work with github workspace directory.
|
||||
- Fix [#809](https://github.com/actions/cache/issues/809) `zstd -d: no such file or directory` error on AWS self-hosted runners.
|
||||
|
||||
### 3.0.0
|
||||
|
||||
- Updated actions/cache to suppress Actions cache server error and log warning for those error [#1122](https://github.com/actions/toolkit/pull/1122)
|
||||
|
||||
### 2.0.6
|
||||
|
||||
- Fix `Tar failed with error: The process '/usr/bin/tar' failed with exit code 1` issue when temp directory where tar is getting created is actually the subdirectory of the path mentioned by the user for caching. ([issue](https://github.com/actions/cache/issues/689))
|
||||
|
||||
### 2.0.5
|
||||
|
||||
- Fix to avoid saving empty cache when no files are available for caching. ([issue](https://github.com/actions/cache/issues/624))
|
||||
|
||||
### 2.0.4
|
||||
|
||||
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
|
||||
|
||||
### 2.0.3
|
||||
|
||||
- Update to v2.0.0 of `@actions/http-client`
|
||||
|
||||
### 2.0.0
|
||||
|
||||
- Added support to check if Actions cache service feature is available or not [#1028](https://github.com/actions/toolkit/pull/1028)
|
||||
|
||||
### 1.0.11
|
||||
|
||||
- Fix file downloads > 2GB([issue](https://github.com/actions/cache/issues/773))
|
||||
|
||||
### 1.0.10
|
||||
|
||||
- Update `lockfileVersion` to `v2` in `package-lock.json [#1022](https://github.com/actions/toolkit/pull/1022)
|
||||
|
||||
### 1.0.9
|
||||
|
||||
- Use @azure/ms-rest-js v2.6.0
|
||||
- Use @azure/storage-blob v12.8.0
|
||||
|
||||
### 1.0.8
|
||||
|
||||
- Increase the allowed artifact cache size from 5GB to 10GB ([issue](https://github.com/actions/cache/discussions/497))
|
||||
|
||||
### 1.0.7
|
||||
|
||||
- Fixes permissions issue extracting archives with GNU tar on macOS ([issue](https://github.com/actions/cache/issues/527))
|
||||
|
||||
### 1.0.6
|
||||
|
||||
- Make caching more verbose [#650](https://github.com/actions/toolkit/pull/650)
|
||||
- Use GNU tar on macOS if available [#701](https://github.com/actions/toolkit/pull/701)
|
||||
|
||||
### 1.0.5
|
||||
|
||||
- Fix to ensure Windows cache paths get resolved correctly
|
||||
|
||||
### 1.0.4
|
||||
|
||||
- Use @actions/core v1.2.6
|
||||
- Fixes uploadChunk to throw an error if any unsuccessful response code is received
|
||||
|
||||
### 1.0.3
|
||||
|
||||
- Use http-client v1.0.9
|
||||
- Fixes error handling so retries are not attempted on non-retryable errors (409 Conflict, for example)
|
||||
- Adds 5 second delay between retry attempts
|
||||
|
||||
### 1.0.2
|
||||
|
||||
- Use posix archive format to add support for some tools
|
||||
|
||||
### 1.0.1
|
||||
|
||||
- Fix bug in downloading large files (> 2 GBs) with the Azure SDK
|
||||
|
||||
### 1.0.0
|
||||
|
||||
@@ -19,152 +157,14 @@
|
||||
- Includes changes that break compatibility with earlier versions, including:
|
||||
- `retry`, `retryTypedResponse`, and `retryHttpClientResponse` moved from `cacheHttpClient` to `requestUtils`
|
||||
|
||||
### 1.0.1
|
||||
### 0.2.1
|
||||
|
||||
- Fix bug in downloading large files (> 2 GBs) with the Azure SDK
|
||||
- Fix to await async function getCompressionMethod
|
||||
|
||||
### 0.2.0
|
||||
|
||||
### 1.0.2
|
||||
- Fixes issues with the zstd compression algorithm on Windows and Ubuntu 16.04 [#469](https://github.com/actions/toolkit/pull/469)
|
||||
|
||||
### 0.1.0
|
||||
|
||||
- Use posix archive format to add support for some tools
|
||||
|
||||
### 1.0.3
|
||||
|
||||
- Use http-client v1.0.9
|
||||
- Fixes error handling so retries are not attempted on non-retryable errors (409 Conflict, for example)
|
||||
- Adds 5 second delay between retry attempts
|
||||
|
||||
### 1.0.4
|
||||
|
||||
- Use @actions/core v1.2.6
|
||||
- Fixes uploadChunk to throw an error if any unsuccessful response code is received
|
||||
|
||||
### 1.0.5
|
||||
|
||||
- Fix to ensure Windows cache paths get resolved correctly
|
||||
|
||||
### 1.0.6
|
||||
|
||||
- Make caching more verbose [#650](https://github.com/actions/toolkit/pull/650)
|
||||
- Use GNU tar on macOS if available [#701](https://github.com/actions/toolkit/pull/701)
|
||||
|
||||
### 1.0.7
|
||||
|
||||
- Fixes permissions issue extracting archives with GNU tar on macOS ([issue](https://github.com/actions/cache/issues/527))
|
||||
|
||||
### 1.0.8
|
||||
|
||||
- Increase the allowed artifact cache size from 5GB to 10GB ([issue](https://github.com/actions/cache/discussions/497))
|
||||
|
||||
### 1.0.9
|
||||
|
||||
- Use @azure/ms-rest-js v2.6.0
|
||||
- Use @azure/storage-blob v12.8.0
|
||||
|
||||
### 1.0.10
|
||||
|
||||
- Update `lockfileVersion` to `v2` in `package-lock.json [#1022](https://github.com/actions/toolkit/pull/1022)
|
||||
|
||||
### 1.0.11
|
||||
|
||||
- Fix file downloads > 2GB([issue](https://github.com/actions/cache/issues/773))
|
||||
|
||||
### 2.0.0
|
||||
|
||||
- Added support to check if Actions cache service feature is available or not [#1028](https://github.com/actions/toolkit/pull/1028)
|
||||
|
||||
### 2.0.3
|
||||
|
||||
- Update to v2.0.0 of `@actions/http-client`
|
||||
|
||||
### 2.0.4
|
||||
|
||||
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
|
||||
|
||||
### 2.0.5
|
||||
|
||||
- Fix to avoid saving empty cache when no files are available for caching. ([issue](https://github.com/actions/cache/issues/624))
|
||||
|
||||
### 2.0.6
|
||||
|
||||
- Fix `Tar failed with error: The process '/usr/bin/tar' failed with exit code 1` issue when temp directory where tar is getting created is actually the subdirectory of the path mentioned by the user for caching. ([issue](https://github.com/actions/cache/issues/689))
|
||||
|
||||
### 3.0.0
|
||||
|
||||
- Updated actions/cache to suppress Actions cache server error and log warning for those error [#1122](https://github.com/actions/toolkit/pull/1122)
|
||||
|
||||
### 3.0.1
|
||||
|
||||
- Fix [#833](https://github.com/actions/cache/issues/833) - cache doesn't work with github workspace directory.
|
||||
- Fix [#809](https://github.com/actions/cache/issues/809) `zstd -d: no such file or directory` error on AWS self-hosted runners.
|
||||
|
||||
### 3.0.2
|
||||
|
||||
- Added 1 hour timeout for the download stuck issue [#810](https://github.com/actions/cache/issues/810).
|
||||
|
||||
### 3.0.3
|
||||
|
||||
- Bug fixes for download stuck issue [#810](https://github.com/actions/cache/issues/810).
|
||||
|
||||
### 3.0.4
|
||||
|
||||
- Fix zstd not working for windows on gnu tar in issues [#888](https://github.com/actions/cache/issues/888) and [#891](https://github.com/actions/cache/issues/891).
|
||||
- Allowing users to provide a custom timeout as input for aborting download of a cache segment using an environment variable `SEGMENT_DOWNLOAD_TIMEOUT_MINS`. Default is 60 minutes.
|
||||
|
||||
### 3.0.5
|
||||
|
||||
- Update `@actions/cache` to use `@actions/core@^1.10.0`
|
||||
|
||||
### 3.0.6
|
||||
|
||||
- Added `@azure/abort-controller` to dependencies to fix compatibility issue with ESM [#1208](https://github.com/actions/toolkit/issues/1208)
|
||||
|
||||
### 3.1.0-beta.1
|
||||
|
||||
- Update actions/cache on windows to use gnu tar and zstd by default and fallback to bsdtar and zstd if gnu tar is not available. ([issue](https://github.com/actions/cache/issues/984))
|
||||
|
||||
### 3.1.0-beta.2
|
||||
|
||||
- Added support for fallback to gzip to restore old caches on windows.
|
||||
|
||||
### 3.1.0-beta.3
|
||||
|
||||
- Bug Fixes for fallback to gzip to restore old caches on windows and bsdtar if gnutar is not available.
|
||||
|
||||
### 3.1.0
|
||||
|
||||
- Update actions/cache on windows to use gnu tar and zstd by default
|
||||
- Update actions/cache on windows to fallback to bsdtar and zstd if gnu tar is not available.
|
||||
- Added support for fallback to gzip to restore old caches on windows.
|
||||
|
||||
### 3.1.1
|
||||
|
||||
- Reverted changes in 3.1.0 to fix issue with symlink restoration on windows.
|
||||
- Added support for verbose logging about cache version during cache miss.
|
||||
|
||||
### 3.1.2
|
||||
|
||||
- Fix issue with symlink restoration on windows.
|
||||
|
||||
### 3.1.3
|
||||
|
||||
- Fix to prevent from setting MYSYS environement variable globally [#1329](https://github.com/actions/toolkit/pull/1329).
|
||||
|
||||
### 3.1.4
|
||||
|
||||
- Fix zstd not being used due to `zstd --version` output change in zstd 1.5.4 release. See [#1353](https://github.com/actions/toolkit/pull/1353).
|
||||
|
||||
### 3.2.0
|
||||
|
||||
- Add `lookupOnly` to cache restore `DownloadOptions`.
|
||||
|
||||
### 3.2.1
|
||||
|
||||
- Updated @azure/storage-blob to `v12.13.0`
|
||||
|
||||
### 3.2.2
|
||||
|
||||
- Add new default cache download method to improve performance and reduce hangs [#1484](https://github.com/actions/toolkit/pull/1484)
|
||||
|
||||
### 3.2.3
|
||||
|
||||
- Fixed a bug that mutated path arguments to `getCacheVersion` [#1378](https://github.com/actions/toolkit/pull/1378)
|
||||
- Initial release
|
||||
|
||||
@@ -5,6 +5,12 @@ import {DownloadOptions, getDownloadOptions} from '../src/options'
|
||||
|
||||
jest.mock('../src/internal/downloadUtils')
|
||||
|
||||
test('getCacheVersion does not mutate arguments', async () => {
|
||||
const paths = ['node_modules']
|
||||
getCacheVersion(paths, undefined, true)
|
||||
expect(paths).toEqual(['node_modules'])
|
||||
})
|
||||
|
||||
test('getCacheVersion with one path returns version', async () => {
|
||||
const paths = ['node_modules']
|
||||
const result = getCacheVersion(paths, undefined, true)
|
||||
|
||||
+24
@@ -2,6 +2,10 @@ import {promises as fs} from 'fs'
|
||||
import * as path from 'path'
|
||||
import * as cacheUtils from '../src/internal/cacheUtils'
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
test('getArchiveFileSizeInBytes returns file size', () => {
|
||||
const filePath = path.join(__dirname, '__fixtures__', 'helloWorld.txt')
|
||||
|
||||
@@ -38,3 +42,23 @@ test('resolvePaths works on github workspace directory', async () => {
|
||||
const paths = await cacheUtils.resolvePaths([workspace])
|
||||
expect(paths.length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
test('isGhes returns false for github.com', async () => {
|
||||
process.env.GITHUB_SERVER_URL = 'https://github.com'
|
||||
expect(cacheUtils.isGhes()).toBe(false)
|
||||
})
|
||||
|
||||
test('isGhes returns false for ghe.com', async () => {
|
||||
process.env.GITHUB_SERVER_URL = 'https://somedomain.ghe.com'
|
||||
expect(cacheUtils.isGhes()).toBe(false)
|
||||
})
|
||||
|
||||
test('isGhes returns true for enterprise URL', async () => {
|
||||
process.env.GITHUB_SERVER_URL = 'https://my-enterprise.github.com'
|
||||
expect(cacheUtils.isGhes()).toBe(true)
|
||||
})
|
||||
|
||||
test('isGhes returns false for ghe.localhost', () => {
|
||||
process.env.GITHUB_SERVER_URL = 'https://my.domain.ghe.localhost'
|
||||
expect(cacheUtils.isGhes()).toBe(false)
|
||||
})
|
||||
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@actions/cache",
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.4",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@actions/cache",
|
||||
"version": "3.2.2",
|
||||
"version": "3.2.4",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/cache",
|
||||
"version": "3.2.3",
|
||||
"version": "3.2.4",
|
||||
"preview": true,
|
||||
"description": "Actions cache lib",
|
||||
"keywords": [
|
||||
|
||||
+7
-1
@@ -135,5 +135,11 @@ export function isGhes(): boolean {
|
||||
const ghUrl = new URL(
|
||||
process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||
)
|
||||
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
|
||||
|
||||
const hostname = ghUrl.hostname.trimEnd().toUpperCase()
|
||||
const isGitHubHost = hostname === 'GITHUB.COM'
|
||||
const isGheHost =
|
||||
hostname.endsWith('.GHE.COM') || hostname.endsWith('.GHE.LOCALHOST')
|
||||
|
||||
return !isGitHubHost && !isGheHost
|
||||
}
|
||||
|
||||
@@ -49,6 +49,22 @@ const testEnvVars = {
|
||||
const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
|
||||
const DELIMITER = `ghadelimiter_${UUID}`
|
||||
|
||||
function extractErrorMetadata(error: Error): {
|
||||
file: string | undefined
|
||||
line: string | undefined
|
||||
column: string | undefined
|
||||
} {
|
||||
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', () => {
|
||||
beforeAll(() => {
|
||||
const filePath = path.join(__dirname, `test`)
|
||||
@@ -379,9 +395,14 @@ describe('@actions/core', () => {
|
||||
|
||||
it('setFailed handles Error', () => {
|
||||
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)
|
||||
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', () => {
|
||||
@@ -396,11 +417,21 @@ describe('@actions/core', () => {
|
||||
|
||||
it('error handles an error object', () => {
|
||||
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}`])
|
||||
})
|
||||
|
||||
it('error handles parameters correctly', () => {
|
||||
it('error handles custom properties correctly', () => {
|
||||
const message = 'this is my error message'
|
||||
core.error(new Error(message), {
|
||||
title: 'A title',
|
||||
@@ -427,11 +458,21 @@ describe('@actions/core', () => {
|
||||
|
||||
it('warning handles an error object', () => {
|
||||
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}`])
|
||||
})
|
||||
|
||||
it('warning handles parameters correctly', () => {
|
||||
it('warning handles custom properties correctly', () => {
|
||||
const message = 'this is my error message'
|
||||
core.warning(new Error(message), {
|
||||
title: 'A title',
|
||||
@@ -458,11 +499,21 @@ describe('@actions/core', () => {
|
||||
|
||||
it('notice handles an error object', () => {
|
||||
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}`])
|
||||
})
|
||||
|
||||
it('notice handles parameters correctly', () => {
|
||||
it('notice handles custom properties correctly', () => {
|
||||
const message = 'this is my error message'
|
||||
core.notice(new Error(message), {
|
||||
title: 'A title',
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
Generated
+27
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"error-stack-parser": "^2.1.4",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -51,6 +52,19 @@
|
||||
"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": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
@@ -102,6 +116,19 @@
|
||||
"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": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
|
||||
@@ -38,10 +38,11 @@
|
||||
"dependencies": {
|
||||
"@actions/exec": "^1.1.1",
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"error-stack-parser": "^2.1.4",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.0.2",
|
||||
"@types/uuid": "^8.3.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {issue, issueCommand} from './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 path from 'path'
|
||||
|
||||
@@ -242,6 +245,21 @@ export function debug(message: string): void {
|
||||
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
|
||||
* @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(
|
||||
message: string | Error,
|
||||
properties: AnnotationProperties = {}
|
||||
properties: AnnotationProperties | undefined = undefined
|
||||
): void {
|
||||
properties = defaultAnnotationPropertes(message, properties)
|
||||
|
||||
issueCommand(
|
||||
'error',
|
||||
toCommandProperties(properties),
|
||||
@@ -265,8 +285,10 @@ export function error(
|
||||
*/
|
||||
export function warning(
|
||||
message: string | Error,
|
||||
properties: AnnotationProperties = {}
|
||||
properties: AnnotationProperties | undefined = undefined
|
||||
): void {
|
||||
properties = defaultAnnotationPropertes(message, properties)
|
||||
|
||||
issueCommand(
|
||||
'warning',
|
||||
toCommandProperties(properties),
|
||||
@@ -281,8 +303,10 @@ export function warning(
|
||||
*/
|
||||
export function notice(
|
||||
message: string | Error,
|
||||
properties: AnnotationProperties = {}
|
||||
properties: AnnotationProperties | undefined = undefined
|
||||
): void {
|
||||
properties = defaultAnnotationPropertes(message, properties)
|
||||
|
||||
issueCommand(
|
||||
'notice',
|
||||
toCommandProperties(properties),
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import {AnnotationProperties} from './core'
|
||||
import {CommandProperties} from './command'
|
||||
import ErrorStackParser from 'error-stack-parser'
|
||||
|
||||
/**
|
||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||
@@ -39,3 +40,21 @@ export function toCommandProperties(
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,12 @@ describe('basics', () => {
|
||||
_http.dispose()
|
||||
})
|
||||
|
||||
it.each([true, false])('creates Agent with keepAlive %s', keepAlive => {
|
||||
const http = new httpm.HttpClient('http-client-tests', [], {keepAlive})
|
||||
const agent = http.getAgent('http://postman-echo.com')
|
||||
expect(agent).toHaveProperty('keepAlive', keepAlive)
|
||||
})
|
||||
|
||||
it('does basic http get request with keepAlive true', async () => {
|
||||
const res: httpm.HttpClientResponse = await _http.get(
|
||||
'http://postman-echo.com/get'
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/http-client",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/http-client",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"description": "Actions Http Client",
|
||||
"keywords": [
|
||||
"github",
|
||||
@@ -48,4 +48,4 @@
|
||||
"tunnel": "^0.0.6",
|
||||
"undici": "^5.25.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -649,7 +649,7 @@ export class HttpClient {
|
||||
agent = this._proxyAgent
|
||||
}
|
||||
|
||||
if (this._keepAlive && !useProxy) {
|
||||
if (!useProxy) {
|
||||
agent = this._agent
|
||||
}
|
||||
|
||||
@@ -690,18 +690,13 @@ export class HttpClient {
|
||||
this._proxyAgent = agent
|
||||
}
|
||||
|
||||
// if reusing agent across request and tunneling agent isn't assigned create a new agent
|
||||
if (this._keepAlive && !agent) {
|
||||
// if tunneling agent isn't assigned create a new agent
|
||||
if (!agent) {
|
||||
const options = {keepAlive: this._keepAlive, maxSockets}
|
||||
agent = usingSsl ? new https.Agent(options) : new http.Agent(options)
|
||||
this._agent = agent
|
||||
}
|
||||
|
||||
// if not using private agent and tunnel agent isn't setup then use global agent
|
||||
if (!agent) {
|
||||
agent = usingSsl ? https.globalAgent : http.globalAgent
|
||||
}
|
||||
|
||||
if (usingSsl && this._ignoreSslError) {
|
||||
// we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process
|
||||
// http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options
|
||||
|
||||
Reference in New Issue
Block a user