diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 97114540..778d5506 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -69,16 +69,16 @@ jobs: id-token: write steps: + - name: Set Node.js 24.x + uses: actions/setup-node@v5 + with: + node-version: 24.x + - name: download artifact uses: actions/download-artifact@v4 with: name: ${{ github.event.inputs.package }} - - name: setup authentication - run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc - env: - NPM_TOKEN: ${{ secrets.TOKEN }} - - name: publish run: npm publish --provenance *.tgz diff --git a/packages/attest/README.md b/packages/attest/README.md index e6761ea6..22a087b3 100644 --- a/packages/attest/README.md +++ b/packages/attest/README.md @@ -15,6 +15,14 @@ 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. +## Table of Contents +- [Usage](#usage) + - [attest](#attest) + - [attestProvenance](#attestprovenance) + - [Attestation](#attestation) +- [Sigstore Instance](#sigstore-instance) +- [Storage](#storage) + ## Usage ### `attest` @@ -165,6 +173,74 @@ export type Attestation = { For details about the Sigstore bundle format, see the [Bundle protobuf specification](https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto). +### createStorageRecord + +The `createStorageRecord` function creates an +[artifact metadata storage record](https://docs.github.com/en/rest/orgs/artifact-metadata?apiVersion=2022-11-28#create-artifact-metadata-storage-record) +on behalf of an attested artifact. It accepts parameters defining artifact +and package registry details. The storage record contains metadata about where the artifact is stored on a given package registry. + +```js +const { createStorageRecord } = 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 record = await createStorageRecord( + artifactOptions: { + name: 'my-artifact-name', + digest: { 'sha256': '36ab4667...'}, + version: "v1.0.0" + }, + packageRegistryOptions: { + registryUrl: "https://my-fave-pkg-registry.com" + }, + token: ghToken + ); + + console.log(record); +} + +run(); +``` + +The `createStorageRecord` function supports the following options: + +```typescript +// Artifact details to associate the record with +export type ArtifactOptions = { + // The name of the artifact + name: string + // The digest of the artifact + digest: string + // The version of the artifact + version?: string + // The status of the artifact + status?: string +} +// Includes details about the package registry the artifact was published to +export type PackageRegistryOptions = { + // The URL of the package registry + registryUrl: string + // The URL of the artifact in the package registry + artifactUrl?: string + // The package registry repository the artifact was published to. + repo?: string + // The path of the artifact in the package registry repository. + path?: string +} +// GitHub token for writing attestations. +token: string +// Optional parameters for the write operation. +// The number of times to retry the request. +retryAttempts?: number +// HTTP headers to include in request to Artifact Metadata API. +headers?: RequestHeaders +``` + ## Sigstore Instance When generating the signed attestation there are two different Sigstore diff --git a/packages/attest/__tests__/artifactMetadata.test.ts b/packages/attest/__tests__/artifactMetadata.test.ts new file mode 100644 index 00000000..50d89dd6 --- /dev/null +++ b/packages/attest/__tests__/artifactMetadata.test.ts @@ -0,0 +1,137 @@ +import {MockAgent, setGlobalDispatcher} from 'undici' +import {createStorageRecord} from '../src/artifactMetadata' + +describe('createStorageRecord', () => { + const originalEnv = process.env + const token = 'token' + const headers = {'X-GitHub-Foo': 'true'} + + const artifactOptions = { + name: 'my-lib', + version: '1.0.0', + digest: `sha256:${'a'.repeat(64)}` + } + const packageRegistryOptions = { + registryUrl: 'https://my-registry.org' + } + + const mockAgent = new MockAgent() + setGlobalDispatcher(mockAgent) + + beforeEach(() => { + process.env = { + ...originalEnv, + GITHUB_REPOSITORY: 'foo/bar' + } + }) + + afterEach(() => { + process.env = originalEnv + }) + + describe('when the api call is successful', () => { + beforeEach(() => { + mockAgent + .get('https://api.github.com') + .intercept({ + path: '/orgs/foo/artifacts/metadata/storage-record', + method: 'POST', + headers: {authorization: `token ${token}`, ...headers}, + body: JSON.stringify({ + name: 'my-lib', + version: '1.0.0', + digest: `sha256:${'a'.repeat(64)}`, + registry_url: 'https://my-registry.org' + }) + }) + .reply(200, {storage_records: [{id: 123}, {id: 456}]}) + }) + + it('persists the storage record', async () => { + await expect( + createStorageRecord( + artifactOptions, + packageRegistryOptions, + token, + undefined, + headers + ) + ).resolves.toEqual([123, 456]) + }) + }) + + describe('when the api call fails', () => { + beforeEach(() => { + mockAgent + .get('https://api.github.com') + .intercept({ + path: '/orgs/foo/artifacts/metadata/storage-record', + method: 'POST', + headers: {authorization: `token ${token}`}, + body: JSON.stringify({ + name: 'my-lib', + version: '1.0.0', + digest: `sha256:${'a'.repeat(64)}`, + registry_url: 'https://my-registry.org' + }) + }) + .reply(500, 'oops') + }) + + it('throws an error', async () => { + await expect( + createStorageRecord( + artifactOptions, + packageRegistryOptions, + token, + 0, + headers + ) + ).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: '/orgs/foo/artifacts/metadata/storage-record', + method: 'POST', + headers: {authorization: `token ${token}`}, + body: JSON.stringify({ + ...artifactOptions, + registry_url: packageRegistryOptions.registryUrl + }) + }) + .reply(500, 'oops') + .times(1) + + pool + .intercept({ + path: '/orgs/foo/artifacts/metadata/storage-record', + method: 'POST', + headers: {authorization: `token ${token}`}, + body: JSON.stringify({ + ...artifactOptions, + registry_url: packageRegistryOptions.registryUrl + }) + }) + .reply(200, {storage_records: [{id: 123}, {id: 456}]}) + .times(1) + }) + + it('persists the storage record', async () => { + await expect( + createStorageRecord( + artifactOptions, + packageRegistryOptions, + token, + undefined, + headers + ) + ).resolves.toEqual([123, 456]) + }) + }) +}) diff --git a/packages/attest/package-lock.json b/packages/attest/package-lock.json index 985d44c4..a3309fcd 100644 --- a/packages/attest/package-lock.json +++ b/packages/attest/package-lock.json @@ -1,12 +1,12 @@ { "name": "@actions/attest", - "version": "2.0.0", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@actions/attest", - "version": "2.0.0", + "version": "2.1.0", "license": "MIT", "dependencies": { "@actions/core": "^1.11.1", @@ -192,7 +192,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", diff --git a/packages/attest/package.json b/packages/attest/package.json index d8b70ee8..371c553f 100644 --- a/packages/attest/package.json +++ b/packages/attest/package.json @@ -1,6 +1,6 @@ { "name": "@actions/attest", - "version": "2.0.0", + "version": "2.1.0", "description": "Actions attestation lib", "keywords": [ "github", @@ -55,4 +55,4 @@ "@octokit/core": "^5.2.0" } } -} \ No newline at end of file +} diff --git a/packages/attest/src/artifactMetadata.ts b/packages/attest/src/artifactMetadata.ts new file mode 100644 index 00000000..a01d4b39 --- /dev/null +++ b/packages/attest/src/artifactMetadata.ts @@ -0,0 +1,85 @@ +import * as github from '@actions/github' +import {retry} from '@octokit/plugin-retry' +import {RequestHeaders} from '@octokit/types' + +const CREATE_STORAGE_RECORD_REQUEST = + 'POST /orgs/{owner}/artifacts/metadata/storage-record' +const DEFAULT_RETRY_COUNT = 5 + +/** + * Options for creating a storage record for an attested artifact. + */ +export type ArtifactOptions = { + // Includes details about the attested artifact + // The name of the artifact + name: string + // The digest of the artifact + digest: string + // The version of the artifact + version?: string + // The status of the artifact + status?: string +} +// Includes details about the package registry the artifact was published to +export type PackageRegistryOptions = { + // The URL of the package registry + registryUrl: string + // The URL of the artifact in the package registry + artifactUrl?: string + // The package registry repository the artifact was published to. + repo?: string + // The path of the artifact in the package registry repository. + path?: string +} + +/** + * Writes a storage record on behalf of an artifact that has been attested + * @param artifactOptions - parameters for the storage record API request. + * @param packageRegistryOptions - parameters for the package registry API request. + * @param token - GitHub token used to authenticate the request. + * @param retryAttempts - The number of retries to attempt if the request fails. + * @param headers - Additional headers to include in the request. + * + * @returns The ID of the storage record. + * @throws Error if the storage record fails to persist. + */ +export async function createStorageRecord( + artifactOptions: ArtifactOptions, + packageRegistryOptions: PackageRegistryOptions, + token: string, + retryAttempts?: number, + headers?: RequestHeaders +): Promise { + const retries = retryAttempts ?? DEFAULT_RETRY_COUNT + const octokit = github.getOctokit(token, {retry: {retries}}, retry) + try { + const response = await octokit.request(CREATE_STORAGE_RECORD_REQUEST, { + owner: github.context.repo.owner, + headers, + ...buildRequestParams(artifactOptions, packageRegistryOptions) + }) + + const data = + typeof response.data == 'string' + ? JSON.parse(response.data) + : response.data + + return data?.storage_records.map((r: {id: number}) => r.id) + } catch (err) { + const message = err instanceof Error ? err.message : err + throw new Error(`Failed to persist storage record: ${message}`) + } +} + +function buildRequestParams( + artifactOptions: ArtifactOptions, + packageRegistryOptions: PackageRegistryOptions +): Record { + const {registryUrl, artifactUrl, ...rest} = packageRegistryOptions + return { + ...artifactOptions, + registry_url: registryUrl, + artifact_url: artifactUrl, + ...rest + } +} diff --git a/packages/attest/src/index.ts b/packages/attest/src/index.ts index 43c0a472..78384d32 100644 --- a/packages/attest/src/index.ts +++ b/packages/attest/src/index.ts @@ -1,3 +1,4 @@ +export {createStorageRecord} from './artifactMetadata' export {AttestOptions, attest} from './attest' export { AttestProvenanceOptions, diff --git a/packages/core/RELEASES.md b/packages/core/RELEASES.md index 3afc9050..47e30ea7 100644 --- a/packages/core/RELEASES.md +++ b/packages/core/RELEASES.md @@ -1,5 +1,8 @@ # @actions/core Releases +## 2.0.1 +- Bump @actions/exec from 1.1.1 to 2.0.0 [#2199](https://github.com/actions/toolkit/pull/2199) + ## 2.0.0 - Add support for Node 24 [#2110](https://github.com/actions/toolkit/pull/2110) - Bump @actions/http-client from 2.0.1 to 3.0.0 diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index 924750d6..68c87516 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -1,15 +1,15 @@ { "name": "@actions/core", - "version": "2.0.0", - "lockfileVersion": 2, + "version": "2.0.1", + "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@actions/core", - "version": "2.0.0", + "version": "2.0.1", "license": "MIT", "dependencies": { - "@actions/exec": "^1.1.1", + "@actions/exec": "^2.0.0", "@actions/http-client": "^3.0.0" }, "devDependencies": { @@ -17,11 +17,12 @@ } }, "node_modules/@actions/exec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", - "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-2.0.0.tgz", + "integrity": "sha512-k8ngrX2voJ/RIN6r9xB82NVqKpnMRtxDoiO+g3olkIUpQNqjArXrCQceduQZCQj3P3xm32pChRLqRrtXTlqhIw==", + "license": "MIT", "dependencies": { - "@actions/io": "^1.0.1" + "@actions/io": "^2.0.0" } }, "node_modules/@actions/http-client": { @@ -35,9 +36,10 @@ } }, "node_modules/@actions/io": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", - "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-2.0.0.tgz", + "integrity": "sha512-Jv33IN09XLO+0HS79aaODsvIRyduiF7NY/F6LYeK5oeUmrsz7aFdRphQjFoESF4jS7lMauDOttKALcpapVDIAg==", + "license": "MIT" }, "node_modules/@fastify/busboy": { "version": "2.1.1", @@ -49,15 +51,17 @@ } }, "node_modules/@types/node": { - "version": "16.18.112", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.112.tgz", - "integrity": "sha512-EKrbKUGJROm17+dY/gMi31aJlGLJ75e1IkTojt9n6u+hnaTBDs+M1bIdOawpk2m6YUAXq/R2W0SxCng1tndHCg==", - "dev": true + "version": "16.18.126", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.126.tgz", + "integrity": "sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==", + "dev": true, + "license": "MIT" }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", "engines": { "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } @@ -74,53 +78,5 @@ "node": ">=14.0" } } - }, - "dependencies": { - "@actions/exec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", - "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", - "requires": { - "@actions/io": "^1.0.1" - } - }, - "@actions/http-client": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-3.0.0.tgz", - "integrity": "sha512-1s3tXAfVMSz9a4ZEBkXXRQD4QhY3+GAsWSbaYpeknPOKEeyRiU3lH+bHiLMZdo2x/fIeQ/hscL1wCkDLVM2DZQ==", - "requires": { - "tunnel": "^0.0.6", - "undici": "^5.28.5" - } - }, - "@actions/io": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz", - "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" - }, - "@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==" - }, - "@types/node": { - "version": "16.18.112", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.112.tgz", - "integrity": "sha512-EKrbKUGJROm17+dY/gMi31aJlGLJ75e1IkTojt9n6u+hnaTBDs+M1bIdOawpk2m6YUAXq/R2W0SxCng1tndHCg==", - "dev": true - }, - "tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" - }, - "undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", - "requires": { - "@fastify/busboy": "^2.0.0" - } - } } } diff --git a/packages/core/package.json b/packages/core/package.json index e8eae164..ae9270fb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@actions/core", - "version": "2.0.0", + "version": "2.0.1", "description": "Actions core lib", "keywords": [ "github", @@ -36,7 +36,7 @@ "url": "https://github.com/actions/toolkit/issues" }, "dependencies": { - "@actions/exec": "^1.1.1", + "@actions/exec": "^2.0.0", "@actions/http-client": "^3.0.0" }, "devDependencies": {