diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 592f7707..a29858c4 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -11,7 +11,7 @@ on: jobs: test: - runs-on: macos-latest + runs-on: macos-latest-large steps: - name: setup repo @@ -48,7 +48,7 @@ jobs: path: packages/${{ github.event.inputs.package }}/*.tgz publish: - runs-on: macos-latest + runs-on: macos-latest-large needs: test environment: npm-publish permissions: diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 952fa6b2..6956df01 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -16,7 +16,11 @@ jobs: strategy: matrix: - runs-on: [ubuntu-latest, macos-latest, windows-latest] + runs-on: [ubuntu-latest, macos-latest-large, windows-latest] + + # Node 18 is the current default Node version in hosted runners, so users may still use the toolkit with it when running tests (see https://github.com/actions/toolkit/issues/1841) + # Node 20 is the currently support Node version for actions - https://docs.github.com/actions/sharing-automations/creating-actions/metadata-syntax-for-github-actions#runsusing-for-javascript-actions + node-version: [18.x, 20.x] fail-fast: false runs-on: ${{ matrix.runs-on }} @@ -25,10 +29,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Set Node.js 20.x + - name: Set up Node ${{ matrix.node-version }} uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: ${{ matrix.node-version }} - name: npm install run: npm install diff --git a/packages/attest/README.md b/packages/attest/README.md index 8f004399..e6761ea6 100644 --- a/packages/attest/README.md +++ b/packages/attest/README.md @@ -32,8 +32,7 @@ async function run() { const ghToken = core.getInput('gh-token'); const attestation = await attest({ - subjectName: 'my-artifact-name', - subjectDigest: { 'sha256': '36ab4667...'}, + subjects: [{name: 'my-artifact-name', digest: { 'sha256': '36ab4667...'}}], predicateType: 'https://in-toto.io/attestation/release', predicate: { . . . }, token: ghToken @@ -49,11 +48,12 @@ 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 + // Deprecated. Use 'subjects' instead. + subjectName?: string + // Deprecated. Use 'subjects' instead. + subjectDigest?: Record + // Collection of subjects to be attested + subjects?: Subject[] // URI identifying the content type of the predicate being attested. predicateType: string // Predicate to be attested. @@ -68,6 +68,13 @@ export type AttestOptions = { // Whether to skip writing the attestation to the GH attestations API. skipWrite?: boolean } + +export type Subject = { + // Name of the subject. + name: string + // Digests of the subject. Should be a map of digest algorithms to their hex-encoded values. + digest: Record +} ``` ### `attestProvenance` @@ -105,12 +112,13 @@ 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 - // GitHub token for writing attestations. + // Deprecated. Use 'subjects' instead. + subjectName?: string + // Deprecated. Use 'subjects' instead. + subjectDigest?: Record + // Collection of subjects to be attested + subjects?: Subject[] + // URI identifying the content type of the predicate being attested. token: string // Sigstore instance to use for signing. Must be one of "public-good" or // "github". diff --git a/packages/attest/RELEASES.md b/packages/attest/RELEASES.md index f6d25193..da623b95 100644 --- a/packages/attest/RELEASES.md +++ b/packages/attest/RELEASES.md @@ -5,6 +5,8 @@ - Bump @actions/core from 1.10.1 to 1.11.1 [#1847](https://github.com/actions/toolkit/pull/1847) - Bump @sigstore/bundle from 2.3.2 to 3.0.0 [#1846](https://github.com/actions/toolkit/pull/1846) - Bump @sigstore/sign from 2.3.2 to 3.0.0 [#1846](https://github.com/actions/toolkit/pull/1846) +- Support for generating multi-subject attestations [#1864](https://github.com/actions/toolkit/pull/1865) +- Fix bug in `buildSLSAProvenancePredicate` related to `workflow_ref` OIDC token claims containing the "@" symbol in the tag name [#1863](https://github.com/actions/toolkit/pull/1863) ### 1.4.2 diff --git a/packages/attest/__tests__/__snapshots__/provenance.test.ts.snap b/packages/attest/__tests__/__snapshots__/provenance.test.ts.snap index 4c199dae..82daca94 100644 --- a/packages/attest/__tests__/__snapshots__/provenance.test.ts.snap +++ b/packages/attest/__tests__/__snapshots__/provenance.test.ts.snap @@ -1,5 +1,47 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`provenance functions buildSLSAProvenancePredicate handle tags including "@" character 1`] = ` +{ + "params": { + "buildDefinition": { + "buildType": "https://actions.github.io/buildtypes/workflow/v1", + "externalParameters": { + "workflow": { + "path": ".github/workflows/main.yml", + "ref": "foo@1.0.0", + "repository": "https://foo.ghe.com/owner/repo", + }, + }, + "internalParameters": { + "github": { + "event_name": "push", + "repository_id": "repo-id", + "repository_owner_id": "owner-id", + "runner_environment": "github-hosted", + }, + }, + "resolvedDependencies": [ + { + "digest": { + "gitCommit": "babca52ab0c93ae16539e5923cb0d7403b9a093b", + }, + "uri": "git+https://foo.ghe.com/owner/repo@refs/heads/main", + }, + ], + }, + "runDetails": { + "builder": { + "id": "https://foo.ghe.com/owner/workflows/.github/workflows/publish.yml@main", + }, + "metadata": { + "invocationId": "https://foo.ghe.com/owner/repo/actions/runs/run-id/attempts/run-attempt", + }, + }, + }, + "type": "https://slsa.dev/provenance/v1", +} +`; + exports[`provenance functions buildSLSAProvenancePredicate returns a provenance hydrated from an OIDC token 1`] = ` { "params": { diff --git a/packages/attest/__tests__/attest.test.ts b/packages/attest/__tests__/attest.test.ts new file mode 100644 index 00000000..d8b07163 --- /dev/null +++ b/packages/attest/__tests__/attest.test.ts @@ -0,0 +1,16 @@ +import {attest} from '../src/attest' + +describe('attest', () => { + describe('when no subject information is provided', () => { + it('throws an error', async () => { + const options = { + predicateType: 'foo', + predicate: {bar: 'baz'}, + token: 'token' + } + expect(attest(options)).rejects.toThrowError( + 'Must provide either subjectName and subjectDigest or subjects' + ) + }) + }) +}) diff --git a/packages/attest/__tests__/intoto.test.ts b/packages/attest/__tests__/intoto.test.ts index dd6a1a95..c69f7d84 100644 --- a/packages/attest/__tests__/intoto.test.ts +++ b/packages/attest/__tests__/intoto.test.ts @@ -17,7 +17,7 @@ describe('buildIntotoStatement', () => { } it('returns an intoto statement', () => { - const statement = buildIntotoStatement(subject, predicate) + const statement = buildIntotoStatement([subject], predicate) expect(statement).toMatchSnapshot() }) }) diff --git a/packages/attest/__tests__/provenance.test.ts b/packages/attest/__tests__/provenance.test.ts index 4dbfef58..ca7941ef 100644 --- a/packages/attest/__tests__/provenance.test.ts +++ b/packages/attest/__tests__/provenance.test.ts @@ -33,15 +33,7 @@ describe('provenance functions', () => { runner_environment: 'github-hosted' } - beforeEach(async () => { - process.env = { - ...originalEnv, - ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`, - ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token', - GITHUB_SERVER_URL: 'https://foo.ghe.com', - GITHUB_REPOSITORY: claims.repository - } - + const mockIssuer = async (claims: jose.JWTPayload): Promise => { // Generate JWT signing key const key = await jose.generateKeyPair('PS256') @@ -60,6 +52,18 @@ describe('provenance functions', () => { // Mock OIDC token endpoint for populating the provenance nock(issuer).get(tokenPath).query({audience}).reply(200, {value: jwt}) + } + + beforeEach(async () => { + process.env = { + ...originalEnv, + ACTIONS_ID_TOKEN_REQUEST_URL: `${issuer}${tokenPath}?`, + ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token', + GITHUB_SERVER_URL: 'https://foo.ghe.com', + GITHUB_REPOSITORY: claims.repository + } + + await mockIssuer(claims) }) afterEach(() => { @@ -71,6 +75,16 @@ describe('provenance functions', () => { const predicate = await buildSLSAProvenancePredicate() expect(predicate).toMatchSnapshot() }) + + it('handle tags including "@" character', async () => { + nock.cleanAll() + await mockIssuer({ + ...claims, + workflow_ref: 'owner/repo/.github/workflows/main.yml@foo@1.0.0' + }) + const predicate = await buildSLSAProvenancePredicate() + expect(predicate).toMatchSnapshot() + }) }) describe('attestProvenance', () => { @@ -115,8 +129,7 @@ describe('provenance functions', () => { describe('when the sigstore instance is explicitly set', () => { it('attests provenance', async () => { const attestation = await attestProvenance({ - subjectName, - subjectDigest, + subjects: [{name: subjectName, digest: subjectDigest}], token: 'token', sigstore: 'github' }) @@ -143,8 +156,7 @@ describe('provenance functions', () => { it('attests provenance', async () => { const attestation = await attestProvenance({ - subjectName, - subjectDigest, + subjects: [{name: subjectName, digest: subjectDigest}], token: 'token' }) @@ -178,8 +190,7 @@ describe('provenance functions', () => { describe('when the sigstore instance is explicitly set', () => { it('attests provenance', async () => { const attestation = await attestProvenance({ - subjectName, - subjectDigest, + subjects: [{name: subjectName, digest: subjectDigest}], token: 'token', sigstore: 'public-good' }) @@ -206,8 +217,7 @@ describe('provenance functions', () => { it('attests provenance', async () => { const attestation = await attestProvenance({ - subjectName, - subjectDigest, + subjects: [{name: subjectName, digest: subjectDigest}], token: 'token' }) diff --git a/packages/attest/src/attest.ts b/packages/attest/src/attest.ts index 85c63013..807a8e5d 100644 --- a/packages/attest/src/attest.ts +++ b/packages/attest/src/attest.ts @@ -14,11 +14,16 @@ 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 + /** + * @deprecated Use `subjects` instead. + **/ + subjectName?: string + /** + * @deprecated Use `subjects` instead. + **/ + subjectDigest?: Record + // Subjects to be attested. + subjects?: Subject[] // Content type of the predicate being attested. predicateType: string // Predicate to be attested. @@ -42,15 +47,24 @@ export type AttestOptions = { * @returns A promise that resolves to the attestation. */ export async function attest(options: AttestOptions): Promise { - const subject: Subject = { - name: options.subjectName, - digest: options.subjectDigest + let subjects: Subject[] + + if (options.subjects) { + subjects = options.subjects + } else if (options.subjectName && options.subjectDigest) { + subjects = [{name: options.subjectName, digest: options.subjectDigest}] + } else { + throw new Error( + 'Must provide either subjectName and subjectDigest or subjects' + ) } + const predicate: Predicate = { type: options.predicateType, params: options.predicate } - const statement = buildIntotoStatement(subject, predicate) + + const statement = buildIntotoStatement(subjects, predicate) // Sign the provenance statement const payload: Payload = { diff --git a/packages/attest/src/intoto.ts b/packages/attest/src/intoto.ts index 9d6a2d0e..5a2dcc9f 100644 --- a/packages/attest/src/intoto.ts +++ b/packages/attest/src/intoto.ts @@ -20,12 +20,12 @@ export type InTotoStatement = { * @returns The constructed in-toto statement. */ export const buildIntotoStatement = ( - subject: Subject, + subjects: Subject[], predicate: Predicate ): InTotoStatement => { return { _type: INTOTO_STATEMENT_V1_TYPE, - subject: [subject], + subject: subjects, predicateType: predicate.type, predicate: predicate.params } diff --git a/packages/attest/src/provenance.ts b/packages/attest/src/provenance.ts index 09aa64f7..faba08fd 100644 --- a/packages/attest/src/provenance.ts +++ b/packages/attest/src/provenance.ts @@ -30,9 +30,11 @@ export const buildSLSAProvenancePredicate = async ( // 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] = claims.workflow_ref + const [workflowPath, ...workflowRefChunks] = claims.workflow_ref .replace(`${claims.repository}/`, '') .split('@') + // Handle case where tag contains `@` (e.g: when using changesets in a monorepo context), + const workflowRef = workflowRefChunks.join('@') return { type: SLSA_PREDICATE_V1_TYPE, diff --git a/packages/cache/RELEASES.md b/packages/cache/RELEASES.md index 43566ef1..85415952 100644 --- a/packages/cache/RELEASES.md +++ b/packages/cache/RELEASES.md @@ -1,9 +1,13 @@ # @actions/cache Releases +### 3.3.0 +- Update `@actions/core` to `1.11.1` +- Remove dependency on `uuid` package [#1824](https://github.com/actions/toolkit/pull/1824), [#1842](https://github.com/actions/toolkit/pull/1842) + ### 3.2.4 - Updated `isGhes` check to include `.ghe.com` and `.ghe.localhost` as accepted hosts - + ### 3.2.3 - Fixed a bug that mutated path arguments to `getCacheVersion` [#1378](https://github.com/actions/toolkit/pull/1378) diff --git a/packages/cache/package-lock.json b/packages/cache/package-lock.json index 6aa58d16..3963e19d 100644 --- a/packages/cache/package-lock.json +++ b/packages/cache/package-lock.json @@ -1,16 +1,15 @@ { "name": "@actions/cache", - "version": "3.2.4", + "version": "3.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@actions/cache", - "version": "3.2.4", + "version": "3.3.0", "license": "MIT", "dependencies": { - "@actions/artifact": "^2.1.7", - "@actions/core": "^1.10.0", + "@actions/core": "^1.11.1", "@actions/exec": "^1.0.1", "@actions/glob": "^0.1.0", "@actions/http-client": "^2.1.1", @@ -47,20 +46,12 @@ } }, "node_modules/@actions/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz", - "integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" - } - }, - "node_modules/@actions/core/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" } }, "node_modules/@actions/exec": { @@ -1856,19 +1847,12 @@ } }, "@actions/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz", - "integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz", + "integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==", "requires": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - } + "@actions/exec": "^1.1.1", + "@actions/http-client": "^2.0.1" } }, "@actions/exec": { diff --git a/packages/cache/package.json b/packages/cache/package.json index 3e5695fb..1d1ee0e2 100644 --- a/packages/cache/package.json +++ b/packages/cache/package.json @@ -1,6 +1,6 @@ { "name": "@actions/cache", - "version": "3.2.4", + "version": "3.3.0", "preview": true, "description": "Actions cache lib", "keywords": [ @@ -37,8 +37,7 @@ "url": "https://github.com/actions/toolkit/issues" }, "dependencies": { - "@actions/core": "^1.10.0", - "@actions/artifact": "^2.1.7", + "@actions/core": "^1.11.1", "@actions/exec": "^1.0.1", "@actions/glob": "^0.1.0", "@actions/http-client": "^2.1.1", @@ -52,4 +51,4 @@ "@types/semver": "^6.0.0", "typescript": "^5.2.2" } -} +} \ No newline at end of file diff --git a/packages/tool-cache/RELEASES.md b/packages/tool-cache/RELEASES.md index 9fdd4898..e2372238 100644 --- a/packages/tool-cache/RELEASES.md +++ b/packages/tool-cache/RELEASES.md @@ -1,5 +1,8 @@ # @actions/tool-cache Releases +### Unreleased +- Remove dependency on `uuid` package [#1824](https://github.com/actions/toolkit/pull/1824), [#1842](https://github.com/actions/toolkit/pull/1842) + ### 2.0.1 - Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)