Compare commits

...

25 Commits

Author SHA1 Message Date
dependabot[bot] 2da562a775 Bump the actions-minor group with 2 updates (#400)
Continuous Integration / Test attest action (push) Has been cancelled
Check Transpiled JavaScript / Check dist/ (push) Has been cancelled
Continuous Integration / TypeScript Tests (push) Has been cancelled
CodeQL / Analyze (TypeScript) (push) Has been cancelled
Bumps the actions-minor group with 2 updates: [actions/setup-node](https://github.com/actions/setup-node) and [github/codeql-action](https://github.com/github/codeql-action).


Updates `actions/setup-node` from 6.3.0 to 6.4.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/53b83947a5a98c8d113130e565377fae1a50d02f...48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e)

Updates `github/codeql-action` from 4.35.1 to 4.35.2
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/c10b8064de6f491fea524254123dbe5e09572f13...95e58e9a2cdfd71adc6e0353d5c52f41a045d225)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor
- dependency-name: github/codeql-action
  dependency-version: 4.35.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 17:31:02 -07:00
dependabot[bot] 96309a74df Bump picomatch from 2.3.1 to 2.3.2 (#398)
Bumps [picomatch](https://github.com/micromatch/picomatch) from 2.3.1 to 2.3.2.
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 17:17:59 -07:00
dependabot[bot] e287548486 Bump the npm-development group across 1 directory with 6 updates (#396)
Bumps the npm-development group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@sigstore/mock](https://github.com/sigstore/sigstore-js) | `0.11.0` | `0.12.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.5.0` | `25.6.0` |
| [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) | `29.15.0` | `29.15.2` |
| [prettier](https://github.com/prettier/prettier) | `3.8.1` | `3.8.2` |
| [ts-jest](https://github.com/kulshekhar/ts-jest) | `29.4.6` | `29.4.9` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.57.1` | `8.58.2` |



Updates `@sigstore/mock` from 0.11.0 to 0.12.0
- [Release notes](https://github.com/sigstore/sigstore-js/releases)
- [Commits](https://github.com/sigstore/sigstore-js/compare/@sigstore/mock@0.11.0...@sigstore/mock@0.12.0)

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

Updates `eslint-plugin-jest` from 29.15.0 to 29.15.2
- [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/v29.15.0...v29.15.2)

Updates `prettier` from 3.8.1 to 3.8.2
- [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.8.1...3.8.2)

Updates `ts-jest` from 29.4.6 to 29.4.9
- [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.4.6...v29.4.9)

Updates `typescript-eslint` from 8.57.1 to 8.58.2
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.58.2/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@sigstore/mock"
  dependency-version: 0.12.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@types/node"
  dependency-version: 25.6.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: eslint-plugin-jest
  dependency-version: 29.15.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: prettier
  dependency-version: 3.8.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: ts-jest
  dependency-version: 29.4.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: typescript-eslint
  dependency-version: 8.58.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 17:08:38 -07:00
dependabot[bot] fd6b752f2b Bump handlebars from 4.7.8 to 4.7.9 (#388)
Bumps [handlebars](https://github.com/handlebars-lang/handlebars.js) from 4.7.8 to 4.7.9.
- [Release notes](https://github.com/handlebars-lang/handlebars.js/releases)
- [Changelog](https://github.com/handlebars-lang/handlebars.js/blob/v4.7.9/release-notes.md)
- [Commits](https://github.com/handlebars-lang/handlebars.js/compare/v4.7.8...v4.7.9)

---
updated-dependencies:
- dependency-name: handlebars
  dependency-version: 4.7.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 17:03:22 -07:00
dependabot[bot] c2fbdcca7f Bump the actions-minor group across 1 directory with 3 updates (#395)
Bumps the actions-minor group with 3 updates in the / directory: [actions/upload-artifact](https://github.com/actions/upload-artifact), [github/codeql-action](https://github.com/github/codeql-action) and [masci/datadog](https://github.com/masci/datadog).


Updates `actions/upload-artifact` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/bbbca2ddaa5d8feaa63e36b76fdaad77386f024f...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

Updates `github/codeql-action` from 4.34.1 to 4.35.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/38697555549f1db7851b81482ff19f1fa5c4fedc...c10b8064de6f491fea524254123dbe5e09572f13)

Updates `masci/datadog` from 2.0.0 to 2.0.2
- [Release notes](https://github.com/masci/datadog/releases)
- [Commits](https://github.com/masci/datadog/compare/c1b14660479f44f1049b08274d1bfaae9405cbae...d7cb6cfc2ba13f7c2ae5227a26f2e2f42a7d179e)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor
- dependency-name: github/codeql-action
  dependency-version: 4.35.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor
- dependency-name: masci/datadog
  dependency-version: 2.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 17:01:52 -07:00
Eugene c65e8d4737 Pin GitHub Actions to commit SHAs for security (#386)
Replace mutable tag references with immutable commit SHAs in
codeql-analysis.yml and check-dist.yml to prevent supply chain attacks.

Actions pinned:
- actions/checkout@v6.0.2
- github/codeql-action/init@v4
- github/codeql-action/autobuild@v4
- github/codeql-action/analyze@v4
- actions/setup-node@v6.3.0
- actions/upload-artifact@v7.0.0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-25 13:13:15 -04:00
dependabot[bot] ffbe5726c3 Bump the npm-development group with 5 updates (#379)
Bumps the npm-development group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [@jest/globals](https://github.com/jestjs/jest/tree/HEAD/packages/jest-globals) | `30.2.0` | `30.3.0` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.4.0` | `25.5.0` |
| [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) | `30.2.0` | `30.3.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.57.0` | `8.57.1` |
| [undici](https://github.com/nodejs/undici) | `7.22.0` | `7.24.4` |


Updates `@jest/globals` from 30.2.0 to 30.3.0
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.3.0/packages/jest-globals)

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

Updates `jest` from 30.2.0 to 30.3.0
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.3.0/packages/jest)

Updates `typescript-eslint` from 8.57.0 to 8.57.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.57.1/packages/typescript-eslint)

Updates `undici` from 7.22.0 to 7.24.4
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.22.0...v7.24.4)

---
updated-dependencies:
- dependency-name: "@jest/globals"
  dependency-version: 30.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: "@types/node"
  dependency-version: 25.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: jest
  dependency-version: 30.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: typescript-eslint
  dependency-version: 8.57.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
- dependency-name: undici
  dependency-version: 7.24.4
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eugene <108841108+ejahnGithub@users.noreply.github.com>
2026-03-23 10:30:09 -04:00
dependabot[bot] 1ddccb76ab Bump masci/datadog from 1.9.3 to 2.0.0 (#378)
Bumps [masci/datadog](https://github.com/masci/datadog) from 1.9.3 to 2.0.0.
- [Release notes](https://github.com/masci/datadog/releases)
- [Commits](https://github.com/masci/datadog/compare/a3f481d2ed0f4e1edde2be2f564b94719d6d4bc2...c1b14660479f44f1049b08274d1bfaae9405cbae)

---
updated-dependencies:
- dependency-name: masci/datadog
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Eugene <108841108+ejahnGithub@users.noreply.github.com>
2026-03-23 10:28:55 -04:00
dependabot[bot] a25a27cab8 Bump flatted from 3.3.3 to 3.4.2 (#380)
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-23 10:28:08 -04:00
dependabot[bot] e18a799f23 Bump tar from 7.5.10 to 7.5.11 (#375)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.10 to 7.5.11.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.10...v7.5.11)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-11 14:32:46 -07:00
dependabot[bot] 5b44f686c6 Bump the npm-development group with 3 updates (#373)
Bumps the npm-development group with 3 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


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

Updates `markdownlint-cli` from 0.47.0 to 0.48.0
- [Release notes](https://github.com/igorshubovych/markdownlint-cli/releases)
- [Commits](https://github.com/igorshubovych/markdownlint-cli/compare/v0.47.0...v0.48.0)

Updates `typescript-eslint` from 8.56.1 to 8.57.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.57.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.4.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: markdownlint-cli
  dependency-version: 0.48.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: typescript-eslint
  dependency-version: 8.57.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 16:55:05 -07:00
dependabot[bot] 9c10929297 Bump actions/setup-node from 6.2.0 to 6.3.0 in the actions-minor group (#372)
Bumps the actions-minor group with 1 update: [actions/setup-node](https://github.com/actions/setup-node).


Updates `actions/setup-node` from 6.2.0 to 6.3.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6.2.0...v6.3.0)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 15:38:03 -07:00
dependabot[bot] 3f6ab60ba4 Bump tar from 7.5.9 to 7.5.10 (#371)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.9 to 7.5.10.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.9...v7.5.10)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.10
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-06 07:22:41 -08:00
dependabot[bot] f7e1c5d786 Bump @types/node from 25.3.0 to 25.3.3 in the npm-development group (#370)
Bumps the npm-development group with 1 update: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node).


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

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 16:31:03 -08:00
dependabot[bot] 088247d089 Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#369)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 16:03:08 -08:00
Brian DeHamer 6bc26cfc5e add e2e tests (#368)
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2026-02-26 14:23:01 -08:00
Brian DeHamer 59d89421af add storage record content to README (#366)
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2026-02-26 12:55:02 -08:00
Brian DeHamer ec072a1cb2 add new subject-version input (#364)
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2026-02-26 12:38:12 -08:00
Brian DeHamer 8b290b8d86 bump @actions/attest from 3.1.0 to 3.2.0 (#365)
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2026-02-26 12:36:53 -08:00
Brian DeHamer 35cfe2422e bump @actions/attest from 3.0.0 to 3.1.0 (#362)
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2026-02-25 16:03:18 -08:00
Brian DeHamer c32b4b8b19 bump version in package.json to v4.0.0 (#360)
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2026-02-24 15:48:06 -08:00
dependabot[bot] 1e73be196c Bump typescript-eslint in the npm-development group (#358)
Bumps the npm-development group with 1 update: [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `typescript-eslint` from 8.56.0 to 8.56.1
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.56.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-24 15:45:28 -08:00
dependabot[bot] e1345cbec4 Bump the npm-development group across 1 directory with 3 updates (#357)
Bumps the npm-development group with 3 updates in the / directory: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node), [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


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

Updates `eslint-plugin-jest` from 29.14.0 to 29.15.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/v29.14.0...v29.15.0)

Updates `typescript-eslint` from 8.55.0 to 8.56.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.56.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: eslint-plugin-jest
  dependency-version: 29.15.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
- dependency-name: typescript-eslint
  dependency-version: 8.56.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 09:02:14 -08:00
dependabot[bot] 09cd5f66cb Bump tar from 7.5.7 to 7.5.9 (#354)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.7 to 7.5.9.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.7...v7.5.9)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-19 10:20:40 -08:00
Brian DeHamer 19ad753d23 test suite re-write (#356)
Signed-off-by: Brian DeHamer <bdehamer@github.com>
2026-02-19 10:14:47 -08:00
33 changed files with 2647 additions and 2474 deletions
+3 -3
View File
@@ -28,11 +28,11 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v6.2.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .node-version
cache: npm
@@ -60,7 +60,7 @@ jobs:
- if: ${{ failure() && steps.diff.outcome == 'failure' }}
name: Upload Artifact
id: upload
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dist
path: dist/
+1 -1
View File
@@ -25,7 +25,7 @@ jobs:
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: .node-version
cache: npm
+4 -4
View File
@@ -32,19 +32,19 @@ jobs:
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v6.0.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize CodeQL
id: initialize
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
languages: ${{ matrix.language }}
source-root: src
- name: Autobuild
id: autobuild
uses: github/codeql-action/autobuild@v4
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
- name: Perform CodeQL Analysis
id: analyze
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
+18
View File
@@ -0,0 +1,18 @@
name: GitHub Sigstore Prober
on:
workflow_dispatch:
schedule:
# run every 5 minutes, as often as Github Actions allows
- cron: '*/5 * * * *'
jobs:
prober:
if: github.repository_owner == 'actions'
permissions:
attestations: write
id-token: write
secrets: inherit
uses: ./.github/workflows/prober.yml
with:
sigstore: github
+18
View File
@@ -0,0 +1,18 @@
name: Public-Good Sigstore Prober
on:
workflow_dispatch:
schedule:
# run every 5 minutes, as often as Github Actions allows
- cron: '*/5 * * * *'
jobs:
prober:
if: github.repository_owner == 'actions'
permissions:
attestations: write
id-token: write
secrets: inherit
uses: ./.github/workflows/prober.yml
with:
sigstore: public-good
+84
View File
@@ -0,0 +1,84 @@
name: Prober Workflow
on:
workflow_call:
inputs:
sigstore:
description: 'Which Sigstore instance to use for signing'
required: true
type: string
jobs:
probe:
runs-on: ubuntu-latest
permissions:
attestations: write
id-token: write
steps:
- name: Request OIDC Token
run: |
curl "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=nobody" \
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
-H "Accept: application/json; api-version=2.0" \
-H "Content-Type: application/json" \
--silent | jq -r '.value' | jq -R 'split(".") | .[0],.[1] | @base64d | fromjson'
- name: Create artifact
run: |
date > artifact
- name: Attest build provenance
uses: actions/attest@main
env:
INPUT_PRIVATE-SIGNING: ${{ inputs.sigstore == 'github' && 'true' || 'false' }}
with:
subject-path: artifact
- name: Verify build artifact
env:
GH_TOKEN: ${{ github.token }}
run: |
gh attestation verify ./artifact --owner "$GITHUB_REPOSITORY_OWNER"
- name: Upload build artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
path: "artifact"
- name: Report attestation prober success
if: ${{ success() }}
uses: masci/datadog@d7cb6cfc2ba13f7c2ae5227a26f2e2f42a7d179e # v2.0.2
with:
api-key: "${{ secrets.DATADOG_API_KEY }}"
service-checks: |
- check: "attestation-integration.actions.prober"
status: 0
host_name: github.com
tags:
- "catalog_service:${{ secrets.CATALOG_SERVICE }}"
- "service:${{ secrets.CATALOG_SERVICE }}"
- "stamp:${{ secrets.STAMP }}"
- "env:production"
- "repo:${{ github.repository }}"
- "team:${{ secrets.TEAM }}"
- "sigstore:${{ inputs.sigstore }}"
- name: Report attestation prober failure
if: ${{ failure() }}
uses: masci/datadog@d7cb6cfc2ba13f7c2ae5227a26f2e2f42a7d179e # v2.0.2
with:
api-key: "${{ secrets.DATADOG_API_KEY }}"
service-checks: |
- check: "attestation-integration.actions.prober"
message: "${{ github.repository_owner }} failed prober check"
status: 2
host_name: github.com
tags:
- "catalog_service:${{ secrets.CATALOG_SERVICE }}"
- "service:${{ secrets.CATALOG_SERVICE }}"
- "stamp:${{ secrets.STAMP }}"
- "env:production"
- "repo:${{ github.repository }}"
- "team:${{ secrets.TEAM }}"
- "sigstore:${{ inputs.sigstore }}"
+25 -9
View File
@@ -48,11 +48,11 @@ the inputs you provide:
<!-- markdownlint-disable MD013 -->
| Mode | When Used | Description |
| -------------- | ------------------------------------------------------ | ------------------------------------------------ |
| **Provenance** | No `sbom-path` or predicate inputs | Auto-generates [SLSA build provenance][10] |
| **SBOM** | `sbom-path` is provided | Creates attestation from SPDX or CycloneDX SBOM |
| **Custom** | `predicate-type`/`predicate`/`predicate-path` provided | User-supplied predicate |
| Mode | When Used | Description |
| -------------- | ------------------------------------------------------ | ----------------------------------------------- |
| **Provenance** | No `sbom-path` or predicate inputs | Auto-generates [SLSA build provenance][10] |
| **SBOM** | `sbom-path` is provided | Creates attestation from SPDX or CycloneDX SBOM |
| **Custom** | `predicate-type`/`predicate`/`predicate-path` provided | User-supplied predicate |
<!-- markdownlint-enable MD013 -->
@@ -159,7 +159,7 @@ See [action.yml](action.yml)
<!-- markdownlint-disable MD013 -->
| Name | Description | Example |
| ------------------- | -------------------------------------------------------------- | ------------------------------------------------ |
| -------------------- | -------------------------------------------------------------- | ------------------------------------------------ |
| `attestation-id` | GitHub ID for the attestation | `123456` |
| `attestation-url` | URL for the attestation summary | `https://github.com/foo/bar/attestations/123456` |
| `bundle-path` | Absolute path to the file containing the generated attestation | `/tmp/attestation.json` |
@@ -320,9 +320,25 @@ fully-qualified image name (e.g. "ghcr.io/user/app" or
"acme.azurecr.io/user/app"). Do NOT include a tag as part of the image name --
the specific image being attested is identified by the supplied digest.
If the `push-to-registry` option is set to true, the Action will also
emit an Artifact Metadata Storage Record. If you do not want to emit a
storage record, set `create-storage-record` to `false`.
#### Artifact Metadata Storage Records
When generating a build provenance attestation, if the `push-to-registry` option
is set to true, the Action will also emit an
[Artifact Metadata Storage Record](https://docs.github.com/en/rest/orgs/artifact-metadata?apiVersion=2022-11-28#create-artifact-metadata-storage-record).
Storage records enrich artifact metadata by capturing storage related details,
such as which registry an image is hosted on and whether it's marked as active.
If you do not want to emit a storage record, set `create-storage-record` to
`false`.
> **NOTE**: Storage records can only be created for artifacts built from
> [organization-owned](https://docs.github.com/en/organizations/collaborating-with-groups-in-organizations/about-organizations)
> repositories.
Artifacts associated with a storage record can be viewed by navigating to the
`Linked Artifacts` page in your organization:
`https://github.com/orgs/YOUR_ORG/artifacts` (replace `YOUR_ORG` with your
organization name).
> **NOTE**: When pushing to Docker Hub, please use "docker.io" as the registry
> portion of the image name.
-195
View File
@@ -1,195 +0,0 @@
import { jest } from '@jest/globals'
import type { Descriptor } from '@sigstore/oci'
// Mock functions
const mockGetOctokit = jest.fn()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockAttest = jest.fn<() => Promise<any>>()
const mockCreateStorageRecord = jest.fn<() => Promise<number[]>>()
const mockGetRegistryCredentials = jest.fn()
const mockAttachArtifactToImage = jest.fn<() => Promise<Descriptor>>()
// Mock @actions/github
jest.unstable_mockModule('@actions/github', () => ({
getOctokit: mockGetOctokit,
context: {
repo: { owner: 'foo', repo: 'bar' },
payload: { repository: { visibility: 'private' } }
}
}))
// Mock @actions/attest
jest.unstable_mockModule('@actions/attest', () => ({
attest: mockAttest,
createStorageRecord: mockCreateStorageRecord
}))
// Mock @sigstore/oci
jest.unstable_mockModule('@sigstore/oci', () => ({
getRegistryCredentials: mockGetRegistryCredentials,
attachArtifactToImage: mockAttachArtifactToImage
}))
// Dynamic imports after mocking
const { createAttestation, repoOwnerIsOrg } = await import('../src/attest')
const subjectName = 'ghcr.io/foo/bar'
const subjectDigest =
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
const predicate = {
type: 'https://in-toto.io/attestation/release/v0.1',
params: {}
}
describe('repoOwnerIsOrg', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('returns true when repo owner is an organization', async () => {
mockGetOctokit.mockReturnValue({
rest: {
repos: {
get: jest
.fn<() => Promise<{ data: { owner: { type: string } } }>>()
.mockResolvedValue({
data: { owner: { type: 'Organization' } }
})
}
}
})
const result = await repoOwnerIsOrg('gh-token')
expect(result).toBe(true)
})
it('returns false when repo owner is a user', async () => {
mockGetOctokit.mockReturnValue({
rest: {
repos: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get: jest.fn<() => Promise<any>>().mockResolvedValue({
data: { owner: { type: 'User' } }
})
}
}
})
const result = await repoOwnerIsOrg('gh-token')
expect(result).toBe(false)
})
})
describe('createAttestation', () => {
beforeEach(() => {
jest.clearAllMocks()
// Default mock implementations
mockAttest.mockResolvedValue({
bundle: { mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json' },
certificate: 'cert',
tlogID: 'tlog-123',
attestationID: 'att-123'
})
mockGetRegistryCredentials.mockReturnValue({
username: 'user',
password: 'pass'
})
mockAttachArtifactToImage.mockResolvedValue({
digest: 'sha256:abc123',
mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json',
size: 100
})
})
describe('when createStorageRecord is false', () => {
it('skips storage record creation', async () => {
const subjects = [
{
name: subjectName,
digest: { sha256: subjectDigest.replace('sha256:', '') }
}
]
const result = await createAttestation(subjects, predicate, {
sigstoreInstance: 'github',
pushToRegistry: true,
createStorageRecord: false,
githubToken: 'gh-token'
})
expect(result.attestationDigest).toBe('sha256:abc123')
expect(mockCreateStorageRecord).not.toHaveBeenCalled()
})
})
describe('when storage records are empty', () => {
beforeEach(() => {
mockGetOctokit.mockReturnValue({
rest: {
repos: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get: jest.fn<() => Promise<any>>().mockResolvedValue({
data: { owner: { type: 'Organization' } }
})
}
}
})
mockCreateStorageRecord.mockResolvedValue([])
})
it('handles empty storage records gracefully', async () => {
const subjects = [
{
name: subjectName,
digest: { sha256: subjectDigest.replace('sha256:', '') }
}
]
const result = await createAttestation(subjects, predicate, {
sigstoreInstance: 'github',
pushToRegistry: true,
createStorageRecord: true,
githubToken: 'gh-token'
})
expect(result.attestationDigest).toBe('sha256:abc123')
})
})
describe('when subject has unsupported protocol', () => {
beforeEach(() => {
mockGetOctokit.mockReturnValue({
rest: {
repos: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get: jest.fn<() => Promise<any>>().mockResolvedValue({
data: { owner: { type: 'Organization' } }
})
}
}
})
mockCreateStorageRecord.mockResolvedValue([123])
})
it('handles unsupported protocol gracefully', async () => {
const subjects = [
{
name: 'http://registry.example.com/foo/bar',
digest: { sha256: subjectDigest.replace('sha256:', '') }
}
]
const result = await createAttestation(subjects, predicate, {
sigstoreInstance: 'github',
pushToRegistry: true,
createStorageRecord: true,
githubToken: 'gh-token'
})
expect(result.attestationDigest).toBe('sha256:abc123')
})
})
})
+253
View File
@@ -0,0 +1,253 @@
import type { Attestation, Predicate, Subject } from '@actions/attest'
import { jest } from '@jest/globals'
import type { RestEndpointMethodTypes } from '@octokit/plugin-rest-endpoint-methods'
import type { Descriptor } from '@sigstore/oci'
// =============================================================================
// @actions/core mock factory
// =============================================================================
export type CoreMock = {
info: jest.Mock
warning: jest.Mock
debug: jest.Mock
startGroup: jest.Mock
endGroup: jest.Mock
setOutput: jest.Mock
setFailed: jest.Mock
summary: SummaryMock
}
export type SummaryMock = {
write: jest.Mock
addRaw: jest.Mock
addHeading: jest.Mock
addLink: jest.Mock
addTable: jest.Mock
addBreak: jest.Mock
addSeparator: jest.Mock
addQuote: jest.Mock
addCodeBlock: jest.Mock
addList: jest.Mock
addImage: jest.Mock
addDetails: jest.Mock
addEOL: jest.Mock
emptyBuffer: jest.Mock
stringify: jest.Mock
isEmptyBuffer: jest.Mock
clear: jest.Mock
}
export const createSummaryMock = (): SummaryMock => {
const mock: SummaryMock = {
write: jest.fn(),
addRaw: jest.fn(),
addHeading: jest.fn(),
addLink: jest.fn(),
addTable: jest.fn(),
addBreak: jest.fn(),
addSeparator: jest.fn(),
addQuote: jest.fn(),
addCodeBlock: jest.fn(),
addList: jest.fn(),
addImage: jest.fn(),
addDetails: jest.fn(),
addEOL: jest.fn(),
emptyBuffer: jest.fn(),
stringify: jest.fn().mockReturnValue(''),
isEmptyBuffer: jest.fn().mockReturnValue(true),
clear: jest.fn()
}
// Make chainable
for (const key of Object.keys(mock) as (keyof SummaryMock)[]) {
if (key !== 'stringify' && key !== 'isEmptyBuffer') {
mock[key].mockReturnThis()
}
}
return mock
}
export const createCoreMock = (): CoreMock => ({
info: jest.fn(),
warning: jest.fn(),
debug: jest.fn(),
startGroup: jest.fn(),
endGroup: jest.fn(),
setOutput: jest.fn(),
setFailed: jest.fn(),
summary: createSummaryMock()
})
// =============================================================================
// @actions/github mock factory
// =============================================================================
export type GitHubContextMock = {
repo: { owner: string; repo: string }
payload: { repository?: { visibility: string } }
serverUrl: string
}
export const createGitHubContextMock = (
overrides: Partial<GitHubContextMock> = {}
): GitHubContextMock => ({
repo: { owner: 'test-owner', repo: 'test-repo' },
payload: { repository: { visibility: 'public' } },
serverUrl: 'https://github.com',
...overrides
})
export type OctokitMock = {
rest: {
repos: {
get: jest.Mock
}
}
}
export const createOctokitMock = (
ownerType: 'Organization' | 'User' = 'Organization'
): OctokitMock => ({
rest: {
repos: {
get: jest
.fn<RestEndpointMethodTypes['repos']['get']['response']>()
.mockResolvedValue({
data: { owner: { type: ownerType } }
})
}
}
})
// =============================================================================
// @actions/attest mock factory
// =============================================================================
export type AttestMock = {
attest: jest.Mock
buildSLSAProvenancePredicate: jest.Mock
createStorageRecord: jest.Mock
}
export const createAttestMock = (): AttestMock => ({
attest: jest.fn(),
buildSLSAProvenancePredicate: jest.fn(),
createStorageRecord: jest.fn()
})
export const createAttestationResult = (
overrides: Partial<Attestation> = {}
): Attestation => ({
bundle: {
mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json' as const,
verificationMaterial: {
certificate: { rawBytes: '' },
publicKey: undefined,
x509CertificateChain: undefined,
tlogEntries: [],
timestampVerificationData: undefined
},
dsseEnvelope: {
payload: '',
payloadType: '',
signatures: []
},
messageSignature: undefined
},
certificate: '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
tlogID: 'tlog-123',
attestationID: 'att-123',
...overrides
})
// =============================================================================
// @sigstore/oci mock factory
// =============================================================================
export type OciMock = {
getRegistryCredentials: jest.Mock
attachArtifactToImage: jest.Mock
}
export const createOciMock = (): OciMock => ({
getRegistryCredentials: jest.fn().mockReturnValue({
username: 'test-user',
password: 'test-pass'
}),
attachArtifactToImage: jest
.fn<() => Promise<Descriptor>>()
.mockResolvedValue({
digest: 'sha256:abc123def456',
mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json',
size: 1234
})
})
// =============================================================================
// Common test data
// =============================================================================
export const TEST_SUBJECT: Subject = {
name: 'test-artifact',
digest: {
sha256: '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
}
}
export const TEST_SUBJECT_WITH_REGISTRY: Subject = {
name: 'ghcr.io/test-owner/test-repo',
digest: {
sha256: '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
}
}
export const TEST_PREDICATE: Predicate = {
type: 'https://example.com/predicate/v1',
params: { foo: 'bar' }
}
export const TEST_PROVENANCE_PREDICATE: Predicate = {
type: 'https://slsa.dev/provenance/v1',
params: {
buildDefinition: {
buildType: 'https://actions.github.io/buildtypes/workflow/v1'
},
runDetails: {
builder: { id: 'https://github.com/actions/runner' }
}
}
}
// =============================================================================
// Environment helpers
// =============================================================================
export const setupTestEnvironment = (
env: Record<string, string> = {}
): (() => void) => {
const originalEnv = { ...process.env }
process.env = {
...process.env,
ACTIONS_ID_TOKEN_REQUEST_URL: 'https://token.url',
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'test-token',
RUNNER_TEMP: '/tmp',
...env
}
return () => {
process.env = originalEnv
}
}
// =============================================================================
// OIDC token helpers
// =============================================================================
export const createOidcToken = (subject = 'test@example.com'): string => {
const payload = {
sub: subject,
iss: 'https://token.actions.githubusercontent.com'
}
return `.${Buffer.from(JSON.stringify(payload)).toString('base64')}.`
}
@@ -0,0 +1,13 @@
{
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:12345678-1234-1234-1234-123456789012",
"version": 1,
"components": [
{
"type": "library",
"name": "test-component",
"version": "1.0.0"
}
]
}
@@ -0,0 +1,9 @@
{
"buildType": "https://example.com/build/v1",
"builder": {
"id": "https://github.com/actions/runner"
},
"metadata": {
"buildStartedOn": "2024-01-01T00:00:00Z"
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"spdxVersion": "SPDX-2.3",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "test-package",
"dataLicense": "CC0-1.0",
"documentNamespace": "https://example.com/test-package",
"packages": [
{
"SPDXID": "SPDXRef-Package",
"name": "test-package",
"versionInfo": "1.0.0",
"downloadLocation": "https://example.com/test-package-1.0.0.tar.gz"
}
]
}
+45 -7
View File
@@ -1,6 +1,3 @@
/**
* Unit tests for the action's entrypoint, src/index.ts
*/
import { jest } from '@jest/globals'
// Mock functions
@@ -22,14 +19,55 @@ jest.unstable_mockModule('../src/main', () => ({
describe('index', () => {
beforeEach(() => {
jest.clearAllMocks()
mockGetBooleanInput.mockReturnValue(false)
mockGetInput.mockReturnValue('')
mockGetBooleanInput.mockReturnValue(false)
})
it('calls run when imported', async () => {
// Dynamic import after mocking
it('should call run with inputs from core.getInput', async () => {
mockGetInput.mockImplementation((name: string) => {
const inputs: Record<string, string> = {
'subject-path': '/path/to/subject',
'subject-name': 'my-artifact',
'subject-digest': '',
'subject-checksums': '',
'subject-version': '',
'predicate-type': 'https://example.com/predicate',
predicate: '{}',
'predicate-path': '',
'sbom-path': '',
'github-token': 'test-token'
}
return inputs[name] || ''
})
mockGetBooleanInput.mockImplementation((name: string) => {
const inputs: Record<string, boolean> = {
'push-to-registry': false,
'create-storage-record': true,
'show-summary': true,
'private-signing': false
}
return inputs[name] || false
})
// Dynamic import triggers the module
await import('../src/index')
expect(mockRun).toHaveBeenCalled()
expect(mockRun).toHaveBeenCalledWith({
subjectPath: '/path/to/subject',
subjectName: 'my-artifact',
subjectDigest: '',
subjectChecksums: '',
subjectVersion: '',
predicateType: 'https://example.com/predicate',
predicate: '{}',
predicatePath: '',
sbomPath: '',
githubToken: 'test-token',
pushToRegistry: false,
createStorageRecord: true,
showSummary: true,
privateSigning: false
})
})
})
+246
View File
@@ -0,0 +1,246 @@
import { jest } from '@jest/globals'
import {
createAttestationResult,
createGitHubContextMock,
createOctokitMock,
TEST_PREDICATE,
TEST_SUBJECT_WITH_REGISTRY
} from '../fixtures/mocks'
import type { Attestation } from '@actions/attest'
import type { Descriptor } from '@sigstore/oci'
// Mock functions
const mockGetOctokit = jest.fn()
const mockAttest = jest.fn<() => Promise<Attestation>>()
const mockCreateStorageRecord = jest.fn<() => Promise<number[]>>()
const mockGetRegistryCredentials = jest.fn()
const mockAttachArtifactToImage = jest.fn<() => Promise<Descriptor>>()
// Mutable context for tests
const mockContext = createGitHubContextMock()
// Mock @actions/github
jest.unstable_mockModule('@actions/github', () => ({
getOctokit: mockGetOctokit,
context: mockContext
}))
// Mock @actions/attest
jest.unstable_mockModule('@actions/attest', () => ({
attest: mockAttest,
createStorageRecord: mockCreateStorageRecord
}))
// Mock @sigstore/oci
jest.unstable_mockModule('@sigstore/oci', () => ({
getRegistryCredentials: mockGetRegistryCredentials,
attachArtifactToImage: mockAttachArtifactToImage
}))
// Dynamic imports after mocking
const { createAttestation, repoOwnerIsOrg } = await import('../../src/attest')
describe('repoOwnerIsOrg', () => {
beforeEach(() => {
jest.clearAllMocks()
})
it('should return true when repo owner is an Organization', async () => {
mockGetOctokit.mockReturnValue(createOctokitMock('Organization'))
const result = await repoOwnerIsOrg('test-token')
expect(result).toBe(true)
expect(mockGetOctokit).toHaveBeenCalledWith('test-token')
})
it('should return false when repo owner is a User', async () => {
mockGetOctokit.mockReturnValue(createOctokitMock('User'))
const result = await repoOwnerIsOrg('test-token')
expect(result).toBe(false)
})
})
describe('createAttestation', () => {
const defaultOpts = {
sigstoreInstance: 'github' as const,
pushToRegistry: false,
createStorageRecord: false,
githubToken: 'test-token'
}
beforeEach(() => {
jest.clearAllMocks()
mockAttest.mockResolvedValue(createAttestationResult())
mockGetRegistryCredentials.mockReturnValue({
username: 'test-user',
password: 'test-pass'
})
mockAttachArtifactToImage.mockResolvedValue({
digest: 'sha256:attestation-digest',
mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json',
size: 1234
})
mockCreateStorageRecord.mockResolvedValue([12345])
mockGetOctokit.mockReturnValue(createOctokitMock('Organization'))
})
describe('basic attestation', () => {
it('should call attest with correct parameters', async () => {
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
await createAttestation(subjects, TEST_PREDICATE, defaultOpts)
expect(mockAttest).toHaveBeenCalledWith({
subjects,
predicateType: TEST_PREDICATE.type,
predicate: TEST_PREDICATE.params,
sigstore: 'github',
token: 'test-token'
})
})
it('should return attestation result', async () => {
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
const result = await createAttestation(
subjects,
TEST_PREDICATE,
defaultOpts
)
expect(result.attestationID).toBe('att-123')
expect(result.certificate).toContain('BEGIN CERTIFICATE')
expect(result.tlogID).toBe('tlog-123')
})
})
describe('registry push', () => {
const pushOpts = { ...defaultOpts, pushToRegistry: true }
it('should push attestation to registry when enabled', async () => {
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
const result = await createAttestation(subjects, TEST_PREDICATE, pushOpts)
expect(mockGetRegistryCredentials).toHaveBeenCalledWith(subjects[0].name)
expect(mockAttachArtifactToImage).toHaveBeenCalled()
expect(result.attestationDigest).toBe('sha256:attestation-digest')
})
it('should skip registry push for multiple subjects', async () => {
const subjects = [TEST_SUBJECT_WITH_REGISTRY, TEST_SUBJECT_WITH_REGISTRY]
await createAttestation(subjects, TEST_PREDICATE, pushOpts)
expect(mockAttachArtifactToImage).not.toHaveBeenCalled()
})
})
describe('storage record creation', () => {
const storageOpts = {
...defaultOpts,
pushToRegistry: true,
createStorageRecord: true,
subjectVersion: '1.2.3'
}
it('should create storage record when enabled and owner is org', async () => {
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
const result = await createAttestation(
subjects,
TEST_PREDICATE,
storageOpts
)
expect(mockCreateStorageRecord).toHaveBeenCalledWith(
expect.objectContaining({ version: '1.2.3' }),
expect.anything(),
expect.anything()
)
expect(result.storageRecordIds).toEqual([12345])
})
it('should omit version from storage record when subjectVersion is empty', async () => {
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
const opts = { ...storageOpts, subjectVersion: '' }
await createAttestation(subjects, TEST_PREDICATE, opts)
expect(mockCreateStorageRecord).toHaveBeenCalledWith(
expect.objectContaining({ version: undefined }),
expect.anything(),
expect.anything()
)
})
it('should skip storage record when owner is User', async () => {
mockGetOctokit.mockReturnValue(createOctokitMock('User'))
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
const result = await createAttestation(
subjects,
TEST_PREDICATE,
storageOpts
)
expect(mockCreateStorageRecord).not.toHaveBeenCalled()
expect(result.storageRecordIds).toBeUndefined()
})
it('should skip storage record when createStorageRecord is false', async () => {
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
const opts = { ...storageOpts, createStorageRecord: false }
await createAttestation(subjects, TEST_PREDICATE, opts)
expect(mockCreateStorageRecord).not.toHaveBeenCalled()
})
it('should handle empty storage records gracefully', async () => {
mockCreateStorageRecord.mockResolvedValue([])
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
const result = await createAttestation(
subjects,
TEST_PREDICATE,
storageOpts
)
expect(result.storageRecordIds).toEqual([])
})
it('should continue when storage record creation fails', async () => {
mockCreateStorageRecord.mockRejectedValue(new Error('Permission denied'))
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
// Should not throw
const result = await createAttestation(
subjects,
TEST_PREDICATE,
storageOpts
)
expect(result.attestationID).toBe('att-123')
expect(result.storageRecordIds).toBeUndefined()
})
})
describe('sigstore instance selection', () => {
it('should use public-good sigstore instance when specified', async () => {
const subjects = [TEST_SUBJECT_WITH_REGISTRY]
const opts = { ...defaultOpts, sigstoreInstance: 'public-good' as const }
await createAttestation(subjects, TEST_PREDICATE, opts)
expect(mockAttest).toHaveBeenCalledWith(
expect.objectContaining({ sigstore: 'public-good' })
)
})
})
})
+455
View File
@@ -0,0 +1,455 @@
import { jest } from '@jest/globals'
import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import {
createAttestationResult,
createOctokitMock,
TEST_PROVENANCE_PREDICATE
} from '../fixtures/mocks'
import type { Attestation, Predicate } from '@actions/attest'
import type { Descriptor } from '@sigstore/oci'
import type { RunInputs } from '../../src/main'
// Create persistent mock functions
const infoMock = jest.fn()
const warningMock = jest.fn()
const debugMock = jest.fn()
const startGroupMock = jest.fn()
const endGroupMock = jest.fn()
const setOutputMock = jest.fn()
const setFailedMock = jest.fn()
// Create chainable summary mock
const summaryMock = {
write: jest.fn().mockReturnThis(),
addRaw: jest.fn().mockReturnThis(),
addHeading: jest.fn().mockReturnThis(),
addLink: jest.fn().mockReturnThis(),
addTable: jest.fn().mockReturnThis(),
addBreak: jest.fn().mockReturnThis(),
addSeparator: jest.fn().mockReturnThis(),
addQuote: jest.fn().mockReturnThis(),
addCodeBlock: jest.fn().mockReturnThis(),
addList: jest.fn().mockReturnThis(),
addImage: jest.fn().mockReturnThis(),
addDetails: jest.fn().mockReturnThis(),
addEOL: jest.fn().mockReturnThis(),
emptyBuffer: jest.fn().mockReturnThis(),
stringify: jest.fn().mockReturnValue(''),
isEmptyBuffer: jest.fn().mockReturnValue(true),
clear: jest.fn().mockReturnThis()
}
const mockGetOctokit = jest.fn()
const mockAttest = jest.fn<() => Promise<Attestation>>()
const mockBuildSLSAProvenancePredicate = jest.fn<() => Promise<Predicate>>()
const mockCreateStorageRecord = jest.fn<() => Promise<number[]>>()
const mockGetRegistryCredentials = jest.fn()
const mockAttachArtifactToImage = jest.fn<() => Promise<Descriptor>>()
// Mutable context for tests
const mockContext = {
repo: { owner: 'test-owner', repo: 'test-repo' },
payload: { repository: { visibility: 'private' } },
serverUrl: 'https://github.com'
}
// Mock @actions/core
jest.unstable_mockModule('@actions/core', () => ({
info: infoMock,
warning: warningMock,
debug: debugMock,
startGroup: startGroupMock,
endGroup: endGroupMock,
setOutput: setOutputMock,
setFailed: setFailedMock,
summary: summaryMock
}))
// Mock @actions/github
jest.unstable_mockModule('@actions/github', () => ({
getOctokit: mockGetOctokit,
context: mockContext
}))
// Mock @actions/attest
jest.unstable_mockModule('@actions/attest', () => ({
attest: mockAttest,
buildSLSAProvenancePredicate: mockBuildSLSAProvenancePredicate,
createStorageRecord: mockCreateStorageRecord
}))
// Mock @sigstore/oci
jest.unstable_mockModule('@sigstore/oci', () => ({
getRegistryCredentials: mockGetRegistryCredentials,
attachArtifactToImage: mockAttachArtifactToImage
}))
// Dynamic import after mocking
const { run } = await import('../../src/main')
const defaultInputs: RunInputs = {
predicate: '',
predicateType: '',
predicatePath: '',
sbomPath: '',
subjectName: '',
subjectDigest: '',
subjectPath: '',
subjectChecksums: '',
pushToRegistry: false,
createStorageRecord: false,
subjectVersion: '',
showSummary: false,
githubToken: 'test-token',
privateSigning: false
}
describe('run', () => {
let tempDir: string
const originalEnv = { ...process.env }
beforeEach(async () => {
jest.clearAllMocks()
// Reset chainable summary mocks
for (const key of Object.keys(summaryMock)) {
if (key !== 'stringify' && key !== 'isEmptyBuffer') {
;(
summaryMock[key as keyof typeof summaryMock] as jest.Mock
).mockReturnThis()
}
}
mockAttest.mockResolvedValue(createAttestationResult())
mockBuildSLSAProvenancePredicate.mockResolvedValue(
TEST_PROVENANCE_PREDICATE
)
mockCreateStorageRecord.mockResolvedValue([12345])
mockGetOctokit.mockReturnValue(createOctokitMock('Organization'))
mockGetRegistryCredentials.mockReturnValue({ username: 'u', password: 'p' })
mockAttachArtifactToImage.mockResolvedValue({
digest: 'sha256:abc',
mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json',
size: 100
})
// Reset context
mockContext.repo = { owner: 'test-owner', repo: 'test-repo' }
mockContext.payload = { repository: { visibility: 'private' } }
// Create temp directory
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'main-test-'))
// Set required environment
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = 'https://token.url'
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = 'test-token'
process.env.RUNNER_TEMP = tempDir
})
afterEach(async () => {
process.env = { ...originalEnv }
await fs.rm(tempDir, { recursive: true, force: true })
})
describe('environment validation', () => {
it('should fail when ACTIONS_ID_TOKEN_REQUEST_URL is not set', async () => {
delete process.env.ACTIONS_ID_TOKEN_REQUEST_URL
await run({
...defaultInputs,
subjectName: 'artifact',
subjectDigest:
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32',
predicateType: 'https://example.com/predicate',
predicate: '{}'
})
expect(setFailedMock).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('id-token')
})
)
})
})
describe('subject validation', () => {
it('should fail when no subject inputs are provided', async () => {
await run(defaultInputs)
expect(setFailedMock).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('subject-path')
})
)
})
})
describe('attestation type detection', () => {
it('should detect provenance attestation when no predicate inputs provided', async () => {
await run({
...defaultInputs,
subjectName: 'artifact',
subjectDigest:
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
})
expect(infoMock).toHaveBeenCalledWith(
'Attestation type: Build Provenance'
)
expect(mockBuildSLSAProvenancePredicate).toHaveBeenCalled()
})
it('should detect custom attestation when predicate inputs provided', async () => {
await run({
...defaultInputs,
subjectName: 'artifact',
subjectDigest:
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32',
predicateType: 'https://example.com/predicate',
predicate: '{}'
})
expect(infoMock).toHaveBeenCalledWith('Attestation type: Custom')
expect(mockBuildSLSAProvenancePredicate).not.toHaveBeenCalled()
})
it('should detect SBOM attestation when sbom-path provided', async () => {
const sbomPath = path.join(tempDir, 'sbom.json')
await fs.writeFile(
sbomPath,
JSON.stringify({
spdxVersion: 'SPDX-2.3',
SPDXID: 'SPDXRef-DOCUMENT',
name: 'test'
})
)
await run({
...defaultInputs,
subjectName: 'artifact',
subjectDigest:
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32',
sbomPath
})
expect(infoMock).toHaveBeenCalledWith('Attestation type: SBOM')
})
it('should fail when sbom-path is combined with predicate inputs', async () => {
await run({
...defaultInputs,
subjectName: 'artifact',
subjectDigest:
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32',
sbomPath: '/path/to/sbom.json',
predicateType: 'https://example.com/predicate'
})
expect(setFailedMock).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining(
'Cannot specify sbom-path together with'
)
})
)
})
})
describe('successful attestation', () => {
const validInputs: RunInputs = {
...defaultInputs,
subjectName: 'artifact',
subjectDigest:
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32',
predicateType: 'https://example.com/predicate',
predicate: '{}'
}
it('should create attestation successfully', async () => {
await run(validInputs)
expect(setFailedMock).not.toHaveBeenCalled()
expect(mockAttest).toHaveBeenCalled()
})
it('should set output for attestation-id', async () => {
await run(validInputs)
expect(setOutputMock).toHaveBeenCalledWith('attestation-id', 'att-123')
})
it('should set output for attestation-url', async () => {
await run(validInputs)
expect(setOutputMock).toHaveBeenCalledWith(
'attestation-url',
'https://github.com/test-owner/test-repo/attestations/att-123'
)
})
it('should set output for bundle-path', async () => {
await run(validInputs)
expect(setOutputMock).toHaveBeenCalledWith(
'bundle-path',
expect.stringContaining('attestation.json')
)
})
it('should write attestation bundle to file', async () => {
await run(validInputs)
const bundlePath = setOutputMock.mock.calls.find(
(call: unknown[]) => call[0] === 'bundle-path'
)?.[1] as string
const content = await fs.readFile(bundlePath, 'utf-8')
expect(content).toContain('mediaType')
})
})
describe('sigstore instance selection', () => {
const validInputs: RunInputs = {
...defaultInputs,
subjectName: 'artifact',
subjectDigest:
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32',
predicateType: 'https://example.com/predicate',
predicate: '{}'
}
it('should use github sigstore for private repos', async () => {
mockContext.payload = { repository: { visibility: 'private' } }
await run(validInputs)
expect(mockAttest).toHaveBeenCalledWith(
expect.objectContaining({ sigstore: 'github' })
)
})
it('should use public-good sigstore for public repos', async () => {
mockContext.payload = { repository: { visibility: 'public' } }
await run(validInputs)
expect(mockAttest).toHaveBeenCalledWith(
expect.objectContaining({ sigstore: 'public-good' })
)
})
it('should use github sigstore when privateSigning is true', async () => {
mockContext.payload = { repository: { visibility: 'public' } }
await run({ ...validInputs, privateSigning: true })
expect(mockAttest).toHaveBeenCalledWith(
expect.objectContaining({ sigstore: 'github' })
)
})
})
describe('multiple subjects', () => {
it('should handle multiple subjects from glob pattern', async () => {
// Create test files
for (let i = 0; i < 3; i++) {
await fs.writeFile(path.join(tempDir, `file-${i}.txt`), `content-${i}`)
}
await run({
...defaultInputs,
subjectPath: path.join(tempDir, 'file-*.txt'),
predicateType: 'https://example.com/predicate',
predicate: '{}'
})
expect(setFailedMock).not.toHaveBeenCalled()
expect(infoMock).toHaveBeenCalledWith(
expect.stringContaining('3 subjects')
)
})
it('should fail when subject count exceeds maximum', async () => {
// Create too many files
for (let i = 0; i < 1025; i++) {
await fs.writeFile(path.join(tempDir, `file-${i}.txt`), `content-${i}`)
}
await run({
...defaultInputs,
subjectPath: path.join(tempDir, 'file-*.txt'),
predicateType: 'https://example.com/predicate',
predicate: '{}'
})
expect(setFailedMock).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Too many subjects')
})
)
})
})
describe('summary output', () => {
const validInputs: RunInputs = {
...defaultInputs,
subjectName: 'artifact',
subjectDigest:
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32',
predicateType: 'https://example.com/predicate',
predicate: '{}',
showSummary: true
}
it('should write summary when showSummary is true', async () => {
await run(validInputs)
expect(summaryMock.addHeading).toHaveBeenCalled()
expect(summaryMock.write).toHaveBeenCalled()
})
it('should not write summary when showSummary is false', async () => {
await run({ ...validInputs, showSummary: false })
expect(summaryMock.write).not.toHaveBeenCalled()
})
})
describe('registry push', () => {
const registryInputs: RunInputs = {
...defaultInputs,
subjectName: 'ghcr.io/test-owner/test-repo',
subjectDigest:
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32',
predicateType: 'https://example.com/predicate',
predicate: '{}',
pushToRegistry: true
}
it('should push attestation to registry when enabled', async () => {
await run(registryInputs)
expect(mockAttachArtifactToImage).toHaveBeenCalled()
})
it('should lowercase subject name for registry push', async () => {
await run({
...registryInputs,
subjectName: 'ghcr.io/TEST-OWNER/Test-Repo'
})
expect(mockAttest).toHaveBeenCalledWith(
expect.objectContaining({
subjects: [
expect.objectContaining({
name: 'ghcr.io/test-owner/test-repo'
})
]
})
)
})
})
})
@@ -1,5 +1,6 @@
import type { Predicate } from '@actions/attest'
import { jest } from '@jest/globals'
import { TEST_PROVENANCE_PREDICATE } from '../fixtures/mocks'
// Mock function
const mockBuildSLSAProvenancePredicate = jest.fn<() => Promise<Predicate>>()
@@ -10,39 +11,27 @@ jest.unstable_mockModule('@actions/attest', () => ({
}))
// Dynamic import after mocking
const { generateProvenancePredicate } = await import('../src/provenance')
const { generateProvenancePredicate } = await import('../../src/provenance')
describe('generateProvenancePredicate', () => {
const mockPredicate = {
type: 'https://slsa.dev/provenance/v1',
params: {
buildDefinition: {
buildType: 'https://actions.github.io/buildtypes/workflow/v1'
},
runDetails: {
builder: { id: 'https://github.com/actions/runner' }
}
}
}
beforeEach(() => {
jest.clearAllMocks()
mockBuildSLSAProvenancePredicate.mockResolvedValue(mockPredicate)
mockBuildSLSAProvenancePredicate.mockResolvedValue(TEST_PROVENANCE_PREDICATE)
})
it('returns the SLSA provenance predicate', async () => {
it('should delegate to buildSLSAProvenancePredicate', async () => {
const result = await generateProvenancePredicate()
expect(mockBuildSLSAProvenancePredicate).toHaveBeenCalledTimes(1)
expect(result).toEqual(mockPredicate)
expect(result).toEqual(TEST_PROVENANCE_PREDICATE)
})
it('propagates errors from buildSLSAProvenancePredicate', async () => {
const error = new Error('Failed to build provenance')
it('should propagate errors from the underlying function', async () => {
const error = new Error('Failed to build provenance predicate')
mockBuildSLSAProvenancePredicate.mockRejectedValue(error)
await expect(generateProvenancePredicate()).rejects.toThrow(
'Failed to build provenance'
'Failed to build provenance predicate'
)
})
})
-676
View File
@@ -1,676 +0,0 @@
/**
* Unit tests for the action's main functionality, src/main.ts
*
* These should be run as if the action was called from a workflow.
* Specifically, the inputs listed in `action.yml` should be set as environment
* variables following the pattern `INPUT_<INPUT_NAME>`.
*/
import type { Predicate } from '@actions/attest'
import { jest } from '@jest/globals'
import type { RunInputs } from '../src/main'
// Create mock functions before mocking modules
const infoMock = jest.fn()
const warningMock = jest.fn()
const startGroupMock = jest.fn()
const endGroupMock = jest.fn()
const setOutputMock = jest.fn()
const setFailedMock = jest.fn()
const debugMock = jest.fn()
// OCI mocks
const getRegCredsMock = jest.fn()
const attachArtifactMock = jest.fn()
// Attest mocks
const attestMock = jest.fn()
const createStorageRecordMock = jest.fn()
// Local attest mocks
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const createAttestationMock = jest.fn<() => Promise<any>>()
const repoOwnerIsOrgMock = jest.fn()
// Provenance mock
const generateProvenancePredicateMock = jest.fn<() => Promise<Predicate>>()
// GitHub context mock
const mockContext = {
repo: { owner: 'foo', repo: 'bar' },
payload: { repository: { visibility: 'private' } }
}
const mockGetOctokit = jest.fn()
// Summary mock with chainable methods
const summaryMock = {
write: jest.fn().mockReturnThis(),
addRaw: jest.fn().mockReturnThis(),
addHeading: jest.fn().mockReturnThis(),
addLink: jest.fn().mockReturnThis(),
addTable: jest.fn().mockReturnThis(),
addBreak: jest.fn().mockReturnThis(),
addSeparator: jest.fn().mockReturnThis(),
addQuote: jest.fn().mockReturnThis(),
addCodeBlock: jest.fn().mockReturnThis(),
addList: jest.fn().mockReturnThis(),
addImage: jest.fn().mockReturnThis(),
addDetails: jest.fn().mockReturnThis(),
addEOL: jest.fn().mockReturnThis(),
emptyBuffer: jest.fn().mockReturnThis(),
stringify: jest.fn().mockReturnValue(''),
isEmptyBuffer: jest.fn().mockReturnValue(true),
clear: jest.fn().mockReturnThis()
}
// Mock @actions/core
jest.unstable_mockModule('@actions/core', () => ({
info: infoMock,
warning: warningMock,
startGroup: startGroupMock,
endGroup: endGroupMock,
setOutput: setOutputMock,
setFailed: setFailedMock,
debug: debugMock,
summary: summaryMock
}))
// Mock @actions/github
jest.unstable_mockModule('@actions/github', () => ({
context: mockContext,
getOctokit: mockGetOctokit
}))
// Mock @sigstore/oci
jest.unstable_mockModule('@sigstore/oci', () => ({
getRegistryCredentials: getRegCredsMock,
attachArtifactToImage: attachArtifactMock
}))
// Mock @actions/attest
jest.unstable_mockModule('@actions/attest', () => ({
attest: attestMock,
createStorageRecord: createStorageRecordMock
}))
// Mock ../src/attest
jest.unstable_mockModule('../src/attest', () => ({
createAttestation: createAttestationMock,
repoOwnerIsOrg: repoOwnerIsOrgMock
}))
// Mock ../src/provenance
jest.unstable_mockModule('../src/provenance', () => ({
generateProvenancePredicate: generateProvenancePredicateMock
}))
// Dynamic imports after mocking
const { mockFulcio, mockRekor, mockTSA } = await import('@sigstore/mock')
const fs = (await import('fs/promises')).default
const nock = (await import('nock')).default
const os = (await import('os')).default
const path = (await import('path')).default
const { MockAgent, setGlobalDispatcher } = await import('undici')
const { run } = await import('../src/main')
// MockAgent for mocking @actions/github
const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
const defaultInputs: RunInputs = {
predicate: '',
predicateType: '',
predicatePath: '',
sbomPath: '',
subjectName: '',
subjectDigest: '',
subjectPath: '',
subjectChecksums: '',
pushToRegistry: false,
createStorageRecord: true,
showSummary: true,
githubToken: '',
privateSigning: false
}
describe('action', () => {
// Capture original environment variables so we can restore after each test
const originalEnv = process.env
const originalContext = {
repo: { owner: 'foo', repo: 'bar' },
payload: { repository: { visibility: 'private' } }
}
// Mock OIDC token endpoint
const tokenURL = 'https://token.url'
// Fake an OIDC token
const oidcSubject = 'foo@bar.com'
const oidcPayload = { sub: oidcSubject, iss: '' }
const oidcToken = `.${Buffer.from(JSON.stringify(oidcPayload)).toString(
'base64'
)}.}`
const subjectName = 'ghcr.io/registry/foo/bar'
const subjectDigest =
'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
const predicate = '{}'
const predicateType = 'https://in-toto.io/attestation/release/v0.1'
const attestationID = '1234567890'
const storageRecordID = 987654321
beforeEach(() => {
jest.clearAllMocks()
nock(tokenURL)
.get('/')
.query({ audience: 'sigstore' })
.reply(200, { value: oidcToken })
const pool = mockAgent.get('https://api.github.com')
pool
.intercept({
path: /^\/repos\/.*\/.*\/attestations$/,
method: 'post'
})
.reply(201, { id: attestationID })
pool
.intercept({
path: /^\/orgs\/.*\/artifacts\/metadata\/storage-record$/,
method: 'post'
})
.reply(200, { storage_records: [{ id: storageRecordID }] })
process.env = {
...originalEnv,
ACTIONS_ID_TOKEN_REQUEST_URL: tokenURL,
ACTIONS_ID_TOKEN_REQUEST_TOKEN: 'token',
RUNNER_TEMP: process.env.RUNNER_TEMP || '/tmp'
}
})
afterEach(() => {
// Restore the original environment
process.env = originalEnv
// Restore the original github.context
setGHContext(originalContext)
})
describe('when ACTIONS_ID_TOKEN_REQUEST_URL is not set', () => {
const inputs: RunInputs = {
...defaultInputs,
subjectDigest,
subjectName,
predicateType,
predicate,
githubToken: 'gh-token'
}
beforeEach(() => {
// Nullify the OIDC token URL
process.env.ACTIONS_ID_TOKEN_REQUEST_URL = ''
})
it('sets a failed status', async () => {
await run(inputs)
expect(setFailedMock).toHaveBeenCalledWith(
new Error(
'missing "id-token" permission. Please add "permissions: id-token: write" to your workflow.'
)
)
})
})
describe('when no inputs are provided', () => {
it('sets a failed status', async () => {
await run(defaultInputs)
expect(setFailedMock).toHaveBeenCalledWith(
new Error(
'One of subject-path, subject-digest, or subject-checksums must be provided'
)
)
})
})
describe('when the repository is private', () => {
const inputs: RunInputs = {
...defaultInputs,
subjectDigest,
subjectName,
predicateType,
predicate,
githubToken: 'gh-token'
}
beforeEach(async () => {
// Set the GH context with private repository visibility and a repo owner.
setGHContext({
payload: { repository: { visibility: 'private' } },
repo: { owner: 'foo', repo: 'bar' }
})
// Mock createAttestation to return expected values
createAttestationMock.mockResolvedValue({
attestationID,
certificate:
'-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
tlogID: 'tlog-123',
attestationDigest: 'sha256:123456',
bundle: { mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json' }
})
await mockFulcio({
baseURL: 'https://fulcio.githubapp.com',
strict: false
})
await mockTSA({ baseURL: 'https://timestamp.githubapp.com' })
})
it('invokes the action w/o error', async () => {
await run(inputs)
expect(setFailedMock).not.toHaveBeenCalled()
expect(infoMock).toHaveBeenCalledWith('Attestation type: Custom')
expect(infoMock).toHaveBeenCalledWith(
expect.stringMatching(
`Attestation created for ${subjectName}@${subjectDigest}`
)
)
expect(createAttestationMock).toHaveBeenCalled()
})
})
describe('when the repository is public', () => {
const inputs: RunInputs = {
...defaultInputs,
subjectDigest,
subjectName,
predicateType,
predicate,
githubToken: 'gh-token',
pushToRegistry: true
}
beforeEach(async () => {
// Set the GH context with public repository visibility and a repo owner.
setGHContext({
payload: { repository: { visibility: 'public' } },
repo: { owner: 'foo', repo: 'bar' }
})
// Setup createAttestation mock
createAttestationMock.mockResolvedValue({
attestationID,
certificate:
'-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
tlogID: 'tlog-123',
attestationDigest: 'sha256:123456',
bundle: { mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json' },
storageRecordIds: [storageRecordID]
})
await mockFulcio({
baseURL: 'https://fulcio.sigstore.dev',
strict: false
})
await mockRekor({ baseURL: 'https://rekor.sigstore.dev' })
mockGetOctokit.mockReturnValue({
rest: {
repos: {
get: jest
.fn<() => Promise<{ data: { owner: { type: string } } }>>()
.mockResolvedValue({
data: { owner: { type: 'Organization' } }
})
}
}
})
})
it('invokes the action w/o error', async () => {
await run(inputs)
expect(setFailedMock).not.toHaveBeenCalled()
expect(createAttestationMock).toHaveBeenCalled()
expect(infoMock).toHaveBeenCalledWith('Attestation type: Custom')
expect(infoMock).toHaveBeenCalledWith(
expect.stringMatching(
`Attestation created for ${subjectName}@${subjectDigest}`
)
)
})
it('catches error when storage record creation fails and continues', async () => {
// Mock createAttestation to simulate storage record failure (but still succeed overall)
createAttestationMock.mockResolvedValue({
attestationID,
certificate:
'-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
tlogID: 'tlog-123',
attestationDigest: 'sha256:123456',
bundle: { mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json' }
// No storageRecordIDs - simulates empty/failed storage record
})
await run(inputs)
expect(createAttestationMock).toHaveBeenCalled()
expect(setFailedMock).not.toHaveBeenCalled()
})
it('does not create a storage record when the repo is owned by a user', async () => {
// Mock createAttestation to not return storage record IDs
createAttestationMock.mockResolvedValue({
attestationID,
certificate:
'-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
tlogID: 'tlog-123',
attestationDigest: 'sha256:123456',
bundle: { mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json' }
})
await run(inputs)
expect(setFailedMock).not.toHaveBeenCalled()
expect(createAttestationMock).toHaveBeenCalled()
expect(infoMock).toHaveBeenCalledWith(
expect.stringMatching(
`Attestation created for ${subjectName}@${subjectDigest}`
)
)
})
})
describe('when the subject count is greater than 1', () => {
let dir = ''
const filename = 'subject'
beforeEach(async () => {
const subjectCount = 5
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)
}
// Set the GH context with private repository visibility and a repo owner.
setGHContext({
payload: { repository: { visibility: 'private' } },
repo: { owner: 'foo', repo: 'bar' }
})
// 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' })
})
afterEach(async () => {
// Clean-up temp directory
await fs.rm(dir, { recursive: true })
})
it('invokes the action w/o error', async () => {
const inputs: RunInputs = {
...defaultInputs,
subjectPath: path.join(dir, `${filename}-*`),
predicateType,
predicate,
githubToken: 'gh-token'
}
await run(inputs)
expect(setFailedMock).not.toHaveBeenCalled()
expect(infoMock).toHaveBeenNthCalledWith(1, 'Attestation type: Custom')
expect(infoMock).toHaveBeenNthCalledWith(
2,
expect.stringMatching('Attestation created for 5 subjects')
)
})
})
describe('when the subject count exceeds the max', () => {
let dir = ''
const filename = 'subject'
beforeEach(async () => {
const subjectCount = 1025
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)
}
// 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('sets a failed status', async () => {
const inputs: RunInputs = {
...defaultInputs,
subjectPath: path.join(dir, `${filename}-*`),
predicateType,
predicate,
githubToken: 'gh-token'
}
await run(inputs)
expect(setFailedMock).toHaveBeenCalledWith(
new Error(
'Too many subjects specified (>1024). The maximum number of subjects is 1024.'
)
)
})
})
describe('attestation type detection', () => {
describe('when sbom-path is provided with predicate inputs', () => {
it('sets a failed status for conflicting inputs', async () => {
const inputs: RunInputs = {
...defaultInputs,
subjectDigest,
subjectName,
sbomPath: '/path/to/sbom.json',
predicateType: 'https://example.com/predicate',
githubToken: 'gh-token'
}
await run(inputs)
expect(setFailedMock).toHaveBeenCalledWith(
new Error(
'Cannot specify sbom-path together with predicate-type, predicate, or predicate-path'
)
)
})
})
describe('when predicate is provided without predicate-type', () => {
it('sets a failed status for missing predicate-type', async () => {
const inputs: RunInputs = {
...defaultInputs,
subjectDigest,
subjectName,
predicate: '{}',
githubToken: 'gh-token'
}
await run(inputs)
expect(setFailedMock).toHaveBeenCalledWith(
new Error(
'predicate-type is required when using predicate or predicate-path'
)
)
})
})
describe('when custom attestation inputs are provided', () => {
const inputs: RunInputs = {
...defaultInputs,
subjectDigest,
subjectName,
predicateType,
predicate,
githubToken: 'gh-token'
}
beforeEach(async () => {
setGHContext({
payload: { repository: { visibility: 'private' } },
repo: { owner: 'foo', repo: 'bar' }
})
await mockFulcio({
baseURL: 'https://fulcio.githubapp.com',
strict: false
})
await mockTSA({ baseURL: 'https://timestamp.githubapp.com' })
})
it('logs the attestation type as Custom', async () => {
await run(inputs)
expect(setFailedMock).not.toHaveBeenCalled()
expect(infoMock).toHaveBeenCalledWith('Attestation type: Custom')
})
})
describe('when provenance attestation is detected', () => {
const inputs: RunInputs = {
...defaultInputs,
subjectDigest,
subjectName,
githubToken: 'gh-token'
}
const mockProvPredicate = {
type: 'https://slsa.dev/provenance/v1',
params: { buildDefinition: {}, runDetails: {} }
}
beforeEach(async () => {
// Configure mock for provenance predicate
generateProvenancePredicateMock.mockResolvedValue(mockProvPredicate)
// Configure mock for createAttestation
createAttestationMock.mockResolvedValue({
attestationID: '1234567890',
certificate:
'-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----',
tlogID: 'tlog-123',
attestationDigest: 'sha256:123456',
bundle: { mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json' }
})
setGHContext({
payload: { repository: { visibility: 'private' } },
repo: { owner: 'foo', repo: 'bar' }
})
await mockFulcio({
baseURL: 'https://fulcio.githubapp.com',
strict: false
})
await mockTSA({ baseURL: 'https://timestamp.githubapp.com' })
})
it('logs the attestation type as Build Provenance and generates predicate', async () => {
await run(inputs)
expect(setFailedMock).not.toHaveBeenCalled()
expect(infoMock).toHaveBeenCalledWith(
'Attestation type: Build Provenance'
)
})
})
describe('when sbom attestation is detected', () => {
let tmpDir: string
let sbomFilePath: string
const spdxSBOM = {
spdxVersion: 'SPDX-2.3',
SPDXID: 'SPDXRef-DOCUMENT',
name: 'test-package',
packages: []
}
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'main-test-'))
sbomFilePath = path.join(tmpDir, 'sbom.spdx.json')
await fs.writeFile(sbomFilePath, JSON.stringify(spdxSBOM))
setGHContext({
payload: { repository: { visibility: 'private' } },
repo: { owner: 'foo', repo: 'bar' }
})
await mockFulcio({
baseURL: 'https://fulcio.githubapp.com',
strict: false
})
await mockTSA({ baseURL: 'https://timestamp.githubapp.com' })
})
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true })
})
it('logs the attestation type as SBOM and generates predicate', async () => {
const inputs: RunInputs = {
...defaultInputs,
subjectDigest,
subjectName,
sbomPath: sbomFilePath,
githubToken: 'gh-token'
}
await run(inputs)
expect(setFailedMock).not.toHaveBeenCalled()
expect(infoMock).toHaveBeenCalledWith('Attestation type: SBOM')
})
})
})
})
// Helper to update the mock context
function setGHContext(context: {
repo?: { owner: string; repo: string }
payload?: { repository?: { visibility: string } }
}): void {
if (context.repo) {
mockContext.repo = context.repo
}
if (context.payload) {
mockContext.payload = context.payload as typeof mockContext.payload
}
}
-133
View File
@@ -1,133 +0,0 @@
import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import { predicateFromInputs, PredicateInputs } from '../src/predicate'
describe('subjectFromInputs', () => {
const blankInputs: PredicateInputs = {
predicateType: '',
predicate: '',
predicatePath: ''
}
describe('when no inputs are provided', () => {
it('throws an error', async () => {
await expect(predicateFromInputs(blankInputs)).rejects.toThrow(
/predicate-type/i
)
})
})
describe('when neither predicate path nor predicate are provided', () => {
it('throws an error', async () => {
const inputs: PredicateInputs = {
...blankInputs,
predicateType: 'https://example.com/predicate'
}
await expect(predicateFromInputs(inputs)).rejects.toThrow(
/one of predicate-path or predicate must be provided/i
)
})
})
describe('when both predicate path and predicate are provided', () => {
it('throws an error', async () => {
const inputs: PredicateInputs = {
predicateType: 'https://example.com/predicate',
predicate: '{}',
predicatePath: 'path/to/predicate'
}
await expect(predicateFromInputs(inputs)).rejects.toThrow(
/only one of predicate-path or predicate may be provided/i
)
})
})
describe('when specifying a predicate path', () => {
const predicateType = 'https://example.com/predicate'
const content = '{}'
let predicatePath = ''
beforeEach(async () => {
// Set-up temp directory
const tmpDir = await fs.realpath(os.tmpdir())
const dir = await fs.mkdtemp(tmpDir + path.sep)
const filename = 'subject'
predicatePath = path.join(dir, filename)
// Write file to temp directory
await fs.writeFile(predicatePath, content)
})
afterEach(async () => {
// Clean-up temp directory
await fs.rm(path.parse(predicatePath).dir, { recursive: true })
})
it('returns the predicate', async () => {
const inputs: PredicateInputs = {
...blankInputs,
predicateType,
predicatePath
}
await expect(predicateFromInputs(inputs)).resolves.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', async () => {
const inputs: PredicateInputs = {
...blankInputs,
predicateType,
predicatePath
}
await expect(predicateFromInputs(inputs)).rejects.toThrow(
/file not found/
)
})
})
describe('when specifying a predicate value', () => {
const predicateType = 'https://example.com/predicate'
const content = '{}'
it('returns the predicate', async () => {
const inputs: PredicateInputs = {
...blankInputs,
predicateType,
predicate: content
}
await expect(predicateFromInputs(inputs)).resolves.toEqual({
type: predicateType,
params: JSON.parse(content)
})
})
})
describe('when specifying a predicate value exceeding the max size', () => {
const predicateType = 'https://example.com/predicate'
const content = JSON.stringify({ a: 'a'.repeat(16 * 1024 * 1024) })
it('throws an error', async () => {
const inputs: PredicateInputs = {
...blankInputs,
predicateType,
predicate: content
}
await expect(predicateFromInputs(inputs)).rejects.toThrow(
/predicate string exceeds maximum/
)
})
})
})
-161
View File
@@ -1,161 +0,0 @@
import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import { parseSBOMFromPath, generateSBOMPredicate, SBOM } from '../src/sbom'
describe('parseSBOMFromPath', () => {
let tmpDir: string
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sbom-test-'))
})
afterEach(async () => {
await fs.rm(tmpDir, { recursive: true })
})
describe('when file does not exist', () => {
it('throws an error', async () => {
await expect(parseSBOMFromPath('/nonexistent/file.json')).rejects.toThrow(
/SBOM file not found/
)
})
})
describe('when file contains valid SPDX SBOM', () => {
const spdxSBOM = {
spdxVersion: 'SPDX-2.3',
SPDXID: 'SPDXRef-DOCUMENT',
name: 'test-package',
packages: []
}
it('returns SBOM with type spdx', async () => {
const filePath = path.join(tmpDir, 'sbom.spdx.json')
await fs.writeFile(filePath, JSON.stringify(spdxSBOM))
const result = await parseSBOMFromPath(filePath)
expect(result.type).toBe('spdx')
expect(result.object).toEqual(spdxSBOM)
})
})
describe('when file contains valid CycloneDX SBOM', () => {
const cyclonedxSBOM = {
bomFormat: 'CycloneDX',
specVersion: '1.4',
serialNumber: 'urn:uuid:12345',
components: []
}
it('returns SBOM with type cyclonedx', async () => {
const filePath = path.join(tmpDir, 'sbom.cdx.json')
await fs.writeFile(filePath, JSON.stringify(cyclonedxSBOM))
const result = await parseSBOMFromPath(filePath)
expect(result.type).toBe('cyclonedx')
expect(result.object).toEqual(cyclonedxSBOM)
})
})
describe('when file contains invalid SBOM format', () => {
it('throws an error', async () => {
const filePath = path.join(tmpDir, 'invalid.json')
await fs.writeFile(filePath, JSON.stringify({ random: 'data' }))
await expect(parseSBOMFromPath(filePath)).rejects.toThrow(
/Unsupported SBOM format/
)
})
})
describe('when file contains invalid JSON', () => {
it('throws an error', async () => {
const filePath = path.join(tmpDir, 'invalid.json')
await fs.writeFile(filePath, 'not valid json')
await expect(parseSBOMFromPath(filePath)).rejects.toThrow()
})
})
describe('when file exceeds maximum size', () => {
it('throws an error', async () => {
const filePath = path.join(tmpDir, 'large.json')
// Create a file larger than 16MB
const largeContent = 'x'.repeat(17 * 1024 * 1024)
await fs.writeFile(filePath, largeContent)
await expect(parseSBOMFromPath(filePath)).rejects.toThrow(
/SBOM file exceeds maximum allowed size/
)
})
})
})
describe('generateSBOMPredicate', () => {
describe('for SPDX SBOM', () => {
const spdxSBOM: SBOM = {
type: 'spdx',
object: {
spdxVersion: 'SPDX-2.3',
SPDXID: 'SPDXRef-DOCUMENT',
name: 'test-package'
}
}
it('returns predicate with correct SPDX type', () => {
const predicate = generateSBOMPredicate(spdxSBOM)
expect(predicate.type).toBe('https://spdx.dev/Document/v2.3')
expect(predicate.params).toEqual(spdxSBOM.object)
})
})
describe('for CycloneDX SBOM', () => {
const cyclonedxSBOM: SBOM = {
type: 'cyclonedx',
object: {
bomFormat: 'CycloneDX',
specVersion: '1.4',
serialNumber: 'urn:uuid:12345'
}
}
it('returns predicate with correct CycloneDX type', () => {
const predicate = generateSBOMPredicate(cyclonedxSBOM)
expect(predicate.type).toBe('https://cyclonedx.org/bom')
expect(predicate.params).toEqual(cyclonedxSBOM.object)
})
})
describe('for SPDX without version', () => {
const invalidSBOM: SBOM = {
type: 'spdx',
object: {
SPDXID: 'SPDXRef-DOCUMENT'
}
}
it('throws an error', () => {
expect(() => generateSBOMPredicate(invalidSBOM)).toThrow(
/Cannot find spdxVersion/
)
})
})
describe('for unsupported SBOM type', () => {
const unsupportedSBOM = {
type: 'unknown' as SBOM['type'],
object: { foo: 'bar' }
}
it('throws an error', () => {
expect(() => generateSBOMPredicate(unsupportedSBOM)).toThrow(
/Unsupported SBOM format/
)
})
})
})
-15
View File
@@ -1,15 +0,0 @@
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')
})
})
})
-600
View File
@@ -1,600 +0,0 @@
import crypto from 'crypto'
import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import {
formatSubjectDigest,
subjectFromInputs,
SubjectInputs
} from '../src/subject'
describe('subjectFromInputs', () => {
const blankInputs: SubjectInputs = {
subjectPath: '',
subjectName: '',
subjectDigest: '',
subjectChecksums: ''
}
describe('when no inputs are provided', () => {
it('throws an error', async () => {
await expect(subjectFromInputs(blankInputs)).rejects.toThrow(
/one of subject-path, subject-digest, or subject-checksums must be provided/i
)
})
})
describe('when both subject path and subject digest are provided', () => {
it('throws an error', async () => {
const inputs: SubjectInputs = {
subjectName: 'foo',
subjectPath: 'path/to/subject',
subjectDigest: 'digest',
subjectChecksums: ''
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
)
})
})
describe('when both subject path and subject checksums are provided', () => {
it('throws an error', async () => {
const inputs: SubjectInputs = {
subjectName: '',
subjectPath: 'path/to/subject',
subjectDigest: '',
subjectChecksums: 'path/to/checksums'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
)
})
})
describe('when both subject digest and subject checksums are provided', () => {
it('throws an error', async () => {
const inputs: SubjectInputs = {
subjectName: 'foo',
subjectPath: '',
subjectDigest: 'digest',
subjectChecksums: 'path/to/checksums'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
)
})
})
describe('when subject digest is provided but not the name', () => {
it('throws an error', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectDigest: 'digest'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject-name must be provided when using subject-digest/i
)
})
})
describe('when specifying a subject digest', () => {
const name = 'Subject'
describe('when the digest is malformed', () => {
it('throws an error', async () => {
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
)
})
})
describe('when the algorithm is not supported', () => {
it('throws an error', async () => {
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
)
})
})
describe('when the sha256 digest is malformed', () => {
it('throws an error', async () => {
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
)
})
})
describe('when the sha256 digest is valid', () => {
const alg = 'sha256'
const digest =
'7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
it('returns the subject', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectDigest: `${alg}:${digest}`,
subjectName: name
}
const subject = await subjectFromInputs(inputs)
expect(subject).toBeDefined()
expect(subject).toHaveLength(1)
expect(subject[0].name).toEqual(name)
expect(subject[0].digest).toEqual({ [alg]: digest })
})
})
describe('when the downcaseName is true', () => {
const imageName = 'ghcr.io/FOO/bar'
const alg = 'sha256'
const digest =
'7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
it('returns the subject (with name downcased)', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectDigest: `${alg}:${digest}`,
subjectName: imageName,
downcaseName: true
}
const subject = await subjectFromInputs(inputs)
expect(subject).toBeDefined()
expect(subject).toHaveLength(1)
expect(subject[0].name).toEqual(imageName.toLowerCase())
expect(subject[0].digest).toEqual({ [alg]: digest })
})
})
})
describe('when specifying a subject path', () => {
describe('when the file does NOT exist', () => {
it('throws an error', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: '/f/a/k/e'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/could not find subject at path/i
)
})
})
})
describe('when the file exists', () => {
let dir = ''
const filename = 'subject'
const content = 'file content'
const expectedDigest = crypto
.createHash('sha256')
.update(content)
.digest('hex')
beforeEach(async () => {
// Set-up temp directory
const tmpDir = await fs.realpath(os.tmpdir())
dir = await fs.mkdtemp(tmpDir + path.sep)
// Write file to temp directory
await fs.writeFile(path.join(dir, filename), content)
// Add files for glob testing
for (let i = 0; i < 3; i++) {
await fs.writeFile(path.join(dir, `${filename}-${i}`), content)
await fs.writeFile(path.join(dir, `other-${i}`), content)
}
})
afterEach(async () => {
// Clean-up temp directory
await fs.rm(dir, { recursive: true })
})
describe('when no name is provided', () => {
it('returns the subject', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: path.join(dir, filename)
}
const subject = await subjectFromInputs(inputs)
expect(subject).toBeDefined()
expect(subject).toHaveLength(1)
expect(subject[0].name).toEqual(filename)
expect(subject[0].digest).toEqual({ sha256: expectedDigest })
})
})
describe('when a name is provided', () => {
const name = 'mysubject'
it('returns the subject', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: path.join(dir, filename),
subjectName: name
}
const subject = await subjectFromInputs(inputs)
expect(subject).toBeDefined()
expect(subject).toHaveLength(1)
expect(subject[0].name).toEqual(name)
expect(subject[0].digest).toEqual({ sha256: expectedDigest })
})
})
describe('when a file glob is supplied', () => {
it('returns the multiple subjects', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: path.join(dir, 'subject-*')
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined()
expect(subjects).toHaveLength(3)
subjects.forEach((subject, i) => {
expect(subject.name).toEqual(`${filename}-${i}`)
expect(subject.digest).toEqual({ sha256: expectedDigest })
})
})
})
describe('when a file glob is supplied which also matches non-files', () => {
it('returns the subjects (excluding non-files)', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${dir}*`
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined()
expect(subjects).toHaveLength(7)
})
})
describe('when a comma-separated list is supplied', () => {
it('returns the multiple subjects', async () => {
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).toHaveLength(2)
expect(subjects).toContainEqual({
name: 'subject-1',
digest: { sha256: expectedDigest }
})
expect(subjects).toContainEqual({
name: 'subject-2',
digest: { sha256: expectedDigest }
})
})
})
describe('when a multi-line list is supplied', () => {
it('returns the multiple subjects', async () => {
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).toHaveLength(2)
expect(subjects).toContainEqual({
name: 'subject-0',
digest: { sha256: expectedDigest }
})
expect(subjects).toContainEqual({
name: 'subject-2',
digest: { sha256: expectedDigest }
})
})
})
describe('when an excluding glob is supplied', () => {
it('returns the multiple subjects', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${path.join(dir, 'subject-*')},!${path.join(dir, 'subject-1')}`
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined()
expect(subjects).toHaveLength(2)
expect(subjects).toContainEqual({
name: 'subject-0',
digest: { sha256: expectedDigest }
})
expect(subjects).toContainEqual({
name: 'subject-2',
digest: { sha256: expectedDigest }
})
})
})
describe('when a multi-line glob list is supplied', () => {
it('returns the multiple subjects', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${path.join(dir, 'subject-*')}\n ${path.join(dir, 'other-*')} `
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined()
expect(subjects).toHaveLength(6)
expect(subjects).toContainEqual({
name: 'subject-0',
digest: { sha256: expectedDigest }
})
expect(subjects).toContainEqual({
name: 'subject-1',
digest: { sha256: expectedDigest }
})
expect(subjects).toContainEqual({
name: 'subject-2',
digest: { sha256: expectedDigest }
})
expect(subjects).toContainEqual({
name: 'other-0',
digest: { sha256: expectedDigest }
})
expect(subjects).toContainEqual({
name: 'other-1',
digest: { sha256: expectedDigest }
})
expect(subjects).toContainEqual({
name: 'other-2',
digest: { sha256: expectedDigest }
})
})
})
describe('when duplicate subjects are supplied', () => {
let otherDir = ''
// Add duplicate subject in alternate directory
beforeEach(async () => {
// Set-up temp directory
const tmpDir = await fs.realpath(os.tmpdir())
otherDir = await fs.mkdtemp(tmpDir + path.sep)
// Write file to temp directory
await fs.writeFile(path.join(otherDir, filename), content)
})
it('returns de-duplicated subjects', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${path.join(dir, 'subject')}, ${path.join(otherDir, 'subject')} `
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined()
expect(subjects).toHaveLength(1)
})
})
})
describe('when specifying a subject checksums file', () => {
const checksums = `
187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d demo_0.0.1_linux_amd64
badline
5d8b4751ef31f9440d843fcfa4e53ca2e25b1cb1f13fd355fdc7c24b41fe645293291ea9297ba3989078abb77ebbaac66be073618a9e4974dbd0361881d4c718 demo_0.0.1_darwin_arm64`
let dir = ''
const filename = 'checksums'
beforeEach(async () => {
// Set-up temp directory
const tmpDir = await fs.realpath(os.tmpdir())
dir = await fs.mkdtemp(tmpDir + path.sep)
// Write file to temp directory
await fs.writeFile(path.join(dir, filename), checksums)
})
afterEach(async () => {
// Clean-up temp directory
await fs.rm(dir, { recursive: true })
})
describe('when the specified path is NOT a file', () => {
it('throws an error', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: dir
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject checksums file not found/i
)
})
})
describe('when the specific path is a file', () => {
it('returns the multiple subjects', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: path.join(dir, filename)
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined()
expect(subjects).toHaveLength(2)
expect(subjects).toContainEqual({
name: 'demo_0.0.1_linux_amd64',
digest: {
sha256:
'187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d'
}
})
expect(subjects).toContainEqual({
name: 'demo_0.0.1_darwin_arm64',
digest: {
sha512:
'5d8b4751ef31f9440d843fcfa4e53ca2e25b1cb1f13fd355fdc7c24b41fe645293291ea9297ba3989078abb77ebbaac66be073618a9e4974dbd0361881d4c718'
}
})
})
})
})
describe('when specifying a subject checksums string', () => {
const checksums = `
f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386
187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d *demo_0.0.1_linux_amd64
9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5 demo_0.0.1_linux_arm64`
it('returns the multiple subjects', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined()
expect(subjects).toHaveLength(3)
expect(subjects).toContainEqual({
name: 'demo_0.0.1_linux_386',
digest: {
sha256:
'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'
}
})
})
})
describe('when specifying a subject checksums string with duplicates', () => {
const checksums = `
f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386
f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386
187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d *demo_0.0.1_linux_amd64`
it('returns de-duplicated subjects', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toBeDefined()
expect(subjects).toHaveLength(2)
expect(subjects).toContainEqual({
name: 'demo_0.0.1_linux_386',
digest: {
sha256:
'f861e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e'
}
})
expect(subjects).toContainEqual({
name: 'demo_0.0.1_linux_amd64',
digest: {
sha256:
'187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d'
}
})
})
})
describe('when specifying a subject checksums string with an unrecognized digest', () => {
const checksums = `f861e demo_0.0.1_linux_386`
it('throws an error', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/unknown digest algorithm/i
)
})
})
describe('when specifying a subject checksums string with an invalid digest', () => {
const checksums =
'!!!!e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e demo_0.0.1_linux_386'
it('throws an error', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(/invalid digest/i)
})
})
})
describe('subjectDigest', () => {
it('returns the digest', () => {
const subject = {
name: 'foo',
digest: { sha1: 'deadbeef' }
}
const digest = formatSubjectDigest(subject)
expect(digest).toEqual('sha1:deadbeef')
})
})
@@ -2,7 +2,7 @@ import {
detectAttestationType,
validateAttestationInputs,
DetectionInputs
} from '../src/detect'
} from '../../src/detect'
describe('detectAttestationType', () => {
const blankInputs: DetectionInputs = {
@@ -12,14 +12,12 @@ describe('detectAttestationType', () => {
predicatePath: ''
}
describe('when no inputs are provided', () => {
it('returns provenance', () => {
expect(detectAttestationType(blankInputs)).toBe('provenance')
})
it('should return provenance when no inputs are provided', () => {
expect(detectAttestationType(blankInputs)).toBe('provenance')
})
describe('when sbom-path is provided', () => {
it('returns sbom', () => {
describe('SBOM detection', () => {
it('should return sbom when sbom-path is provided', () => {
const inputs: DetectionInputs = {
...blankInputs,
sbomPath: '/path/to/sbom.json'
@@ -27,7 +25,7 @@ describe('detectAttestationType', () => {
expect(detectAttestationType(inputs)).toBe('sbom')
})
it('returns sbom even when predicate inputs are also provided', () => {
it('should prioritize sbom over custom predicate inputs', () => {
const inputs: DetectionInputs = {
...blankInputs,
sbomPath: '/path/to/sbom.json',
@@ -37,38 +35,32 @@ describe('detectAttestationType', () => {
})
})
describe('when predicate-type is provided', () => {
it('returns custom', () => {
describe('custom detection', () => {
it('should return custom when predicate-type is provided', () => {
const inputs: DetectionInputs = {
...blankInputs,
predicateType: 'https://example.com/predicate'
}
expect(detectAttestationType(inputs)).toBe('custom')
})
})
describe('when predicate is provided', () => {
it('returns custom', () => {
it('should return custom when predicate is provided', () => {
const inputs: DetectionInputs = {
...blankInputs,
predicate: '{}'
}
expect(detectAttestationType(inputs)).toBe('custom')
})
})
describe('when predicate-path is provided', () => {
it('returns custom', () => {
it('should return custom when predicate-path is provided', () => {
const inputs: DetectionInputs = {
...blankInputs,
predicatePath: '/path/to/predicate.json'
}
expect(detectAttestationType(inputs)).toBe('custom')
})
})
describe('when predicate-type and predicate are provided', () => {
it('returns custom', () => {
it('should return custom when predicate-type and predicate are both provided', () => {
const inputs: DetectionInputs = {
...blankInputs,
predicateType: 'https://example.com/predicate',
@@ -87,24 +79,20 @@ describe('validateAttestationInputs', () => {
predicatePath: ''
}
describe('when no inputs are provided', () => {
it('does not throw', () => {
expect(() => validateAttestationInputs(blankInputs)).not.toThrow()
})
it('should not throw when no inputs are provided', () => {
expect(() => validateAttestationInputs(blankInputs)).not.toThrow()
})
describe('when sbom-path is provided alone', () => {
it('does not throw', () => {
const inputs: DetectionInputs = {
...blankInputs,
sbomPath: '/path/to/sbom.json'
}
expect(() => validateAttestationInputs(inputs)).not.toThrow()
})
it('should not throw when sbom-path is provided alone', () => {
const inputs: DetectionInputs = {
...blankInputs,
sbomPath: '/path/to/sbom.json'
}
expect(() => validateAttestationInputs(inputs)).not.toThrow()
})
describe('when sbom-path is combined with predicate-type', () => {
it('throws an error', () => {
describe('sbom-path conflicts', () => {
it('should throw when sbom-path is combined with predicate-type', () => {
const inputs: DetectionInputs = {
...blankInputs,
sbomPath: '/path/to/sbom.json',
@@ -114,10 +102,8 @@ describe('validateAttestationInputs', () => {
/Cannot specify sbom-path together with/
)
})
})
describe('when sbom-path is combined with predicate', () => {
it('throws an error', () => {
it('should throw when sbom-path is combined with predicate', () => {
const inputs: DetectionInputs = {
...blankInputs,
sbomPath: '/path/to/sbom.json',
@@ -127,10 +113,8 @@ describe('validateAttestationInputs', () => {
/Cannot specify sbom-path together with/
)
})
})
describe('when sbom-path is combined with predicate-path', () => {
it('throws an error', () => {
it('should throw when sbom-path is combined with predicate-path', () => {
const inputs: DetectionInputs = {
...blankInputs,
sbomPath: '/path/to/sbom.json',
@@ -142,8 +126,8 @@ describe('validateAttestationInputs', () => {
})
})
describe('when predicate is provided without predicate-type', () => {
it('throws an error', () => {
describe('predicate-type requirements', () => {
it('should throw when predicate is provided without predicate-type', () => {
const inputs: DetectionInputs = {
...blankInputs,
predicate: '{}'
@@ -152,10 +136,8 @@ describe('validateAttestationInputs', () => {
/predicate-type is required/
)
})
})
describe('when predicate-path is provided without predicate-type', () => {
it('throws an error', () => {
it('should throw when predicate-path is provided without predicate-type', () => {
const inputs: DetectionInputs = {
...blankInputs,
predicatePath: '/path/to/predicate.json'
@@ -164,10 +146,8 @@ describe('validateAttestationInputs', () => {
/predicate-type is required/
)
})
})
describe('when predicate-type and predicate are provided', () => {
it('does not throw', () => {
it('should not throw when predicate-type and predicate are provided', () => {
const inputs: DetectionInputs = {
...blankInputs,
predicateType: 'https://example.com/predicate',
@@ -175,10 +155,8 @@ describe('validateAttestationInputs', () => {
}
expect(() => validateAttestationInputs(inputs)).not.toThrow()
})
})
describe('when predicate-type and predicate-path are provided', () => {
it('does not throw', () => {
it('should not throw when predicate-type and predicate-path are provided', () => {
const inputs: DetectionInputs = {
...blankInputs,
predicateType: 'https://example.com/predicate',
+142
View File
@@ -0,0 +1,142 @@
import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import { predicateFromInputs, PredicateInputs } from '../../src/predicate'
describe('predicateFromInputs', () => {
const blankInputs: PredicateInputs = {
predicateType: '',
predicate: '',
predicatePath: ''
}
let tempDir: string
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'predicate-test-'))
})
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true })
})
describe('input validation', () => {
it('should throw when predicate-type is not provided', async () => {
await expect(predicateFromInputs(blankInputs)).rejects.toThrow(
/predicate-type must be provided/
)
})
it('should throw when neither predicate nor predicate-path is provided', async () => {
const inputs: PredicateInputs = {
...blankInputs,
predicateType: 'https://example.com/predicate'
}
await expect(predicateFromInputs(inputs)).rejects.toThrow(
/one of predicate-path or predicate must be provided/i
)
})
it('should throw when both predicate and predicate-path are provided', async () => {
const inputs: PredicateInputs = {
predicateType: 'https://example.com/predicate',
predicate: '{}',
predicatePath: '/path/to/predicate.json'
}
await expect(predicateFromInputs(inputs)).rejects.toThrow(
/only one of predicate-path or predicate may be provided/i
)
})
})
describe('with predicate string', () => {
it('should parse and return the predicate', async () => {
const predicateType = 'https://example.com/predicate'
const predicateContent = { foo: 'bar', nested: { value: 123 } }
const inputs: PredicateInputs = {
...blankInputs,
predicateType,
predicate: JSON.stringify(predicateContent)
}
const result = await predicateFromInputs(inputs)
expect(result).toEqual({
type: predicateType,
params: predicateContent
})
})
it('should throw when predicate string exceeds max size', async () => {
const predicateType = 'https://example.com/predicate'
const largeContent = JSON.stringify({ data: 'x'.repeat(16 * 1024 * 1024) })
const inputs: PredicateInputs = {
...blankInputs,
predicateType,
predicate: largeContent
}
await expect(predicateFromInputs(inputs)).rejects.toThrow(
/predicate string exceeds maximum/
)
})
it('should throw when predicate is invalid JSON', async () => {
const inputs: PredicateInputs = {
...blankInputs,
predicateType: 'https://example.com/predicate',
predicate: 'not valid json'
}
await expect(predicateFromInputs(inputs)).rejects.toThrow(/JSON/)
})
})
describe('with predicate path', () => {
it('should read and parse predicate from file', async () => {
const predicateType = 'https://example.com/predicate'
const predicateContent = { buildType: 'test', metadata: { version: '1.0' } }
const filePath = path.join(tempDir, 'predicate.json')
await fs.writeFile(filePath, JSON.stringify(predicateContent))
const inputs: PredicateInputs = {
...blankInputs,
predicateType,
predicatePath: filePath
}
const result = await predicateFromInputs(inputs)
expect(result).toEqual({
type: predicateType,
params: predicateContent
})
})
it('should throw when predicate file does not exist', async () => {
const inputs: PredicateInputs = {
...blankInputs,
predicateType: 'https://example.com/predicate',
predicatePath: '/nonexistent/file.json'
}
await expect(predicateFromInputs(inputs)).rejects.toThrow(/file not found/)
})
it('should throw when predicate file contains invalid JSON', async () => {
const filePath = path.join(tempDir, 'invalid.json')
await fs.writeFile(filePath, 'not valid json')
const inputs: PredicateInputs = {
...blankInputs,
predicateType: 'https://example.com/predicate',
predicatePath: filePath
}
await expect(predicateFromInputs(inputs)).rejects.toThrow(/JSON/)
})
})
})
+181
View File
@@ -0,0 +1,181 @@
import { jest } from '@jest/globals'
import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import { parseSBOMFromPath, generateSBOMPredicate, SBOM } from '../../src/sbom'
describe('parseSBOMFromPath', () => {
let tempDir: string
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sbom-test-'))
})
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true })
})
describe('file handling', () => {
it('should throw when file does not exist', async () => {
await expect(parseSBOMFromPath('/nonexistent/file.json')).rejects.toThrow(
/SBOM file not found/
)
})
it('should rethrow non-ENOENT errors', async () => {
const statSpy = jest.spyOn(fs, 'stat').mockRejectedValueOnce(
Object.assign(new Error('Permission denied'), { code: 'EACCES' })
)
await expect(parseSBOMFromPath('/some/file.json')).rejects.toThrow(
/Permission denied/
)
statSpy.mockRestore()
})
it('should throw when file contains invalid JSON', async () => {
const filePath = path.join(tempDir, 'invalid.json')
await fs.writeFile(filePath, 'not valid json')
await expect(parseSBOMFromPath(filePath)).rejects.toThrow(/JSON/)
})
it('should throw when file exceeds maximum size', async () => {
const filePath = path.join(tempDir, 'large.json')
const largeContent = 'x'.repeat(17 * 1024 * 1024)
await fs.writeFile(filePath, largeContent)
await expect(parseSBOMFromPath(filePath)).rejects.toThrow(
/SBOM file exceeds maximum allowed size/
)
})
})
describe('SPDX format', () => {
const spdxSBOM = {
spdxVersion: 'SPDX-2.3',
SPDXID: 'SPDXRef-DOCUMENT',
name: 'test-package',
packages: []
}
it('should parse valid SPDX SBOM', async () => {
const filePath = path.join(tempDir, 'sbom.spdx.json')
await fs.writeFile(filePath, JSON.stringify(spdxSBOM))
const result = await parseSBOMFromPath(filePath)
expect(result.type).toBe('spdx')
expect(result.object).toEqual(spdxSBOM)
})
})
describe('CycloneDX format', () => {
const cyclonedxSBOM = {
bomFormat: 'CycloneDX',
specVersion: '1.4',
serialNumber: 'urn:uuid:12345',
components: []
}
it('should parse valid CycloneDX SBOM', async () => {
const filePath = path.join(tempDir, 'sbom.cdx.json')
await fs.writeFile(filePath, JSON.stringify(cyclonedxSBOM))
const result = await parseSBOMFromPath(filePath)
expect(result.type).toBe('cyclonedx')
expect(result.object).toEqual(cyclonedxSBOM)
})
})
describe('unsupported formats', () => {
it('should throw for unrecognized SBOM format', async () => {
const filePath = path.join(tempDir, 'invalid-sbom.json')
await fs.writeFile(filePath, JSON.stringify({ random: 'data' }))
await expect(parseSBOMFromPath(filePath)).rejects.toThrow(
/Unsupported SBOM format/
)
})
it('should throw for SPDX missing SPDXID', async () => {
const filePath = path.join(tempDir, 'partial-spdx.json')
await fs.writeFile(filePath, JSON.stringify({ spdxVersion: 'SPDX-2.3' }))
await expect(parseSBOMFromPath(filePath)).rejects.toThrow(
/Unsupported SBOM format/
)
})
it('should throw for CycloneDX missing required fields', async () => {
const filePath = path.join(tempDir, 'partial-cdx.json')
await fs.writeFile(filePath, JSON.stringify({ bomFormat: 'CycloneDX' }))
await expect(parseSBOMFromPath(filePath)).rejects.toThrow(
/Unsupported SBOM format/
)
})
})
})
describe('generateSBOMPredicate', () => {
describe('SPDX predicates', () => {
it('should generate predicate with correct SPDX type URL', () => {
const sbom: SBOM = {
type: 'spdx',
object: {
spdxVersion: 'SPDX-2.3',
SPDXID: 'SPDXRef-DOCUMENT',
name: 'test-package'
}
}
const predicate = generateSBOMPredicate(sbom)
expect(predicate.type).toBe('https://spdx.dev/Document/v2.3')
expect(predicate.params).toEqual(sbom.object)
})
it('should throw when spdxVersion is missing', () => {
const sbom: SBOM = {
type: 'spdx',
object: { SPDXID: 'SPDXRef-DOCUMENT' }
}
expect(() => generateSBOMPredicate(sbom)).toThrow(
/Cannot find spdxVersion/
)
})
})
describe('CycloneDX predicates', () => {
it('should generate predicate with correct CycloneDX type URL', () => {
const sbom: SBOM = {
type: 'cyclonedx',
object: {
bomFormat: 'CycloneDX',
specVersion: '1.4',
serialNumber: 'urn:uuid:12345'
}
}
const predicate = generateSBOMPredicate(sbom)
expect(predicate.type).toBe('https://cyclonedx.org/bom')
expect(predicate.params).toEqual(sbom.object)
})
})
describe('unsupported types', () => {
it('should throw for unsupported SBOM type', () => {
const sbom = {
type: 'unknown' as SBOM['type'],
object: { foo: 'bar' }
}
expect(() => generateSBOMPredicate(sbom)).toThrow(/Unsupported SBOM format/)
})
})
})
+27
View File
@@ -0,0 +1,27 @@
import { highlight, mute } from '../../src/style'
describe('style', () => {
describe('highlight', () => {
it('should wrap text with cyan ANSI color codes', () => {
const result = highlight('test message')
expect(result).toBe('\x1B[36mtest message\x1B[39m')
})
it('should handle empty strings', () => {
const result = highlight('')
expect(result).toBe('\x1B[36m\x1B[39m')
})
})
describe('mute', () => {
it('should wrap text with gray ANSI color codes', () => {
const result = mute('test message')
expect(result).toBe('\x1B[38;5;244mtest message\x1B[39m')
})
it('should handle empty strings', () => {
const result = mute('')
expect(result).toBe('\x1B[38;5;244m\x1B[39m')
})
})
})
+462
View File
@@ -0,0 +1,462 @@
import crypto from 'crypto'
import fs from 'fs/promises'
import os from 'os'
import path from 'path'
import {
subjectFromInputs,
formatSubjectDigest,
SubjectInputs
} from '../../src/subject'
describe('subjectFromInputs', () => {
const blankInputs: SubjectInputs = {
subjectPath: '',
subjectName: '',
subjectDigest: '',
subjectChecksums: ''
}
let tempDir: string
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'subject-test-'))
})
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true })
})
describe('input validation', () => {
it('should throw when no inputs are provided', async () => {
await expect(subjectFromInputs(blankInputs)).rejects.toThrow(
/one of subject-path, subject-digest, or subject-checksums must be provided/i
)
})
it('should throw when multiple subject inputs are provided', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: '/some/path',
subjectDigest: 'sha256:abc123'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/only one of subject-path, subject-digest, or subject-checksums may be provided/i
)
})
it('should throw when subject-digest is provided without subject-name', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectDigest: 'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject-name must be provided when using subject-digest/i
)
})
})
describe('with subject-digest', () => {
const validDigest = 'sha256:7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
it('should return subject with provided name and digest', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectName: 'my-artifact',
subjectDigest: validDigest
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(1)
expect(subjects[0].name).toBe('my-artifact')
expect(subjects[0].digest).toEqual({
sha256: '7d070f6b64d9bcc530fe99cc21eaaa4b3c364e0b2d367d7735671fa202a03b32'
})
})
it('should lowercase name when downcaseName is true', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectName: 'ghcr.io/FOO/Bar',
subjectDigest: validDigest,
downcaseName: true
}
const subjects = await subjectFromInputs(inputs)
expect(subjects[0].name).toBe('ghcr.io/foo/bar')
})
it('should throw for malformed digest format', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectName: 'artifact',
subjectDigest: 'invalid-digest'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject-digest must be in the format/
)
})
it('should throw for unsupported hash algorithm', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectName: 'artifact',
subjectDigest: 'md5:d41d8cd98f00b204e9800998ecf8427e'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject-digest must be in the format/
)
})
it('should throw for incorrect sha256 digest length', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectName: 'artifact',
subjectDigest: 'sha256:deadbeef'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject-digest must be in the format/
)
})
})
describe('with subject-path', () => {
const fileContent = 'test file content'
const expectedDigest = crypto.createHash('sha256').update(fileContent).digest('hex')
it('should calculate digest from file', async () => {
const filePath = path.join(tempDir, 'artifact.bin')
await fs.writeFile(filePath, fileContent)
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: filePath
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(1)
expect(subjects[0].name).toBe('artifact.bin')
expect(subjects[0].digest).toEqual({ sha256: expectedDigest })
})
it('should use provided name instead of filename', async () => {
const filePath = path.join(tempDir, 'artifact.bin')
await fs.writeFile(filePath, fileContent)
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: filePath,
subjectName: 'custom-name'
}
const subjects = await subjectFromInputs(inputs)
expect(subjects[0].name).toBe('custom-name')
})
it('should throw when file does not exist', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: '/nonexistent/file'
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/could not find subject at path/i
)
})
describe('glob patterns', () => {
beforeEach(async () => {
for (let i = 0; i < 3; i++) {
await fs.writeFile(path.join(tempDir, `file-${i}.txt`), fileContent)
}
})
it('should expand glob pattern to multiple subjects', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: path.join(tempDir, 'file-*.txt')
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(3)
expect(subjects.map(s => s.name).sort()).toEqual([
'file-0.txt',
'file-1.txt',
'file-2.txt'
])
})
it('should handle comma-separated paths', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${path.join(tempDir, 'file-0.txt')},${path.join(tempDir, 'file-1.txt')}`
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(2)
})
it('should handle newline-separated paths', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${path.join(tempDir, 'file-0.txt')}\n${path.join(tempDir, 'file-2.txt')}`
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(2)
})
it('should support exclusion patterns', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${path.join(tempDir, 'file-*.txt')},!${path.join(tempDir, 'file-1.txt')}`
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(2)
expect(subjects.map(s => s.name)).not.toContain('file-1.txt')
})
it('should deduplicate subjects with same name and digest', async () => {
// Create another directory with same file
const otherDir = await fs.mkdtemp(path.join(os.tmpdir(), 'subject-dup-'))
await fs.writeFile(path.join(otherDir, 'file-0.txt'), fileContent)
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: `${path.join(tempDir, 'file-0.txt')},${path.join(otherDir, 'file-0.txt')}`
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(1)
await fs.rm(otherDir, { recursive: true, force: true })
})
})
it('should exclude directories from glob results', async () => {
await fs.mkdir(path.join(tempDir, 'subdir'))
await fs.writeFile(path.join(tempDir, 'file.txt'), fileContent)
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: path.join(tempDir, '*')
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(1)
expect(subjects[0].name).toBe('file.txt')
})
it('should throw when too many subjects are specified', async () => {
// Create 1025 files (exceeds MAX_SUBJECT_COUNT of 1024)
for (let i = 0; i < 1025; i++) {
await fs.writeFile(path.join(tempDir, `file-${i}.txt`), `content-${i}`)
}
const inputs: SubjectInputs = {
...blankInputs,
subjectPath: path.join(tempDir, 'file-*.txt')
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(/too many subjects/i)
})
})
describe('with subject-checksums', () => {
describe('from string', () => {
it('should parse sha256 checksums', async () => {
const checksums = `187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d artifact-linux
9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5 artifact-darwin`
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(2)
expect(subjects).toContainEqual({
name: 'artifact-linux',
digest: { sha256: '187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d' }
})
expect(subjects).toContainEqual({
name: 'artifact-darwin',
digest: { sha256: '9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5' }
})
})
it('should parse sha512 checksums', async () => {
const sha512 = '5d8b4751ef31f9440d843fcfa4e53ca2e25b1cb1f13fd355fdc7c24b41fe645293291ea9297ba3989078abb77ebbaac66be073618a9e4974dbd0361881d4c718'
const checksums = `${sha512} artifact-amd64`
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(1)
expect(subjects[0].digest).toEqual({ sha512 })
})
it('should handle binary mode flag (*)', async () => {
const checksums = `187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d *artifact.bin`
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
const subjects = await subjectFromInputs(inputs)
expect(subjects[0].name).toBe('artifact.bin')
})
it('should handle text mode flag (space)', async () => {
const checksums = `187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d artifact.txt`
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
const subjects = await subjectFromInputs(inputs)
expect(subjects[0].name).toBe('artifact.txt')
})
it('should handle checksums without mode flag', async () => {
// Single space between digest and name (no flag character)
const checksums = `187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d artifact-no-flag`
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
const subjects = await subjectFromInputs(inputs)
expect(subjects[0].name).toBe('artifact-no-flag')
})
it('should skip malformed lines', async () => {
const checksums = `187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d valid-artifact
badline
9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5 another-artifact`
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(2)
})
it('should deduplicate identical entries', async () => {
const checksums = `187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d artifact
187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d artifact`
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(1)
})
it('should throw for invalid digest characters', async () => {
const checksums = `!!!!e68a080799ca83104630b56abb90d8dbcc5f8b5a8639cb691e269838f29e artifact`
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(/invalid digest/i)
})
it('should throw for unknown digest algorithm', async () => {
const checksums = `f861e artifact`
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksums
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(/unknown digest algorithm/i)
})
})
describe('from file', () => {
it('should read checksums from file', async () => {
const checksumFile = path.join(tempDir, 'SHA256SUMS')
const checksums = `187dcd1506a170337415589ff00c8743f19d41cc31fca246c2739dfd450d0b9d artifact-linux
9ecbf449e286a8a8748c161c52aa28b6b2fc64ab86f94161c5d1b3abc18156c5 artifact-darwin`
await fs.writeFile(checksumFile, checksums)
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: checksumFile
}
const subjects = await subjectFromInputs(inputs)
expect(subjects).toHaveLength(2)
})
it('should throw when checksums path is a directory', async () => {
const inputs: SubjectInputs = {
...blankInputs,
subjectChecksums: tempDir
}
await expect(subjectFromInputs(inputs)).rejects.toThrow(
/subject checksums file not found/i
)
})
})
})
})
describe('formatSubjectDigest', () => {
it('should format digest as algorithm:hash', () => {
const subject = {
name: 'artifact',
digest: { sha256: 'abc123def456' }
}
expect(formatSubjectDigest(subject)).toBe('sha256:abc123def456')
})
it('should use first algorithm alphabetically when multiple exist', () => {
const subject = {
name: 'artifact',
digest: {
sha512: 'longer-hash',
sha256: 'shorter-hash'
}
}
expect(formatSubjectDigest(subject)).toBe('sha256:shorter-hash')
})
})
+5
View File
@@ -30,6 +30,11 @@ inputs:
attestation. Must specify exactly one of "subject-path", "subject-digest",
or "subject-checksums".
required: false
subject-version:
description: >
Version of the subject for the attestation. Only used when
"push-to-registry" and "create-storage-record" are both set to true.
required: false
sbom-path:
description: >
Path to the JSON-formatted SBOM file (SPDX or CycloneDX) to attest.
Generated Vendored
+51 -3
View File
@@ -108507,6 +108507,27 @@ exports.LRUCache = LRUCache;
/***/ }),
/***/ 86705:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
// This file exists as a CommonJS module to read the version from package.json.
// In an ESM package, using `require()` directly in .ts files requires disabling
// ESLint rules and doesn't work reliably across all Node.js versions.
// By keeping this as a .cjs file, we can use require() naturally and export
// the version for the ESM modules to import.
const packageJson = __nccwpck_require__(47849)
module.exports = {version: packageJson.version}
/***/ }),
/***/ 47849:
/***/ ((module) => {
module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/attest","version":"3.2.0","description":"Actions attestation lib","keywords":["github","actions","attestation"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/attest","license":"MIT","type":"module","main":"lib/index.js","types":"lib/index.d.ts","exports":{".":{"types":"./lib/index.d.ts","import":"./lib/index.js"}},"directories":{"lib":"lib","test":"__tests__"},"files":["lib"],"publishConfig":{"access":"public","provenance":true},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/attest"},"scripts":{"test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc && cp src/internal/package-version.cjs lib/internal/"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"devDependencies":{"@sigstore/mock":"^0.10.0","@sigstore/rekor-types":"^3.0.0","@types/jsonwebtoken":"^9.0.6","nock":"^13.5.1","undici":"^6.23.0"},"dependencies":{"@actions/core":"^3.0.0","@actions/github":"^9.0.0","@actions/http-client":"^4.0.0","@octokit/plugin-retry":"^8.0.3","@sigstore/bundle":"^3.1.0","@sigstore/sign":"^3.1.0","jose":"^5.10.0"}}');
/***/ }),
/***/ 4592:
/***/ ((module) => {
@@ -115926,6 +115947,22 @@ function retry(octokit, octokitOptions) {
retry.VERSION = plugin_retry_dist_bundle_VERSION;
// EXTERNAL MODULE: ./node_modules/@actions/attest/lib/internal/package-version.cjs
var package_version = __nccwpck_require__(86705);
;// CONCATENATED MODULE: ./node_modules/@actions/attest/lib/internal/utils.js
const utils_getUserAgent = () => {
const baseUserAgent = `@actions/attest-${package_version.version}`;
const orchId = process.env['ACTIONS_ORCHESTRATION_ID'];
if (orchId) {
// Sanitize the orchestration ID to ensure it contains only valid characters
// Valid characters: 0-9, a-z, _, -, .
const sanitizedId = orchId.replace(/[^a-z0-9_.-]/gi, '_');
return `${baseUserAgent} actions_orchestration_id/${sanitizedId}`;
}
return baseUserAgent;
};
//# sourceMappingURL=utils.js.map
;// CONCATENATED MODULE: ./node_modules/@actions/attest/lib/artifactMetadata.js
var artifactMetadata_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
@@ -115949,6 +115986,7 @@ var __rest = (undefined && undefined.__rest) || function (s, e) {
};
const CREATE_STORAGE_RECORD_REQUEST = 'POST /orgs/{owner}/artifacts/metadata/storage-record';
const DEFAULT_RETRY_COUNT = 5;
/**
@@ -115966,8 +116004,9 @@ function createStorageRecord(artifactOptions, packageRegistryOptions, token, ret
return artifactMetadata_awaiter(this, void 0, void 0, function* () {
const retries = retryAttempts !== null && retryAttempts !== void 0 ? retryAttempts : DEFAULT_RETRY_COUNT;
const octokit = getOctokit(token, { retry: { retries } }, retry);
const headersWithUserAgent = Object.assign({ 'User-Agent': utils_getUserAgent() }, headers);
try {
const response = yield octokit.request(CREATE_STORAGE_RECORD_REQUEST, Object.assign({ owner: github_context.repo.owner, headers }, buildRequestParams(artifactOptions, packageRegistryOptions)));
const response = yield octokit.request(CREATE_STORAGE_RECORD_REQUEST, Object.assign({ owner: github_context.repo.owner, headers: headersWithUserAgent }, buildRequestParams(artifactOptions, packageRegistryOptions)));
const data = typeof response.data == 'string'
? JSON.parse(response.data)
: response.data;
@@ -116121,6 +116160,7 @@ var store_awaiter = (undefined && undefined.__awaiter) || function (thisArg, _ar
};
const CREATE_ATTESTATION_REQUEST = 'POST /repos/{owner}/{repo}/attestations';
const store_DEFAULT_RETRY_COUNT = 5;
/**
@@ -116134,11 +116174,12 @@ const writeAttestation = (attestation_1, token_1, ...args_1) => store_awaiter(vo
var _a;
const retries = (_a = options.retry) !== null && _a !== void 0 ? _a : store_DEFAULT_RETRY_COUNT;
const octokit = getOctokit(token, { retry: { retries } }, retry);
const headers = Object.assign({ 'User-Agent': utils_getUserAgent() }, options.headers);
try {
const response = yield octokit.request(CREATE_ATTESTATION_REQUEST, {
owner: github_context.repo.owner,
repo: github_context.repo.repo,
headers: options.headers,
headers,
bundle: attestation
});
const data = typeof response.data == 'string'
@@ -120858,7 +120899,8 @@ const createAttestation = async (subjects, predicate, opts) => {
const registryUrl = getRegistryURL(subject.name);
const artifactOpts = {
name: subject.name,
digest: subjectDigest
digest: subjectDigest,
version: opts.subjectVersion || undefined
};
const packageRegistryOpts = {
registryUrl
@@ -120896,6 +120938,7 @@ function getRegistryURL(subjectName) {
catch {
url = new URL(`https://${subjectName}`);
}
/* istanbul ignore if */
if (url.protocol !== 'https:') {
throw new Error(`Unsupported protocol ${url.protocol} in subject name ${subjectName}`);
}
@@ -121115,6 +121158,7 @@ async function run(inputs) {
sigstoreInstance,
pushToRegistry: inputs.pushToRegistry,
createStorageRecord: inputs.createStorageRecord,
subjectVersion: inputs.subjectVersion,
githubToken: inputs.githubToken
});
logAttestation(subjects, att, sigstoreInstance);
@@ -121141,6 +121185,7 @@ async function run(inputs) {
setOutput('attestation-id', att.attestationID);
setOutput('attestation-url', attestationURL(att.attestationID));
}
/* istanbul ignore if */
if (att.storageRecordIds) {
setOutput('storage-record-ids', att.storageRecordIds.join(','));
}
@@ -121175,6 +121220,7 @@ const logAttestation = (subjects, attestation, sigstoreInstance) => {
startGroup(highlight(`Attestation signed using certificate from ${instanceName} Sigstore instance`));
info(attestation.certificate);
endGroup();
/* istanbul ignore if */
if (attestation.tlogID) {
info(highlight('Attestation signature uploaded to Rekor transparency log'));
info(`${SEARCH_PUBLIC_GOOD_URL}?logIndex=${attestation.tlogID}`);
@@ -121188,6 +121234,7 @@ const logAttestation = (subjects, attestation, sigstoreInstance) => {
info(highlight('Attestation uploaded to registry'));
info(`${subjects[0].name}@${attestation.attestationDigest}`);
}
/* istanbul ignore next */
if (attestation.storageRecordIds && attestation.storageRecordIds.length > 0) {
info(highlight('Storage record created'));
info(`Storage record IDs: ${attestation.storageRecordIds.join(',')}`);
@@ -121253,6 +121300,7 @@ const inputs = {
predicatePath: getInput('predicate-path'),
pushToRegistry: getBooleanInput('push-to-registry'),
createStorageRecord: getBooleanInput('create-storage-record'),
subjectVersion: getInput('subject-version'),
showSummary: getBooleanInput('show-summary'),
githubToken: getInput('github-token'),
// undocumented -- not part of public interface
+532 -585
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -1,7 +1,7 @@
{
"name": "actions/attest",
"description": "Generate signed attestations for workflow artifacts",
"version": "3.2.0",
"version": "4.1.0",
"author": "",
"private": true,
"type": "module",
@@ -78,7 +78,7 @@
]
},
"dependencies": {
"@actions/attest": "^3.0.0",
"@actions/attest": "^3.2.0",
"@actions/core": "^3.0.0",
"@actions/github": "^9.0.0",
"@actions/glob": "^0.6.1",
@@ -87,23 +87,23 @@
},
"devDependencies": {
"@eslint/js": "^9.39.2",
"@jest/globals": "^30.2.0",
"@sigstore/mock": "^0.11.0",
"@jest/globals": "^30.3.0",
"@sigstore/mock": "^0.12.0",
"@types/jest": "^30.0.0",
"@types/make-fetch-happen": "^10.0.4",
"@types/node": "^25.2.0",
"@types/node": "^25.6.0",
"@vercel/ncc": "^0.38.4",
"eslint": "^9.39.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.12.1",
"jest": "^30.2.0",
"eslint-plugin-jest": "^29.15.2",
"jest": "^30.3.0",
"js-yaml": "^4.1.1",
"markdownlint-cli": "^0.47.0",
"markdownlint-cli": "^0.48.0",
"nock": "^13.5.6",
"prettier": "^3.8.1",
"ts-jest": "^29.4.6",
"prettier": "^3.8.3",
"ts-jest": "^29.4.9",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"undici": "^7.20.0"
"typescript-eslint": "^8.58.2",
"undici": "^7.24.4"
}
}
+4 -1
View File
@@ -26,6 +26,7 @@ export const createAttestation = async (
sigstoreInstance: SigstoreInstance
pushToRegistry: boolean
createStorageRecord: boolean
subjectVersion?: string
githubToken: string
}
): Promise<AttestResult> => {
@@ -77,7 +78,8 @@ export const createAttestation = async (
const registryUrl = getRegistryURL(subject.name)
const artifactOpts = {
name: subject.name,
digest: subjectDigest
digest: subjectDigest,
version: opts.subjectVersion || undefined
}
const packageRegistryOpts = {
registryUrl
@@ -126,6 +128,7 @@ function getRegistryURL(subjectName: string): string {
url = new URL(`https://${subjectName}`)
}
/* istanbul ignore if */
if (url.protocol !== 'https:') {
throw new Error(
`Unsupported protocol ${url.protocol} in subject name ${subjectName}`
+1
View File
@@ -15,6 +15,7 @@ const inputs: RunInputs = {
predicatePath: core.getInput('predicate-path'),
pushToRegistry: core.getBooleanInput('push-to-registry'),
createStorageRecord: core.getBooleanInput('create-storage-record'),
subjectVersion: core.getInput('subject-version'),
showSummary: core.getBooleanInput('show-summary'),
githubToken: core.getInput('github-token'),
// undocumented -- not part of public interface
+5
View File
@@ -35,6 +35,7 @@ export type RunInputs = SubjectInputs &
SBOMInputs & {
pushToRegistry: boolean
createStorageRecord: boolean
subjectVersion: string
githubToken: string
showSummary: boolean
privateSigning: boolean
@@ -97,6 +98,7 @@ export async function run(inputs: RunInputs): Promise<void> {
sigstoreInstance,
pushToRegistry: inputs.pushToRegistry,
createStorageRecord: inputs.createStorageRecord,
subjectVersion: inputs.subjectVersion,
githubToken: inputs.githubToken
})
@@ -129,6 +131,7 @@ export async function run(inputs: RunInputs): Promise<void> {
core.setOutput('attestation-url', attestationURL(att.attestationID))
}
/* istanbul ignore if */
if (att.storageRecordIds) {
core.setOutput('storage-record-ids', att.storageRecordIds.join(','))
}
@@ -182,6 +185,7 @@ const logAttestation = (
core.info(attestation.certificate)
core.endGroup()
/* istanbul ignore if */
if (attestation.tlogID) {
core.info(
style.highlight(
@@ -202,6 +206,7 @@ const logAttestation = (
core.info(`${subjects[0].name}@${attestation.attestationDigest}`)
}
/* istanbul ignore next */
if (attestation.storageRecordIds && attestation.storageRecordIds.length > 0) {
core.info(style.highlight('Storage record created'))
core.info(`Storage record IDs: ${attestation.storageRecordIds.join(',')}`)