Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
704a5f9
first pass at creating storage record
malancas Dec 9, 2025
01e19e4
include storage record param in action config
malancas Dec 9, 2025
329d9f0
use latest actions/attest version
malancas Dec 10, 2025
786ceee
update storage record params
malancas Dec 10, 2025
91f7991
include storage record id in result
malancas Dec 10, 2025
eaf1f6d
regenerate dist
malancas Dec 10, 2025
38325ea
add documentation on storage records
malancas Dec 10, 2025
5069c12
log storage record creation
malancas Dec 12, 2025
0bad00c
add storage record output
malancas Dec 12, 2025
f3b2cf9
add new param
malancas Dec 12, 2025
cdd345d
add storage record id output
malancas Dec 12, 2025
28696a6
fix linter errors
malancas Dec 12, 2025
b980d6e
return all storage record ids
malancas Dec 15, 2025
4793f29
bump minor version
malancas Dec 15, 2025
55908d6
use expect string match function
malancas Dec 15, 2025
206e132
Merge branch 'main' into create-storage-records-on-registry-push
malancas Dec 15, 2025
c506aa6
add try catch block for storage record creation
malancas Dec 15, 2025
a232a3c
Merge branch 'create-storage-records-on-registry-push' of github.com:…
malancas Dec 15, 2025
a96a20b
fix table column spacing
malancas Dec 15, 2025
dd5b000
Merge branch 'main' into create-storage-records-on-registry-push
malancas Dec 16, 2025
335a34a
check for protocol
malancas Dec 16, 2025
2318c5e
Merge branch 'create-storage-records-on-registry-push' of github.com:…
malancas Dec 16, 2025
f792830
check for artifact url protocol
malancas Dec 16, 2025
26f61ba
only fill registry_url for now
malancas Dec 17, 2025
997dc63
cleanup protocol handling
malancas Dec 17, 2025
8fc45bd
regenerate dist
malancas Dec 17, 2025
99f1710
handle subject name correctly
malancas Dec 17, 2025
cdc507c
move test
malancas Dec 17, 2025
a0885ef
add back assert statements
malancas Dec 17, 2025
89e382e
add back output assert statements
malancas Dec 17, 2025
de64e7c
Apply suggestion from @Copilot
malancas Dec 17, 2025
7438155
Apply suggestion from @Copilot
malancas Dec 17, 2025
77cc220
Apply suggestion from @Copilot
malancas Dec 17, 2025
0b85e87
use url for subject name parsing
malancas Dec 18, 2025
5966256
add missing test setpu
malancas Dec 18, 2025
fc206fe
fix storage record fail test
malancas Dec 18, 2025
e37f493
regenerate dist
malancas Dec 18, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ attest:
permissions:
id-token: write
attestations: write
artifact-metadata: write
```

The `id-token` permission gives the action the ability to mint the OIDC token
necessary to request a Sigstore signing certificate. The `attestations`
permission is necessary to persist the attestation.
permission is necessary to persist the attestation. The `artifact-metadata`
permission is necessary to create the artifact storage record.

1. Add the following to your workflow after your artifact has been built:

Expand Down Expand Up @@ -118,6 +120,12 @@ See [action.yml](action.yml)
# the "subject-digest" parameter be specified. Defaults to false.
push-to-registry:

# Whether to create a storage record for the artifact.
# Requires that push-to-registry is set to true.
# Requires that the "subject-name" parameter specify the fully-qualified
# image name. Defaults to true.
create-storage-record:

# Whether to attach a list of generated attestations to the workflow run
# summary page. Defaults to true.
show-summary:
Expand All @@ -131,11 +139,12 @@ See [action.yml](action.yml)

<!-- markdownlint-disable MD013 -->

| Name | Description | Example |
| ----------------- | -------------------------------------------------------------- | ------------------------------------------------ |
| `attestation-id` | GitHub ID for the attestation | `123456` |
| `attestation-url` | URL for the attestation summary | `https://github.com/foo/bar/attestations/123456` |
| `bundle-path` | Absolute path to the file containing the generated attestation | `/tmp/attestation.json` |
| Name | Description | Example |
| ------------------- | -------------------------------------------------------------- | ------------------------------------------------ |
| `attestation-id` | GitHub ID for the attestation | `123456` |
| `attestation-url` | URL for the attestation summary | `https://github.com/foo/bar/attestations/123456` |
| `bundle-path` | Absolute path to the file containing the generated attestation | `/tmp/attestation.json` |
| `storage-record-ids` | GitHub IDs for the storage records | `987654` |

<!-- markdownlint-enable MD013 -->

Expand Down Expand Up @@ -269,6 +278,10 @@ fully-qualified image name (e.g. "ghcr.io/user/app" or
"acme.azurecr.io/user/app"). Do NOT include a tag as part of the image name --
the specific image being attested is identified by the supplied digest.

If the `push-to-registry` option is set to true, the Action will also
emit an Artifact Metadata Storage Record. If you do not want to emit a
storage record, set `create-storage-record` to `false`.

> **NOTE**: When pushing to Docker Hub, please use "docker.io" as the registry
> portion of the image name.

Expand All @@ -287,6 +300,7 @@ jobs:
packages: write
contents: read
attestations: write
artifact-metadata: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
Expand Down
48 changes: 45 additions & 3 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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')
Expand All @@ -45,6 +47,7 @@ const defaultInputs: main.RunInputs = {
subjectPath: '',
subjectChecksums: '',
pushToRegistry: false,
createStorageRecord: true,
showSummary: true,
githubToken: '',
privateSigning: false
Expand All @@ -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()
Expand All @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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',
Expand All @@ -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', () => {
Expand Down
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ inputs:
the "subject-digest" parameter be specified. Defaults to false.
default: false
required: false
create-storage-record:
description: >
Whether to create a storage record for the artifact.
Requires that push-to-registry is set to true. Defaults to true.
default: true
required: false
show-summary:
description: >
Whether to attach a list of generated attestations to the workflow run
Expand All @@ -71,6 +77,8 @@ outputs:
description: 'The ID of the attestation.'
attestation-url:
description: 'The URL for the attestation summary.'
storage-record-ids:
description: 'The IDs of the storage records created for the artifact.'

runs:
using: node24
Expand Down
Loading