refactor core attestation logic (#73)
Signed-off-by: Brian DeHamer <[email protected]>
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
import { highlight, mute } from '../src/style'
|
||||||
|
|
||||||
|
describe('style', () => {
|
||||||
|
describe('highlight', () => {
|
||||||
|
it('adds cyan color to the string', () => {
|
||||||
|
expect(highlight('foo')).toBe('\x1B[36mfoo\x1B[39m')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mute', () => {
|
||||||
|
it('adds gray color to the string', () => {
|
||||||
|
expect(mute('foo')).toBe('\x1B[38;5;244mfoo\x1B[39m')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
+108
-64
@@ -79896,6 +79896,63 @@ function wrappy (fn, cb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 26056:
|
||||||
|
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
|
exports.createAttestation = void 0;
|
||||||
|
const attest_1 = __nccwpck_require__(74113);
|
||||||
|
const oci_1 = __nccwpck_require__(47353);
|
||||||
|
const OCI_TIMEOUT = 2000;
|
||||||
|
const OCI_RETRY = 3;
|
||||||
|
const createAttestation = async (subject, predicate, opts) => {
|
||||||
|
// Sign provenance w/ Sigstore
|
||||||
|
const attestation = await (0, attest_1.attest)({
|
||||||
|
subjectName: subject.name,
|
||||||
|
subjectDigest: subject.digest,
|
||||||
|
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 credentials = (0, oci_1.getRegistryCredentials)(subject.name);
|
||||||
|
const artifact = await (0, oci_1.attachArtifactToImage)({
|
||||||
|
credentials,
|
||||||
|
imageName: subject.name,
|
||||||
|
imageDigest: subDigest,
|
||||||
|
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
|
||||||
|
mediaType: attestation.bundle.mediaType,
|
||||||
|
annotations: {
|
||||||
|
'dev.sigstore.bundle.content': 'dsse-envelope',
|
||||||
|
'dev.sigstore.bundle.predicateType': predicate.type
|
||||||
|
},
|
||||||
|
fetchOpts: { timeout: OCI_TIMEOUT, retry: OCI_RETRY }
|
||||||
|
});
|
||||||
|
// Add the attestation's digest to the result
|
||||||
|
result.attestationDigest = artifact.digest;
|
||||||
|
}
|
||||||
|
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]}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 69112:
|
/***/ 69112:
|
||||||
@@ -80000,22 +80057,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||||||
};
|
};
|
||||||
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
exports.run = void 0;
|
exports.run = void 0;
|
||||||
const attest_1 = __nccwpck_require__(74113);
|
|
||||||
const core = __importStar(__nccwpck_require__(42186));
|
const core = __importStar(__nccwpck_require__(42186));
|
||||||
const github = __importStar(__nccwpck_require__(95438));
|
const github = __importStar(__nccwpck_require__(95438));
|
||||||
const oci_1 = __nccwpck_require__(47353);
|
|
||||||
const fs_1 = __importDefault(__nccwpck_require__(57147));
|
const fs_1 = __importDefault(__nccwpck_require__(57147));
|
||||||
const os_1 = __importDefault(__nccwpck_require__(22037));
|
const os_1 = __importDefault(__nccwpck_require__(22037));
|
||||||
const path_1 = __importDefault(__nccwpck_require__(71017));
|
const path_1 = __importDefault(__nccwpck_require__(71017));
|
||||||
|
const attest_1 = __nccwpck_require__(26056);
|
||||||
const endpoints_1 = __nccwpck_require__(69112);
|
const endpoints_1 = __nccwpck_require__(69112);
|
||||||
const predicate_1 = __nccwpck_require__(72103);
|
const predicate_1 = __nccwpck_require__(72103);
|
||||||
|
const style = __importStar(__nccwpck_require__(41583));
|
||||||
const subject_1 = __nccwpck_require__(95206);
|
const subject_1 = __nccwpck_require__(95206);
|
||||||
const COLOR_CYAN = '\x1B[36m';
|
|
||||||
const COLOR_GRAY = '\x1B[38;5;244m';
|
|
||||||
const COLOR_DEFAULT = '\x1B[39m';
|
|
||||||
const ATTESTATION_FILE_NAME = 'attestation.jsonl';
|
const ATTESTATION_FILE_NAME = 'attestation.jsonl';
|
||||||
const OCI_TIMEOUT = 2000;
|
|
||||||
const OCI_RETRY = 3;
|
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
const logHandler = (level, ...args) => {
|
const logHandler = (level, ...args) => {
|
||||||
// Send any HTTP-related log events to the GitHub Actions debug log
|
// Send any HTTP-related log events to the GitHub Actions debug log
|
||||||
@@ -80047,6 +80099,7 @@ async function run(inputs) {
|
|||||||
});
|
});
|
||||||
const predicate = (0, predicate_1.predicateFromInputs)(inputs);
|
const predicate = (0, predicate_1.predicateFromInputs)(inputs);
|
||||||
const outputPath = path_1.default.join(tempDir(), ATTESTATION_FILE_NAME);
|
const outputPath = path_1.default.join(tempDir(), ATTESTATION_FILE_NAME);
|
||||||
|
core.setOutput('bundle-path', outputPath);
|
||||||
const subjectChunks = chunkArray(subjects, inputs.batchSize);
|
const subjectChunks = chunkArray(subjects, inputs.batchSize);
|
||||||
let chunkCount = 0;
|
let chunkCount = 0;
|
||||||
// Generate attestations for each subject serially, working in batches
|
// Generate attestations for each subject serially, working in batches
|
||||||
@@ -80059,31 +80112,21 @@ async function run(inputs) {
|
|||||||
core.info(`Processing subject batch ${chunkCount}/${subjectChunks.length}`);
|
core.info(`Processing subject batch ${chunkCount}/${subjectChunks.length}`);
|
||||||
}
|
}
|
||||||
for (const subject of subjectChunk) {
|
for (const subject of subjectChunk) {
|
||||||
const att = await createAttestation(subject, predicate, {
|
const att = await (0, attest_1.createAttestation)(subject, predicate, {
|
||||||
sigstoreInstance,
|
sigstoreInstance,
|
||||||
pushToRegistry: inputs.pushToRegistry,
|
pushToRegistry: inputs.pushToRegistry,
|
||||||
githubToken: inputs.githubToken
|
githubToken: inputs.githubToken
|
||||||
});
|
});
|
||||||
|
atts.push(att);
|
||||||
|
logAttestation(att, sigstoreInstance);
|
||||||
// Write attestation bundle to output file
|
// Write attestation bundle to output file
|
||||||
fs_1.default.writeFileSync(outputPath, JSON.stringify(att.bundle) + os_1.default.EOL, {
|
fs_1.default.writeFileSync(outputPath, JSON.stringify(att.bundle) + os_1.default.EOL, {
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
flag: 'a'
|
flag: 'a'
|
||||||
});
|
});
|
||||||
if (att.attestationID) {
|
|
||||||
atts.push({ subject, attestationID: att.attestationID });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
logSummary(atts);
|
||||||
if (atts.length > 0) {
|
|
||||||
core.summary.addHeading(
|
|
||||||
/* istanbul ignore next */
|
|
||||||
atts.length > 1 ? 'Attestations Created' : 'Attestation Created', 3);
|
|
||||||
for (const { subject, attestationID } of atts) {
|
|
||||||
core.summary.addLink(`${subject.name}@${subjectDigest(subject)}`, attestationURL(attestationID));
|
|
||||||
}
|
|
||||||
core.summary.write();
|
|
||||||
}
|
|
||||||
core.setOutput('bundle-path', outputPath);
|
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
// Fail the workflow run if an error occurs
|
// Fail the workflow run if an error occurs
|
||||||
@@ -80092,7 +80135,7 @@ async function run(inputs) {
|
|||||||
/* istanbul ignore if */
|
/* istanbul ignore if */
|
||||||
if (err instanceof Error && 'cause' in err) {
|
if (err instanceof Error && 'cause' in err) {
|
||||||
const innerErr = err.cause;
|
const innerErr = err.cause;
|
||||||
core.info(mute(innerErr instanceof Error ? innerErr.toString() : `${innerErr}`));
|
core.info(style.mute(innerErr instanceof Error ? innerErr.toString() : `${innerErr}`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -80100,53 +80143,40 @@ async function run(inputs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
exports.run = run;
|
exports.run = run;
|
||||||
const createAttestation = async (subject, predicate, opts) => {
|
// Log details about the attestation to the GitHub Actions run
|
||||||
// Sign provenance w/ Sigstore
|
const logAttestation = (attestation, sigstoreInstance) => {
|
||||||
const attestation = await (0, attest_1.attest)({
|
core.info(`Attestation created for ${attestation.subjectName}@${attestation.subjectDigest}`);
|
||||||
subjectName: subject.name,
|
const instanceName = sigstoreInstance === 'public-good' ? 'Public Good' : 'GitHub';
|
||||||
subjectDigest: subject.digest,
|
core.startGroup(style.highlight(`Attestation signed using certificate from ${instanceName} Sigstore instance`));
|
||||||
predicateType: predicate.type,
|
|
||||||
predicate: predicate.params,
|
|
||||||
sigstore: opts.sigstoreInstance,
|
|
||||||
token: opts.githubToken
|
|
||||||
});
|
|
||||||
core.info(`Attestation created for ${subject.name}@${subjectDigest(subject)}`);
|
|
||||||
const instanceName = opts.sigstoreInstance === 'public-good' ? 'Public Good' : 'GitHub';
|
|
||||||
core.startGroup(highlight(`Attestation signed using certificate from ${instanceName} Sigstore instance`));
|
|
||||||
core.info(attestation.certificate);
|
core.info(attestation.certificate);
|
||||||
core.endGroup();
|
core.endGroup();
|
||||||
if (attestation.tlogID) {
|
if (attestation.tlogID) {
|
||||||
core.info(highlight('Attestation signature uploaded to Rekor transparency log'));
|
core.info(style.highlight('Attestation signature uploaded to Rekor transparency log'));
|
||||||
core.info(`${endpoints_1.SEARCH_PUBLIC_GOOD_URL}?logIndex=${attestation.tlogID}`);
|
core.info(`${endpoints_1.SEARCH_PUBLIC_GOOD_URL}?logIndex=${attestation.tlogID}`);
|
||||||
}
|
}
|
||||||
if (attestation.attestationID) {
|
if (attestation.attestationID) {
|
||||||
core.info(highlight('Attestation uploaded to repository'));
|
core.info(style.highlight('Attestation uploaded to repository'));
|
||||||
core.info(attestationURL(attestation.attestationID));
|
core.info(attestationURL(attestation.attestationID));
|
||||||
}
|
}
|
||||||
if (opts.pushToRegistry) {
|
if (attestation.attestationDigest) {
|
||||||
const credentials = (0, oci_1.getRegistryCredentials)(subject.name);
|
core.info(style.highlight('Attestation uploaded to registry'));
|
||||||
const artifact = await (0, oci_1.attachArtifactToImage)({
|
core.info(`${attestation.subjectName}@${attestation.attestationDigest}`);
|
||||||
credentials,
|
|
||||||
imageName: subject.name,
|
|
||||||
imageDigest: subjectDigest(subject),
|
|
||||||
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
|
|
||||||
mediaType: attestation.bundle.mediaType,
|
|
||||||
annotations: {
|
|
||||||
'dev.sigstore.bundle.content': 'dsse-envelope',
|
|
||||||
'dev.sigstore.bundle.predicateType': core.getInput('predicate-type')
|
|
||||||
},
|
|
||||||
fetchOpts: { timeout: OCI_TIMEOUT, retry: OCI_RETRY }
|
|
||||||
});
|
|
||||||
core.info(highlight('Attestation uploaded to registry'));
|
|
||||||
core.info(`${subject.name}@${artifact.digest}`);
|
|
||||||
}
|
}
|
||||||
return attestation;
|
|
||||||
};
|
};
|
||||||
// Emphasis string using ANSI color codes
|
// Attach summary information to the GitHub Actions run
|
||||||
const highlight = (str) => `${COLOR_CYAN}${str}${COLOR_DEFAULT}`;
|
const logSummary = (attestations) => {
|
||||||
// De-emphasize string using ANSI color codes
|
if (attestations.length > 0) {
|
||||||
|
core.summary.addHeading(
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
const mute = (str) => `${COLOR_GRAY}${str}${COLOR_DEFAULT}`;
|
attestations.length > 1 ? 'Attestations Created' : 'Attestation Created', 3);
|
||||||
|
for (const { subjectName, subjectDigest, attestationID } of attestations) {
|
||||||
|
if (attestationID) {
|
||||||
|
core.summary.addLink(`${subjectName}@${subjectDigest}`, attestationURL(attestationID));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.summary.write();
|
||||||
|
}
|
||||||
|
};
|
||||||
const tempDir = () => {
|
const tempDir = () => {
|
||||||
const basePath = process.env['RUNNER_TEMP'];
|
const basePath = process.env['RUNNER_TEMP'];
|
||||||
/* istanbul ignore if */
|
/* istanbul ignore if */
|
||||||
@@ -80160,12 +80190,6 @@ const tempDir = () => {
|
|||||||
const chunkArray = (array, chunkSize) => {
|
const chunkArray = (array, chunkSize) => {
|
||||||
return Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, index) => array.slice(index * chunkSize, (index + 1) * chunkSize));
|
return Array.from({ length: Math.ceil(array.length / chunkSize) }, (_, index) => array.slice(index * chunkSize, (index + 1) * chunkSize));
|
||||||
};
|
};
|
||||||
// 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]}`;
|
|
||||||
};
|
|
||||||
const attestationURL = (id) => `${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/attestations/${id}`;
|
const attestationURL = (id) => `${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/attestations/${id}`;
|
||||||
|
|
||||||
|
|
||||||
@@ -80203,6 +80227,26 @@ const predicateFromInputs = (inputs) => {
|
|||||||
exports.predicateFromInputs = predicateFromInputs;
|
exports.predicateFromInputs = predicateFromInputs;
|
||||||
|
|
||||||
|
|
||||||
|
/***/ }),
|
||||||
|
|
||||||
|
/***/ 41583:
|
||||||
|
/***/ ((__unused_webpack_module, exports) => {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
||||||
|
exports.mute = exports.highlight = void 0;
|
||||||
|
const COLOR_CYAN = '\x1B[36m';
|
||||||
|
const COLOR_GRAY = '\x1B[38;5;244m';
|
||||||
|
const COLOR_DEFAULT = '\x1B[39m';
|
||||||
|
// Emphasis string using ANSI color codes
|
||||||
|
const highlight = (str) => `${COLOR_CYAN}${str}${COLOR_DEFAULT}`;
|
||||||
|
exports.highlight = highlight;
|
||||||
|
// De-emphasize string using ANSI color codes
|
||||||
|
const mute = (str) => `${COLOR_GRAY}${str}${COLOR_DEFAULT}`;
|
||||||
|
exports.mute = mute;
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|
||||||
/***/ 95206:
|
/***/ 95206:
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { Attestation, Predicate, Subject, attest } from '@actions/attest'
|
||||||
|
import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci'
|
||||||
|
|
||||||
|
const OCI_TIMEOUT = 2000
|
||||||
|
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,
|
||||||
|
predicate: Predicate,
|
||||||
|
opts: {
|
||||||
|
sigstoreInstance: SigstoreInstance
|
||||||
|
pushToRegistry: boolean
|
||||||
|
githubToken: string
|
||||||
|
}
|
||||||
|
): Promise<AttestResult> => {
|
||||||
|
// Sign provenance w/ Sigstore
|
||||||
|
const attestation = await attest({
|
||||||
|
subjectName: subject.name,
|
||||||
|
subjectDigest: subject.digest,
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.pushToRegistry) {
|
||||||
|
const credentials = getRegistryCredentials(subject.name)
|
||||||
|
const artifact = await attachArtifactToImage({
|
||||||
|
credentials,
|
||||||
|
imageName: subject.name,
|
||||||
|
imageDigest: subDigest,
|
||||||
|
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
|
||||||
|
mediaType: attestation.bundle.mediaType,
|
||||||
|
annotations: {
|
||||||
|
'dev.sigstore.bundle.content': 'dsse-envelope',
|
||||||
|
'dev.sigstore.bundle.predicateType': predicate.type
|
||||||
|
},
|
||||||
|
fetchOpts: { timeout: OCI_TIMEOUT, retry: OCI_RETRY }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add the attestation's digest to the result
|
||||||
|
result.attestationDigest = artifact.digest
|
||||||
|
}
|
||||||
|
|
||||||
|
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]}`
|
||||||
|
}
|
||||||
+48
-89
@@ -1,30 +1,20 @@
|
|||||||
import { Attestation, Predicate, Subject, attest } from '@actions/attest'
|
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci'
|
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
|
import { AttestResult, SigstoreInstance, createAttestation } from './attest'
|
||||||
import { SEARCH_PUBLIC_GOOD_URL } from './endpoints'
|
import { SEARCH_PUBLIC_GOOD_URL } from './endpoints'
|
||||||
import { PredicateInputs, predicateFromInputs } from './predicate'
|
import { PredicateInputs, predicateFromInputs } from './predicate'
|
||||||
|
import * as style from './style'
|
||||||
import { SubjectInputs, subjectFromInputs } from './subject'
|
import { SubjectInputs, subjectFromInputs } from './subject'
|
||||||
|
|
||||||
type SigstoreInstance = 'public-good' | 'github'
|
|
||||||
type AttestedSubject = { subject: Subject; attestationID: string }
|
|
||||||
|
|
||||||
const COLOR_CYAN = '\x1B[36m'
|
|
||||||
const COLOR_GRAY = '\x1B[38;5;244m'
|
|
||||||
const COLOR_DEFAULT = '\x1B[39m'
|
|
||||||
const ATTESTATION_FILE_NAME = 'attestation.jsonl'
|
const ATTESTATION_FILE_NAME = 'attestation.jsonl'
|
||||||
|
|
||||||
const OCI_TIMEOUT = 2000
|
|
||||||
const OCI_RETRY = 3
|
|
||||||
|
|
||||||
export type RunInputs = SubjectInputs &
|
export type RunInputs = SubjectInputs &
|
||||||
PredicateInputs & {
|
PredicateInputs & {
|
||||||
pushToRegistry: boolean
|
pushToRegistry: boolean
|
||||||
githubToken: string
|
githubToken: string
|
||||||
// undocumented
|
|
||||||
privateSigning: boolean
|
privateSigning: boolean
|
||||||
batchSize: number
|
batchSize: number
|
||||||
batchDelay: number
|
batchDelay: number
|
||||||
@@ -55,7 +45,7 @@ export async function run(inputs: RunInputs): Promise<void> {
|
|||||||
: 'github'
|
: 'github'
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const atts: AttestedSubject[] = []
|
const atts: AttestResult[] = []
|
||||||
if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) {
|
if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'missing "id-token" permission. Please add "permissions: id-token: write" to your workflow.'
|
'missing "id-token" permission. Please add "permissions: id-token: write" to your workflow.'
|
||||||
@@ -67,7 +57,9 @@ export async function run(inputs: RunInputs): Promise<void> {
|
|||||||
downcaseName: inputs.pushToRegistry
|
downcaseName: inputs.pushToRegistry
|
||||||
})
|
})
|
||||||
const predicate = predicateFromInputs(inputs)
|
const predicate = predicateFromInputs(inputs)
|
||||||
|
|
||||||
const outputPath = path.join(tempDir(), ATTESTATION_FILE_NAME)
|
const outputPath = path.join(tempDir(), ATTESTATION_FILE_NAME)
|
||||||
|
core.setOutput('bundle-path', outputPath)
|
||||||
|
|
||||||
const subjectChunks = chunkArray(subjects, inputs.batchSize)
|
const subjectChunks = chunkArray(subjects, inputs.batchSize)
|
||||||
let chunkCount = 0
|
let chunkCount = 0
|
||||||
@@ -91,36 +83,19 @@ export async function run(inputs: RunInputs): Promise<void> {
|
|||||||
pushToRegistry: inputs.pushToRegistry,
|
pushToRegistry: inputs.pushToRegistry,
|
||||||
githubToken: inputs.githubToken
|
githubToken: inputs.githubToken
|
||||||
})
|
})
|
||||||
|
atts.push(att)
|
||||||
|
|
||||||
|
logAttestation(att, sigstoreInstance)
|
||||||
|
|
||||||
// Write attestation bundle to output file
|
// Write attestation bundle to output file
|
||||||
fs.writeFileSync(outputPath, JSON.stringify(att.bundle) + os.EOL, {
|
fs.writeFileSync(outputPath, JSON.stringify(att.bundle) + os.EOL, {
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
flag: 'a'
|
flag: 'a'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (att.attestationID) {
|
|
||||||
atts.push({ subject, attestationID: att.attestationID })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (atts.length > 0) {
|
logSummary(atts)
|
||||||
core.summary.addHeading(
|
|
||||||
/* istanbul ignore next */
|
|
||||||
atts.length > 1 ? 'Attestations Created' : 'Attestation Created',
|
|
||||||
3
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const { subject, attestationID } of atts) {
|
|
||||||
core.summary.addLink(
|
|
||||||
`${subject.name}@${subjectDigest(subject)}`,
|
|
||||||
attestationURL(attestationID)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
core.summary.write()
|
|
||||||
}
|
|
||||||
|
|
||||||
core.setOutput('bundle-path', outputPath)
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Fail the workflow run if an error occurs
|
// Fail the workflow run if an error occurs
|
||||||
core.setFailed(
|
core.setFailed(
|
||||||
@@ -132,7 +107,9 @@ export async function run(inputs: RunInputs): Promise<void> {
|
|||||||
if (err instanceof Error && 'cause' in err) {
|
if (err instanceof Error && 'cause' in err) {
|
||||||
const innerErr = err.cause
|
const innerErr = err.cause
|
||||||
core.info(
|
core.info(
|
||||||
mute(innerErr instanceof Error ? innerErr.toString() : `${innerErr}`)
|
style.mute(
|
||||||
|
innerErr instanceof Error ? innerErr.toString() : `${innerErr}`
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -140,31 +117,19 @@ export async function run(inputs: RunInputs): Promise<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createAttestation = async (
|
// Log details about the attestation to the GitHub Actions run
|
||||||
subject: Subject,
|
const logAttestation = (
|
||||||
predicate: Predicate,
|
attestation: AttestResult,
|
||||||
opts: {
|
|
||||||
sigstoreInstance: SigstoreInstance
|
sigstoreInstance: SigstoreInstance
|
||||||
pushToRegistry: boolean
|
): void => {
|
||||||
githubToken: string
|
core.info(
|
||||||
}
|
`Attestation created for ${attestation.subjectName}@${attestation.subjectDigest}`
|
||||||
): Promise<Attestation> => {
|
)
|
||||||
// Sign provenance w/ Sigstore
|
|
||||||
const attestation = await attest({
|
|
||||||
subjectName: subject.name,
|
|
||||||
subjectDigest: subject.digest,
|
|
||||||
predicateType: predicate.type,
|
|
||||||
predicate: predicate.params,
|
|
||||||
sigstore: opts.sigstoreInstance,
|
|
||||||
token: opts.githubToken
|
|
||||||
})
|
|
||||||
|
|
||||||
core.info(`Attestation created for ${subject.name}@${subjectDigest(subject)}`)
|
|
||||||
|
|
||||||
const instanceName =
|
const instanceName =
|
||||||
opts.sigstoreInstance === 'public-good' ? 'Public Good' : 'GitHub'
|
sigstoreInstance === 'public-good' ? 'Public Good' : 'GitHub'
|
||||||
core.startGroup(
|
core.startGroup(
|
||||||
highlight(
|
style.highlight(
|
||||||
`Attestation signed using certificate from ${instanceName} Sigstore instance`
|
`Attestation signed using certificate from ${instanceName} Sigstore instance`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -173,43 +138,44 @@ const createAttestation = async (
|
|||||||
|
|
||||||
if (attestation.tlogID) {
|
if (attestation.tlogID) {
|
||||||
core.info(
|
core.info(
|
||||||
highlight('Attestation signature uploaded to Rekor transparency log')
|
style.highlight(
|
||||||
|
'Attestation signature uploaded to Rekor transparency log'
|
||||||
|
)
|
||||||
)
|
)
|
||||||
core.info(`${SEARCH_PUBLIC_GOOD_URL}?logIndex=${attestation.tlogID}`)
|
core.info(`${SEARCH_PUBLIC_GOOD_URL}?logIndex=${attestation.tlogID}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attestation.attestationID) {
|
if (attestation.attestationID) {
|
||||||
core.info(highlight('Attestation uploaded to repository'))
|
core.info(style.highlight('Attestation uploaded to repository'))
|
||||||
core.info(attestationURL(attestation.attestationID))
|
core.info(attestationURL(attestation.attestationID))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts.pushToRegistry) {
|
if (attestation.attestationDigest) {
|
||||||
const credentials = getRegistryCredentials(subject.name)
|
core.info(style.highlight('Attestation uploaded to registry'))
|
||||||
const artifact = await attachArtifactToImage({
|
core.info(`${attestation.subjectName}@${attestation.attestationDigest}`)
|
||||||
credentials,
|
}
|
||||||
imageName: subject.name,
|
|
||||||
imageDigest: subjectDigest(subject),
|
|
||||||
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
|
|
||||||
mediaType: attestation.bundle.mediaType,
|
|
||||||
annotations: {
|
|
||||||
'dev.sigstore.bundle.content': 'dsse-envelope',
|
|
||||||
'dev.sigstore.bundle.predicateType': core.getInput('predicate-type')
|
|
||||||
},
|
|
||||||
fetchOpts: { timeout: OCI_TIMEOUT, retry: OCI_RETRY }
|
|
||||||
})
|
|
||||||
core.info(highlight('Attestation uploaded to registry'))
|
|
||||||
core.info(`${subject.name}@${artifact.digest}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return attestation
|
// Attach summary information to the GitHub Actions run
|
||||||
}
|
const logSummary = (attestations: AttestResult[]): void => {
|
||||||
|
if (attestations.length > 0) {
|
||||||
// Emphasis string using ANSI color codes
|
core.summary.addHeading(
|
||||||
const highlight = (str: string): string => `${COLOR_CYAN}${str}${COLOR_DEFAULT}`
|
|
||||||
|
|
||||||
// De-emphasize string using ANSI color codes
|
|
||||||
/* istanbul ignore next */
|
/* istanbul ignore next */
|
||||||
const mute = (str: string): string => `${COLOR_GRAY}${str}${COLOR_DEFAULT}`
|
attestations.length > 1 ? 'Attestations Created' : 'Attestation Created',
|
||||||
|
3
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const { subjectName, subjectDigest, attestationID } of attestations) {
|
||||||
|
if (attestationID) {
|
||||||
|
core.summary.addLink(
|
||||||
|
`${subjectName}@${subjectDigest}`,
|
||||||
|
attestationURL(attestationID)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
core.summary.write()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tempDir = (): string => {
|
const tempDir = (): string => {
|
||||||
const basePath = process.env['RUNNER_TEMP']
|
const basePath = process.env['RUNNER_TEMP']
|
||||||
@@ -231,12 +197,5 @@ const chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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]}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const attestationURL = (id: string): string =>
|
const attestationURL = (id: string): string =>
|
||||||
`${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/attestations/${id}`
|
`${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/attestations/${id}`
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
const COLOR_CYAN = '\x1B[36m'
|
||||||
|
const COLOR_GRAY = '\x1B[38;5;244m'
|
||||||
|
const COLOR_DEFAULT = '\x1B[39m'
|
||||||
|
|
||||||
|
// Emphasis string using ANSI color codes
|
||||||
|
export const highlight = (str: string): string =>
|
||||||
|
`${COLOR_CYAN}${str}${COLOR_DEFAULT}`
|
||||||
|
|
||||||
|
// De-emphasize string using ANSI color codes
|
||||||
|
export const mute = (str: string): string =>
|
||||||
|
`${COLOR_GRAY}${str}${COLOR_DEFAULT}`
|
||||||
Reference in New Issue
Block a user