Compare commits

...
19 Commits
Author SHA1 Message Date
Brian DeHamer 6c3e7c389e testing
Signed-off-by: Brian DeHamer <[email protected]>
2024-06-13 10:50:45 -07:00
Brian DeHamerandGitHub b24527d9cb Bump @actions/attest from 1.2.1 to 1.3.0 (#89)
Signed-off-by: Brian DeHamer <[email protected]>
2024-06-12 13:17:25 -07:00
Brian DeHamerandGitHub 65e3b8bbb5 bump @sigstore/oci to 0.3.6 (#88)
Signed-off-by: Brian DeHamer <[email protected]>
2024-06-12 11:27:41 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0164ca8f6f Bump braces from 3.0.2 to 3.0.3 (#87)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-12 11:02:35 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b7c5f92e1b Bump the npm-development group with 6 updates (#86)
Bumps the npm-development group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `20.14.0` | `20.14.2` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `7.12.0` | `7.13.0` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `7.12.0` | `7.13.0` |
| [eslint-plugin-github](https://github.com/github/eslint-plugin-github) | `5.0.0` | `5.0.1` |
| [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) | `28.5.0` | `28.6.0` |
| [prettier](https://github.com/prettier/prettier) | `3.3.0` | `3.3.1` |


Updates `@types/node` from 20.14.0 to 20.14.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@typescript-eslint/eslint-plugin` from 7.12.0 to 7.13.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.13.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 7.12.0 to 7.13.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.13.0/packages/parser)

Updates `eslint-plugin-github` from 5.0.0 to 5.0.1
- [Release notes](https://github.com/github/eslint-plugin-github/releases)
- [Commits](https://github.com/github/eslint-plugin-github/compare/v5.0.0...v5.0.1)

Updates `eslint-plugin-jest` from 28.5.0 to 28.6.0
- [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases)
- [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v28.5.0...v28.6.0)

Updates `prettier` from 3.3.0 to 3.3.1
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.3.0...3.3.1)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: eslint-plugin-github
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: eslint-plugin-jest
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-12 10:38:20 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Brian DeHamer
f80a8431fd Bump eslint-plugin-github from 4.10.2 to 5.0.0 (#84)
Bumps [eslint-plugin-github](https://github.com/github/eslint-plugin-github) from 4.10.2 to 5.0.0.
- [Release notes](https://github.com/github/eslint-plugin-github/releases)
- [Commits](https://github.com/github/eslint-plugin-github/compare/v4.10.2...v5.0.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-github
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Brian DeHamer <[email protected]>
2024-06-04 11:58:40 -07:00
Brian DeHamerandGitHub 805ae990d5 Revert "disable github action linting (#49)" (#85)
This reverts commit 74e71f701d.
2024-06-04 08:37:19 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4b199e0571 Bump the npm-development group with 5 updates (#82)
Bumps the npm-development group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `20.12.12` | `20.14.0` |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `7.11.0` | `7.12.0` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `7.11.0` | `7.12.0` |
| [prettier](https://github.com/prettier/prettier) | `3.2.5` | `3.3.0` |
| [ts-jest](https://github.com/kulshekhar/ts-jest) | `29.1.3` | `29.1.4` |


Updates `@types/node` from 20.12.12 to 20.14.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@typescript-eslint/eslint-plugin` from 7.11.0 to 7.12.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.12.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 7.11.0 to 7.12.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.12.0/packages/parser)

Updates `prettier` from 3.2.5 to 3.3.0
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.2.5...3.3.0)

Updates `ts-jest` from 29.1.3 to 29.1.4
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.1.3...v29.1.4)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: ts-jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-04 07:57:52 -07:00
Brian DeHamerandGitHub 32795ed917 bump package version to 1.2.0 (#81)
Signed-off-by: Brian DeHamer <[email protected]>
2024-06-03 10:10:05 -07:00
Brian DeHamerandGitHub 4fa34e85c5 enforce 16MB limit on predicate size (#80)
Signed-off-by: Brian DeHamer <[email protected]>
2024-06-03 09:41:25 -07:00
Brian DeHamerandGitHub 9e752e3d76 batch processing w/ exponential backoff (#79)
Signed-off-by: Brian DeHamer <[email protected]>
2024-06-03 07:56:25 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Brian DeHamer
a0652efe33 Bump the npm-development group with 5 updates (#75)
* Bump the npm-development group with 5 updates

Bumps the npm-development group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) | `7.10.0` | `7.11.0` |
| [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) | `7.10.0` | `7.11.0` |
| [eslint-plugin-jsonc](https://github.com/ota-meshi/eslint-plugin-jsonc) | `2.15.1` | `2.16.0` |
| [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli) | `0.40.0` | `0.41.0` |
| [ts-jest](https://github.com/kulshekhar/ts-jest) | `29.1.2` | `29.1.3` |


Updates `@typescript-eslint/eslint-plugin` from 7.10.0 to 7.11.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.11.0/packages/eslint-plugin)

Updates `@typescript-eslint/parser` from 7.10.0 to 7.11.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v7.11.0/packages/parser)

Updates `eslint-plugin-jsonc` from 2.15.1 to 2.16.0
- [Release notes](https://github.com/ota-meshi/eslint-plugin-jsonc/releases)
- [Changelog](https://github.com/ota-meshi/eslint-plugin-jsonc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ota-meshi/eslint-plugin-jsonc/compare/v2.15.1...v2.16.0)

Updates `markdownlint-cli` from 0.40.0 to 0.41.0
- [Release notes](https://github.com/igorshubovych/markdownlint-cli/releases)
- [Commits](https://github.com/igorshubovych/markdownlint-cli/compare/v0.40.0...v0.41.0)

Updates `ts-jest` from 29.1.2 to 29.1.3
- [Release notes](https://github.com/kulshekhar/ts-jest/releases)
- [Changelog](https://github.com/kulshekhar/ts-jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/kulshekhar/ts-jest/compare/v29.1.2...v29.1.3)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: eslint-plugin-jsonc
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: markdownlint-cli
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: ts-jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <[email protected]>

* rebuild dist

Signed-off-by: Brian DeHamer <[email protected]>

---------

Signed-off-by: dependabot[bot] <[email protected]>
Signed-off-by: Brian DeHamer <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Brian DeHamer <[email protected]>
2024-05-28 13:27:14 -07:00
Brian DeHamerandGitHub 5b17eb7cb0 fix bug w/ private-signing input (#77)
Signed-off-by: Brian DeHamer <[email protected]>
2024-05-28 13:26:14 -07:00
Brian DeHamerandGitHub faa6467995 refactor core attestation logic (#73)
Signed-off-by: Brian DeHamer <[email protected]>
2024-05-28 11:00:03 -07:00
Brian DeHamerandGitHub 3ff4eb4c69 centralize collection of action inputs (#72)
Signed-off-by: Brian DeHamer <[email protected]>
2024-05-24 11:01:44 -07:00
Brian DeHamerandGitHub 074a7714de bump @sigstore/oci from 0.3.3 to 0.3.4 (#71)
Signed-off-by: Brian DeHamer <[email protected]>
2024-05-23 08:32:51 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Brian DeHamer
72776582f8 Bump csv-parse from 5.5.5 to 5.5.6 in the npm-production group (#69)
* ---
updated-dependencies:
- dependency-name: csv-parse
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-production
...

Signed-off-by: dependabot[bot] <[email protected]>

* regenerate dist

Signed-off-by: Brian DeHamer <[email protected]>

---------

Signed-off-by: dependabot[bot] <[email protected]>
Signed-off-by: Brian DeHamer <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Brian DeHamer <[email protected]>
2024-05-23 08:18:25 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Brian DeHamer
e4e9a599b8 Bump the npm-development group with 4 updates (#68)
* ---
updated-dependencies:
- dependency-name: "@sigstore/mock"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <[email protected]>

* rebuild dist

Signed-off-by: Brian DeHamer <[email protected]>

---------

Signed-off-by: dependabot[bot] <[email protected]>
Signed-off-by: Brian DeHamer <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Brian DeHamer <[email protected]>
2024-05-23 08:13:06 -07:00
Brian DeHamerandGitHub 80d9f23382 process subjects in batches (#67)
Signed-off-by: Brian DeHamer <[email protected]>
2024-05-22 07:55:00 -07:00
17 changed files with 1371 additions and 864 deletions
-1
View File
@@ -47,4 +47,3 @@ jobs:
VALIDATE_ALL_CODEBASE: true VALIDATE_ALL_CODEBASE: true
VALIDATE_JAVASCRIPT_STANDARD: false VALIDATE_JAVASCRIPT_STANDARD: false
VALIDATE_JSCPD: false VALIDATE_JSCPD: false
VALIDATE_GITHUB_ACTIONS: false
+23 -7
View File
@@ -65,7 +65,7 @@ See [action.yml](action.yml)
with: with:
# Path to the artifact serving as the subject of the attestation. Must # Path to the artifact serving as the subject of the attestation. Must
# specify exactly one of "subject-path" or "subject-digest". May contain # specify exactly one of "subject-path" or "subject-digest". May contain
# a glob pattern or list of paths (total subject count cannot exceed 64). # a glob pattern or list of paths (total subject count cannot exceed 2500).
subject-path: subject-path:
# SHA256 digest of the subject for the attestation. Must be in the form # SHA256 digest of the subject for the attestation. Must be in the form
@@ -81,12 +81,14 @@ See [action.yml](action.yml)
# URI identifying the type of the predicate. # URI identifying the type of the predicate.
predicate-type: predicate-type:
# JSON string containing the value for the attestation predicate. Must # String containing the value for the attestation predicate. String length
# supply exactly one of "predicate-path" or "predicate". # cannot exceed 16MB. Must supply exactly one of "predicate-path" or
# "predicate".
predicate: predicate:
# Path to the file which contains the JSON content for the attestation # Path to the file which contains the content for the attestation predicate.
# predicate. Must supply exactly one of "predicate-path" or "predicate". # File size cannot exceed 16MB. Must supply exactly one of "predicate-path"
# or "predicate".
predicate-path: predicate-path:
# Whether to push the attestation to the image registry. Requires that the # Whether to push the attestation to the image registry. Requires that the
@@ -115,6 +117,20 @@ If multiple subjects are being attested at the same time, each attestation will
be written to the output file on a separate line (using the [JSON Lines][7] be written to the output file on a separate line (using the [JSON Lines][7]
format). format).
## Attestation Limits
### Subject Limits
No more than 2500 subjects can be attested at the same time. Subjects will be
processed in batches 50. After the initial group of 50, each subsequent batch
will incur an exponentially increasing amount of delay (capped at 1 minute of
delay per batch) to avoid overwhelming the attestation API.
### Predicate Limits
Whether supplied via the `predicate` or `predicatePath` input, the predicate
string cannot exceed 16MB.
## Examples ## Examples
### Identify Subject by Path ### Identify Subject by Path
@@ -175,8 +191,8 @@ 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.
> **NOTE**: When pushing to Docker Hub, please use "docker.io" as the > **NOTE**: When pushing to Docker Hub, please use "docker.io" as the registry
> registry portion of the image name. > portion of the image name.
```yaml ```yaml
name: build-attested-image name: build-attested-image
+5
View File
@@ -2,12 +2,17 @@
* Unit tests for the action's entrypoint, src/index.ts * Unit tests for the action's entrypoint, src/index.ts
*/ */
import * as core from '@actions/core'
import * as main from '../src/main' import * as main from '../src/main'
// Mock the action's entrypoint // Mock the action's entrypoint
const runMock = jest.spyOn(main, 'run').mockImplementation() const runMock = jest.spyOn(main, 'run').mockImplementation()
const getBooleanInputMock = jest.spyOn(core, 'getBooleanInput')
describe('index', () => { describe('index', () => {
beforeEach(() => {
getBooleanInputMock.mockImplementation(() => false)
})
it('calls run when imported', async () => { it('calls run when imported', async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports // eslint-disable-next-line @typescript-eslint/no-require-imports
require('../src/index') require('../src/index')
+139 -61
View File
@@ -20,8 +20,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 startGroupMock = jest.spyOn(core, 'startGroup') const startGroupMock = jest.spyOn(core, 'startGroup')
const getInputMock = jest.spyOn(core, 'getInput')
const getBooleanInputMock = jest.spyOn(core, 'getBooleanInput')
const setOutputMock = jest.spyOn(core, 'setOutput') const setOutputMock = jest.spyOn(core, 'setOutput')
const setFailedMock = jest.spyOn(core, 'setFailed') const setFailedMock = jest.spyOn(core, 'setFailed')
@@ -38,12 +36,28 @@ const runMock = jest.spyOn(main, 'run')
const mockAgent = new MockAgent() const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent) setGlobalDispatcher(mockAgent)
const defaultInputs: main.RunInputs = {
predicate: '',
predicateType: '',
predicatePath: '',
subjectName: '',
subjectDigest: '',
subjectPath: '',
pushToRegistry: false,
githubToken: '',
privateSigning: false,
batchSize: 50
}
describe('action', () => { describe('action', () => {
// Capture original environment variables and GitHub context so we can restore // Capture original environment variables and GitHub context so we can restore
// them after each test // them after each test
const originalEnv = process.env const originalEnv = process.env
const originalContext = { ...github.context } const originalContext = { ...github.context }
// Mock OIDC token endpoint
const tokenURL = 'https://token.url'
// Fake an OIDC token // Fake an OIDC token
const oidcSubject = '[email protected]' const oidcSubject = '[email protected]'
const oidcPayload = { sub: oidcSubject, iss: '' } const oidcPayload = { sub: oidcSubject, iss: '' }
@@ -62,9 +76,6 @@ describe('action', () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
// Mock OIDC token endpoint
const tokenURL = 'https://token.url'
nock(tokenURL) nock(tokenURL)
.get('/') .get('/')
.query({ audience: 'sigstore' }) .query({ audience: 'sigstore' })
@@ -95,24 +106,22 @@ describe('action', () => {
}) })
describe('when ACTIONS_ID_TOKEN_REQUEST_URL is not set', () => { describe('when ACTIONS_ID_TOKEN_REQUEST_URL is not set', () => {
const inputs = { const inputs: main.RunInputs = {
'subject-digest': subjectDigest, ...defaultInputs,
'subject-name': subjectName, subjectDigest,
'predicate-type': predicateType, subjectName,
predicateType,
predicate, predicate,
'github-token': 'gh-token' githubToken: 'gh-token'
} }
beforeEach(() => { beforeEach(() => {
// Nullify the OIDC token URL // Nullify the OIDC token URL
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = '' process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ''
getInputMock.mockImplementation(mockInput(inputs))
getBooleanInputMock.mockImplementation(() => false)
}) })
it('sets a failed status', async () => { it('sets a failed status', async () => {
await main.run() await main.run(inputs)
expect(runMock).toHaveReturned() expect(runMock).toHaveReturned()
expect(setFailedMock).toHaveBeenCalledWith( expect(setFailedMock).toHaveBeenCalledWith(
@@ -124,12 +133,8 @@ describe('action', () => {
}) })
describe('when no inputs are provided', () => { describe('when no inputs are provided', () => {
beforeEach(() => {
getInputMock.mockImplementation(() => '')
})
it('sets a failed status', async () => { it('sets a failed status', async () => {
await main.run() await main.run(defaultInputs)
expect(runMock).toHaveReturned() expect(runMock).toHaveReturned()
expect(setFailedMock).toHaveBeenCalledWith( expect(setFailedMock).toHaveBeenCalledWith(
@@ -139,12 +144,13 @@ describe('action', () => {
}) })
describe('when the repository is private', () => { describe('when the repository is private', () => {
const inputs = { const inputs: main.RunInputs = {
'subject-digest': subjectDigest, ...defaultInputs,
'subject-name': subjectName, subjectDigest,
'predicate-type': predicateType, subjectName,
predicateType,
predicate, predicate,
'github-token': 'gh-token' githubToken: 'gh-token'
} }
beforeEach(async () => { beforeEach(async () => {
@@ -154,9 +160,6 @@ describe('action', () => {
repo: { owner: 'foo', repo: 'bar' } repo: { owner: 'foo', repo: 'bar' }
}) })
getInputMock.mockImplementation(mockInput(inputs))
getBooleanInputMock.mockImplementation(() => false)
await mockFulcio({ await mockFulcio({
baseURL: 'https://fulcio.githubapp.com', baseURL: 'https://fulcio.githubapp.com',
strict: false strict: false
@@ -165,7 +168,7 @@ describe('action', () => {
}) })
it('invokes the action w/o error', async () => { it('invokes the action w/o error', async () => {
await main.run() await main.run(inputs)
expect(runMock).toHaveReturned() expect(runMock).toHaveReturned()
expect(setFailedMock).not.toHaveBeenCalledWith() expect(setFailedMock).not.toHaveBeenCalledWith()
@@ -204,12 +207,14 @@ describe('action', () => {
const getRegCredsSpy = jest.spyOn(oci, 'getRegistryCredentials') const getRegCredsSpy = jest.spyOn(oci, 'getRegistryCredentials')
const attachArtifactSpy = jest.spyOn(oci, 'attachArtifactToImage') const attachArtifactSpy = jest.spyOn(oci, 'attachArtifactToImage')
const inputs = { const inputs: main.RunInputs = {
'subject-digest': subjectDigest, ...defaultInputs,
'subject-name': subjectName, subjectDigest,
'predicate-type': predicateType, subjectName,
predicateType,
predicate, predicate,
'github-token': 'gh-token' githubToken: 'gh-token',
pushToRegistry: true
} }
beforeEach(async () => { beforeEach(async () => {
@@ -219,11 +224,6 @@ describe('action', () => {
repo: { owner: 'foo', repo: 'bar' } repo: { owner: 'foo', repo: 'bar' }
}) })
// Mock the action's inputs
getInputMock.mockImplementation(mockInput(inputs))
// This is where we mock the push-to-registry input
getBooleanInputMock.mockImplementation(() => true)
await mockFulcio({ await mockFulcio({
baseURL: 'https://fulcio.sigstore.dev', baseURL: 'https://fulcio.sigstore.dev',
strict: false strict: false
@@ -244,7 +244,7 @@ describe('action', () => {
}) })
it('invokes the action w/o error', async () => { it('invokes the action w/o error', async () => {
await main.run() await main.run(inputs)
expect(runMock).toHaveReturned() expect(runMock).toHaveReturned()
expect(setFailedMock).not.toHaveBeenCalled() expect(setFailedMock).not.toHaveBeenCalled()
@@ -289,11 +289,16 @@ describe('action', () => {
}) })
}) })
describe('when too many subjects are specified', () => { describe('when the subject count exceeds the batch size', () => {
let dir = '' let dir = ''
const filename = 'subject'
let scope: nock.Scope
beforeEach(async () => { beforeEach(async () => {
const filename = 'subject' // Start from scratch
nock.cleanAll()
const subjectCount = 5
const content = 'file content' const content = 'file content'
// Set-up temp directory // Set-up temp directory
@@ -301,7 +306,90 @@ describe('action', () => {
dir = await fs.mkdtemp(tmpDir + path.sep) dir = await fs.mkdtemp(tmpDir + path.sep)
// Add files for glob testing // Add files for glob testing
for (let i = 0; i < 65; i++) { for (let i = 0; i < subjectCount; i++) {
await fs.writeFile(path.join(dir, `${filename}-${i}`), content)
// Set-up a Fulcio mock for each subject
await mockFulcio({
baseURL: 'https://fulcio.githubapp.com',
strict: false
})
// Set-up a TSA mock for each subject
await mockTSA({ baseURL: 'https://timestamp.githubapp.com' })
// Set-up a GH API mock for each subject
mockAgent
.get('https://api.github.com')
.intercept({
path: /^\/repos\/.*\/.*\/attestations$/,
method: 'post'
})
.reply(201, { id: attestationID })
}
// Set-up a OIDC token mock for each subject
scope = nock(tokenURL)
.get('/')
.query({ audience: 'sigstore' })
.times(subjectCount)
.reply(200, { value: oidcToken })
// Set the GH context with private repository visibility and a repo owner.
setGHContext({
payload: { repository: { visibility: 'private' } },
repo: { owner: 'foo', repo: 'bar' }
})
})
afterEach(async () => {
// Clean-up temp directory
await fs.rm(dir, { recursive: true })
})
it('invokes the action w/o error', async () => {
const inputs: main.RunInputs = {
...defaultInputs,
subjectPath: path.join(dir, `${filename}-*`),
predicateType,
predicate,
githubToken: 'gh-token',
batchSize: 2
}
await main.run(inputs)
expect(runMock).toHaveReturned()
expect(setFailedMock).not.toHaveBeenCalled()
expect(infoMock).toHaveBeenNthCalledWith(
1,
expect.stringMatching('Processing subject batch 1/3')
)
expect(infoMock).toHaveBeenNthCalledWith(
10,
expect.stringMatching('Processing subject batch 2/3')
)
expect(infoMock).toHaveBeenNthCalledWith(
19,
expect.stringMatching('Processing subject batch 3/3')
)
expect(scope.isDone()).toBe(true)
})
})
describe('when the subject count exceeds the max', () => {
let dir = ''
const filename = 'subject'
beforeEach(async () => {
const subjectCount = 2501
const content = 'file content'
// Set-up temp directory
const tmpDir = await fs.realpath(os.tmpdir())
dir = await fs.mkdtemp(tmpDir + path.sep)
// Add files for glob testing
for (let i = 0; i < subjectCount; i++) {
await fs.writeFile(path.join(dir, `${filename}-${i}`), content) await fs.writeFile(path.join(dir, `${filename}-${i}`), content)
} }
@@ -310,14 +398,6 @@ describe('action', () => {
payload: { repository: { visibility: 'private' } }, payload: { repository: { visibility: 'private' } },
repo: { owner: 'foo', repo: 'bar' } repo: { owner: 'foo', repo: 'bar' }
}) })
// Mock the action's inputs
getInputMock.mockImplementation(
mockInput({
predicate: '{}',
'subject-path': path.join(dir, `${filename}-*`)
})
)
}) })
afterEach(async () => { afterEach(async () => {
@@ -326,27 +406,25 @@ describe('action', () => {
}) })
it('sets a failed status', async () => { it('sets a failed status', async () => {
await main.run() const inputs: main.RunInputs = {
...defaultInputs,
subjectPath: path.join(dir, `${filename}-*`),
predicateType,
predicate,
githubToken: 'gh-token'
}
await main.run(inputs)
expect(runMock).toHaveReturned() expect(runMock).toHaveReturned()
expect(setFailedMock).toHaveBeenCalledWith( expect(setFailedMock).toHaveBeenCalledWith(
new Error( new Error(
'Too many subjects specified. The maximum number of subjects is 64.' 'Too many subjects specified. The maximum number of subjects is 2500.'
) )
) )
}) })
}) })
}) })
function mockInput(inputs: Record<string, string>): typeof core.getInput {
return (name: string): string => {
if (name in inputs) {
return inputs[name]
}
return ''
}
}
// Stubbing the GitHub context is a bit tricky. We need to use // Stubbing the GitHub context is a bit tricky. We need to use
// `Object.defineProperty` because `github.context` is read-only. // `Object.defineProperty` because `github.context` is read-only.
function setGHContext(context: object): void { function setGHContext(context: object): void {
+77 -40
View File
@@ -1,92 +1,129 @@
import fs from 'fs/promises' import fs from 'fs/promises'
import os from 'os' import os from 'os'
import path from 'path' import path from 'path'
import { predicateFromInputs } from '../src/predicate' import { predicateFromInputs, PredicateInputs } from '../src/predicate'
describe('subjectFromInputs', () => { describe('subjectFromInputs', () => {
afterEach(() => { const blankInputs: PredicateInputs = {
process.env['INPUT_PREDICATE'] = '' predicateType: '',
process.env['INPUT_PREDICATE-PATH'] = '' predicate: '',
process.env['INPUT_PREDICATE-TYPE'] = '' predicatePath: ''
}) }
describe('when no inputs are provided', () => { describe('when no inputs are provided', () => {
it('throws an error', () => { it('throws an error', () => {
expect(() => predicateFromInputs()).toThrow(/predicate-type/i) expect(() => predicateFromInputs(blankInputs)).toThrow(/predicate-type/i)
}) })
}) })
describe('when neither predicate path nor predicate are provided', () => { describe('when neither predicate path nor predicate are provided', () => {
beforeEach(() => {
process.env['INPUT_PREDICATE-TYPE'] = 'https://example.com/predicate'
})
it('throws an error', () => { it('throws an error', () => {
expect(() => predicateFromInputs()).toThrow( const inputs: PredicateInputs = {
...blankInputs,
predicateType: 'https://example.com/predicate'
}
expect(() => predicateFromInputs(inputs)).toThrow(
/one of predicate-path or predicate must be provided/i /one of predicate-path or predicate must be provided/i
) )
}) })
}) })
describe('when both predicate path and predicate are provided', () => { describe('when both predicate path and predicate are provided', () => {
beforeEach(() => {
process.env['INPUT_PREDICATE-PATH'] = 'path/to/predicate'
process.env['INPUT_PREDICATE'] = '{}'
process.env['INPUT_PREDICATE-TYPE'] = 'https://example.com/predicate'
})
it('throws an error', () => { it('throws an error', () => {
expect(() => predicateFromInputs()).toThrow( const inputs: PredicateInputs = {
predicateType: 'https://example.com/predicate',
predicate: '{}',
predicatePath: 'path/to/predicate'
}
expect(() => predicateFromInputs(inputs)).toThrow(
/only one of predicate-path or predicate may be provided/i /only one of predicate-path or predicate may be provided/i
) )
}) })
}) })
describe('when specifying a predicate path', () => { describe('when specifying a predicate path', () => {
let dir = '' const predicateType = 'https://example.com/predicate'
const filename = 'subject'
const content = '{}' const content = '{}'
let predicatePath = ''
beforeEach(async () => { beforeEach(async () => {
// Set-up temp directory // Set-up temp directory
const tmpDir = await fs.realpath(os.tmpdir()) const tmpDir = await fs.realpath(os.tmpdir())
dir = await fs.mkdtemp(tmpDir + path.sep) const dir = await fs.mkdtemp(tmpDir + path.sep)
const filename = 'subject'
predicatePath = path.join(dir, filename)
// Write file to temp directory // Write file to temp directory
await fs.writeFile(path.join(dir, filename), content) await fs.writeFile(predicatePath, content)
}) })
afterEach(async () => { afterEach(async () => {
// Clean-up temp directory // Clean-up temp directory
await fs.rm(dir, { recursive: true }) await fs.rm(path.parse(predicatePath).dir, { recursive: true })
})
beforeEach(() => {
process.env['INPUT_PREDICATE-PATH'] = path.join(dir, filename)
process.env['INPUT_PREDICATE-TYPE'] = 'https://example.com/predicate'
}) })
it('returns the predicate', () => { it('returns the predicate', () => {
expect(predicateFromInputs()).toEqual({ const inputs: PredicateInputs = {
type: 'https://example.com/predicate', ...blankInputs,
params: {} predicateType,
predicatePath
}
expect(predicateFromInputs(inputs)).toEqual({
type: predicateType,
params: JSON.parse(content)
}) })
}) })
}) })
describe('when specifying a predicate path that does not exist', () => {
const predicateType = 'https://example.com/predicate'
const predicatePath = 'foo'
it('returns the predicate', () => {
const inputs: PredicateInputs = {
...blankInputs,
predicateType,
predicatePath
}
expect(() => predicateFromInputs(inputs)).toThrow(/file not found/)
})
})
describe('when specifying a predicate value', () => { describe('when specifying a predicate value', () => {
const predicateType = 'https://example.com/predicate'
const content = '{}' const content = '{}'
beforeEach(() => { it('returns the predicate', () => {
process.env['INPUT_PREDICATE'] = content const inputs: PredicateInputs = {
process.env['INPUT_PREDICATE-TYPE'] = 'https://example.com/predicate' ...blankInputs,
predicateType,
predicate: content
}
expect(predicateFromInputs(inputs)).toEqual({
type: predicateType,
params: JSON.parse(content)
})
})
}) })
it('returns the predicate', () => { describe('when specifying a predicate value exceeding the max size', () => {
expect(predicateFromInputs()).toEqual({ const predicateType = 'https://example.com/predicate'
type: 'https://example.com/predicate', const content = JSON.stringify({ a: 'a'.repeat(16 * 1024 * 1024) })
params: {}
}) it('throws an error', () => {
const inputs: PredicateInputs = {
...blankInputs,
predicateType,
predicate: content
}
expect(() => predicateFromInputs(inputs)).toThrow(
/predicate string exceeds maximum/
)
}) })
}) })
}) })
+15
View File
@@ -0,0 +1,15 @@
import { highlight, mute } from '../src/style'
describe('style', () => {
describe('highlight', () => {
it('adds cyan color to the string', () => {
expect(highlight('foo')).toBe('\x1B[36mfoo\x1B[39m')
})
})
describe('mute', () => {
it('adds gray color to the string', () => {
expect(mute('foo')).toBe('\x1B[38;5;244mfoo\x1B[39m')
})
})
})
+107 -99
View File
@@ -2,47 +2,45 @@ import crypto from 'crypto'
import fs from 'fs/promises' import fs from 'fs/promises'
import os from 'os' import os from 'os'
import path from 'path' import path from 'path'
import { subjectFromInputs } from '../src/subject' import { subjectFromInputs, SubjectInputs } from '../src/subject'
describe('subjectFromInputs', () => { describe('subjectFromInputs', () => {
beforeEach(() => { const blankInputs: SubjectInputs = {
process.env['INPUT_PUSH-TO-REGISTRY'] = 'false' subjectPath: '',
}) subjectName: '',
subjectDigest: ''
afterEach(() => { }
process.env['INPUT_SUBJECT-PATH'] = ''
process.env['INPUT_SUBJECT-DIGEST'] = ''
process.env['INPUT_SUBJECT-NAME'] = ''
})
describe('when no inputs are provided', () => { describe('when no inputs are provided', () => {
it('throws an error', async () => { it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow( await expect(subjectFromInputs(blankInputs)).rejects.toThrow(
/one of subject-path or subject-digest must be provided/i /one of subject-path or subject-digest must be provided/i
) )
}) })
}) })
describe('when both subject path and subject digest are provided', () => { describe('when both subject path and subject digest are provided', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-PATH'] = 'path/to/subject'
process.env['INPUT_SUBJECT-DIGEST'] = 'digest'
})
it('throws an error', async () => { it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow( const inputs: SubjectInputs = {
subjectName: 'foo',
subjectPath: 'path/to/subject',
subjectDigest: 'digest'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/only one of subject-path or subject-digest may be provided/i /only one of subject-path or subject-digest may be provided/i
) )
}) })
}) })
describe('when subject digest is provided but not the name', () => { describe('when subject digest is provided but not the name', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-DIGEST'] = 'digest'
})
it('throws an error', async () => { it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow( const inputs: SubjectInputs = {
...blankInputs,
subjectDigest: 'digest'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject-name must be provided when using subject-digest/i /subject-name must be provided when using subject-digest/i
) )
}) })
@@ -52,39 +50,42 @@ describe('subjectFromInputs', () => {
const name = 'Subject' const name = 'Subject'
describe('when the digest is malformed', () => { describe('when the digest is malformed', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-DIGEST'] = 'digest'
process.env['INPUT_SUBJECT-NAME'] = name
})
it('throws an error', async () => { it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow( const inputs: SubjectInputs = {
...blankInputs,
subjectDigest: 'digest',
subjectName: name
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject-digest must be in the format "sha256:<hex-digest>"/i /subject-digest must be in the format "sha256:<hex-digest>"/i
) )
}) })
}) })
describe('when the alogrithm is not supported', () => { describe('when the alogrithm is not supported', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-DIGEST'] = 'md5:deadbeef'
process.env['INPUT_SUBJECT-NAME'] = name
})
it('throws an error', async () => { it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow( const inputs: SubjectInputs = {
...blankInputs,
subjectDigest: 'md5:deadbeef',
subjectName: name
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject-digest must be in the format "sha256:<hex-digest>"/i /subject-digest must be in the format "sha256:<hex-digest>"/i
) )
}) })
}) })
describe('when the sha256 digest is malformed', () => { describe('when the sha256 digest is malformed', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-DIGEST'] = 'sha256:deadbeef'
process.env['INPUT_SUBJECT-NAME'] = name
})
it('throws an error', async () => { it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow( const inputs: SubjectInputs = {
...blankInputs,
subjectDigest: 'sha256:deadbeef',
subjectName: name
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject-digest must be in the format "sha256:<hex-digest>"/i /subject-digest must be in the format "sha256:<hex-digest>"/i
) )
}) })
@@ -95,13 +96,14 @@ describe('subjectFromInputs', () => {
const digest = const digest =
'7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32' '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
beforeEach(() => {
process.env['INPUT_SUBJECT-DIGEST'] = `${alg}:${digest}`
process.env['INPUT_SUBJECT-NAME'] = name
})
it('returns the subject', async () => { it('returns the subject', async () => {
const subject = await subjectFromInputs() const inputs: SubjectInputs = {
...blankInputs,
subjectDigest: `${alg}:${digest}`,
subjectName: name
}
const subject = await subjectFromInputs(inputs)
expect(subject).toBeDefined() expect(subject).toBeDefined()
expect(subject).toHaveLength(1) expect(subject).toHaveLength(1)
@@ -110,20 +112,21 @@ describe('subjectFromInputs', () => {
}) })
}) })
describe('when the push-to-registry is true', () => { describe('when the downcaseName is true', () => {
const imageName = 'ghcr.io/FOO/bar' const imageName = 'ghcr.io/FOO/bar'
const alg = 'sha256' const alg = 'sha256'
const digest = const digest =
'7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32' '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
beforeEach(() => {
process.env['INPUT_SUBJECT-DIGEST'] = `${alg}:${digest}`
process.env['INPUT_SUBJECT-NAME'] = imageName
process.env['INPUT_PUSH-TO-REGISTRY'] = 'true'
})
it('returns the subject (with name downcased)', async () => { it('returns the subject (with name downcased)', async () => {
const subject = await subjectFromInputs() const inputs: SubjectInputs = {
...blankInputs,
subjectDigest: `${alg}:${digest}`,
subjectName: imageName,
downcaseName: true
}
const subject = await subjectFromInputs(inputs)
expect(subject).toBeDefined() expect(subject).toBeDefined()
expect(subject).toHaveLength(1) expect(subject).toHaveLength(1)
@@ -135,19 +138,20 @@ describe('subjectFromInputs', () => {
describe('when specifying a subject path', () => { describe('when specifying a subject path', () => {
describe('when the file does NOT exist', () => { describe('when the file does NOT exist', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-PATH'] = '/f/a/k/e'
})
it('throws an error', async () => { it('throws an error', async () => {
await expect(subjectFromInputs()).rejects.toThrow( const inputs: SubjectInputs = {
...blankInputs,
subjectPath: '/f/a/k/e'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/could not find subject at path/i /could not find subject at path/i
) )
}) })
}) })
}) })
describe('when the file eixts', () => { describe('when the file exists', () => {
let dir = '' let dir = ''
const filename = 'subject' const filename = 'subject'
const content = 'file content' const content = 'file content'
@@ -178,12 +182,13 @@ describe('subjectFromInputs', () => {
}) })
describe('when no name is provided', () => { describe('when no name is provided', () => {
beforeEach(() => {
process.env['INPUT_SUBJECT-PATH'] = path.join(dir, filename)
})
it('returns the subject', async () => { it('returns the subject', async () => {
const subject = await subjectFromInputs() const inputs: SubjectInputs = {
...blankInputs,
subjectPath: path.join(dir, filename)
}
const subject = await subjectFromInputs(inputs)
expect(subject).toBeDefined() expect(subject).toBeDefined()
expect(subject).toHaveLength(1) expect(subject).toHaveLength(1)
@@ -195,13 +200,14 @@ describe('subjectFromInputs', () => {
describe('when a name is provided', () => { describe('when a name is provided', () => {
const name = 'mysubject' const name = 'mysubject'
beforeEach(() => {
process.env['INPUT_SUBJECT-PATH'] = path.join(dir, filename)
process.env['INPUT_SUBJECT-NAME'] = name
})
it('returns the subject', async () => { it('returns the subject', async () => {
const subject = await subjectFromInputs() const inputs: SubjectInputs = {
...blankInputs,
subjectPath: path.join(dir, filename),
subjectName: name
}
const subject = await subjectFromInputs(inputs)
expect(subject).toBeDefined() expect(subject).toBeDefined()
expect(subject).toHaveLength(1) expect(subject).toHaveLength(1)
@@ -211,12 +217,13 @@ describe('subjectFromInputs', () => {
}) })
describe('when a file glob is supplied', () => { describe('when a file glob is supplied', () => {
beforeEach(async () => {
process.env['INPUT_SUBJECT-PATH'] = path.join(dir, 'subject-*')
})
it('returns the multiple subjects', async () => { it('returns the multiple subjects', async () => {
const subjects = await subjectFromInputs() const inputs: SubjectInputs = {
...blankInputs,
subjectPath: path.join(dir, 'subject-*')
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined() expect(subjects).toBeDefined()
expect(subjects).toHaveLength(3) expect(subjects).toHaveLength(3)
@@ -230,12 +237,13 @@ describe('subjectFromInputs', () => {
}) })
describe('when a file glob is supplied which also matches non-files', () => { describe('when a file glob is supplied which also matches non-files', () => {
beforeEach(async () => {
process.env['INPUT_SUBJECT-PATH'] = `${dir}*`
})
it('returns the subjects (excluding non-files)', async () => { it('returns the subjects (excluding non-files)', async () => {
const subjects = await subjectFromInputs() const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${dir}*`
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined() expect(subjects).toBeDefined()
expect(subjects).toHaveLength(7) expect(subjects).toHaveLength(7)
@@ -243,13 +251,13 @@ describe('subjectFromInputs', () => {
}) })
describe('when a comma-separated list is supplied', () => { describe('when a comma-separated list is supplied', () => {
beforeEach(async () => {
process.env['INPUT_SUBJECT-PATH'] =
`${path.join(dir, 'subject-1')},${path.join(dir, 'subject-2')}`
})
it('returns the multiple subjects', async () => { it('returns the multiple subjects', async () => {
const subjects = await subjectFromInputs() const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${path.join(dir, 'subject-1')},${path.join(dir, 'subject-2')}`
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined() expect(subjects).toBeDefined()
expect(subjects).toHaveLength(2) expect(subjects).toHaveLength(2)
@@ -266,13 +274,13 @@ describe('subjectFromInputs', () => {
}) })
describe('when a multi-line list is supplied', () => { describe('when a multi-line list is supplied', () => {
beforeEach(async () => {
process.env['INPUT_SUBJECT-PATH'] =
`${path.join(dir, 'subject-0')}\n${path.join(dir, 'subject-2')}`
})
it('returns the multiple subjects', async () => { it('returns the multiple subjects', async () => {
const subjects = await subjectFromInputs() const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${path.join(dir, 'subject-0')}\n${path.join(dir, 'subject-2')}`
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined() expect(subjects).toBeDefined()
expect(subjects).toHaveLength(2) expect(subjects).toHaveLength(2)
@@ -289,13 +297,13 @@ describe('subjectFromInputs', () => {
}) })
describe('when a multi-line glob list is supplied', () => { describe('when a multi-line glob list is supplied', () => {
beforeEach(async () => {
process.env['INPUT_SUBJECT-PATH'] =
`${path.join(dir, 'subject-*')}\n ${path.join(dir, 'other-*')} `
})
it('returns the multiple subjects', async () => { it('returns the multiple subjects', async () => {
const subjects = await subjectFromInputs() const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${path.join(dir, 'subject-*')}\n ${path.join(dir, 'other-*')} `
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined() expect(subjects).toBeDefined()
expect(subjects).toHaveLength(6) expect(subjects).toHaveLength(6)
+6 -4
View File
@@ -10,7 +10,7 @@ inputs:
description: > description: >
Path to the artifact serving as the subject of the attestation. Must Path to the artifact serving as the subject of the attestation. Must
specify exactly one of "subject-path" or "subject-digest". May contain a specify exactly one of "subject-path" or "subject-digest". May contain a
glob pattern or list of paths (total subject count cannot exceed 64). glob pattern or list of paths (total subject count cannot exceed 2500).
required: false required: false
subject-digest: subject-digest:
description: > description: >
@@ -30,13 +30,15 @@ inputs:
required: true required: true
predicate: predicate:
description: > description: >
String containing the value for the attestation predicate. Must supply String containing the value for the attestation predicate. String length
exactly one of "predicate-path" or "predicate". cannot exceed 16MB. Must supply exactly one of "predicate-path" or
"predicate".
required: false required: false
predicate-path: predicate-path:
description: > description: >
Path to the file which contains the content for the attestation predicate. Path to the file which contains the content for the attestation predicate.
Must supply exactly one of "predicate-path" or "predicate". File size cannot exceed 16MB. Must supply exactly one of "predicate-path"
or "predicate".
required: false required: false
push-to-registry: push-to-registry:
description: > description: >
Generated Vendored
+383 -232
View File
File diff suppressed because it is too large Load Diff
+306 -249
View File
File diff suppressed because it is too large Load Diff
+14 -14
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": "1.1.2", "version": "1.3.0",
"author": "", "author": "",
"private": true, "private": true,
"homepage": "https://github.com/actions/attest", "homepage": "https://github.com/actions/attest",
@@ -69,32 +69,32 @@
] ]
}, },
"dependencies": { "dependencies": {
"@actions/attest": "^1.2.1", "@actions/attest": "^1.3.0",
"@actions/core": "^1.10.1", "@actions/core": "^1.10.1",
"@actions/glob": "^0.4.0", "@actions/glob": "^0.4.0",
"@sigstore/oci": "^0.3.3", "@sigstore/oci": "^0.3.6",
"csv-parse": "^5.5.5" "csv-parse": "^5.5.6"
}, },
"devDependencies": { "devDependencies": {
"@sigstore/mock": "^0.7.3", "@sigstore/mock": "^0.7.4",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/make-fetch-happen": "^10.0.4", "@types/make-fetch-happen": "^10.0.4",
"@types/node": "^20.12.11", "@types/node": "^20.14.2",
"@typescript-eslint/eslint-plugin": "^7.9.0", "@typescript-eslint/eslint-plugin": "^7.13.0",
"@typescript-eslint/parser": "^7.9.0", "@typescript-eslint/parser": "^7.13.0",
"@vercel/ncc": "^0.38.1", "@vercel/ncc": "^0.38.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-github": "^4.10.2", "eslint-plugin-github": "^5.0.1",
"eslint-plugin-jest": "^28.5.0", "eslint-plugin-jest": "^28.6.0",
"eslint-plugin-jsonc": "^2.15.1", "eslint-plugin-jsonc": "^2.16.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0", "jest": "^29.7.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"markdownlint-cli": "^0.40.0", "markdownlint-cli": "^0.41.0",
"nock": "^13.5.4", "nock": "^13.5.4",
"prettier": "^3.2.5", "prettier": "^3.3.1",
"prettier-eslint": "^16.3.0", "prettier-eslint": "^16.3.0",
"ts-jest": "^29.1.2", "ts-jest": "^29.1.4",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"undici": "^5.28.4" "undici": "^5.28.4"
} }
+67
View File
@@ -0,0 +1,67 @@
import { Attestation, Predicate, Subject, attest } from '@actions/attest'
import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci'
const OCI_TIMEOUT = 2000
const OCI_RETRY = 3
export type SigstoreInstance = 'public-good' | 'github'
export type AttestResult = Attestation & {
subjectName: string
subjectDigest: string
attestationDigest?: string
}
export const createAttestation = async (
subject: Subject,
predicate: Predicate,
opts: {
sigstoreInstance: SigstoreInstance
pushToRegistry: boolean
githubToken: string
}
): Promise<AttestResult> => {
// Sign provenance w/ Sigstore
const attestation = await attest({
subjectName: subject.name,
subjectDigest: subject.digest,
predicateType: predicate.type,
predicate: predicate.params,
sigstore: opts.sigstoreInstance,
token: opts.githubToken
})
const subDigest = subjectDigest(subject)
const result: AttestResult = {
...attestation,
subjectName: subject.name,
subjectDigest: subDigest
}
if (opts.pushToRegistry) {
const credentials = getRegistryCredentials(subject.name)
const artifact = await attachArtifactToImage({
credentials,
imageName: subject.name,
imageDigest: subDigest,
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
mediaType: attestation.bundle.mediaType,
annotations: {
'dev.sigstore.bundle.content': 'dsse-envelope',
'dev.sigstore.bundle.predicateType': predicate.type
},
fetchOpts: { timeout: OCI_TIMEOUT, retry: OCI_RETRY }
})
// Add the attestation's digest to the result
result.attestationDigest = artifact.digest
}
return result
}
// Returns the subject's digest as a formatted string of the form
// "<algorithm>:<digest>".
const subjectDigest = (subject: Subject): string => {
const alg = Object.keys(subject.digest).sort()[0]
return `${alg}:${subject.digest[alg]}`
}
+22 -2
View File
@@ -1,7 +1,27 @@
/** /**
* The entrypoint for the action. * The entrypoint for the action.
*/ */
import { run } from './main' import * as core from '@actions/core'
import { run, RunInputs } from './main'
const DEFAULT_BATCH_SIZE = 50
const inputs: RunInputs = {
subjectPath: core.getInput('subject-path'),
subjectName: core.getInput('subject-name'),
subjectDigest: core.getInput('subject-digest'),
predicateType: core.getInput('predicate-type'),
predicate: core.getInput('predicate'),
predicatePath: core.getInput('predicate-path'),
pushToRegistry: core.getBooleanInput('push-to-registry'),
githubToken: core.getInput('github-token'),
// undocumented -- not part of public interface
privateSigning: ['true', 'True', 'TRUE', '1'].includes(
core.getInput('private-signing')
),
// internal only
batchSize: DEFAULT_BATCH_SIZE
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
run() run(inputs)
+97 -96
View File
@@ -1,26 +1,25 @@
import { Attestation, Predicate, Subject, attest } from '@actions/attest'
import * as core from '@actions/core' import * as core from '@actions/core'
import * as github from '@actions/github' import * as github from '@actions/github'
import { attachArtifactToImage, getRegistryCredentials } from '@sigstore/oci'
import fs from 'fs' import fs from 'fs'
import os from 'os' import os from 'os'
import path from 'path' import path from 'path'
import { AttestResult, SigstoreInstance, createAttestation } from './attest'
import { SEARCH_PUBLIC_GOOD_URL } from './endpoints' import { SEARCH_PUBLIC_GOOD_URL } from './endpoints'
import { predicateFromInputs } from './predicate' import { PredicateInputs, predicateFromInputs } from './predicate'
import { subjectFromInputs } from './subject' import * as style from './style'
import { SubjectInputs, subjectFromInputs } from './subject'
type SigstoreInstance = 'public-good' | 'github'
type AttestedSubject = { subject: Subject; attestationID: string }
const COLOR_CYAN = '\x1B[36m'
const COLOR_GRAY = '\x1B[38;5;244m'
const COLOR_DEFAULT = '\x1B[39m'
const ATTESTATION_FILE_NAME = 'attestation.jsonl' const ATTESTATION_FILE_NAME = 'attestation.jsonl'
const DELAY_INTERVAL_MS = 75
const DELAY_MAX_MS = 1200
const MAX_SUBJECT_COUNT = 64 export type RunInputs = SubjectInputs &
PredicateInputs & {
const OCI_TIMEOUT = 2000 pushToRegistry: boolean
const OCI_RETRY = 3 githubToken: string
privateSigning: boolean
batchSize: number
}
/* istanbul ignore next */ /* istanbul ignore next */
const logHandler = (level: string, ...args: unknown[]): void => { const logHandler = (level: string, ...args: unknown[]): void => {
@@ -34,7 +33,7 @@ const logHandler = (level: string, ...args: unknown[]): void => {
* The main function for the action. * The main function for the action.
* @returns {Promise<void>} Resolves when the action is complete. * @returns {Promise<void>} Resolves when the action is complete.
*/ */
export async function run(): Promise<void> { export async function run(inputs: RunInputs): Promise<void> {
process.on('log', logHandler) process.on('log', logHandler)
// Provenance visibility will be public ONLY if we can confirm that the // Provenance visibility will be public ONLY if we can confirm that the
@@ -42,61 +41,62 @@ export async function run(): Promise<void> {
// Otherwise, it will be private. // Otherwise, it will be private.
const sigstoreInstance: SigstoreInstance = const sigstoreInstance: SigstoreInstance =
github.context.payload.repository?.visibility === 'public' && github.context.payload.repository?.visibility === 'public' &&
core.getInput('private-signing') !== 'true' !inputs.privateSigning
? 'public-good' ? 'public-good'
: 'github' : 'github'
try { try {
const atts: AttestedSubject[] = [] const atts: AttestResult[] = []
if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) { if (!process.env.ACTIONS_ID_TOKEN_REQUEST_URL) {
throw new Error( throw new Error(
'missing "id-token" permission. Please add "permissions: id-token: write" to your workflow.' 'missing "id-token" permission. Please add "permissions: id-token: write" to your workflow.'
) )
} }
// Gather list of subjets const subjects = await subjectFromInputs({
const subjects = await subjectFromInputs() ...inputs,
if (subjects.length > MAX_SUBJECT_COUNT) { downcaseName: inputs.pushToRegistry
throw new Error( })
`Too many subjects specified. The maximum number of subjects is ${MAX_SUBJECT_COUNT}.` const predicate = predicateFromInputs(inputs)
)
const outputPath = path.join(tempDir(), ATTESTATION_FILE_NAME)
core.setOutput('bundle-path', outputPath)
const subjectChunks = chunkArray(subjects, inputs.batchSize)
// Generate attestations for each subject serially, working in batches
for (let i = 0; i < subjectChunks.length; i++) {
if (subjectChunks.length > 1) {
core.info(`Processing subject batch ${i + 1}/${subjectChunks.length}`)
} }
const predicate = predicateFromInputs() // Calculate the delay time for this batch
const outputPath = path.join(tempDir(), ATTESTATION_FILE_NAME) const delayTime = delay(i)
// Generate attestations for each subject serially for (const subject of subjectChunks[i]) {
for (const subject of subjects) { // Delay between attestations (only when chunk size > 1)
const att = await createAttestation(subject, predicate, sigstoreInstance) if (i > 0) {
await new Promise(resolve => setTimeout(resolve, delayTime))
}
const att = await createAttestation(subject, predicate, {
sigstoreInstance,
pushToRegistry: inputs.pushToRegistry,
githubToken: inputs.githubToken
})
atts.push(att)
logAttestation(att, sigstoreInstance)
// Write attestation bundle to output file // Write attestation bundle to output file
fs.writeFileSync(outputPath, JSON.stringify(att.bundle) + os.EOL, { fs.writeFileSync(outputPath, JSON.stringify(att.bundle) + os.EOL, {
encoding: 'utf-8', encoding: 'utf-8',
flag: 'a' flag: 'a'
}) })
if (att.attestationID) {
atts.push({ subject, attestationID: att.attestationID })
} }
} }
if (atts.length > 0) { logSummary(atts)
core.summary.addHeading(
/* istanbul ignore next */
atts.length > 1 ? 'Attestations Created' : 'Attestation Created',
3
)
for (const { subject, attestationID } of atts) {
core.summary.addLink(
`${subject.name}@${subjectDigest(subject)}`,
attestationURL(attestationID)
)
}
core.summary.write()
}
core.setOutput('bundle-path', outputPath)
} catch (err) { } catch (err) {
// Fail the workflow run if an error occurs // Fail the workflow run if an error occurs
core.setFailed( core.setFailed(
@@ -108,7 +108,9 @@ export async function run(): Promise<void> {
if (err instanceof Error && 'cause' in err) { if (err instanceof Error && 'cause' in err) {
const innerErr = err.cause const innerErr = err.cause
core.info( core.info(
mute(innerErr instanceof Error ? innerErr.toString() : `${innerErr}`) style.mute(
innerErr instanceof Error ? innerErr.toString() : `${innerErr}`
)
) )
} }
} finally { } finally {
@@ -116,27 +118,19 @@ export async function run(): Promise<void> {
} }
} }
const createAttestation = async ( // Log details about the attestation to the GitHub Actions run
subject: Subject, const logAttestation = (
predicate: Predicate, attestation: AttestResult,
sigstoreInstance: SigstoreInstance sigstoreInstance: SigstoreInstance
): Promise<Attestation> => { ): void => {
// Sign provenance w/ Sigstore core.info(
const attestation = await attest({ `Attestation created for ${attestation.subjectName}@${attestation.subjectDigest}`
subjectName: subject.name, )
subjectDigest: subject.digest,
predicateType: predicate.type,
predicate: predicate.params,
sigstore: sigstoreInstance,
token: core.getInput('github-token')
})
core.info(`Attestation created for ${subject.name}@${subjectDigest(subject)}`)
const instanceName = const instanceName =
sigstoreInstance === 'public-good' ? 'Public Good' : 'GitHub' sigstoreInstance === 'public-good' ? 'Public Good' : 'GitHub'
core.startGroup( core.startGroup(
highlight( style.highlight(
`Attestation signed using certificate from ${instanceName} Sigstore instance` `Attestation signed using certificate from ${instanceName} Sigstore instance`
) )
) )
@@ -145,43 +139,44 @@ const createAttestation = async (
if (attestation.tlogID) { if (attestation.tlogID) {
core.info( core.info(
highlight('Attestation signature uploaded to Rekor transparency log') style.highlight(
'Attestation signature uploaded to Rekor transparency log'
)
) )
core.info(`${SEARCH_PUBLIC_GOOD_URL}?logIndex=${attestation.tlogID}`) core.info(`${SEARCH_PUBLIC_GOOD_URL}?logIndex=${attestation.tlogID}`)
} }
if (attestation.attestationID) { if (attestation.attestationID) {
core.info(highlight('Attestation uploaded to repository')) core.info(style.highlight('Attestation uploaded to repository'))
core.info(attestationURL(attestation.attestationID)) core.info(attestationURL(attestation.attestationID))
} }
if (core.getBooleanInput('push-to-registry', { required: false })) { if (attestation.attestationDigest) {
const credentials = getRegistryCredentials(subject.name) core.info(style.highlight('Attestation uploaded to registry'))
const artifact = await attachArtifactToImage({ core.info(`${attestation.subjectName}@${attestation.attestationDigest}`)
credentials, }
imageName: subject.name,
imageDigest: subjectDigest(subject),
artifact: Buffer.from(JSON.stringify(attestation.bundle)),
mediaType: attestation.bundle.mediaType,
annotations: {
'dev.sigstore.bundle.content': 'dsse-envelope',
'dev.sigstore.bundle.predicateType': core.getInput('predicate-type')
},
fetchOpts: { timeout: OCI_TIMEOUT, retry: OCI_RETRY }
})
core.info(highlight('Attestation uploaded to registry'))
core.info(`${subject.name}@${artifact.digest}`)
} }
return attestation // Attach summary information to the GitHub Actions run
} const logSummary = (attestations: AttestResult[]): void => {
if (attestations.length > 0) {
// Emphasis string using ANSI color codes core.summary.addHeading(
const highlight = (str: string): string => `${COLOR_CYAN}${str}${COLOR_DEFAULT}`
// De-emphasize string using ANSI color codes
/* istanbul ignore next */ /* istanbul ignore next */
const mute = (str: string): string => `${COLOR_GRAY}${str}${COLOR_DEFAULT}` attestations.length > 1 ? 'Attestations Created' : 'Attestation Created',
3
)
for (const { subjectName, subjectDigest, attestationID } of attestations) {
if (attestationID) {
core.summary.addLink(
`${subjectName}@${subjectDigest}`,
attestationURL(attestationID)
)
}
}
core.summary.write()
}
}
const tempDir = (): string => { const tempDir = (): string => {
const basePath = process.env['RUNNER_TEMP'] const basePath = process.env['RUNNER_TEMP']
@@ -194,12 +189,18 @@ const tempDir = (): string => {
return fs.mkdtempSync(path.join(basePath, path.sep)) return fs.mkdtempSync(path.join(basePath, path.sep))
} }
// Returns the subject's digest as a formatted string of the form // Transforms an array into an array of arrays, each containing at most
// "<algorithm>:<digest>". // `chunkSize` elements.
const subjectDigest = (subject: Subject): string => { const chunkArray = <T>(array: T[], chunkSize: number): T[][] => {
const alg = Object.keys(subject.digest).sort()[0] return Array.from(
return `${alg}:${subject.digest[alg]}` { length: Math.ceil(array.length / chunkSize) },
(_, index) => array.slice(index * chunkSize, (index + 1) * chunkSize)
)
} }
// Calculate the delay time for a given iteration
const delay = (iteration: number): number =>
Math.min(DELAY_INTERVAL_MS * 2 ** iteration, DELAY_MAX_MS)
const attestationURL = (id: string): string => const attestationURL = (id: string): string =>
`${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/attestations/${id}` `${github.context.serverUrl}/${github.context.repo.owner}/${github.context.repo.repo}/attestations/${id}`
+40 -10
View File
@@ -1,26 +1,56 @@
import * as core from '@actions/core'
import fs from 'fs' import fs from 'fs'
import type { Predicate } from '@actions/attest' import type { Predicate } from '@actions/attest'
export type PredicateInputs = {
predicateType: string
predicate: string
predicatePath: string
}
const MAX_PREDICATE_SIZE_BYTES = 16 * 1024 * 1024
// Returns the predicate specified by the action's inputs. The predicate value // Returns the predicate specified by the action's inputs. The predicate value
// may be specified as a path to a file or as a string. // may be specified as a path to a file or as a string.
export const predicateFromInputs = (): Predicate => { export const predicateFromInputs = (inputs: PredicateInputs): Predicate => {
const predicateType = core.getInput('predicate-type', { required: true }) const { predicateType, predicate, predicatePath } = inputs
const predicateStr = core.getInput('predicate', { required: false })
const predicatePath = core.getInput('predicate-path', { required: false })
if (!predicatePath && !predicateStr) { if (!predicateType) {
throw new Error('predicate-type must be provided')
}
if (!predicatePath && !predicate) {
throw new Error('One of predicate-path or predicate must be provided') throw new Error('One of predicate-path or predicate must be provided')
} }
if (predicatePath && predicateStr) { if (predicatePath && predicate) {
throw new Error('Only one of predicate-path or predicate may be provided') throw new Error('Only one of predicate-path or predicate may be provided')
} }
const params = predicatePath let params: string = predicate
? fs.readFileSync(predicatePath, 'utf-8')
: predicateStr if (predicatePath) {
if (!fs.existsSync(predicatePath)) {
throw new Error(`predicate file not found: ${predicatePath}`)
}
/* istanbul ignore next */
if (fs.statSync(predicatePath).size > MAX_PREDICATE_SIZE_BYTES) {
throw new Error(
`predicate file exceeds maximum allowed size: ${MAX_PREDICATE_SIZE_BYTES} bytes`
)
}
params = fs.readFileSync(predicatePath, 'utf-8')
} else {
if (predicate.length > MAX_PREDICATE_SIZE_BYTES) {
throw new Error(
`predicate string exceeds maximum allowed size: ${MAX_PREDICATE_SIZE_BYTES} bytes`
)
}
params = predicate
}
return { type: predicateType, params: JSON.parse(params) } return { type: predicateType, params: JSON.parse(params) }
} }
+11
View File
@@ -0,0 +1,11 @@
const COLOR_CYAN = '\x1B[36m'
const COLOR_GRAY = '\x1B[38;5;244m'
const COLOR_DEFAULT = '\x1B[39m'
// Emphasis string using ANSI color codes
export const highlight = (str: string): string =>
`${COLOR_CYAN}${str}${COLOR_DEFAULT}`
// De-emphasize string using ANSI color codes
export const mute = (str: string): string =>
`${COLOR_GRAY}${str}${COLOR_DEFAULT}`
+26 -16
View File
@@ -1,4 +1,3 @@
import * as core from '@actions/core'
import * as glob from '@actions/glob' import * as glob from '@actions/glob'
import crypto from 'crypto' import crypto from 'crypto'
import { parse } from 'csv-parse/sync' import { parse } from 'csv-parse/sync'
@@ -7,19 +6,23 @@ import path from 'path'
import type { Subject } from '@actions/attest' import type { Subject } from '@actions/attest'
const MAX_SUBJECT_COUNT = 2500
const DIGEST_ALGORITHM = 'sha256' const DIGEST_ALGORITHM = 'sha256'
export type SubjectInputs = {
subjectPath: string
subjectName: string
subjectDigest: string
downcaseName?: boolean
}
// Returns the subject specified by the action's inputs. The subject may be // Returns the subject specified by the action's inputs. The subject may be
// specified as a path to a file or as a digest. If a path is provided, the // specified as a path to a file or as a digest. If a path is provided, the
// file's digest is calculated and returned along with the subject's name. If a // file's digest is calculated and returned along with the subject's name. If a
// digest is provided, the name must also be provided. // digest is provided, the name must also be provided.
export const subjectFromInputs = async (): Promise<Subject[]> => { export const subjectFromInputs = async (
const subjectPath = core.getInput('subject-path', { required: false }) inputs: SubjectInputs
const subjectDigest = core.getInput('subject-digest', { required: false }) ): Promise<Subject[]> => {
const subjectName = core.getInput('subject-name', { required: false }) const { subjectPath, subjectDigest, subjectName, downcaseName } = inputs
const pushToRegistry = core.getBooleanInput('push-to-registry', {
required: false
})
if (!subjectPath && !subjectDigest) { if (!subjectPath && !subjectDigest) {
throw new Error('One of subject-path or subject-digest must be provided') throw new Error('One of subject-path or subject-digest must be provided')
@@ -37,7 +40,7 @@ export const subjectFromInputs = async (): Promise<Subject[]> => {
// If push-to-registry is enabled, ensure the subject name is lowercase // If push-to-registry is enabled, ensure the subject name is lowercase
// to conform to OCI image naming conventions // to conform to OCI image naming conventions
const name = pushToRegistry ? subjectName.toLowerCase() : subjectName const name = downcaseName ? subjectName.toLowerCase() : subjectName
if (subjectPath) { if (subjectPath) {
return await getSubjectFromPath(subjectPath, name) return await getSubjectFromPath(subjectPath, name)
@@ -52,15 +55,23 @@ const getSubjectFromPath = async (
subjectPath: string, subjectPath: string,
subjectName?: string subjectName?: string
): Promise<Subject[]> => { ): Promise<Subject[]> => {
const subjects: Subject[] = [] const digestedSubjects: Subject[] = []
const files: string[] = []
// Parse the list of subject paths // Parse the list of subject paths
const subjectPaths = parseList(subjectPath) const subjectPaths = parseList(subjectPath)
// Expand the globbed paths to a list of files
for (const subPath of subjectPaths) { for (const subPath of subjectPaths) {
// Expand the globbed path to a list of files
/* eslint-disable-next-line github/no-then */ /* eslint-disable-next-line github/no-then */
const files = await glob.create(subPath).then(async g => g.glob()) files.push(...(await glob.create(subPath).then(async g => g.glob())))
}
if (files.length > MAX_SUBJECT_COUNT) {
throw new Error(
`Too many subjects specified. The maximum number of subjects is ${MAX_SUBJECT_COUNT}.`
)
}
for (const file of files) { for (const file of files) {
// Skip anything that is NOT a file // Skip anything that is NOT a file
@@ -71,15 +82,14 @@ const getSubjectFromPath = async (
const name = subjectName || path.parse(file).base const name = subjectName || path.parse(file).base
const digest = await digestFile(DIGEST_ALGORITHM, file) const digest = await digestFile(DIGEST_ALGORITHM, file)
subjects.push({ name, digest: { [DIGEST_ALGORITHM]: digest } }) digestedSubjects.push({ name, digest: { [DIGEST_ALGORITHM]: digest } })
}
} }
if (subjects.length === 0) { if (digestedSubjects.length === 0) {
throw new Error(`Could not find subject at path ${subjectPath}`) throw new Error(`Could not find subject at path ${subjectPath}`)
} }
return Promise.all(subjects) return digestedSubjects
} }
// Returns the subject specified by the digest of a file. The digest is returned // Returns the subject specified by the digest of a file. The digest is returned