From 85e94cb74196682e8a2c5133e48e6b404bde3021 Mon Sep 17 00:00:00 2001 From: Brian DeHamer Date: Tue, 5 Nov 2024 09:16:07 -0800 Subject: [PATCH] support multi-subject attestations (#164) Signed-off-by: Brian DeHamer --- README.md | 11 +--- __tests__/main.test.ts | 61 +++++------------- __tests__/subject.test.ts | 18 +++++- dist/index.js | 129 ++++++++++++++------------------------ package-lock.json | 4 +- package.json | 2 +- src/attest.ts | 27 +++----- src/index.ts | 6 +- src/main.ts | 105 ++++++++++--------------------- src/subject.ts | 21 ++++--- 10 files changed, 140 insertions(+), 244 deletions(-) diff --git a/README.md b/README.md index 81df412..b772f5d 100644 --- a/README.md +++ b/README.md @@ -117,18 +117,14 @@ See [action.yml](action.yml) Attestations are saved in the JSON-serialized [Sigstore bundle][6] format. -If multiple subjects are being attested at the same time, each attestation will -be written to the output file on a separate line (using the [JSON Lines][7] -format). +If multiple subjects are being attested at the same time, a single attestation +will be created with references to each of the supplied subjects. ## Attestation Limits ### Subject Limits -No more than 2500 subjects can be attested at the same time. Subjects will be -processed in batches 50. After the initial group of 50, each subsequent batch -will incur an exponentially increasing amount of delay (capped at 1 minute of -delay per batch) to avoid overwhelming the attestation API. +No more than 1024 subjects can be attested at the same time. ### Predicate Limits @@ -269,7 +265,6 @@ jobs: [5]: https://cli.github.com/manual/gh_attestation_verify [6]: https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto -[7]: https://jsonlines.org/ [8]: https://github.com/actions/toolkit/tree/main/packages/glob#patterns [9]: https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index bd33cd9..ef4a18e 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -46,8 +46,7 @@ const defaultInputs: main.RunInputs = { pushToRegistry: false, showSummary: true, githubToken: '', - privateSigning: false, - batchSize: 50 + privateSigning: false } describe('action', () => { @@ -290,15 +289,11 @@ describe('action', () => { }) }) - describe('when the subject count exceeds the batch size', () => { + describe('when the subject count is greater than 1', () => { let dir = '' const filename = 'subject' - let scope: nock.Scope beforeEach(async () => { - // Start from scratch - nock.cleanAll() - const subjectCount = 5 const content = 'file content' @@ -309,38 +304,22 @@ describe('action', () => { // Add files for glob testing for (let i = 0; i < subjectCount; i++) { await fs.writeFile(path.join(dir, `${filename}-${i}`), content) - - // Set-up a Fulcio mock for each subject - await mockFulcio({ - baseURL: 'https://fulcio.githubapp.com', - strict: false - }) - - // Set-up a TSA mock for each subject - await mockTSA({ baseURL: 'https://timestamp.githubapp.com' }) - - // Set-up a GH API mock for each subject - mockAgent - .get('https://api.github.com') - .intercept({ - path: /^\/repos\/.*\/.*\/attestations$/, - method: 'post' - }) - .reply(201, { id: attestationID }) } - // Set-up a OIDC token mock for each subject - scope = nock(tokenURL) - .get('/') - .query({ audience: 'sigstore' }) - .times(subjectCount) - .reply(200, { value: oidcToken }) - // Set the GH context with private repository visibility and a repo owner. setGHContext({ payload: { repository: { visibility: 'private' } }, repo: { owner: 'foo', repo: 'bar' } }) + + // Set-up a Fulcio mock for each subject + await mockFulcio({ + baseURL: 'https://fulcio.githubapp.com', + strict: false + }) + + // Set-up a TSA mock for each subject + await mockTSA({ baseURL: 'https://timestamp.githubapp.com' }) }) afterEach(async () => { @@ -354,8 +333,7 @@ describe('action', () => { subjectPath: path.join(dir, `${filename}-*`), predicateType, predicate, - githubToken: 'gh-token', - batchSize: 2 + githubToken: 'gh-token' } await main.run(inputs) @@ -363,17 +341,8 @@ describe('action', () => { expect(setFailedMock).not.toHaveBeenCalled() expect(infoMock).toHaveBeenNthCalledWith( 1, - expect.stringMatching('Processing subject batch 1/3') + expect.stringMatching('Attestation created for 5 subjects') ) - expect(infoMock).toHaveBeenNthCalledWith( - 10, - expect.stringMatching('Processing subject batch 2/3') - ) - expect(infoMock).toHaveBeenNthCalledWith( - 19, - expect.stringMatching('Processing subject batch 3/3') - ) - expect(scope.isDone()).toBe(true) }) }) @@ -382,7 +351,7 @@ describe('action', () => { const filename = 'subject' beforeEach(async () => { - const subjectCount = 2501 + const subjectCount = 1025 const content = 'file content' // Set-up temp directory @@ -419,7 +388,7 @@ describe('action', () => { expect(runMock).toHaveReturned() expect(setFailedMock).toHaveBeenCalledWith( new Error( - 'Too many subjects specified. The maximum number of subjects is 2500.' + 'Too many subjects specified. The maximum number of subjects is 1024.' ) ) }) diff --git a/__tests__/subject.test.ts b/__tests__/subject.test.ts index fd35b56..c4250b0 100644 --- a/__tests__/subject.test.ts +++ b/__tests__/subject.test.ts @@ -2,7 +2,11 @@ import crypto from 'crypto' import fs from 'fs/promises' import os from 'os' import path from 'path' -import { subjectFromInputs, SubjectInputs } from '../src/subject' +import { + formatSubjectDigest, + subjectFromInputs, + SubjectInputs +} from '../src/subject' describe('subjectFromInputs', () => { const blankInputs: SubjectInputs = { @@ -360,3 +364,15 @@ describe('subjectFromInputs', () => { }) }) }) + +describe('subjectDigest', () => { + it('returns the digest', () => { + const subject = { + name: 'foo', + digest: { sha1: 'deadbeef' } + } + + const digest = formatSubjectDigest(subject) + expect(digest).toEqual('sha1:deadbeef') + }) +}) diff --git a/dist/index.js b/dist/index.js index b9ef88b..b2a4c0f 100644 --- a/dist/index.js +++ b/dist/index.js @@ -70761,30 +70761,26 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.createAttestation = void 0; const attest_1 = __nccwpck_require__(11485); const oci_1 = __nccwpck_require__(81057); +const subject_1 = __nccwpck_require__(36303); const OCI_TIMEOUT = 30000; const OCI_RETRY = 3; -const createAttestation = async (subject, predicate, opts) => { +const createAttestation = async (subjects, predicate, opts) => { // Sign provenance w/ Sigstore const attestation = await (0, attest_1.attest)({ - subjectName: subject.name, - subjectDigest: subject.digest, + subjects, predicateType: predicate.type, predicate: predicate.params, sigstore: opts.sigstoreInstance, token: opts.githubToken }); - const subDigest = subjectDigest(subject); - const result = { - ...attestation, - subjectName: subject.name, - subjectDigest: subDigest - }; - if (opts.pushToRegistry) { + const result = attestation; + if (subjects.length === 1 && opts.pushToRegistry) { + const subject = subjects[0]; const credentials = (0, oci_1.getRegistryCredentials)(subject.name); const artifact = await (0, oci_1.attachArtifactToImage)({ credentials, imageName: subject.name, - imageDigest: subDigest, + imageDigest: (0, subject_1.formatSubjectDigest)(subject), artifact: Buffer.from(JSON.stringify(attestation.bundle)), mediaType: attestation.bundle.mediaType, annotations: { @@ -70799,12 +70795,6 @@ const createAttestation = async (subject, predicate, opts) => { return result; }; exports.createAttestation = createAttestation; -// Returns the subject's digest as a formatted string of the form -// ":". -const subjectDigest = (subject) => { - const alg = Object.keys(subject.digest).sort()[0]; - return `${alg}:${subject.digest[alg]}`; -}; /***/ }), @@ -70855,7 +70845,6 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); */ const core = __importStar(__nccwpck_require__(37484)); const main_1 = __nccwpck_require__(41730); -const DEFAULT_BATCH_SIZE = 50; const inputs = { subjectPath: core.getInput('subject-path'), subjectName: core.getInput('subject-name'), @@ -70867,9 +70856,7 @@ const inputs = { showSummary: core.getBooleanInput('show-summary'), githubToken: core.getInput('github-token'), // undocumented -- not part of public interface - privateSigning: ['true', 'True', 'TRUE', '1'].includes(core.getInput('private-signing')), - // internal only - batchSize: DEFAULT_BATCH_SIZE + privateSigning: ['true', 'True', 'TRUE', '1'].includes(core.getInput('private-signing')) }; // eslint-disable-next-line @typescript-eslint/no-floating-promises (0, main_1.run)(inputs); @@ -70921,8 +70908,6 @@ const predicate_1 = __nccwpck_require__(84982); const style = __importStar(__nccwpck_require__(64542)); const subject_1 = __nccwpck_require__(36303); const ATTESTATION_FILE_NAME = 'attestation.jsonl'; -const DELAY_INTERVAL_MS = 75; -const DELAY_MAX_MS = 1200; /* istanbul ignore next */ const logHandler = (level, ...args) => { // Send any HTTP-related log events to the GitHub Actions debug log @@ -70944,7 +70929,6 @@ async function run(inputs) { ? 'public-good' : 'github'; try { - const atts = []; if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) { throw new Error('missing "id-token" permission. Please add "permissions: id-token: write" to your workflow.'); } @@ -70955,35 +70939,19 @@ async function run(inputs) { const predicate = (0, predicate_1.predicateFromInputs)(inputs); const outputPath = path_1.default.join(tempDir(), ATTESTATION_FILE_NAME); core.setOutput('bundle-path', outputPath); - const subjectChunks = chunkArray(subjects, inputs.batchSize); - // Generate attestations for each subject serially, working in batches - for (let i = 0; i < subjectChunks.length; i++) { - if (subjectChunks.length > 1) { - core.info(`Processing subject batch ${i + 1}/${subjectChunks.length}`); - } - // Calculate the delay time for this batch - const delayTime = delay(i); - for (const subject of subjectChunks[i]) { - // Delay between attestations (only when chunk size > 1) - if (i > 0) { - await new Promise(resolve => setTimeout(resolve, delayTime)); - } - const att = await (0, attest_1.createAttestation)(subject, predicate, { - sigstoreInstance, - pushToRegistry: inputs.pushToRegistry, - githubToken: inputs.githubToken - }); - atts.push(att); - logAttestation(att, sigstoreInstance); - // Write attestation bundle to output file - fs_1.default.writeFileSync(outputPath, JSON.stringify(att.bundle) + os_1.default.EOL, { - encoding: 'utf-8', - flag: 'a' - }); - } - } + const att = await (0, attest_1.createAttestation)(subjects, predicate, { + sigstoreInstance, + pushToRegistry: inputs.pushToRegistry, + githubToken: inputs.githubToken + }); + logAttestation(subjects, att, sigstoreInstance); + // Write attestation bundle to output file + fs_1.default.writeFileSync(outputPath, JSON.stringify(att.bundle) + os_1.default.EOL, { + encoding: 'utf-8', + flag: 'a' + }); if (inputs.showSummary) { - logSummary(atts); + logSummary(att); } } catch (err) { @@ -71001,8 +70969,13 @@ async function run(inputs) { } } // Log details about the attestation to the GitHub Actions run -const logAttestation = (attestation, sigstoreInstance) => { - core.info(`Attestation created for ${attestation.subjectName}@${attestation.subjectDigest}`); +const logAttestation = (subjects, attestation, sigstoreInstance) => { + if (subjects.length === 1) { + core.info(`Attestation created for ${subjects[0].name}@${(0, subject_1.formatSubjectDigest)(subjects[0])}`); + } + else { + core.info(`Attestation created for ${subjects.length} subjects`); + } const instanceName = sigstoreInstance === 'public-good' ? 'Public Good' : 'GitHub'; core.startGroup(style.highlight(`Attestation signed using certificate from ${instanceName} Sigstore instance`)); core.info(attestation.certificate); @@ -71017,22 +70990,16 @@ const logAttestation = (attestation, sigstoreInstance) => { } if (attestation.attestationDigest) { core.info(style.highlight('Attestation uploaded to registry')); - core.info(`${attestation.subjectName}@${attestation.attestationDigest}`); + core.info(`${subjects[0].name}@${attestation.attestationDigest}`); } }; // Attach summary information to the GitHub Actions run -const logSummary = (attestations) => { - if (attestations.length > 0) { - core.summary.addHeading( - /* istanbul ignore next */ - attestations.length > 1 ? 'Attestations Created' : 'Attestation Created', 3); - const listItems = []; - for (const { subjectName, subjectDigest, attestationID } of attestations) { - if (attestationID) { - listItems.push(`${subjectName}@${subjectDigest}`); - } - } - core.summary.addList(listItems); +const logSummary = (attestation) => { + const { attestationID } = attestation; + if (attestationID) { + const url = attestationURL(attestationID); + core.summary.addHeading('Attestation Created', 3); + core.summary.addList([`${url}`]); core.summary.write(); } }; @@ -71044,13 +71011,6 @@ const tempDir = () => { } return fs_1.default.mkdtempSync(path_1.default.join(basePath, path_1.default.sep)); }; -// Transforms an array into an array of arrays, each containing at most -// `chunkSize` elements. -const chunkArray = (array, chunkSize) => { - return Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, index) => array.slice(index * chunkSize, (index + 1) * chunkSize)); -}; -// Calculate the delay time for a given iteration -const delay = (iteration) => Math.min(DELAY_INTERVAL_MS * 2 ** iteration, DELAY_MAX_MS); const attestationURL = (id) => `${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/attestations/${id}`; @@ -71157,13 +71117,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.subjectFromInputs = void 0; +exports.formatSubjectDigest = exports.subjectFromInputs = void 0; const glob = __importStar(__nccwpck_require__(47206)); const crypto_1 = __importDefault(__nccwpck_require__(76982)); const sync_1 = __nccwpck_require__(61110); const fs_1 = __importDefault(__nccwpck_require__(79896)); const path_1 = __importDefault(__nccwpck_require__(16928)); -const MAX_SUBJECT_COUNT = 2500; +const MAX_SUBJECT_COUNT = 1024; const DIGEST_ALGORITHM = 'sha256'; // Returns the subject specified by the action's inputs. The subject may be // specified as a path to a file or as a digest. If a path is provided, the @@ -71191,23 +71151,28 @@ const subjectFromInputs = async (inputs) => { } }; exports.subjectFromInputs = subjectFromInputs; +// Returns the subject's digest as a formatted string of the form +// ":". +const formatSubjectDigest = (subject) => { + const alg = Object.keys(subject.digest).sort()[0]; + return `${alg}:${subject.digest[alg]}`; +}; +exports.formatSubjectDigest = formatSubjectDigest; // Returns the subject specified by the path to a file. The file's digest is // calculated and returned along with the subject's name. const getSubjectFromPath = async (subjectPath, subjectName) => { const digestedSubjects = []; // Parse the list of subject paths const subjectPaths = parseList(subjectPath).join('\n'); - // Expand the globbed paths to a list of files + // Expand the globbed paths to a list of actual paths /* eslint-disable-next-line github/no-then */ - const files = await glob.create(subjectPaths).then(async (g) => g.glob()); + const paths = await glob.create(subjectPaths).then(async (g) => g.glob()); + // Filter path list to just the files (not directories) + const files = paths.filter(p => fs_1.default.statSync(p).isFile()); if (files.length > MAX_SUBJECT_COUNT) { throw new Error(`Too many subjects specified. The maximum number of subjects is ${MAX_SUBJECT_COUNT}.`); } for (const file of files) { - // Skip anything that is NOT a file - if (!fs_1.default.statSync(file).isFile()) { - continue; - } const name = subjectName || path_1.default.parse(file).base; const digest = await digestFile(DIGEST_ALGORITHM, file); digestedSubjects.push({ name, digest: { [DIGEST_ALGORITHM]: digest } }); diff --git a/package-lock.json b/package-lock.json index 3128ded..3991f38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "actions/attest", - "version": "1.4.1", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "actions/attest", - "version": "1.4.1", + "version": "2.0.0", "license": "MIT", "dependencies": { "@actions/attest": "^1.5.0", diff --git a/package.json b/package.json index df6dc50..41588db 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "actions/attest", "description": "Generate signed attestations for workflow artifacts", - "version": "1.4.1", + "version": "2.0.0", "author": "", "private": true, "homepage": "https://github.com/actions/attest", diff --git a/src/attest.ts b/src/attest.ts index 187ffef..3b59cdb 100644 --- a/src/attest.ts +++ b/src/attest.ts @@ -1,18 +1,17 @@ import { Attestation, Predicate, Subject, attest } from '@actions/attest' import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci' +import { formatSubjectDigest } from './subject' const OCI_TIMEOUT = 30000 const OCI_RETRY = 3 export type SigstoreInstance = 'public-good' | 'github' export type AttestResult = Attestation & { - subjectName: string - subjectDigest: string attestationDigest?: string } export const createAttestation = async ( - subject: Subject, + subjects: Subject[], predicate: Predicate, opts: { sigstoreInstance: SigstoreInstance @@ -22,27 +21,22 @@ export const createAttestation = async ( ): Promise => { // Sign provenance w/ Sigstore const attestation = await attest({ - subjectName: subject.name, - subjectDigest: subject.digest, + subjects, predicateType: predicate.type, predicate: predicate.params, sigstore: opts.sigstoreInstance, token: opts.githubToken }) - const subDigest = subjectDigest(subject) - const result: AttestResult = { - ...attestation, - subjectName: subject.name, - subjectDigest: subDigest - } + const result: AttestResult = attestation - if (opts.pushToRegistry) { + if (subjects.length === 1 && opts.pushToRegistry) { + const subject = subjects[0] const credentials = getRegistryCredentials(subject.name) const artifact = await attachArtifactToImage({ credentials, imageName: subject.name, - imageDigest: subDigest, + imageDigest: formatSubjectDigest(subject), artifact: Buffer.from(JSON.stringify(attestation.bundle)), mediaType: attestation.bundle.mediaType, annotations: { @@ -58,10 +52,3 @@ export const createAttestation = async ( return result } - -// Returns the subject's digest as a formatted string of the form -// ":". -const subjectDigest = (subject: Subject): string => { - const alg = Object.keys(subject.digest).sort()[0] - return `${alg}:${subject.digest[alg]}` -} diff --git a/src/index.ts b/src/index.ts index a838090..8dd8370 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,6 @@ import * as core from '@actions/core' import { run, RunInputs } from './main' -const DEFAULT_BATCH_SIZE = 50 - const inputs: RunInputs = { subjectPath: core.getInput('subject-path'), subjectName: core.getInput('subject-name'), @@ -19,9 +17,7 @@ const inputs: RunInputs = { // undocumented -- not part of public interface privateSigning: ['true', 'True', 'TRUE', '1'].includes( core.getInput('private-signing') - ), - // internal only - batchSize: DEFAULT_BATCH_SIZE + ) } // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/src/main.ts b/src/main.ts index 638fd7e..a5bbecc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,11 +7,15 @@ import { AttestResult, SigstoreInstance, createAttestation } from './attest' import { SEARCH_PUBLIC_GOOD_URL } from './endpoints' import { PredicateInputs, predicateFromInputs } from './predicate' import * as style from './style' -import { SubjectInputs, subjectFromInputs } from './subject' +import { + SubjectInputs, + formatSubjectDigest, + subjectFromInputs +} from './subject' + +import type { Subject } from '@actions/attest' const ATTESTATION_FILE_NAME = 'attestation.jsonl' -const DELAY_INTERVAL_MS = 75 -const DELAY_MAX_MS = 1200 export type RunInputs = SubjectInputs & PredicateInputs & { @@ -19,7 +23,6 @@ export type RunInputs = SubjectInputs & githubToken: string showSummary: boolean privateSigning: boolean - batchSize: number } /* istanbul ignore next */ @@ -47,7 +50,6 @@ export async function run(inputs: RunInputs): Promise { : 'github' try { - const atts: AttestResult[] = [] if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) { throw new Error( 'missing "id-token" permission. Please add "permissions: id-token: write" to your workflow.' @@ -63,42 +65,22 @@ export async function run(inputs: RunInputs): Promise { const outputPath = path.join(tempDir(), ATTESTATION_FILE_NAME) core.setOutput('bundle-path', outputPath) - const subjectChunks = chunkArray(subjects, inputs.batchSize) + const att = await createAttestation(subjects, predicate, { + sigstoreInstance, + pushToRegistry: inputs.pushToRegistry, + githubToken: inputs.githubToken + }) - // Generate attestations for each subject serially, working in batches - for (let i = 0; i < subjectChunks.length; i++) { - if (subjectChunks.length > 1) { - core.info(`Processing subject batch ${i + 1}/${subjectChunks.length}`) - } + logAttestation(subjects, att, sigstoreInstance) - // Calculate the delay time for this batch - const delayTime = delay(i) - - for (const subject of subjectChunks[i]) { - // Delay between attestations (only when chunk size > 1) - if (i > 0) { - await new Promise(resolve => setTimeout(resolve, delayTime)) - } - - const att = await createAttestation(subject, predicate, { - sigstoreInstance, - pushToRegistry: inputs.pushToRegistry, - githubToken: inputs.githubToken - }) - atts.push(att) - - logAttestation(att, sigstoreInstance) - - // Write attestation bundle to output file - fs.writeFileSync(outputPath, JSON.stringify(att.bundle) + os.EOL, { - encoding: 'utf-8', - flag: 'a' - }) - } - } + // Write attestation bundle to output file + fs.writeFileSync(outputPath, JSON.stringify(att.bundle) + os.EOL, { + encoding: 'utf-8', + flag: 'a' + }) if (inputs.showSummary) { - logSummary(atts) + logSummary(att) } } catch (err) { // Fail the workflow run if an error occurs @@ -123,12 +105,17 @@ export async function run(inputs: RunInputs): Promise { // Log details about the attestation to the GitHub Actions run const logAttestation = ( + subjects: Subject[], attestation: AttestResult, sigstoreInstance: SigstoreInstance ): void => { - core.info( - `Attestation created for ${attestation.subjectName}@${attestation.subjectDigest}` - ) + if (subjects.length === 1) { + core.info( + `Attestation created for ${subjects[0].name}@${formatSubjectDigest(subjects[0])}` + ) + } else { + core.info(`Attestation created for ${subjects.length} subjects`) + } const instanceName = sigstoreInstance === 'public-good' ? 'Public Good' : 'GitHub' @@ -156,29 +143,18 @@ const logAttestation = ( if (attestation.attestationDigest) { core.info(style.highlight('Attestation uploaded to registry')) - core.info(`${attestation.subjectName}@${attestation.attestationDigest}`) + core.info(`${subjects[0].name}@${attestation.attestationDigest}`) } } // Attach summary information to the GitHub Actions run -const logSummary = (attestations: AttestResult[]): void => { - if (attestations.length > 0) { - core.summary.addHeading( - /* istanbul ignore next */ - attestations.length > 1 ? 'Attestations Created' : 'Attestation Created', - 3 - ) +const logSummary = (attestation: AttestResult): void => { + const { attestationID } = attestation - const listItems = [] - for (const { subjectName, subjectDigest, attestationID } of attestations) { - if (attestationID) { - listItems.push( - `${subjectName}@${subjectDigest}` - ) - } - } - - core.summary.addList(listItems) + if (attestationID) { + const url = attestationURL(attestationID) + core.summary.addHeading('Attestation Created', 3) + core.summary.addList([`${url}`]) core.summary.write() } } @@ -194,18 +170,5 @@ const tempDir = (): string => { return fs.mkdtempSync(path.join(basePath, path.sep)) } -// Transforms an array into an array of arrays, each containing at most -// `chunkSize` elements. -const chunkArray = (array: T[], chunkSize: number): T[][] => { - return Array.from( - { length: Math.ceil(array.length / chunkSize) }, - (_, index) => array.slice(index * chunkSize, (index + 1) * chunkSize) - ) -} - -// Calculate the delay time for a given iteration -const delay = (iteration: number): number => - Math.min(DELAY_INTERVAL_MS * 2 ** iteration, DELAY_MAX_MS) - const attestationURL = (id: string): string => `${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/attestations/${id}` diff --git a/src/subject.ts b/src/subject.ts index 5157907..ae27a5a 100644 --- a/src/subject.ts +++ b/src/subject.ts @@ -6,7 +6,7 @@ import path from 'path' import type { Subject } from '@actions/attest' -const MAX_SUBJECT_COUNT = 2500 +const MAX_SUBJECT_COUNT = 1024 const DIGEST_ALGORITHM = 'sha256' export type SubjectInputs = { @@ -49,6 +49,13 @@ export const subjectFromInputs = async ( } } +// Returns the subject's digest as a formatted string of the form +// ":". +export const formatSubjectDigest = (subject: Subject): string => { + const alg = Object.keys(subject.digest).sort()[0] + return `${alg}:${subject.digest[alg]}` +} + // Returns the subject specified by the path to a file. The file's digest is // calculated and returned along with the subject's name. const getSubjectFromPath = async ( @@ -60,9 +67,12 @@ const getSubjectFromPath = async ( // Parse the list of subject paths const subjectPaths = parseList(subjectPath).join('\n') - // Expand the globbed paths to a list of files + // Expand the globbed paths to a list of actual paths /* eslint-disable-next-line github/no-then */ - const files = await glob.create(subjectPaths).then(async g => g.glob()) + const paths = await glob.create(subjectPaths).then(async g => g.glob()) + + // Filter path list to just the files (not directories) + const files = paths.filter(p => fs.statSync(p).isFile()) if (files.length > MAX_SUBJECT_COUNT) { throw new Error( @@ -71,11 +81,6 @@ const getSubjectFromPath = async ( } for (const file of files) { - // Skip anything that is NOT a file - if (!fs.statSync(file).isFile()) { - continue - } - const name = subjectName || path.parse(file).base const digest = await digestFile(DIGEST_ALGORITHM, file)