centralize collection of action inputs (#72)

Signed-off-by: Brian DeHamer <bdehamer@github.com>
This commit is contained in:
Brian DeHamer
2024-05-24 11:01:44 -07:00
committed by GitHub
parent 074a7714de
commit 3ff4eb4c69
9 changed files with 381 additions and 316 deletions
+22 -2
View File
@@ -1,7 +1,27 @@
/**
* The entrypoint for the action.
*/
import { run } from './main'
import * as core from '@actions/core'
import { run, RunInputs } from './main'
const DEFAULT_BATCH_SIZE = 50
const DEFAULT_BATCH_DELAY = 5000
const inputs: RunInputs = {
subjectPath: core.getInput('subject-path'),
subjectName: core.getInput('subject-name'),
subjectDigest: core.getInput('subject-digest'),
predicateType: core.getInput('predicate-type'),
predicate: core.getInput('predicate'),
predicatePath: core.getInput('predicate-path'),
pushToRegistry: core.getBooleanInput('push-to-registry'),
githubToken: core.getInput('github-token'),
// undocumented -- not part of public interface
privateSigning: core.getBooleanInput('private-signing'),
// internal only
batchSize: DEFAULT_BATCH_SIZE,
batchDelay: DEFAULT_BATCH_DELAY
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
run()
run(inputs)
+35 -27
View File
@@ -6,8 +6,8 @@ import fs from 'fs'
import os from 'os'
import path from 'path'
import { SEARCH_PUBLIC_GOOD_URL } from './endpoints'
import { predicateFromInputs } from './predicate'
import { subjectFromInputs } from './subject'
import { PredicateInputs, predicateFromInputs } from './predicate'
import { SubjectInputs, subjectFromInputs } from './subject'
type SigstoreInstance = 'public-good' | 'github'
type AttestedSubject = { subject: Subject; attestationID: string }
@@ -17,12 +17,19 @@ const COLOR_GRAY = '\x1B[38;5;244m'
const COLOR_DEFAULT = '\x1B[39m'
const ATTESTATION_FILE_NAME = 'attestation.jsonl'
const DEFAULT_BATCH_SIZE = 50
const DEFAULT_BATCH_DELAY = 5000
const OCI_TIMEOUT = 2000
const OCI_RETRY = 3
export type RunInputs = SubjectInputs &
PredicateInputs & {
pushToRegistry: boolean
githubToken: string
// undocumented
privateSigning: boolean
batchSize: number
batchDelay: number
}
/* istanbul ignore next */
const logHandler = (level: string, ...args: unknown[]): void => {
// Send any HTTP-related log events to the GitHub Actions debug log
@@ -35,7 +42,7 @@ const logHandler = (level: string, ...args: unknown[]): void => {
* The main function for the action.
* @returns {Promise<void>} Resolves when the action is complete.
*/
export async function run(): Promise<void> {
export async function run(inputs: RunInputs): Promise<void> {
process.on('log', logHandler)
// Provenance visibility will be public ONLY if we can confirm that the
@@ -43,7 +50,7 @@ export async function run(): Promise<void> {
// Otherwise, it will be private.
const sigstoreInstance: SigstoreInstance =
github.context.payload.repository?.visibility === 'public' &&
core.getInput('private-signing') !== 'true'
!inputs.privateSigning
? 'public-good'
: 'github'
@@ -55,24 +62,21 @@ export async function run(): Promise<void> {
)
}
const subjects = await subjectFromInputs()
const predicate = predicateFromInputs()
const subjects = await subjectFromInputs({
...inputs,
downcaseName: inputs.pushToRegistry
})
const predicate = predicateFromInputs(inputs)
const outputPath = path.join(tempDir(), ATTESTATION_FILE_NAME)
// Batch size and delay for rate limiting
const batchSize =
parseInt(core.getInput('batch-size')) || DEFAULT_BATCH_SIZE
const batchDelay =
parseInt(core.getInput('batch-delay')) || DEFAULT_BATCH_DELAY
const subjectChunks = chunkArray(subjects, batchSize)
const subjectChunks = chunkArray(subjects, inputs.batchSize)
let chunkCount = 0
// Generate attestations for each subject serially, working in batches
for (const subjectChunk of subjectChunks) {
// Delay between batches (only when chunkCount > 0)
if (chunkCount++) {
await new Promise(resolve => setTimeout(resolve, batchDelay))
await new Promise(resolve => setTimeout(resolve, inputs.batchDelay))
}
if (subjectChunks.length > 1) {
@@ -82,11 +86,11 @@ export async function run(): Promise<void> {
}
for (const subject of subjectChunk) {
const att = await createAttestation(
subject,
predicate,
sigstoreInstance
)
const att = await createAttestation(subject, predicate, {
sigstoreInstance,
pushToRegistry: inputs.pushToRegistry,
githubToken: inputs.githubToken
})
// Write attestation bundle to output file
fs.writeFileSync(outputPath, JSON.stringify(att.bundle) + os.EOL, {
@@ -139,7 +143,11 @@ export async function run(): Promise<void> {
const createAttestation = async (
subject: Subject,
predicate: Predicate,
sigstoreInstance: SigstoreInstance
opts: {
sigstoreInstance: SigstoreInstance
pushToRegistry: boolean
githubToken: string
}
): Promise<Attestation> => {
// Sign provenance w/ Sigstore
const attestation = await attest({
@@ -147,14 +155,14 @@ const createAttestation = async (
subjectDigest: subject.digest,
predicateType: predicate.type,
predicate: predicate.params,
sigstore: sigstoreInstance,
token: core.getInput('github-token')
sigstore: opts.sigstoreInstance,
token: opts.githubToken
})
core.info(`Attestation created for ${subject.name}@${subjectDigest(subject)}`)
const instanceName =
sigstoreInstance === 'public-good' ? 'Public Good' : 'GitHub'
opts.sigstoreInstance === 'public-good' ? 'Public Good' : 'GitHub'
core.startGroup(
highlight(
`Attestation signed using certificate from ${instanceName} Sigstore instance`
@@ -175,7 +183,7 @@ const createAttestation = async (
core.info(attestationURL(attestation.attestationID))
}
if (core.getBooleanInput('push-to-registry', { required: false })) {
if (opts.pushToRegistry) {
const credentials = getRegistryCredentials(subject.name)
const artifact = await attachArtifactToImage({
credentials,
+14 -8
View File
@@ -1,26 +1,32 @@
import * as core from '@actions/core'
import fs from 'fs'
import type { Predicate } from '@actions/attest'
export type PredicateInputs = {
predicateType: string
predicate: string
predicatePath: string
}
// Returns the predicate specified by the action's inputs. The predicate value
// may be specified as a path to a file or as a string.
export const predicateFromInputs = (): Predicate => {
const predicateType = core.getInput('predicate-type', { required: true })
const predicateStr = core.getInput('predicate', { required: false })
const predicatePath = core.getInput('predicate-path', { required: false })
export const predicateFromInputs = (inputs: PredicateInputs): Predicate => {
const { predicateType, predicate, predicatePath } = inputs
if (!predicatePath && !predicateStr) {
if (!predicateType) {
throw new Error('predicate-type must be provided')
}
if (!predicatePath && !predicate) {
throw new Error('One of predicate-path or predicate must be provided')
}
if (predicatePath && predicateStr) {
if (predicatePath && predicate) {
throw new Error('Only one of predicate-path or predicate may be provided')
}
const params = predicatePath
? fs.readFileSync(predicatePath, 'utf-8')
: predicateStr
: predicate
return { type: predicateType, params: JSON.parse(params) }
}
+11 -9
View File
@@ -1,4 +1,3 @@
import * as core from '@actions/core'
import * as glob from '@actions/glob'
import crypto from 'crypto'
import { parse } from 'csv-parse/sync'
@@ -9,17 +8,20 @@ import type { Subject } from '@actions/attest'
const DIGEST_ALGORITHM = 'sha256'
export type SubjectInputs = {
subjectPath: string
subjectName: string
subjectDigest: string
downcaseName?: boolean
}
// 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
// file's digest is calculated and returned along with the subject's name. If a
// digest is provided, the name must also be provided.
export const subjectFromInputs = async (): Promise<Subject[]> => {
const subjectPath = core.getInput('subject-path', { required: false })
const subjectDigest = core.getInput('subject-digest', { required: false })
const subjectName = core.getInput('subject-name', { required: false })
const pushToRegistry = core.getBooleanInput('push-to-registry', {
required: false
})
export const subjectFromInputs = async (
inputs: SubjectInputs
): Promise<Subject[]> => {
const { subjectPath, subjectDigest, subjectName, downcaseName } = inputs
if (!subjectPath && !subjectDigest) {
throw new Error('One of subject-path or subject-digest must be provided')
@@ -37,7 +39,7 @@ export const subjectFromInputs = async (): Promise<Subject[]> => {
// If push-to-registry is enabled, ensure the subject name is lowercase
// to conform to OCI image naming conventions
const name = pushToRegistry ? subjectName.toLowerCase() : subjectName
const name = downcaseName ? subjectName.toLowerCase() : subjectName
if (subjectPath) {
return await getSubjectFromPath(subjectPath, name)