Files
attest/src/attest.ts
T

137 lines
3.9 KiB
TypeScript
Raw Normal View History

import {
Attestation,
Predicate,
Subject,
attest,
createStorageRecord
} from '@actions/attest'
import * as core from '@actions/core'
import * as github from '@actions/github'
2026-02-13 14:33:38 -08:00
import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci'
import { formatSubjectDigest } from './subject'
2024-05-28 11:00:03 -07:00
2024-06-17 06:51:46 -07:00
const OCI_TIMEOUT = 30000
2024-05-28 11:00:03 -07:00
const OCI_RETRY = 3
export type SigstoreInstance = 'public-good' | 'github'
export type AttestResult = Attestation & {
attestationDigest?: string
storageRecordIds?: number[]
2024-05-28 11:00:03 -07:00
}
export const createAttestation = async (
2024-11-05 09:16:07 -08:00
subjects: Subject[],
2024-05-28 11:00:03 -07:00
predicate: Predicate,
opts: {
sigstoreInstance: SigstoreInstance
pushToRegistry: boolean
createStorageRecord: boolean
2024-05-28 11:00:03 -07:00
githubToken: string
}
): Promise<AttestResult> => {
// Sign provenance w/ Sigstore
const attestation = await attest({
2024-11-05 09:16:07 -08:00
subjects,
2024-05-28 11:00:03 -07:00
predicateType: predicate.type,
predicate: predicate.params,
sigstore: opts.sigstoreInstance,
token: opts.githubToken
})
2024-11-05 09:16:07 -08:00
const result: AttestResult = attestation
2024-05-28 11:00:03 -07:00
2024-11-05 09:16:07 -08:00
if (subjects.length === 1 && opts.pushToRegistry) {
const subject = subjects[0]
2024-05-28 11:00:03 -07:00
const credentials = getRegistryCredentials(subject.name)
const subjectDigest = formatSubjectDigest(subject)
2024-05-28 11:00:03 -07:00
const artifact = await attachArtifactToImage({
credentials,
imageName: subject.name,
imageDigest: subjectDigest,
2024-05-28 11:00:03 -07:00
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
// Because creating a storage record requires the 'artifact-metadata:write'
// permission, we wrap this in a try/catch to avoid failing the entire
// attestation process if the token does not have the correct permissions.
if (opts.createStorageRecord) {
try {
const token = opts.githubToken
const isOrg = await repoOwnerIsOrg(token)
if (!isOrg) {
// The Artifact Metadata Storage Record API is only available to
// organizations. So if the repo owner is not an organization,
// storage record creation should not be attempted.
return result
}
const registryUrl = getRegistryURL(subject.name)
const artifactOpts = {
name: subject.name,
digest: subjectDigest
}
const packageRegistryOpts = {
registryUrl
}
const records = await createStorageRecord(
artifactOpts,
packageRegistryOpts,
token
)
if (!records || records.length === 0) {
core.warning('No storage records were created.')
}
result.storageRecordIds = records
} catch (error) {
core.warning(`Failed to create storage record: ${error}`)
core.warning(
'Please check that the "artifact-metadata:write" permission has been included'
)
}
}
2024-05-28 11:00:03 -07:00
}
return result
}
// Call the GET /repos/{owner}/{repo} endpoint to determine if the repo
// owner is an organization. This is used to determine if storage
// record creation should be attempted.
export const repoOwnerIsOrg = async (githubToken: string): Promise<boolean> => {
const octokit = github.getOctokit(githubToken)
const { data: repo } = await octokit.rest.repos.get({
owner: github.context.repo.owner,
repo: github.context.repo.repo
})
return repo.owner?.type === 'Organization'
}
function getRegistryURL(subjectName: string): string {
let url: URL
try {
url = new URL(subjectName)
} catch {
url = new URL(`https://${subjectName}`)
}
if (url.protocol !== 'https:') {
throw new Error(
`Unsupported protocol ${url.protocol} in subject name ${subjectName}`
)
}
return url.origin
}