7667f588f2
* first pass at creating storage record Signed-off-by: Meredith Lancaster <malancas@github.com> * include storage record param in action config Signed-off-by: Meredith Lancaster <malancas@github.com> * use latest actions/attest version Signed-off-by: Meredith Lancaster <malancas@github.com> * update storage record params Signed-off-by: Meredith Lancaster <malancas@github.com> * include storage record id in result Signed-off-by: Meredith Lancaster <malancas@github.com> * regenerate dist Signed-off-by: Meredith Lancaster <malancas@github.com> * add documentation on storage records Signed-off-by: Meredith Lancaster <malancas@github.com> * log storage record creation Signed-off-by: Meredith Lancaster <malancas@github.com> * add storage record output Signed-off-by: Meredith Lancaster <malancas@github.com> * add new param Signed-off-by: Meredith Lancaster <malancas@github.com> * add storage record id output Signed-off-by: Meredith Lancaster <malancas@github.com> * fix linter errors Signed-off-by: Meredith Lancaster <malancas@github.com> * return all storage record ids Signed-off-by: Meredith Lancaster <malancas@github.com> * bump minor version Signed-off-by: Meredith Lancaster <malancas@github.com> * use expect string match function Signed-off-by: Meredith Lancaster <malancas@github.com> * add try catch block for storage record creation Signed-off-by: Meredith Lancaster <malancas@github.com> * fix table column spacing Signed-off-by: Meredith Lancaster <malancas@github.com> * check for protocol Signed-off-by: Meredith Lancaster <malancas@github.com> * check for artifact url protocol Signed-off-by: Meredith Lancaster <malancas@github.com> * only fill registry_url for now Signed-off-by: Meredith Lancaster <malancas@github.com> * cleanup protocol handling Signed-off-by: Meredith Lancaster <malancas@github.com> * regenerate dist Signed-off-by: Meredith Lancaster <malancas@github.com> * handle subject name correctly Signed-off-by: Meredith Lancaster <malancas@github.com> * move test Signed-off-by: Meredith Lancaster <malancas@github.com> * add back assert statements Signed-off-by: Meredith Lancaster <malancas@github.com> * add back output assert statements Signed-off-by: Meredith Lancaster <malancas@github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * use url for subject name parsing Signed-off-by: Meredith Lancaster <malancas@github.com> * add missing test setpu Signed-off-by: Meredith Lancaster <malancas@github.com> * fix storage record fail test Signed-off-by: Meredith Lancaster <malancas@github.com> * regenerate dist Signed-off-by: Meredith Lancaster <malancas@github.com> --------- Signed-off-by: Meredith Lancaster <malancas@github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
210 lines
6.2 KiB
TypeScript
210 lines
6.2 KiB
TypeScript
import * as core from '@actions/core'
|
|
import * as github from '@actions/github'
|
|
import fs from 'fs'
|
|
import os from 'os'
|
|
import path from 'path'
|
|
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,
|
|
formatSubjectDigest,
|
|
subjectFromInputs
|
|
} from './subject'
|
|
|
|
import type { Subject } from '@actions/attest'
|
|
|
|
const ATTESTATION_FILE_NAME = 'attestation.json'
|
|
const ATTESTATION_PATHS_FILE_NAME = 'created_attestation_paths.txt'
|
|
|
|
export type RunInputs = SubjectInputs &
|
|
PredicateInputs & {
|
|
pushToRegistry: boolean
|
|
createStorageRecord: boolean
|
|
githubToken: string
|
|
showSummary: boolean
|
|
privateSigning: boolean
|
|
}
|
|
|
|
/* istanbul ignore next */
|
|
const logHandler = (level: string, ...args: unknown[]): void => {
|
|
// Send any HTTP-related log events to the GitHub Actions debug log
|
|
if (level === 'http') {
|
|
core.debug(args.join(' '))
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The main function for the action.
|
|
* @returns {Promise<void>} Resolves when the action is complete.
|
|
*/
|
|
export async function run(inputs: RunInputs): Promise<void> {
|
|
process.on('log', logHandler)
|
|
|
|
// Provenance visibility will be public ONLY if we can confirm that the
|
|
// repository is public AND the undocumented "private-signing" arg is NOT set.
|
|
// Otherwise, it will be private.
|
|
const sigstoreInstance: SigstoreInstance =
|
|
github.context.payload.repository?.visibility === 'public' &&
|
|
!inputs.privateSigning
|
|
? 'public-good'
|
|
: 'github'
|
|
|
|
try {
|
|
if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) {
|
|
throw new Error(
|
|
'missing "id-token" permission. Please add "permissions: id-token: write" to your workflow.'
|
|
)
|
|
}
|
|
|
|
const subjects = await subjectFromInputs({
|
|
...inputs,
|
|
downcaseName: inputs.pushToRegistry
|
|
})
|
|
const predicate = predicateFromInputs(inputs)
|
|
|
|
const outputPath = path.join(tempDir(), ATTESTATION_FILE_NAME)
|
|
core.setOutput('bundle-path', outputPath)
|
|
|
|
const att = await createAttestation(subjects, predicate, {
|
|
sigstoreInstance,
|
|
pushToRegistry: inputs.pushToRegistry,
|
|
createStorageRecord: inputs.createStorageRecord,
|
|
githubToken: inputs.githubToken
|
|
})
|
|
|
|
logAttestation(subjects, att, sigstoreInstance)
|
|
|
|
// Write attestation bundle to output file
|
|
fs.writeFileSync(outputPath, JSON.stringify(att.bundle) + os.EOL, {
|
|
encoding: 'utf-8',
|
|
flag: 'a'
|
|
})
|
|
|
|
const baseDir = process.env.RUNNER_TEMP
|
|
/* istanbul ignore else */
|
|
if (baseDir) {
|
|
const outputSummaryPath = path.join(baseDir, ATTESTATION_PATHS_FILE_NAME)
|
|
// Append the output path to the attestations paths file
|
|
fs.appendFileSync(outputSummaryPath, outputPath + os.EOL, {
|
|
encoding: 'utf-8',
|
|
flag: 'a'
|
|
})
|
|
} else {
|
|
core.warning(
|
|
'RUNNER_TEMP environment variable is not set. Cannot write attestation paths file.'
|
|
)
|
|
}
|
|
|
|
/* istanbul ignore else */
|
|
if (att.attestationID) {
|
|
core.setOutput('attestation-id', att.attestationID)
|
|
core.setOutput('attestation-url', attestationURL(att.attestationID))
|
|
}
|
|
if (att.storageRecordIds) {
|
|
core.setOutput('storage-record-ids', att.storageRecordIds.join(','))
|
|
}
|
|
|
|
/* istanbul ignore else */
|
|
if (inputs.showSummary) {
|
|
await logSummary(att)
|
|
}
|
|
} catch (err) {
|
|
// Fail the workflow run if an error occurs
|
|
core.setFailed(
|
|
err instanceof Error ? err : /* istanbul ignore next */ `${err}`
|
|
)
|
|
|
|
// Log the cause of the error if one is available
|
|
/* istanbul ignore if */
|
|
if (err instanceof Error && 'cause' in err) {
|
|
const innerErr = err.cause
|
|
core.info(
|
|
style.mute(
|
|
innerErr instanceof Error ? innerErr.toString() : `${innerErr}`
|
|
)
|
|
)
|
|
}
|
|
} finally {
|
|
process.removeListener('log', logHandler)
|
|
}
|
|
}
|
|
|
|
// Log details about the attestation to the GitHub Actions run
|
|
const logAttestation = (
|
|
subjects: Subject[],
|
|
attestation: AttestResult,
|
|
sigstoreInstance: SigstoreInstance
|
|
): void => {
|
|
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'
|
|
core.startGroup(
|
|
style.highlight(
|
|
`Attestation signed using certificate from ${instanceName} Sigstore instance`
|
|
)
|
|
)
|
|
core.info(attestation.certificate)
|
|
core.endGroup()
|
|
|
|
if (attestation.tlogID) {
|
|
core.info(
|
|
style.highlight(
|
|
'Attestation signature uploaded to Rekor transparency log'
|
|
)
|
|
)
|
|
core.info(`${SEARCH_PUBLIC_GOOD_URL}?logIndex=${attestation.tlogID}`)
|
|
}
|
|
|
|
/* istanbul ignore else */
|
|
if (attestation.attestationID) {
|
|
core.info(style.highlight('Attestation uploaded to repository'))
|
|
core.info(attestationURL(attestation.attestationID))
|
|
}
|
|
|
|
if (attestation.attestationDigest) {
|
|
core.info(style.highlight('Attestation uploaded to registry'))
|
|
core.info(`${subjects[0].name}@${attestation.attestationDigest}`)
|
|
}
|
|
|
|
if (attestation.storageRecordIds && attestation.storageRecordIds.length > 0) {
|
|
core.info(style.highlight('Storage record created'))
|
|
core.info(`Storage record IDs: ${attestation.storageRecordIds.join(',')}`)
|
|
}
|
|
}
|
|
|
|
// Attach summary information to the GitHub Actions run
|
|
const logSummary = async (attestation: AttestResult): Promise<void> => {
|
|
const { attestationID } = attestation
|
|
|
|
/* istanbul ignore else */
|
|
if (attestationID) {
|
|
const url = attestationURL(attestationID)
|
|
core.summary.addHeading('Attestation Created', 3)
|
|
core.summary.addList([`<a href="${url}">${url}</a>`])
|
|
await core.summary.write()
|
|
}
|
|
}
|
|
|
|
const tempDir = (): string => {
|
|
const basePath = process.env['RUNNER_TEMP']
|
|
|
|
/* istanbul ignore if */
|
|
if (!basePath) {
|
|
throw new Error('Missing RUNNER_TEMP environment variable')
|
|
}
|
|
|
|
return fs.mkdtempSync(path.join(basePath, path.sep))
|
|
}
|
|
|
|
const attestationURL = (id: string): string =>
|
|
`${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/attestations/${id}`
|