Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56832696fc | ||
|
|
176b40a888 | ||
|
|
361a115e53 | ||
|
|
dddc440d56 | ||
|
|
08d6f14ea8 | ||
|
|
73100a7f85 | ||
|
|
c6b487124a | ||
|
|
8735a7e2da | ||
|
|
d1df13e178 | ||
|
|
d3d7736bae | ||
|
|
7d18e7aa0d | ||
|
|
e60694077d | ||
|
|
ae38557bb0 | ||
|
|
abb586d71e | ||
|
|
81a73aba8b | ||
|
|
0e8fe8af62 | ||
|
|
29885a805e | ||
|
|
9eb3d3a673 | ||
|
|
6e642f628f | ||
|
|
0159bbe7f2 | ||
|
|
476276bf98 | ||
|
|
d82fd09f99 | ||
|
|
2961d73391 | ||
|
|
eb1cb3649c | ||
|
|
b384fe17ba | ||
|
|
ccb1df45d1 | ||
|
|
5a736647a1 |
Generated
+10
-10
@@ -5175,12 +5175,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.1.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -6400,9 +6400,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/ejs": {
|
"node_modules/ejs": {
|
||||||
"version": "3.1.9",
|
"version": "3.1.10",
|
||||||
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz",
|
||||||
"integrity": "sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ==",
|
"integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jake": "^10.8.5"
|
"jake": "^10.8.5"
|
||||||
@@ -7457,9 +7457,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
|
|||||||
@@ -1,5 +1,21 @@
|
|||||||
# @actions/artifact Releases
|
# @actions/artifact Releases
|
||||||
|
|
||||||
|
### 2.1.8
|
||||||
|
|
||||||
|
- Allows `*.localhost` domains for hostname checks for local development.
|
||||||
|
|
||||||
|
### 2.1.7
|
||||||
|
|
||||||
|
- Update unzip-stream dependency and reverted to using `unzip.Extract()`
|
||||||
|
|
||||||
|
### 2.1.6
|
||||||
|
|
||||||
|
- Will retry on invalid request responses.
|
||||||
|
|
||||||
|
### 2.1.5
|
||||||
|
|
||||||
|
- Bumped `archiver` dependency to 7.0.1
|
||||||
|
|
||||||
### 2.1.4
|
### 2.1.4
|
||||||
|
|
||||||
- Adds info-level logging for zip extraction
|
- Adds info-level logging for zip extraction
|
||||||
|
|||||||
@@ -116,6 +116,54 @@ 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,6 +20,11 @@ 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,14 +200,12 @@ describe('download-artifact', () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
await expect(
|
const response = await downloadArtifactPublic(
|
||||||
downloadArtifactPublic(
|
fixtures.artifactID,
|
||||||
fixtures.artifactID,
|
fixtures.repositoryOwner,
|
||||||
fixtures.repositoryOwner,
|
fixtures.repositoryName,
|
||||||
fixtures.repositoryName,
|
fixtures.token
|
||||||
fixtures.token
|
)
|
||||||
)
|
|
||||||
).rejects.toBeInstanceOf(Error)
|
|
||||||
|
|
||||||
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
||||||
owner: fixtures.repositoryOwner,
|
owner: fixtures.repositoryOwner,
|
||||||
@@ -223,6 +221,16 @@ 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 () => {
|
||||||
|
|||||||
Generated
+5
-5
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/artifact",
|
"name": "@actions/artifact",
|
||||||
"version": "2.1.5",
|
"version": "2.1.8",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@actions/artifact",
|
"name": "@actions/artifact",
|
||||||
"version": "2.1.5",
|
"version": "2.1.8",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
@@ -1738,9 +1738,9 @@
|
|||||||
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
"integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w=="
|
||||||
},
|
},
|
||||||
"node_modules/unzip-stream": {
|
"node_modules/unzip-stream": {
|
||||||
"version": "0.3.1",
|
"version": "0.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.4.tgz",
|
||||||
"integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==",
|
"integrity": "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"binary": "^0.3.0",
|
"binary": "^0.3.0",
|
||||||
"mkdirp": "^0.5.1"
|
"mkdirp": "^0.5.1"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/artifact",
|
"name": "@actions/artifact",
|
||||||
"version": "2.1.5",
|
"version": "2.1.8",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"description": "Actions artifact lib",
|
"description": "Actions artifact lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
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'
|
||||||
@@ -47,11 +44,6 @@ 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...`
|
||||||
@@ -86,8 +78,6 @@ 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()
|
||||||
@@ -99,46 +89,8 @@ export async function streamExtractExternal(
|
|||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
reject(error)
|
reject(error)
|
||||||
})
|
})
|
||||||
.pipe(unzip.Parse())
|
.pipe(unzip.Extract({path: directory}))
|
||||||
.pipe(
|
.on('close', () => {
|
||||||
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,7 +102,6 @@ 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 =
|
const isGheHost = hostname.endsWith('.GHE.COM')
|
||||||
hostname.endsWith('.GHE.COM') || hostname.endsWith('.GHE.LOCALHOST')
|
const isLocalHost = hostname.endsWith('.LOCALHOST')
|
||||||
|
|
||||||
return !isGitHubHost && !isGheHost
|
return !isGitHubHost && !isGheHost && !isLocalHost
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGitHubWorkspaceDir(): string {
|
export function getGitHubWorkspaceDir(): string {
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ 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`
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
# @actions/attest Releases
|
# @actions/attest Releases
|
||||||
|
|
||||||
|
### 1.3.0
|
||||||
|
|
||||||
|
- Dynamic construction of Sigstore API URLs
|
||||||
|
- Switch to new GH provenance build type
|
||||||
|
- Fetch existing Rekor entry on 409 conflict error
|
||||||
|
- Bump @sigstore/bundle from 2.3.0 to 2.3.2
|
||||||
|
- Bump @sigstore/sign from 2.3.0 to 2.3.2
|
||||||
|
|
||||||
|
### 1.2.1
|
||||||
|
|
||||||
|
- Retry request on attestation persistence failure
|
||||||
|
|
||||||
### 1.2.0
|
### 1.2.0
|
||||||
|
|
||||||
- Generate attestations using the v0.3 Sigstore bundle format.
|
- Generate attestations using the v0.3 Sigstore bundle format.
|
||||||
- Bump @sigstore/bundle from 2.2.0 to 2.3.0.
|
- Bump @sigstore/bundle from 2.2.0 to 2.3.0.
|
||||||
- Bump @sigstore/sign from 2.2.3 to 2.3.0.
|
- Bump @sigstore/sign from 2.2.3 to 2.3.0.
|
||||||
|
- Remove dependency on make-fetch-happen
|
||||||
|
|
||||||
### 1.1.0
|
### 1.1.0
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ exports[`provenance functions buildSLSAProvenancePredicate returns a provenance
|
|||||||
{
|
{
|
||||||
"params": {
|
"params": {
|
||||||
"buildDefinition": {
|
"buildDefinition": {
|
||||||
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
|
"buildType": "https://actions.github.io/buildtypes/workflow/v1",
|
||||||
"externalParameters": {
|
"externalParameters": {
|
||||||
"workflow": {
|
"workflow": {
|
||||||
"path": ".github/workflows/main.yml",
|
"path": ".github/workflows/main.yml",
|
||||||
@@ -17,6 +17,7 @@ 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": [
|
||||||
@@ -30,7 +31,7 @@ exports[`provenance functions buildSLSAProvenancePredicate returns a provenance
|
|||||||
},
|
},
|
||||||
"runDetails": {
|
"runDetails": {
|
||||||
"builder": {
|
"builder": {
|
||||||
"id": "https://github.com/actions/runner/github-hosted",
|
"id": "https://github.com/owner/workflows/.github/workflows/publish.yml@main",
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"invocationId": "https://github.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
|
"invocationId": "https://github.com/owner/repo/actions/runs/run-id/attempts/run-attempt",
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -45,7 +45,8 @@ describe('getIDTokenClaims', () => {
|
|||||||
sha: 'sha',
|
sha: 'sha',
|
||||||
repository: 'repo',
|
repository: 'repo',
|
||||||
event_name: 'push',
|
event_name: 'push',
|
||||||
workflow_ref: 'main',
|
job_workflow_ref: 'job_workflow_ref',
|
||||||
|
workflow_ref: 'workflow',
|
||||||
repository_id: '1',
|
repository_id: '1',
|
||||||
repository_owner_id: '1',
|
repository_owner_id: '1',
|
||||||
runner_environment: 'github-hosted',
|
runner_environment: 'github-hosted',
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ 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 * as jose from 'jose'
|
||||||
import nock from 'nock'
|
import nock from 'nock'
|
||||||
import {SIGSTORE_GITHUB, SIGSTORE_PUBLIC_GOOD} from '../src/endpoints'
|
import {MockAgent, setGlobalDispatcher} from 'undici'
|
||||||
|
import {SIGSTORE_PUBLIC_GOOD, signingEndpoints} from '../src/endpoints'
|
||||||
import {attestProvenance, buildSLSAProvenancePredicate} from '../src/provenance'
|
import {attestProvenance, buildSLSAProvenancePredicate} from '../src/provenance'
|
||||||
|
|
||||||
describe('provenance functions', () => {
|
describe('provenance functions', () => {
|
||||||
@@ -12,12 +13,17 @@ describe('provenance functions', () => {
|
|||||||
const jwksPath = '/.well-known/jwks.json'
|
const jwksPath = '/.well-known/jwks.json'
|
||||||
const tokenPath = '/token'
|
const tokenPath = '/token'
|
||||||
|
|
||||||
|
// MockAgent for mocking @actions/github
|
||||||
|
const mockAgent = new MockAgent()
|
||||||
|
setGlobalDispatcher(mockAgent)
|
||||||
|
|
||||||
const claims = {
|
const claims = {
|
||||||
iss: issuer,
|
iss: issuer,
|
||||||
aud: 'nobody',
|
aud: 'nobody',
|
||||||
repository: 'owner/repo',
|
repository: 'owner/repo',
|
||||||
ref: 'refs/heads/main',
|
ref: 'refs/heads/main',
|
||||||
sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
|
sha: 'babca52ab0c93ae16539e5923cb0d7403b9a093b',
|
||||||
|
job_workflow_ref: 'owner/workflows/.github/workflows/publish.yml@main',
|
||||||
workflow_ref: 'owner/repo/.github/workflows/main.yml@main',
|
workflow_ref: 'owner/repo/.github/workflows/main.yml@main',
|
||||||
event_name: 'push',
|
event_name: 'push',
|
||||||
repository_id: 'repo-id',
|
repository_id: 'repo-id',
|
||||||
@@ -90,16 +96,19 @@ describe('provenance functions', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('when using the github Sigstore instance', () => {
|
describe('when using the github Sigstore instance', () => {
|
||||||
const {fulcioURL, tsaServerURL} = SIGSTORE_GITHUB
|
const {fulcioURL, tsaServerURL} = signingEndpoints('github')
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Mock Sigstore
|
// Mock Sigstore
|
||||||
await mockFulcio({baseURL: fulcioURL, strict: false})
|
await mockFulcio({baseURL: fulcioURL, strict: false})
|
||||||
await mockTSA({baseURL: tsaServerURL})
|
await mockTSA({baseURL: tsaServerURL})
|
||||||
|
|
||||||
// Mock GH attestations API
|
mockAgent
|
||||||
nock('https://api.github.com')
|
.get('https://api.github.com')
|
||||||
.post(/^\/repos\/.*\/.*\/attestations$/)
|
.intercept({
|
||||||
|
path: /^\/repos\/.*\/.*\/attestations$/,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
.reply(201, {id: attestationID})
|
.reply(201, {id: attestationID})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -159,8 +168,12 @@ describe('provenance functions', () => {
|
|||||||
await mockRekor({baseURL: rekorURL})
|
await mockRekor({baseURL: rekorURL})
|
||||||
|
|
||||||
// Mock GH attestations API
|
// Mock GH attestations API
|
||||||
nock('https://api.github.com')
|
mockAgent
|
||||||
.post(/^\/repos\/.*\/.*\/attestations$/)
|
.get('https://api.github.com')
|
||||||
|
.intercept({
|
||||||
|
path: /^\/repos\/.*\/.*\/attestations$/,
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
.reply(201, {id: attestationID})
|
.reply(201, {id: attestationID})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import nock from 'nock'
|
import {MockAgent, setGlobalDispatcher} from 'undici'
|
||||||
import {writeAttestation} from '../src/store'
|
import {writeAttestation} from '../src/store'
|
||||||
|
|
||||||
describe('writeAttestation', () => {
|
describe('writeAttestation', () => {
|
||||||
@@ -6,6 +6,9 @@ describe('writeAttestation', () => {
|
|||||||
const attestation = {foo: 'bar '}
|
const attestation = {foo: 'bar '}
|
||||||
const token = 'token'
|
const token = 'token'
|
||||||
|
|
||||||
|
const mockAgent = new MockAgent()
|
||||||
|
setGlobalDispatcher(mockAgent)
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env = {
|
process.env = {
|
||||||
...originalEnv,
|
...originalEnv,
|
||||||
@@ -19,9 +22,14 @@ describe('writeAttestation', () => {
|
|||||||
|
|
||||||
describe('when the api call is successful', () => {
|
describe('when the api call is successful', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
nock('https://api.github.com')
|
mockAgent
|
||||||
.matchHeader('authorization', `token ${token}`)
|
.get('https://api.github.com')
|
||||||
.post('/repos/foo/bar/attestations', {bundle: attestation})
|
.intercept({
|
||||||
|
path: '/repos/foo/bar/attestations',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {authorization: `token ${token}`},
|
||||||
|
body: JSON.stringify({bundle: attestation})
|
||||||
|
})
|
||||||
.reply(201, {id: '123'})
|
.reply(201, {id: '123'})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -32,14 +40,51 @@ describe('writeAttestation', () => {
|
|||||||
|
|
||||||
describe('when the api call fails', () => {
|
describe('when the api call fails', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
nock('https://api.github.com')
|
mockAgent
|
||||||
.matchHeader('authorization', `token ${token}`)
|
.get('https://api.github.com')
|
||||||
.post('/repos/foo/bar/attestations', {bundle: attestation})
|
.intercept({
|
||||||
|
path: '/repos/foo/bar/attestations',
|
||||||
|
method: 'POST',
|
||||||
|
headers: {authorization: `token ${token}`},
|
||||||
|
body: JSON.stringify({bundle: attestation})
|
||||||
|
})
|
||||||
.reply(500, 'oops')
|
.reply(500, 'oops')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
await expect(writeAttestation(attestation, token)).rejects.toThrow(/oops/)
|
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 () => {
|
||||||
|
await expect(writeAttestation(attestation, token)).resolves.toEqual('123')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Generated
+446
-432
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/attest",
|
"name": "@actions/attest",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"description": "Actions attestation lib",
|
"description": "Actions attestation lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"github",
|
"github",
|
||||||
@@ -35,21 +35,26 @@
|
|||||||
"url": "https://github.com/actions/toolkit/issues"
|
"url": "https://github.com/actions/toolkit/issues"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sigstore/mock": "^0.6.5",
|
"@sigstore/mock": "^0.7.4",
|
||||||
"@sigstore/rekor-types": "^2.0.0",
|
"@sigstore/rekor-types": "^2.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.6",
|
"@types/jsonwebtoken": "^9.0.6",
|
||||||
"@types/make-fetch-happen": "^10.0.4",
|
|
||||||
"jose": "^5.2.3",
|
"jose": "^5.2.3",
|
||||||
"nock": "^13.5.1"
|
"nock": "^13.5.1",
|
||||||
|
"undici": "^5.28.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@actions/github": "^6.0.0",
|
"@actions/github": "^6.0.0",
|
||||||
"@actions/http-client": "^2.2.1",
|
"@actions/http-client": "^2.2.1",
|
||||||
"@sigstore/bundle": "^2.3.0",
|
"@octokit/plugin-retry": "^6.0.1",
|
||||||
"@sigstore/sign": "^2.3.0",
|
"@sigstore/bundle": "^2.3.2",
|
||||||
|
"@sigstore/sign": "^2.3.2",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"jwks-rsa": "^3.1.0",
|
"jwks-rsa": "^3.1.0"
|
||||||
"make-fetch-happen": "^13.0.0"
|
},
|
||||||
|
"overrides": {
|
||||||
|
"@octokit/plugin-retry": {
|
||||||
|
"@octokit/core": "^5.2.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ 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 = {
|
||||||
@@ -22,11 +19,6 @@ 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
|
||||||
|
|
||||||
@@ -45,6 +37,19 @@ 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 SIGSTORE_GITHUB
|
return buildGitHubEndpoints()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const REQUIRED_CLAIMS = [
|
|||||||
'sha',
|
'sha',
|
||||||
'repository',
|
'repository',
|
||||||
'event_name',
|
'event_name',
|
||||||
|
'job_workflow_ref',
|
||||||
'workflow_ref',
|
'workflow_ref',
|
||||||
'repository_id',
|
'repository_id',
|
||||||
'repository_owner_id',
|
'repository_owner_id',
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ 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'
|
|
||||||
|
|
||||||
const DEFAULT_ISSUER = 'https://token.actions.githubusercontent.com'
|
const DEFAULT_ISSUER = 'https://token.actions.githubusercontent.com'
|
||||||
|
|
||||||
@@ -55,7 +52,8 @@ export const buildSLSAProvenancePredicate = async (
|
|||||||
github: {
|
github: {
|
||||||
event_name: claims.event_name,
|
event_name: claims.event_name,
|
||||||
repository_id: claims.repository_id,
|
repository_id: claims.repository_id,
|
||||||
repository_owner_id: claims.repository_owner_id
|
repository_owner_id: claims.repository_owner_id,
|
||||||
|
runner_environment: claims.runner_environment
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resolvedDependencies: [
|
resolvedDependencies: [
|
||||||
@@ -69,7 +67,7 @@ export const buildSLSAProvenancePredicate = async (
|
|||||||
},
|
},
|
||||||
runDetails: {
|
runDetails: {
|
||||||
builder: {
|
builder: {
|
||||||
id: `${GITHUB_BUILDER_ID_PREFIX}/${claims.runner_environment}`
|
id: `${serverURL}/${claims.job_workflow_ref}`
|
||||||
},
|
},
|
||||||
metadata: {
|
metadata: {
|
||||||
invocationId: `${serverURL}/${claims.repository}/actions/runs/${claims.run_id}/attempts/${claims.run_attempt}`
|
invocationId: `${serverURL}/${claims.repository}/actions/runs/${claims.run_id}/attempts/${claims.run_attempt}`
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ const initBundleBuilder = (opts: SignOptions): BundleBuilder => {
|
|||||||
new RekorWitness({
|
new RekorWitness({
|
||||||
rekorBaseURL: opts.rekorURL,
|
rekorBaseURL: opts.rekorURL,
|
||||||
entryType: 'dsse',
|
entryType: 'dsse',
|
||||||
|
fetchOnConflict: true,
|
||||||
timeout,
|
timeout,
|
||||||
retry
|
retry
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import fetch from 'make-fetch-happen'
|
import {retry} from '@octokit/plugin-retry'
|
||||||
|
|
||||||
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
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@@ -12,9 +16,11 @@ const CREATE_ATTESTATION_REQUEST = 'POST /repos/{owner}/{repo}/attestations'
|
|||||||
*/
|
*/
|
||||||
export const writeAttestation = async (
|
export const writeAttestation = async (
|
||||||
attestation: unknown,
|
attestation: unknown,
|
||||||
token: string
|
token: string,
|
||||||
|
options: WriteOptions = {}
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
const octokit = github.getOctokit(token, {request: {fetch}})
|
const retries = options.retry ?? DEFAULT_RETRY_COUNT
|
||||||
|
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, {
|
||||||
@@ -23,7 +29,11 @@ export const writeAttestation = async (
|
|||||||
data: {bundle: attestation}
|
data: {bundle: attestation}
|
||||||
})
|
})
|
||||||
|
|
||||||
return response.data?.id
|
const data =
|
||||||
|
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}`)
|
||||||
|
|||||||
Reference in New Issue
Block a user