Compare commits

..

2 Commits

Author SHA1 Message Date
Brian DeHamer a0e3b9618f hacking
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2026-02-24 20:10:53 -08:00
Brian DeHamer 8459a46a44 wip
Signed-off-by: Brian DeHamer <bdehamer@github.com>
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 -->
| Mode | When Used | Description |
| -------------- | ------------------------------------------------------ | ----------------------------------------------- |
| **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 |
| **Custom** | `predicate-type`/`predicate`/`predicate-path` provided | User-supplied predicate |
| Mode | When Used | Description |
| -------------- | ------------------------------------------------------ | ------------------------------------------------ |
| **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 |
| **Custom** | `predicate-type`/`predicate`/`predicate-path` provided | User-supplied predicate |
<!-- markdownlint-enable MD013 -->
@@ -159,7 +159,7 @@ 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` |
@@ -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 --
the specific image being attested is identified by the supplied digest.
#### Artifact Metadata Storage Records
When generating a build provenance attestation, if the `push-to-registry` option
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).
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.
-2
View File
@@ -30,7 +30,6 @@ describe('index', () => {
'subject-name': 'my-artifact',
'subject-digest': '',
'subject-checksums': '',
'subject-version': '',
'predicate-type': 'https://example.com/predicate',
predicate: '{}',
'predicate-path': '',
@@ -58,7 +57,6 @@ describe('index', () => {
subjectName: 'my-artifact',
subjectDigest: '',
subjectChecksums: '',
subjectVersion: '',
predicateType: 'https://example.com/predicate',
predicate: '{}',
predicatePath: '',
+2 -20
View File
@@ -145,8 +145,7 @@ describe('createAttestation', () => {
const storageOpts = {
...defaultOpts,
pushToRegistry: true,
createStorageRecord: true,
subjectVersion: '1.2.3'
createStorageRecord: true
}
it('should create storage record when enabled and owner is org', async () => {
@@ -158,27 +157,10 @@ describe('createAttestation', () => {
storageOpts
)
expect(mockCreateStorageRecord).toHaveBeenCalledWith(
expect.objectContaining({ version: '1.2.3' }),
expect.anything(),
expect.anything()
)
expect(mockCreateStorageRecord).toHaveBeenCalled()
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 () => {
mockGetOctokit.mockReturnValue(createOctokitMock('User'))
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
-1
View File
@@ -101,7 +101,6 @@ const defaultInputs: RunInputs = {
subjectChecksums: '',
pushToRegistry: false,
createStorageRecord: false,
subjectVersion: '',
showSummary: false,
githubToken: 'test-token',
privateSigning: false
-5
View File
@@ -30,11 +30,6 @@ inputs:
attestation. Must specify exactly one of "subject-path", "subject-digest",
or "subject-checksums".
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:
description: >
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",
"version": "4.1.0",
"version": "4.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "actions/attest",
"version": "4.1.0",
"version": "4.0.0",
"license": "MIT",
"dependencies": {
"@actions/attest": "^3.2.0",
"@actions/attest": "^3.0.0",
"@actions/core": "^3.0.0",
"@actions/github": "^9.0.0",
"@actions/glob": "^0.6.1",
@@ -42,9 +42,9 @@
}
},
"node_modules/@actions/attest": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@actions/attest/-/attest-3.2.0.tgz",
"integrity": "sha512-Mdpqfyfp4dp7VZt9lVBmQTlnpK0PBrIXSblzeseP4w6Gn4Bbl5bpScJ+8zgwOMfTz1049wPzSUda5XtTYIZloQ==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@actions/attest/-/attest-3.0.0.tgz",
"integrity": "sha512-XrGmxFA3rZO4ACtVEUHFUI318lMycHQjHep3SX/AqU8IwR0y9afw8URsGrQZhGqwMDTYxYFST9PaNQCksIyE8A==",
"license": "MIT",
"dependencies": {
"@actions/core": "^3.0.0",
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "actions/attest",
"description": "Generate signed attestations for workflow artifacts",
"version": "4.1.0",
"version": "4.0.0",
"author": "",
"private": true,
"type": "module",
@@ -78,7 +78,7 @@
]
},
"dependencies": {
"@actions/attest": "^3.2.0",
"@actions/attest": "^3.0.0",
"@actions/core": "^3.0.0",
"@actions/github": "^9.0.0",
"@actions/glob": "^0.6.1",
+1 -3
View File
@@ -26,7 +26,6 @@ export const createAttestation = async (
sigstoreInstance: SigstoreInstance
pushToRegistry: boolean
createStorageRecord: boolean
subjectVersion?: string
githubToken: string
}
): Promise<AttestResult> => {
@@ -78,8 +77,7 @@ export const createAttestation = async (
const registryUrl = getRegistryURL(subject.name)
const artifactOpts = {
name: subject.name,
digest: subjectDigest,
version: opts.subjectVersion || undefined
digest: subjectDigest
}
const packageRegistryOpts = {
registryUrl
-1
View File
@@ -15,7 +15,6 @@ const inputs: RunInputs = {
predicatePath: core.getInput('predicate-path'),
pushToRegistry: core.getBooleanInput('push-to-registry'),
createStorageRecord: core.getBooleanInput('create-storage-record'),
subjectVersion: core.getInput('subject-version'),
showSummary: core.getBooleanInput('show-summary'),
githubToken: core.getInput('github-token'),
// undocumented -- not part of public interface
-2
View File
@@ -35,7 +35,6 @@ export type RunInputs = SubjectInputs &
SBOMInputs & {
pushToRegistry: boolean
createStorageRecord: boolean
subjectVersion: string
githubToken: string
showSummary: boolean
privateSigning: boolean
@@ -98,7 +97,6 @@ export async function run(inputs: RunInputs): Promise<void> {
sigstoreInstance,
pushToRegistry: inputs.pushToRegistry,
createStorageRecord: inputs.createStorageRecord,
subjectVersion: inputs.subjectVersion,
githubToken: inputs.githubToken
})