Create Artifact Metadata Storage Record on registry push (#313)

* first pass at creating storage record

Signed-off-by: Meredith Lancaster <[email protected]>

* include storage record param in action config

Signed-off-by: Meredith Lancaster <[email protected]>

* use latest actions/attest version

Signed-off-by: Meredith Lancaster <[email protected]>

* update storage record params

Signed-off-by: Meredith Lancaster <[email protected]>

* include storage record id in result

Signed-off-by: Meredith Lancaster <[email protected]>

* regenerate dist

Signed-off-by: Meredith Lancaster <[email protected]>

* add documentation on storage records

Signed-off-by: Meredith Lancaster <[email protected]>

* log storage record creation

Signed-off-by: Meredith Lancaster <[email protected]>

* add storage record output

Signed-off-by: Meredith Lancaster <[email protected]>

* add new param

Signed-off-by: Meredith Lancaster <[email protected]>

* add storage record id output

Signed-off-by: Meredith Lancaster <[email protected]>

* fix linter errors

Signed-off-by: Meredith Lancaster <[email protected]>

* return all storage record ids

Signed-off-by: Meredith Lancaster <[email protected]>

* bump minor version

Signed-off-by: Meredith Lancaster <[email protected]>

* use expect string match function

Signed-off-by: Meredith Lancaster <[email protected]>

* add try catch block for storage record creation

Signed-off-by: Meredith Lancaster <[email protected]>

* fix table column spacing

Signed-off-by: Meredith Lancaster <[email protected]>

* check for protocol

Signed-off-by: Meredith Lancaster <[email protected]>

* check for artifact url protocol

Signed-off-by: Meredith Lancaster <[email protected]>

* only fill registry_url for now

Signed-off-by: Meredith Lancaster <[email protected]>

* cleanup protocol handling

Signed-off-by: Meredith Lancaster <[email protected]>

* regenerate dist

Signed-off-by: Meredith Lancaster <[email protected]>

* handle subject name correctly

Signed-off-by: Meredith Lancaster <[email protected]>

* move test

Signed-off-by: Meredith Lancaster <[email protected]>

* add back assert statements

Signed-off-by: Meredith Lancaster <[email protected]>

* add back output assert statements

Signed-off-by: Meredith Lancaster <[email protected]>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <[email protected]>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <[email protected]>

* Apply suggestion from @Copilot

Co-authored-by: Copilot <[email protected]>

* use url for subject name parsing

Signed-off-by: Meredith Lancaster <[email protected]>

* add missing test setpu

Signed-off-by: Meredith Lancaster <[email protected]>

* fix storage record fail test

Signed-off-by: Meredith Lancaster <[email protected]>

* regenerate dist

Signed-off-by: Meredith Lancaster <[email protected]>

---------

Signed-off-by: Meredith Lancaster <[email protected]>
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Meredith Lancaster
2025-12-18 11:30:45 -08:00
committed by GitHub
co-authored by Copilot
parent 0512723b04
commit 7667f588f2
9 changed files with 344 additions and 25 deletions
+62 -2
View File
@@ -1,6 +1,13 @@
import { Attestation, Predicate, Subject, attest } from '@actions/attest'
import {
Attestation,
Predicate,
Subject,
attest,
createStorageRecord
} from '@actions/attest'
import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci'
import { formatSubjectDigest } from './subject'
import * as core from '@actions/core'
const OCI_TIMEOUT = 30000
const OCI_RETRY = 3
@@ -8,6 +15,7 @@ const OCI_RETRY = 3
export type SigstoreInstance = 'public-good' | 'github'
export type AttestResult = Attestation & {
attestationDigest?: string
storageRecordIds?: number[]
}
export const createAttestation = async (
@@ -16,6 +24,7 @@ export const createAttestation = async (
opts: {
sigstoreInstance: SigstoreInstance
pushToRegistry: boolean
createStorageRecord: boolean
githubToken: string
}
): Promise<AttestResult> => {
@@ -33,10 +42,11 @@ export const createAttestation = async (
if (subjects.length === 1 && opts.pushToRegistry) {
const subject = subjects[0]
const credentials = getRegistryCredentials(subject.name)
const subjectDigest = formatSubjectDigest(subject)
const artifact = await attachArtifactToImage({
credentials,
imageName: subject.name,
imageDigest: formatSubjectDigest(subject),
imageDigest: subjectDigest,
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
mediaType: attestation.bundle.mediaType,
annotations: {
@@ -48,7 +58,57 @@ export const createAttestation = async (
// 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 registryUrl = getRegistryURL(subject.name)
const artifactOpts = {
name: subject.name,
digest: subjectDigest
}
const packageRegistryOpts = {
registryUrl
}
const records = await createStorageRecord(
artifactOpts,
packageRegistryOpts,
opts.githubToken
)
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'
)
}
}
}
return result
}
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
}
+1
View File
@@ -13,6 +13,7 @@ const inputs: RunInputs = {
predicate: core.getInput('predicate'),
predicatePath: core.getInput('predicate-path'),
pushToRegistry: core.getBooleanInput('push-to-registry'),
createStorageRecord: core.getBooleanInput('create-storage-record'),
showSummary: core.getBooleanInput('show-summary'),
githubToken: core.getInput('github-token'),
// undocumented -- not part of public interface
+10
View File
@@ -21,6 +21,7 @@ const ATTESTATION_PATHS_FILE_NAME = 'created_attestation_paths.txt'
export type RunInputs = SubjectInputs &
PredicateInputs & {
pushToRegistry: boolean
createStorageRecord: boolean
githubToken: string
showSummary: boolean
privateSigning: boolean
@@ -69,6 +70,7 @@ export async function run(inputs: RunInputs): Promise<void> {
const att = await createAttestation(subjects, predicate, {
sigstoreInstance,
pushToRegistry: inputs.pushToRegistry,
createStorageRecord: inputs.createStorageRecord,
githubToken: inputs.githubToken
})
@@ -100,6 +102,9 @@ export async function run(inputs: RunInputs): Promise<void> {
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) {
@@ -169,6 +174,11 @@ const logAttestation = (
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