Compare commits

..
Author SHA1 Message Date
Brian DeHamer 8cb9193776 new OCI compat mode flag
Signed-off-by: Brian DeHamer <[email protected]>
2025-04-10 09:41:02 -07:00
Brian DeHamer 99cf707746 another take on an empty config
Signed-off-by: Brian DeHamer <[email protected]>
2025-04-10 08:47:16 -07:00
Brian DeHamer 6e1e49a5d4 try empty descriptor
Signed-off-by: Brian DeHamer <[email protected]>
2025-04-09 17:41:13 -07:00
Brian DeHamer c8edbd7693 redefine empty blob
Signed-off-by: Brian DeHamer <[email protected]>
2025-04-09 16:41:18 -07:00
Brian DeHamer f4f308e094 tweak logging
Signed-off-by: Brian DeHamer <[email protected]>
2025-04-09 14:43:34 -07:00
Brian DeHamer 3a105d9b34 dump probe response headers
Signed-off-by: Brian DeHamer <[email protected]>
2025-04-09 10:56:02 -07:00
Brian DeHamer bb7f52f719 more debug logging
Signed-off-by: Brian DeHamer <[email protected]>
2025-04-09 10:33:50 -07:00
Brian DeHamer 6d85257406 experiment w/ OCI header auth
Signed-off-by: Brian DeHamer <[email protected]>
2025-04-03 17:47:14 -07:00
24 changed files with 43039 additions and 23675 deletions
+10
View File
@@ -0,0 +1,10 @@
rules:
document-end: disable
document-start:
level: warning
present: false
line-length:
level: warning
max: 80
allow-non-breakable-words: true
allow-non-breakable-inline-mappings: true
@@ -1,14 +1,13 @@
import eslint from '@eslint/js' import eslint from '@eslint/js'
import importplugin from 'eslint-plugin-import' import importplugin from 'eslint-plugin-import'
import jestplugin from 'eslint-plugin-jest' import jestplugin from 'eslint-plugin-jest'
import path from 'node:path'
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint'
export default tseslint.config( export default tseslint.config(
// Ignore non-project files // Ignore non-project files
{ {
name: 'ignore', name: 'ignore',
ignores: ['.github', 'dist', 'coverage', '**/*.json', 'jest.setup.js', 'eslint.config.mjs'] ignores: ['.github', 'dist', 'coverage', '**/*.json', 'jest.setup.js']
}, },
// Use recommended rules from ESLint, TypeScript, and other plugins // Use recommended rules from ESLint, TypeScript, and other plugins
eslint.configs.recommended, eslint.configs.recommended,
@@ -22,7 +21,7 @@ export default tseslint.config(
languageOptions: { languageOptions: {
ecmaVersion: 2023, ecmaVersion: 2023,
parserOptions: { parserOptions: {
project: [ './tsconfig.lint.json' ] project: ['./.github/linters/tsconfig.json', './tsconfig.json']
} }
}, },
rules: { rules: {
+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["../../__tests__/**/*", "../../src/**/*"],
"exclude": ["../../dist", "../../node_modules", "../../coverage", "*.json"]
}
+3 -3
View File
@@ -28,11 +28,11 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@v6.0.1 uses: actions/checkout@v4
- name: Setup Node.js - name: Setup Node.js
id: setup-node id: setup-node
uses: actions/setup-node@v6.1.0 uses: actions/setup-node@v4
with: with:
node-version-file: .node-version node-version-file: .node-version
cache: npm cache: npm
@@ -60,7 +60,7 @@ jobs:
- if: ${{ failure() && steps.diff.outcome == 'failure' }} - if: ${{ failure() && steps.diff.outcome == 'failure' }}
name: Upload Artifact name: Upload Artifact
id: upload id: upload
uses: actions/upload-artifact@v6 uses: actions/upload-artifact@v4
with: with:
name: dist name: dist
path: dist/ path: dist/
+3 -3
View File
@@ -21,11 +21,11 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Setup Node.js - name: Setup Node.js
id: setup-node id: setup-node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@b39b52d1213e96004bfcb1c61a8a6fa8ab84f3e8 # v4.0.1
with: with:
node-version-file: .node-version node-version-file: .node-version
cache: npm cache: npm
@@ -58,7 +58,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v5.0.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Calculate subject digest - name: Calculate subject digest
id: subject id: subject
env: env:
+4 -4
View File
@@ -32,19 +32,19 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
id: checkout id: checkout
uses: actions/checkout@v6.0.1 uses: actions/checkout@v4
- name: Initialize CodeQL - name: Initialize CodeQL
id: initialize id: initialize
uses: github/codeql-action/init@v4 uses: github/codeql-action/init@v3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
source-root: src source-root: src
- name: Autobuild - name: Autobuild
id: autobuild id: autobuild
uses: github/codeql-action/autobuild@v4 uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
id: analyze id: analyze
uses: github/codeql-action/analyze@v4 uses: github/codeql-action/analyze@v3
+50
View File
@@ -0,0 +1,50 @@
name: Lint Codebase
on:
pull_request:
branches:
- main
push:
branches:
- main
permissions:
contents: read
packages: read
statuses: write
jobs:
lint:
name: Lint Codebase
runs-on: ubuntu-latest
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm
- name: Install Dependencies
id: install
run: npm ci
- name: Lint Codebase
id: super-linter
uses: super-linter/super-linter/[email protected]
env:
DEFAULT_BRANCH: main
FILTER_REGEX_EXCLUDE: dist/**/*
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TYPESCRIPT_DEFAULT_STYLE: prettier
VALIDATE_ALL_CODEBASE: true
VALIDATE_JAVASCRIPT_STANDARD: false
VALIDATE_TYPESCRIPT_STANDARD: false
VALIDATE_JSCPD: false
@@ -0,0 +1,22 @@
name: 'Publish Immutable Action Version'
on:
release:
types: [published]
permissions: {}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
steps:
- name: Checking out
uses: actions/checkout@v4
- name: Publish
id: publish
uses: actions/[email protected]
+1 -1
View File
@@ -1 +1 @@
24.5.0 20.6.0
+6 -26
View File
@@ -18,12 +18,6 @@ Once the attestation has been created and signed, it will be uploaded to the GH
attestations API and associated with the repository from which the workflow was attestations API and associated with the repository from which the workflow was
initiated. initiated.
When an attestation is created, the attestation is stored on the local
filesystem used by the runner. For each attestation created, the filesystem path
will be appended to the file `${RUNNER_TEMP}/created_attestation_paths.txt`.
This can be used to gather all attestations created by all jobs during a the
workflow.
Attestations can be verified using the [`attestation` command in the GitHub Attestations can be verified using the [`attestation` command in the GitHub
CLI][5]. CLI][5].
@@ -52,13 +46,11 @@ attest:
permissions: permissions:
id-token: write id-token: write
attestations: write attestations: write
artifact-metadata: write
``` ```
The `id-token` permission gives the action the ability to mint the OIDC token The `id-token` permission gives the action the ability to mint the OIDC token
necessary to request a Sigstore signing certificate. The `attestations` necessary to request a Sigstore signing certificate. The `attestations`
permission is necessary to persist the attestation. The `artifact-metadata` permission is necessary to persist the attestation.
permission is necessary to create the artifact storage record.
1. Add the following to your workflow after your artifact has been built: 1. Add the following to your workflow after your artifact has been built:
@@ -120,12 +112,6 @@ See [action.yml](action.yml)
# the "subject-digest" parameter be specified. Defaults to false. # the "subject-digest" parameter be specified. Defaults to false.
push-to-registry: 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 # Whether to attach a list of generated attestations to the workflow run
# summary page. Defaults to true. # summary page. Defaults to true.
show-summary: show-summary:
@@ -139,12 +125,11 @@ 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` |
| `storage-record-ids` | GitHub IDs for the storage records | `987654` |
<!-- markdownlint-enable MD013 --> <!-- markdownlint-enable MD013 -->
@@ -278,10 +263,6 @@ 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.
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 > **NOTE**: When pushing to Docker Hub, please use "docker.io" as the registry
> portion of the image name. > portion of the image name.
@@ -300,7 +281,6 @@ jobs:
packages: write packages: write
contents: read contents: read
attestations: write attestations: write
artifact-metadata: write
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
+3 -45
View File
@@ -9,7 +9,6 @@ import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock' import { mockFulcio, mockRekor, mockTSA } from '@sigstore/mock'
import * as oci from '@sigstore/oci' import * as oci from '@sigstore/oci'
import * as attest from '@actions/attest'
import fs from 'fs/promises' import fs from 'fs/promises'
import nock from 'nock' import nock from 'nock'
import os from 'os' import os from 'os'
@@ -20,7 +19,6 @@ import * as main from '../src/main'
// Mock the GitHub Actions core library // Mock the GitHub Actions core library
const infoMock = jest.spyOn(core, 'info') const infoMock = jest.spyOn(core, 'info')
const warningMock = jest.spyOn(core, 'warning')
const startGroupMock = jest.spyOn(core, 'startGroup') const startGroupMock = jest.spyOn(core, 'startGroup')
const setOutputMock = jest.spyOn(core, 'setOutput') const setOutputMock = jest.spyOn(core, 'setOutput')
const setFailedMock = jest.spyOn(core, 'setFailed') const setFailedMock = jest.spyOn(core, 'setFailed')
@@ -47,7 +45,6 @@ const defaultInputs: main.RunInputs = {
subjectPath: '', subjectPath: '',
subjectChecksums: '', subjectChecksums: '',
pushToRegistry: false, pushToRegistry: false,
createStorageRecord: true,
showSummary: true, showSummary: true,
githubToken: '', githubToken: '',
privateSigning: false privateSigning: false
@@ -69,14 +66,13 @@ describe('action', () => {
'base64' 'base64'
)}.}` )}.}`
const subjectName = 'ghcr.io/registry/foo/bar' const subjectName = 'registry/foo/bar'
const subjectDigest = const subjectDigest =
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32' 'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
const predicate = '{}' const predicate = '{}'
const predicateType = 'https://in-toto.io/attestation/release/v0.1' const predicateType = 'https://in-toto.io/attestation/release/v0.1'
const attestationID = '1234567890' const attestationID = '1234567890'
const storageRecordID = 987654321
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
@@ -86,21 +82,14 @@ describe('action', () => {
.query({ audience: 'sigstore' }) .query({ audience: 'sigstore' })
.reply(200, { value: oidcToken }) .reply(200, { value: oidcToken })
const pool = mockAgent.get('https://api.github.com') mockAgent
pool .get('https://api.github.com')
.intercept({ .intercept({
path: /^\/repos\/.*\/.*\/attestations$/, path: /^\/repos\/.*\/.*\/attestations$/,
method: 'post' method: 'post'
}) })
.reply(201, { id: attestationID }) .reply(201, { id: attestationID })
pool
.intercept({
path: /^\/orgs\/.*\/artifacts\/metadata\/storage-record$/,
method: 'post'
})
.reply(200, { storage_records: [{ id: storageRecordID }] })
process.env = { process.env = {
...originalEnv, ...originalEnv,
ACTIONS_ID_TOKEN_REQUEST_URL: tokenURL, ACTIONS_ID_TOKEN_REQUEST_URL: tokenURL,
@@ -274,7 +263,6 @@ describe('action', () => {
expect(setFailedMock).not.toHaveBeenCalled() expect(setFailedMock).not.toHaveBeenCalled()
expect(getRegCredsSpy).toHaveBeenCalledWith(subjectName) expect(getRegCredsSpy).toHaveBeenCalledWith(subjectName)
expect(attachArtifactSpy).toHaveBeenCalled() expect(attachArtifactSpy).toHaveBeenCalled()
expect(warningMock).not.toHaveBeenCalled()
expect(infoMock).toHaveBeenNthCalledWith( expect(infoMock).toHaveBeenNthCalledWith(
1, 1,
expect.stringMatching( expect.stringMatching(
@@ -305,14 +293,6 @@ describe('action', () => {
6, 6,
expect.stringMatching(attestationID) 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( expect(setOutputMock).toHaveBeenNthCalledWith(
1, 1,
'bundle-path', 'bundle-path',
@@ -328,30 +308,8 @@ describe('action', () => {
'attestation-url', 'attestation-url',
expect.stringContaining(`foo/bar/attestations/${attestationID}`) expect.stringContaining(`foo/bar/attestations/${attestationID}`)
) )
expect(setOutputMock).toHaveBeenNthCalledWith(
4,
'storage-record-ids',
expect.stringMatching(storageRecordID.toString())
)
expect(setFailedMock).not.toHaveBeenCalled() 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', () => { describe('when the subject count is greater than 1', () => {
+2 -23
View File
@@ -473,13 +473,6 @@ badline
'187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d' '187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d'
} }
}) })
expect(subjects).toContainEqual({
name: 'demo_0.0.1_darwin_arm64',
digest: {
sha512:
'5d8b4751ef31f9440d843fcfa4e53ca2e25b1cb1f13fd355fdc7c24b41fe645293291ea9297ba3989078abb77ebbaac66be073618a9e4974dbd0361881d4c718'
}
})
}) })
}) })
}) })
@@ -487,8 +480,8 @@ badline
describe('when specifying a subject checksums string', () => { describe('when specifying a subject checksums string', () => {
const checksums = ` const checksums = `
f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386 f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386
187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d *demo_0.0.1_linux_amd64 187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d demo_0.0.1_linux_amd64
9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5 demo_0.0.1_linux_arm64` 9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5 demo_0.0.1_linux_arm64`
it('returns the multiple subjects', async () => { it('returns the multiple subjects', async () => {
const inputs: SubjectInputs = { const inputs: SubjectInputs = {
@@ -507,20 +500,6 @@ f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_lin
'f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e' 'f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e'
} }
}) })
expect(subjects).toContainEqual({
name: 'demo_0.0.1_linux_amd64',
digest: {
sha256:
'187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d'
}
})
expect(subjects).toContainEqual({
name: 'demo_0.0.1_linux_arm64',
digest: {
sha256:
'9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5'
}
})
}) })
}) })
+1 -9
View File
@@ -53,12 +53,6 @@ inputs:
the "subject-digest" parameter be specified. Defaults to false. the "subject-digest" parameter be specified. Defaults to false.
default: false default: false
required: 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: show-summary:
description: > description: >
Whether to attach a list of generated attestations to the workflow run Whether to attach a list of generated attestations to the workflow run
@@ -77,9 +71,7 @@ outputs:
description: 'The ID of the attestation.' description: 'The ID of the attestation.'
attestation-url: attestation-url:
description: 'The URL for the attestation summary.' description: 'The URL for the attestation summary.'
storage-record-ids:
description: 'The IDs of the storage records created for the artifact.'
runs: runs:
using: node24 using: node20
main: ./dist/index.js main: ./dist/index.js
Generated Vendored
+287
View File
@@ -0,0 +1,287 @@
"use strict";
exports.id = 184;
exports.ids = [184];
exports.modules = {
/***/ 91184:
/***/ ((__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 reject = reason => {
isRejected = true;
isResolved = true;
reject_(reason);
};
if (signal) {
if (signal.aborted) {
reject(signal.reason);
}
signal.addEventListener('abort', () => {
reject(signal.reason);
});
}
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
+38601 -18015
View File
File diff suppressed because one or more lines are too long
+4007 -5408
View File
File diff suppressed because it is too large Load Diff
+24 -24
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": "3.1.0", "version": "2.2.1",
"author": "", "author": "",
"private": true, "private": true,
"homepage": "https://github.com/actions/attest", "homepage": "https://github.com/actions/attest",
@@ -20,15 +20,15 @@
".": "./dist/index.js" ".": "./dist/index.js"
}, },
"engines": { "engines": {
"node": ">=24" "node": ">=20"
}, },
"scripts": { "scripts": {
"bundle": "npm run format:write && npm run package", "bundle": "npm run format:write && npm run package",
"ci-test": "jest", "ci-test": "jest",
"format:write": "prettier --write **/*.ts", "format:write": "prettier --write **/*.ts",
"format:check": "prettier --check **/*.ts", "format:check": "prettier --check **/*.ts",
"lint:eslint": "npx eslint", "lint:eslint": "npx eslint . -c ./.github/linters/eslint.config.mjs",
"lint:markdown": "npx markdownlint --config .markdown-lint.yml \"*.md\"", "lint:markdown": "npx markdownlint --config .github/linters/.markdown-lint.yml \"*.md\"",
"lint": "npm run lint:eslint && npm run lint:markdown", "lint": "npm run lint:eslint && npm run lint:markdown",
"package": "ncc build src/index.ts --license licenses.txt", "package": "ncc build src/index.ts --license licenses.txt",
"package:watch": "npm run package -- --watch", "package:watch": "npm run package -- --watch",
@@ -69,31 +69,31 @@
] ]
}, },
"dependencies": { "dependencies": {
"@actions/attest": "^2.1.0", "@actions/attest": "^1.6.0",
"@actions/core": "^2.0.1", "@actions/core": "^1.11.1",
"@actions/github": "^6.0.1", "@actions/github": "^6.0.0",
"@actions/glob": "^0.5.0", "@actions/glob": "^0.5.0",
"@sigstore/oci": "^0.6.0", "@sigstore/oci": "^0.4.0",
"csv-parse": "^5.6.0" "csv-parse": "^5.6.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@eslint/js": "^9.23.0",
"@sigstore/mock": "^0.11.0", "@sigstore/mock": "^0.10.0",
"@types/jest": "^30.0.0", "@types/jest": "^29.5.14",
"@types/make-fetch-happen": "^10.0.4", "@types/make-fetch-happen": "^10.0.4",
"@types/node": "^25.0.2", "@types/node": "^22.13.14",
"@vercel/ncc": "^0.38.4", "@vercel/ncc": "^0.38.3",
"eslint": "^9.39.2", "eslint": "^9.23.0",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^29.5.0", "eslint-plugin-jest": "^28.11.0",
"jest": "^30.2.0", "jest": "^29.7.0",
"js-yaml": "^4.1.1", "js-yaml": "^4.1.0",
"markdownlint-cli": "^0.47.0", "markdownlint-cli": "^0.44.0",
"nock": "^13.5.6", "nock": "^13.5.6",
"prettier": "^3.7.4", "prettier": "^3.5.3",
"ts-jest": "^29.4.6", "ts-jest": "^29.3.1",
"typescript": "^5.9.3", "typescript": "^5.8.2",
"typescript-eslint": "^8.50.0", "typescript-eslint": "^8.29.0",
"undici": "^5.29.0" "undici": "^5.28.5"
} }
} }
+2 -62
View File
@@ -1,13 +1,6 @@
import { import { Attestation, Predicate, Subject, attest } from '@actions/attest'
Attestation,
Predicate,
Subject,
attest,
createStorageRecord
} from '@actions/attest'
import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci' import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci'
import { formatSubjectDigest } from './subject' import { formatSubjectDigest } from './subject'
import * as core from '@actions/core'
const OCI_TIMEOUT = 30000 const OCI_TIMEOUT = 30000
const OCI_RETRY = 3 const OCI_RETRY = 3
@@ -15,7 +8,6 @@ const OCI_RETRY = 3
export type SigstoreInstance = 'public-good' | 'github' export type SigstoreInstance = 'public-good' | 'github'
export type AttestResult = Attestation & { export type AttestResult = Attestation & {
attestationDigest?: string attestationDigest?: string
storageRecordIds?: number[]
} }
export const createAttestation = async ( export const createAttestation = async (
@@ -24,7 +16,6 @@ export const createAttestation = async (
opts: { opts: {
sigstoreInstance: SigstoreInstance sigstoreInstance: SigstoreInstance
pushToRegistry: boolean pushToRegistry: boolean
createStorageRecord: boolean
githubToken: string githubToken: string
} }
): Promise<AttestResult> => { ): Promise<AttestResult> => {
@@ -42,11 +33,10 @@ export const createAttestation = async (
if (subjects.length === 1 && opts.pushToRegistry) { if (subjects.length === 1 && opts.pushToRegistry) {
const subject = subjects[0] const subject = subjects[0]
const credentials = getRegistryCredentials(subject.name) const credentials = getRegistryCredentials(subject.name)
const subjectDigest = formatSubjectDigest(subject)
const artifact = await attachArtifactToImage({ const artifact = await attachArtifactToImage({
credentials, credentials,
imageName: subject.name, imageName: subject.name,
imageDigest: subjectDigest, imageDigest: formatSubjectDigest(subject),
artifact: Buffer.from(JSON.stringify(attestation.bundle)), artifact: Buffer.from(JSON.stringify(attestation.bundle)),
mediaType: attestation.bundle.mediaType, mediaType: attestation.bundle.mediaType,
annotations: { annotations: {
@@ -58,57 +48,7 @@ export const createAttestation = async (
// Add the attestation's digest to the result // Add the attestation's digest to the result
result.attestationDigest = artifact.digest 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 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,7 +13,6 @@ const inputs: RunInputs = {
predicate: core.getInput('predicate'), predicate: core.getInput('predicate'),
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'),
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
-30
View File
@@ -16,12 +16,10 @@ import {
import type { Subject } from '@actions/attest' import type { Subject } from '@actions/attest'
const ATTESTATION_FILE_NAME = 'attestation.json' const ATTESTATION_FILE_NAME = 'attestation.json'
const ATTESTATION_PATHS_FILE_NAME = 'created_attestation_paths.txt'
export type RunInputs = SubjectInputs & export type RunInputs = SubjectInputs &
PredicateInputs & { PredicateInputs & {
pushToRegistry: boolean pushToRegistry: boolean
createStorageRecord: boolean
githubToken: string githubToken: string
showSummary: boolean showSummary: boolean
privateSigning: boolean privateSigning: boolean
@@ -70,7 +68,6 @@ export async function run(inputs: RunInputs): Promise<void> {
const att = await createAttestation(subjects, predicate, { const att = await createAttestation(subjects, predicate, {
sigstoreInstance, sigstoreInstance,
pushToRegistry: inputs.pushToRegistry, pushToRegistry: inputs.pushToRegistry,
createStorageRecord: inputs.createStorageRecord,
githubToken: inputs.githubToken githubToken: inputs.githubToken
}) })
@@ -82,31 +79,11 @@ export async function run(inputs: RunInputs): Promise<void> {
flag: 'a' flag: 'a'
}) })
const baseDir = process.env.RUNNER_TEMP
/* istanbul ignore else */
if (baseDir) {
const outputSummaryPath = path.join(baseDir, ATTESTATION_PATHS_FILE_NAME)
// Append the output path to the attestations paths file
fs.appendFileSync(outputSummaryPath, outputPath + os.EOL, {
encoding: 'utf-8',
flag: 'a'
})
} else {
core.warning(
'RUNNER_TEMP environment variable is not set. Cannot write attestation paths file.'
)
}
/* istanbul ignore else */
if (att.attestationID) { if (att.attestationID) {
core.setOutput('attestation-id', att.attestationID) core.setOutput('attestation-id', att.attestationID)
core.setOutput('attestation-url', attestationURL(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) { if (inputs.showSummary) {
await logSummary(att) await logSummary(att)
} }
@@ -164,7 +141,6 @@ const logAttestation = (
core.info(`${SEARCH_PUBLIC_GOOD_URL}?logIndex=${attestation.tlogID}`) core.info(`${SEARCH_PUBLIC_GOOD_URL}?logIndex=${attestation.tlogID}`)
} }
/* istanbul ignore else */
if (attestation.attestationID) { if (attestation.attestationID) {
core.info(style.highlight('Attestation uploaded to repository')) core.info(style.highlight('Attestation uploaded to repository'))
core.info(attestationURL(attestation.attestationID)) core.info(attestationURL(attestation.attestationID))
@@ -174,18 +150,12 @@ const logAttestation = (
core.info(style.highlight('Attestation uploaded to registry')) core.info(style.highlight('Attestation uploaded to registry'))
core.info(`${subjects[0].name}@${attestation.attestationDigest}`) 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 // Attach summary information to the GitHub Actions run
const logSummary = async (attestation: AttestResult): Promise<void> => { const logSummary = async (attestation: AttestResult): Promise<void> => {
const { attestationID } = attestation const { attestationID } = attestation
/* istanbul ignore else */
if (attestationID) { if (attestationID) {
const url = attestationURL(attestationID) const url = attestationURL(attestationID)
core.summary.addHeading('Attestation Created', 3) core.summary.addHeading('Attestation Created', 3)
+2 -8
View File
@@ -181,14 +181,8 @@ const getSubjectFromChecksumsString = (checksums: string): Subject[] => {
continue continue
} }
// It's common for checksum records to have a leading flag character before // Swallow the type identifier character at the beginning of the name
// the artifact name. It will be either a '*' or a space. const name = record.slice(delimIndex + 2)
const flag_and_name = record.slice(delimIndex + 1)
const name =
flag_and_name.startsWith('*') || flag_and_name.startsWith(' ')
? flag_and_name.slice(1)
: flag_and_name
const digest = record.slice(0, delimIndex) const digest = record.slice(0, delimIndex)
if (!HEX_STRING_RE.test(digest)) { if (!HEX_STRING_RE.test(digest)) {
-1
View File
@@ -5,7 +5,6 @@
"module": "NodeNext", "module": "NodeNext",
"rootDir": "./src", "rootDir": "./src",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",
"isolatedModules": true,
"baseUrl": "./", "baseUrl": "./",
"sourceMap": true, "sourceMap": true,
"outDir": "./dist", "outDir": "./dist",
-9
View File
@@ -1,9 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["./__tests__/**/*", "./src/**/*"],
"exclude": ["./dist", "./node_modules", "./coverage", "*.json"]
}