Compare commits

..
Author SHA1 Message Date
Brian DeHamer a0e3b9618f hacking
Signed-off-by: Brian DeHamer <[email protected]>
2026-02-24 20:10:53 -08:00
Brian DeHamer 8459a46a44 wip
Signed-off-by: Brian DeHamer <[email protected]>
2026-02-24 19:10:07 -08:00
12 changed files with 91740 additions and 23667 deletions
+9 -25
View File
@@ -48,11 +48,11 @@ the inputs you provide:
<!-- markdownlint-disable MD013 --> <!-- markdownlint-disable MD013 -->
| Mode | When Used | Description | | Mode | When Used | Description |
| -------------- | ------------------------------------------------------ | ----------------------------------------------- | | -------------- | ------------------------------------------------------ | ------------------------------------------------ |
| **Provenance** | No `sbom-path` or predicate inputs | Auto-generates [SLSA build provenance][10] | | **Provenance** | No `sbom-path` or predicate inputs | Auto-generates [SLSA build provenance][10] |
| **SBOM** | `sbom-path` is provided | Creates attestation from SPDX or CycloneDX SBOM | | **SBOM** | `sbom-path` is provided | Creates attestation from SPDX or CycloneDX SBOM |
| **Custom** | `predicate-type`/`predicate`/`predicate-path` provided | User-supplied predicate | | **Custom** | `predicate-type`/`predicate`/`predicate-path` provided | User-supplied predicate |
<!-- markdownlint-enable MD013 --> <!-- markdownlint-enable MD013 -->
@@ -159,7 +159,7 @@ See [action.yml](action.yml)
<!-- markdownlint-disable MD013 --> <!-- markdownlint-disable MD013 -->
| Name | Description | Example | | Name | Description | Example |
| -------------------- | -------------------------------------------------------------- | ------------------------------------------------ | | ------------------- | -------------------------------------------------------------- | ------------------------------------------------ |
| `attestation-id` | GitHub ID for the attestation | `123456` | | `attestation-id` | GitHub ID for the attestation | `123456` |
| `attestation-url` | URL for the attestation summary | `https://github.com/foo/bar/attestations/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` | | `bundle-path` | Absolute path to the file containing the generated attestation | `/tmp/attestation.json` |
@@ -320,25 +320,9 @@ 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 -- "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. the specific image being attested is identified by the supplied digest.
#### Artifact Metadata Storage Records 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
When generating a build provenance attestation, if the `push-to-registry` option storage record, set `create-storage-record` to `false`.
is set to true, the Action will also emit an
[Artifact Metadata Storage Record](https://docs.github.com/en/rest/orgs/artifact-metadata?apiVersion=2022-11-28#create-artifact-metadata-storage-record).
Storage records enrich artifact metadata by capturing storage related details,
such as which registry an image is hosted on and whether it's marked as active.
If you do not want to emit a storage record, set `create-storage-record` to
`false`.
> **NOTE**: Storage records can only be created for artifacts built from
> [organization-owned](https://docs.github.com/en/organizations/collaborating-with-groups-in-organizations/about-organizations)
> repositories.
Artifacts associated with a storage record can be viewed by navigating to the
`Linked Artifacts` page in your organization:
`https://github.com/orgs/YOUR_ORG/artifacts` (replace `YOUR_ORG` with your
organization name).
> **NOTE**: When pushing to Docker Hub, please use "docker.io" as the registry > **NOTE**: When pushing to Docker Hub, please use "docker.io" as the registry
> portion of the image name. > portion of the image name.
-2
View File
@@ -30,7 +30,6 @@ describe('index', () => {
'subject-name': 'my-artifact', 'subject-name': 'my-artifact',
'subject-digest': '', 'subject-digest': '',
'subject-checksums': '', 'subject-checksums': '',
'subject-version': '',
'predicate-type': 'https://example.com/predicate', 'predicate-type': 'https://example.com/predicate',
predicate: '{}', predicate: '{}',
'predicate-path': '', 'predicate-path': '',
@@ -58,7 +57,6 @@ describe('index', () => {
subjectName: 'my-artifact', subjectName: 'my-artifact',
subjectDigest: '', subjectDigest: '',
subjectChecksums: '', subjectChecksums: '',
subjectVersion: '',
predicateType: 'https://example.com/predicate', predicateType: 'https://example.com/predicate',
predicate: '{}', predicate: '{}',
predicatePath: '', predicatePath: '',
+2 -20
View File
@@ -145,8 +145,7 @@ describe('createAttestation', () => {
const storageOpts = { const storageOpts = {
...defaultOpts, ...defaultOpts,
pushToRegistry: true, pushToRegistry: true,
createStorageRecord: true, createStorageRecord: true
subjectVersion: '1.2.3'
} }
it('should create storage record when enabled and owner is org', async () => { it('should create storage record when enabled and owner is org', async () => {
@@ -158,27 +157,10 @@ describe('createAttestation', () => {
storageOpts storageOpts
) )
expect(mockCreateStorageRecord).toHaveBeenCalledWith( expect(mockCreateStorageRecord).toHaveBeenCalled()
expect.objectContaining({ version: '1.2.3' }),
expect.anything(),
expect.anything()
)
expect(result.storageRecordIds).toEqual([12345]) expect(result.storageRecordIds).toEqual([12345])
}) })
it('should omit version from storage record when subjectVersion is empty', async () => {
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
const opts = { ...storageOpts, subjectVersion: '' }
await createAttestation(subjects, TEST_PREDICATE, opts)
expect(mockCreateStorageRecord).toHaveBeenCalledWith(
expect.objectContaining({ version: undefined }),
expect.anything(),
expect.anything()
)
})
it('should skip storage record when owner is User', async () => { it('should skip storage record when owner is User', async () => {
mockGetOctokit.mockReturnValue(createOctokitMock('User')) mockGetOctokit.mockReturnValue(createOctokitMock('User'))
const subjects = [TEST_SUBJECT_WITH_REGISTRY] const subjects = [TEST_SUBJECT_WITH_REGISTRY]
-1
View File
@@ -101,7 +101,6 @@ const defaultInputs: RunInputs = {
subjectChecksums: '', subjectChecksums: '',
pushToRegistry: false, pushToRegistry: false,
createStorageRecord: false, createStorageRecord: false,
subjectVersion: '',
showSummary: false, showSummary: false,
githubToken: 'test-token', githubToken: 'test-token',
privateSigning: false privateSigning: false
-5
View File
@@ -30,11 +30,6 @@ inputs:
attestation. Must specify exactly one of "subject-path", "subject-digest", attestation. Must specify exactly one of "subject-path", "subject-digest",
or "subject-checksums". or "subject-checksums".
required: false required: false
subject-version:
description: >
Version of the subject for the attestation. Only used when
"push-to-registry" and "create-storage-record" are both set to true.
required: false
sbom-path: sbom-path:
description: > description: >
Path to the JSON-formatted SBOM file (SPDX or CycloneDX) to attest. Path to the JSON-formatted SBOM file (SPDX or CycloneDX) to attest.
Generated Vendored
+297
View File
@@ -0,0 +1,297 @@
export const id = 717;
export const ids = [717];
export const modules = {
/***/ 4717:
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ "default": () => (/* binding */ pMap)
/* harmony export */ });
/* unused harmony exports pMapIterable, pMapSkip */
async function pMap(
iterable,
mapper,
{
concurrency = Number.POSITIVE_INFINITY,
stopOnError = true,
signal,
} = {},
) {
return new Promise((resolve_, reject_) => {
if (iterable[Symbol.iterator] === undefined && iterable[Symbol.asyncIterator] === undefined) {
throw new TypeError(`Expected \`input\` to be either an \`Iterable\` or \`AsyncIterable\`, got (${typeof iterable})`);
}
if (typeof mapper !== 'function') {
throw new TypeError('Mapper function is required');
}
if (!((Number.isSafeInteger(concurrency) && concurrency >= 1) || concurrency === Number.POSITIVE_INFINITY)) {
throw new TypeError(`Expected \`concurrency\` to be an integer from 1 and up or \`Infinity\`, got \`${concurrency}\` (${typeof concurrency})`);
}
const result = [];
const errors = [];
const skippedIndexesMap = new Map();
let isRejected = false;
let isResolved = false;
let isIterableDone = false;
let resolvingCount = 0;
let currentIndex = 0;
const iterator = iterable[Symbol.iterator] === undefined ? iterable[Symbol.asyncIterator]() : iterable[Symbol.iterator]();
const signalListener = () => {
reject(signal.reason);
};
const cleanup = () => {
signal?.removeEventListener('abort', signalListener);
};
const resolve = value => {
resolve_(value);
cleanup();
};
const reject = reason => {
isRejected = true;
isResolved = true;
reject_(reason);
cleanup();
};
if (signal) {
if (signal.aborted) {
reject(signal.reason);
}
signal.addEventListener('abort', signalListener, {once: true});
}
const next = async () => {
if (isResolved) {
return;
}
const nextItem = await iterator.next();
const index = currentIndex;
currentIndex++;
// Note: `iterator.next()` can be called many times in parallel.
// This can cause multiple calls to this `next()` function to
// receive a `nextItem` with `done === true`.
// The shutdown logic that rejects/resolves must be protected
// so it runs only one time as the `skippedIndex` logic is
// non-idempotent.
if (nextItem.done) {
isIterableDone = true;
if (resolvingCount === 0 && !isResolved) {
if (!stopOnError && errors.length > 0) {
reject(new AggregateError(errors)); // eslint-disable-line unicorn/error-message
return;
}
isResolved = true;
if (skippedIndexesMap.size === 0) {
resolve(result);
return;
}
const pureResult = [];
// Support multiple `pMapSkip`'s.
for (const [index, value] of result.entries()) {
if (skippedIndexesMap.get(index) === pMapSkip) {
continue;
}
pureResult.push(value);
}
resolve(pureResult);
}
return;
}
resolvingCount++;
// Intentionally detached
(async () => {
try {
const element = await nextItem.value;
if (isResolved) {
return;
}
const value = await mapper(element, index);
// Use Map to stage the index of the element.
if (value === pMapSkip) {
skippedIndexesMap.set(index, value);
}
result[index] = value;
resolvingCount--;
await next();
} catch (error) {
if (stopOnError) {
reject(error);
} else {
errors.push(error);
resolvingCount--;
// In that case we can't really continue regardless of `stopOnError` state
// since an iterable is likely to continue throwing after it throws once.
// If we continue calling `next()` indefinitely we will likely end up
// in an infinite loop of failed iteration.
try {
await next();
} catch (error) {
reject(error);
}
}
}
})();
};
// Create the concurrent runners in a detached (non-awaited)
// promise. We need this so we can await the `next()` calls
// to stop creating runners before hitting the concurrency limit
// if the iterable has already been marked as done.
// NOTE: We *must* do this for async iterators otherwise we'll spin up
// infinite `next()` calls by default and never start the event loop.
(async () => {
for (let index = 0; index < concurrency; index++) {
try {
// eslint-disable-next-line no-await-in-loop
await next();
} catch (error) {
reject(error);
break;
}
if (isIterableDone || isRejected) {
break;
}
}
})();
});
}
function pMapIterable(
iterable,
mapper,
{
concurrency = Number.POSITIVE_INFINITY,
backpressure = concurrency,
} = {},
) {
if (iterable[Symbol.iterator] === undefined && iterable[Symbol.asyncIterator] === undefined) {
throw new TypeError(`Expected \`input\` to be either an \`Iterable\` or \`AsyncIterable\`, got (${typeof iterable})`);
}
if (typeof mapper !== 'function') {
throw new TypeError('Mapper function is required');
}
if (!((Number.isSafeInteger(concurrency) && concurrency >= 1) || concurrency === Number.POSITIVE_INFINITY)) {
throw new TypeError(`Expected \`concurrency\` to be an integer from 1 and up or \`Infinity\`, got \`${concurrency}\` (${typeof concurrency})`);
}
if (!((Number.isSafeInteger(backpressure) && backpressure >= concurrency) || backpressure === Number.POSITIVE_INFINITY)) {
throw new TypeError(`Expected \`backpressure\` to be an integer from \`concurrency\` (${concurrency}) and up or \`Infinity\`, got \`${backpressure}\` (${typeof backpressure})`);
}
return {
async * [Symbol.asyncIterator]() {
const iterator = iterable[Symbol.asyncIterator] === undefined ? iterable[Symbol.iterator]() : iterable[Symbol.asyncIterator]();
const promises = [];
let runningMappersCount = 0;
let isDone = false;
let index = 0;
function trySpawn() {
if (isDone || !(runningMappersCount < concurrency && promises.length < backpressure)) {
return;
}
const promise = (async () => {
const {done, value} = await iterator.next();
if (done) {
return {done: true};
}
runningMappersCount++;
// Spawn if still below concurrency and backpressure limit
trySpawn();
try {
const returnValue = await mapper(await value, index++);
runningMappersCount--;
if (returnValue === pMapSkip) {
const index = promises.indexOf(promise);
if (index > 0) {
promises.splice(index, 1);
}
}
// Spawn if still below backpressure limit and just dropped below concurrency limit
trySpawn();
return {done: false, value: returnValue};
} catch (error) {
isDone = true;
return {error};
}
})();
promises.push(promise);
}
trySpawn();
while (promises.length > 0) {
const {error, done, value} = await promises[0]; // eslint-disable-line no-await-in-loop
promises.shift();
if (error) {
throw error;
}
if (done) {
return;
}
// Spawn if just dropped below backpressure limit and below the concurrency limit
trySpawn();
if (value === pMapSkip) {
continue;
}
yield value;
}
},
};
}
const pMapSkip = Symbol('skip');
/***/ })
};
Generated Vendored
+91423 -23600
View File
File diff suppressed because one or more lines are too long
+6 -6
View File
@@ -1,15 +1,15 @@
{ {
"name": "actions/attest", "name": "actions/attest",
"version": "4.1.0", "version": "4.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "actions/attest", "name": "actions/attest",
"version": "4.1.0", "version": "4.0.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/attest": "^3.2.0", "@actions/attest": "^3.0.0",
"@actions/core": "^3.0.0", "@actions/core": "^3.0.0",
"@actions/github": "^9.0.0", "@actions/github": "^9.0.0",
"@actions/glob": "^0.6.1", "@actions/glob": "^0.6.1",
@@ -42,9 +42,9 @@
} }
}, },
"node_modules/@actions/attest": { "node_modules/@actions/attest": {
"version": "3.2.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/attest/-/attest-3.2.0.tgz", "resolved": "https://registry.npmjs.org/@actions/attest/-/attest-3.0.0.tgz",
"integrity": "sha512-Mdpqfyfp4dp7VZt9lVBmQTlnpK0PBrIXSblzeseP4w6Gn4Bbl5bpScJ+8zgwOMfTz1049wPzSUda5XtTYIZloQ==", "integrity": "sha512-XrGmxFA3rZO4ACtVEUHFUI318lMycHQjHep3SX/AqU8IwR0y9afw8URsGrQZhGqwMDTYxYFST9PaNQCksIyE8A==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@actions/core": "^3.0.0", "@actions/core": "^3.0.0",
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "actions/attest", "name": "actions/attest",
"description": "Generate signed attestations for workflow artifacts", "description": "Generate signed attestations for workflow artifacts",
"version": "4.1.0", "version": "4.0.0",
"author": "", "author": "",
"private": true, "private": true,
"type": "module", "type": "module",
@@ -78,7 +78,7 @@
] ]
}, },
"dependencies": { "dependencies": {
"@actions/attest": "^3.2.0", "@actions/attest": "^3.0.0",
"@actions/core": "^3.0.0", "@actions/core": "^3.0.0",
"@actions/github": "^9.0.0", "@actions/github": "^9.0.0",
"@actions/glob": "^0.6.1", "@actions/glob": "^0.6.1",
+1 -3
View File
@@ -26,7 +26,6 @@ export const createAttestation = async (
sigstoreInstance: SigstoreInstance sigstoreInstance: SigstoreInstance
pushToRegistry: boolean pushToRegistry: boolean
createStorageRecord: boolean createStorageRecord: boolean
subjectVersion?: string
githubToken: string githubToken: string
} }
): Promise<AttestResult> => { ): Promise<AttestResult> => {
@@ -78,8 +77,7 @@ export const createAttestation = async (
const registryUrl = getRegistryURL(subject.name) const registryUrl = getRegistryURL(subject.name)
const artifactOpts = { const artifactOpts = {
name: subject.name, name: subject.name,
digest: subjectDigest, digest: subjectDigest
version: opts.subjectVersion || undefined
} }
const packageRegistryOpts = { const packageRegistryOpts = {
registryUrl registryUrl
-1
View File
@@ -15,7 +15,6 @@ const inputs: RunInputs = {
predicatePath: core.getInput('predicate-path'), predicatePath: core.getInput('predicate-path'),
pushToRegistry: core.getBooleanInput('push-to-registry'), pushToRegistry: core.getBooleanInput('push-to-registry'),
createStorageRecord: core.getBooleanInput('create-storage-record'), createStorageRecord: core.getBooleanInput('create-storage-record'),
subjectVersion: core.getInput('subject-version'),
showSummary: core.getBooleanInput('show-summary'), showSummary: core.getBooleanInput('show-summary'),
githubToken: core.getInput('github-token'), githubToken: core.getInput('github-token'),
// undocumented -- not part of public interface // undocumented -- not part of public interface
-2
View File
@@ -35,7 +35,6 @@ export type RunInputs = SubjectInputs &
SBOMInputs & { SBOMInputs & {
pushToRegistry: boolean pushToRegistry: boolean
createStorageRecord: boolean createStorageRecord: boolean
subjectVersion: string
githubToken: string githubToken: string
showSummary: boolean showSummary: boolean
privateSigning: boolean privateSigning: boolean
@@ -98,7 +97,6 @@ export async function run(inputs: RunInputs): Promise<void> {
sigstoreInstance, sigstoreInstance,
pushToRegistry: inputs.pushToRegistry, pushToRegistry: inputs.pushToRegistry,
createStorageRecord: inputs.createStorageRecord, createStorageRecord: inputs.createStorageRecord,
subjectVersion: inputs.subjectVersion,
githubToken: inputs.githubToken githubToken: inputs.githubToken
}) })