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
+45 -3
View File
@@ -9,6 +9,7 @@ import * as core from '@actions/core'
import * as github from '@actions/github'
import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock'
import * as oci from '@sigstore/oci'
import * as attest from '@actions/attest'
import fs from 'fs/promises'
import nock from 'nock'
import os from 'os'
@@ -19,6 +20,7 @@ import * as main from '../src/main'
// Mock the GitHub Actions core library
const infoMock = jest.spyOn(core, 'info')
const warningMock = jest.spyOn(core, 'warning')
const startGroupMock = jest.spyOn(core, 'startGroup')
const setOutputMock = jest.spyOn(core, 'setOutput')
const setFailedMock = jest.spyOn(core, 'setFailed')
@@ -45,6 +47,7 @@ const defaultInputs: main.RunInputs = {
subjectPath: '',
subjectChecksums: '',
pushToRegistry: false,
createStorageRecord: true,
showSummary: true,
githubToken: '',
privateSigning: false
@@ -66,13 +69,14 @@ describe('action', () => {
'base64'
)}.}`
const subjectName = 'registry/foo/bar'
const subjectName = 'ghcr.io/registry/foo/bar'
const subjectDigest =
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
const predicate = '{}'
const predicateType = 'https://in-toto.io/attestation/release/v0.1'
const attestationID = '1234567890'
const storageRecordID = 987654321
beforeEach(() => {
jest.clearAllMocks()
@@ -82,14 +86,21 @@ describe('action', () => {
.query({ audience: 'sigstore' })
.reply(200, { value: oidcToken })
mockAgent
.get('https://api.github.com')
const pool = mockAgent.get('https://api.github.com')
pool
.intercept({
path: /^\/repos\/.*\/.*\/attestations$/,
method: 'post'
})
.reply(201, { id: attestationID })
pool
.intercept({
path: /^\/orgs\/.*\/artifacts\/metadata\/storage-record$/,
method: 'post'
})
.reply(200, { storage_records: [{ id: storageRecordID }] })
process.env = {
...originalEnv,
ACTIONS_ID_TOKEN_REQUEST_URL: tokenURL,
@@ -263,6 +274,7 @@ describe('action', () => {
expect(setFailedMock).not.toHaveBeenCalled()
expect(getRegCredsSpy).toHaveBeenCalledWith(subjectName)
expect(attachArtifactSpy).toHaveBeenCalled()
expect(warningMock).not.toHaveBeenCalled()
expect(infoMock).toHaveBeenNthCalledWith(
1,
expect.stringMatching(
@@ -293,6 +305,14 @@ describe('action', () => {
6,
expect.stringMatching(attestationID)
)
expect(infoMock).toHaveBeenNthCalledWith(
9,
expect.stringMatching('Storage record created')
)
expect(infoMock).toHaveBeenNthCalledWith(
10,
expect.stringMatching('Storage record IDs: 987654321')
)
expect(setOutputMock).toHaveBeenNthCalledWith(
1,
'bundle-path',
@@ -308,8 +328,30 @@ describe('action', () => {
'attestation-url',
expect.stringContaining(`foo/bar/attestations/${attestationID}`)
)
expect(setOutputMock).toHaveBeenNthCalledWith(
4,
'storage-record-ids',
expect.stringMatching(storageRecordID.toString())
)
expect(setFailedMock).not.toHaveBeenCalled()
})
it('catches error when storage record creation fails and continues', async () => {
// Mock the createStorageRecord function and throw an error
const createStorageRecordSpy = jest.spyOn(attest, 'createStorageRecord')
createStorageRecordSpy.mockRejectedValueOnce(
new Error('Failed to persist storage record: Not Found')
)
await main.run(inputs)
expect(runMock).toHaveReturned()
expect(setFailedMock).not.toHaveBeenCalled()
expect(warningMock).toHaveBeenNthCalledWith(
1,
expect.stringMatching('Failed to create storage record')
)
})
})
describe('when the subject count is greater than 1', () => {