support multi-subject attestations (#164)

Signed-off-by: Brian DeHamer <bdehamer@github.com>
This commit is contained in:
Brian DeHamer
2024-11-05 09:16:07 -08:00
committed by GitHub
parent b485edd412
commit 85e94cb741
10 changed files with 140 additions and 244 deletions

View File

@@ -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

View File

@@ -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.'
)
)
})

View File

@@ -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')
})
})

129
dist/index.js generated vendored
View File

@@ -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
// "<algorithm>:<digest>".
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(`<a href="${attestationURL(attestationID)}">${subjectName}@${subjectDigest}</a>`);
}
}
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([`<a href="${url}">${url}</a>`]);
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
// "<algorithm>:<digest>".
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 } });

4
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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<AttestResult> => {
// 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
// "<algorithm>:<digest>".
const subjectDigest = (subject: Subject): string => {
const alg = Object.keys(subject.digest).sort()[0]
return `${alg}:${subject.digest[alg]}`
}

View File

@@ -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

View File

@@ -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<void> {
: '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<void> {
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<void> {
// 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(
`<a href="${attestationURL(attestationID)}">${subjectName}@${subjectDigest}</a>`
)
}
}
core.summary.addList(listItems)
if (attestationID) {
const url = attestationURL(attestationID)
core.summary.addHeading('Attestation Created', 3)
core.summary.addList([`<a href="${url}">${url}</a>`])
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 = <T>(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}`

View File

@@ -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
// "<algorithm>:<digest>".
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)