Compare commits

..

163 Commits

Author SHA1 Message Date
Edwin Sirko 275b6a383b only do e2e test on workflow_dispatch (#76) 2024-01-31 16:52:16 -05:00
Edwin Sirko df1bbcf10f use runner's RUNNER_TEMP for temp directory (#75)
* use runner tempdir

* fix tests etc

* feedback

* ran npm install before generating dist
2024-01-31 16:50:45 -05:00
David Daly d6635df138 Merge pull request #74 from immutable-actions/ddivad195/bump-dist
rebuild dist after dependabot updates
2024-01-31 11:01:31 +00:00
ddivad195 70798fe149 rebuild dist after dependabot updates 2024-01-31 10:53:27 +00:00
David Daly adc18237cc Merge pull request #72 from immutable-actions/dependabot/npm_and_yarn/types/node-20.11.13
Bump @types/node from 20.11.4 to 20.11.13
2024-01-31 10:48:02 +00:00
David Daly 8ffc259ac7 Merge pull request #70 from immutable-actions/dependabot/npm_and_yarn/fs-extra-11.2.0
Bump fs-extra from 11.1.1 to 11.2.0
2024-01-31 10:47:51 +00:00
David Daly c86b91b073 Merge pull request #69 from immutable-actions/dependabot/npm_and_yarn/eslint-plugin-jest-27.6.3
Bump eslint-plugin-jest from 27.6.0 to 27.6.3
2024-01-31 10:47:40 +00:00
David Daly d90a4fb4a6 Merge pull request #73 from immutable-actions/ddivad195/fix-lint
fix lint
2024-01-31 10:30:47 +00:00
David Daly 009aa87fef fix lint 2024-01-31 10:25:13 +00:00
David Daly bfd91a146f fix lint 2024-01-31 10:13:50 +00:00
David Daly 2698e0bf92 Merge branch 'main' into ddivad195/fix-lint 2024-01-31 10:06:53 +00:00
David Daly 45a150da44 fix lint 2024-01-31 10:06:04 +00:00
David Daly 578e581556 fix lint 2024-01-31 10:02:43 +00:00
David Daly 879b696ad3 fix lint 2024-01-31 09:41:40 +00:00
David Daly 10ef4a54d9 fix lint 2024-01-31 09:17:58 +00:00
David Daly bb8616bad2 fix lint 2024-01-31 09:13:07 +00:00
David Daly 514cb8cfd1 fix lint 2024-01-31 09:04:06 +00:00
dependabot[bot] dc0a682128 Bump eslint-plugin-jest from 27.6.0 to 27.6.3
Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 27.6.0 to 27.6.3.
- [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/v27.6.0...v27.6.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-jest
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-31 08:54:31 +00:00
dependabot[bot] 75cbbe2dac Bump @types/node from 20.11.4 to 20.11.13
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.11.4 to 20.11.13.
- [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-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-31 08:54:26 +00:00
David Daly 37fdf001e6 Merge pull request #67 from immutable-actions/dependabot/npm_and_yarn/eslint-plugin-prettier-5.1.3
Bump eslint-plugin-prettier from 5.0.1 to 5.1.3
2024-01-31 08:53:51 +00:00
David Daly 67ce24a600 Merge pull request #66 from immutable-actions/dependabot/npm_and_yarn/types/tar-6.1.11
Bump @types/tar from 6.1.10 to 6.1.11
2024-01-31 08:53:40 +00:00
Edwin Sirko f5fa12abfb e2e test (#71) 2024-01-30 23:46:14 -05:00
dependabot[bot] bd5d02ca8b Bump fs-extra from 11.1.1 to 11.2.0
Bumps [fs-extra](https://github.com/jprichardson/node-fs-extra) from 11.1.1 to 11.2.0.
- [Changelog](https://github.com/jprichardson/node-fs-extra/blob/master/CHANGELOG.md)
- [Commits](https://github.com/jprichardson/node-fs-extra/compare/11.1.1...11.2.0)

---
updated-dependencies:
- dependency-name: fs-extra
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 23:06:41 +00:00
dependabot[bot] 9459dc13a1 Bump eslint-plugin-prettier from 5.0.1 to 5.1.3
Bumps [eslint-plugin-prettier](https://github.com/prettier/eslint-plugin-prettier) from 5.0.1 to 5.1.3.
- [Release notes](https://github.com/prettier/eslint-plugin-prettier/releases)
- [Changelog](https://github.com/prettier/eslint-plugin-prettier/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/eslint-plugin-prettier/compare/v5.0.1...v5.1.3)

---
updated-dependencies:
- dependency-name: eslint-plugin-prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 23:06:02 +00:00
dependabot[bot] b12061d021 Bump @types/tar from 6.1.10 to 6.1.11
Bumps [@types/tar](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/tar) from 6.1.10 to 6.1.11.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/tar)

---
updated-dependencies:
- dependency-name: "@types/tar"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 23:05:51 +00:00
David Daly e9fcf73691 Merge pull request #65 from immutable-actions/ddivad195/fix-ghes-condition
fix ghes condition
2024-01-30 17:39:29 +00:00
David Daly 4cdee1e5e8 fix ghes condition 2024-01-30 17:24:32 +00:00
David Daly d868f0b26b Merge pull request #63 from immutable-actions/ddivad195/fix-lint-and-tests
fix failing lint and test errors
2024-01-30 16:36:16 +00:00
ddivad195 908d89c8d9 fix readme lint 2024-01-30 16:29:57 +00:00
ddivad195 c589a2a7d4 fix lint errors, remove generate-new-version file 2024-01-30 16:24:25 +00:00
ddivad195 8d2cafe1d2 fix failing lint and test errors 2024-01-30 16:13:51 +00:00
David Daly 06bd4f4498 Merge pull request #58 from immutable-actions/dependabot/npm_and_yarn/typescript-eslint/eslint-plugin-6.20.0
Bump @typescript-eslint/eslint-plugin from 6.12.0 to 6.20.0
2024-01-30 15:44:52 +00:00
David Daly 6f14e6710a Merge pull request #52 from immutable-actions/dependabot/npm_and_yarn/eslint-plugin-jsonc-2.13.0
Bump eslint-plugin-jsonc from 2.10.0 to 2.13.0
2024-01-30 15:44:36 +00:00
David Daly c3bb20a679 Merge pull request #50 from immutable-actions/dependabot/npm_and_yarn/prettier-eslint-16.3.0
Bump prettier-eslint from 16.1.2 to 16.3.0
2024-01-30 15:44:24 +00:00
dependabot[bot] af252b46b2 Bump prettier-eslint from 16.1.2 to 16.3.0
Bumps [prettier-eslint](https://github.com/prettier/prettier-eslint) from 16.1.2 to 16.3.0.
- [Release notes](https://github.com/prettier/prettier-eslint/releases)
- [Changelog](https://github.com/prettier/prettier-eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier-eslint/compare/v16.1.2...v16.3.0)

---
updated-dependencies:
- dependency-name: prettier-eslint
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-30 15:43:23 +00:00
David Daly d56b8b6a7d Merge pull request #49 from immutable-actions/dependabot/npm_and_yarn/prettier-3.2.4
Bump prettier from 3.1.0 to 3.2.4
2024-01-30 15:42:47 +00:00
David Daly b86c87d753 Merge pull request #46 from immutable-actions/dependabot/npm_and_yarn/typescript-5.3.3
Bump typescript from 5.3.2 to 5.3.3
2024-01-30 15:42:35 +00:00
David Daly 3c0615c0b0 Merge pull request #60 from immutable-actions/ddivad195/misc-cleanup
remove path input
2024-01-30 15:41:34 +00:00
David Daly d2333ec560 Merge pull request #62 from immutable-actions/ddivad195/skip-provenance-ghes
only run provenance step if environment is not ghes
2024-01-30 15:39:27 +00:00
David Daly baee642dcc only run if environment is not ghes 2024-01-30 14:02:59 +00:00
David Daly 55d1a91db3 remove permissions from action.yml 2024-01-30 12:46:29 +00:00
ddivad195 5fc6c86976 remove path input param 2024-01-30 11:04:39 +00:00
Edwin Sirko b0301f588c forgot to npm run bundle 2024-01-29 21:05:30 -05:00
Edwin Sirko 207498716a fixed repoId/ownerId as string in json manifest (#59) 2024-01-29 21:03:59 -05:00
Edwin Sirko 20e4d5d06f npm run package 2024-01-29 18:36:21 -05:00
Edwin Sirko c0f3a2de99 auth header 2024-01-29 18:35:12 -05:00
dependabot[bot] 40b38ecf87 Bump @typescript-eslint/eslint-plugin from 6.12.0 to 6.20.0
Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 6.12.0 to 6.20.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.20.0/packages/eslint-plugin)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/eslint-plugin"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 22:22:20 +00:00
Edwin Sirko 54d0a402f9 npm run bundle (#57)
* 2 small fixes

* npm run bundle
2024-01-29 17:11:29 -05:00
Edwin Sirko 1c17c22b51 2 small fixes (#56) 2024-01-29 17:09:19 -05:00
Edwin Sirko 77fbf96c58 fixed bug with fsExtra.copySync (#55) 2024-01-29 16:44:15 -05:00
Conor Sloan b6419ce067 Tying up loose ends (#54)
* various qol updates to publish action

* review comments and run bundle
2024-01-29 15:31:00 -05:00
David Daly b5c14d38d4 Update release.yml 2024-01-29 16:32:02 +00:00
David Daly ea0d6338c1 add missing permissions to workflow 2024-01-29 16:25:15 +00:00
David Daly 3de30988eb fix indentation (again) 2024-01-29 15:03:42 +00:00
David Daly b79d3edb6f fix indentation 2024-01-29 14:56:41 +00:00
David Daly af4e6c17e2 re-enable generate-build-provenance step 2024-01-29 14:54:24 +00:00
dependabot[bot] eca17b8b76 Bump typescript from 5.3.2 to 5.3.3
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 5.3.2 to 5.3.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v5.3.2...v5.3.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-29 12:16:01 +00:00
David Daly 488a01bcb4 Merge pull request #53 from ddivad195/main
Implement new functionality, add testing and update documentation
2024-01-29 12:14:59 +00:00
Edwin Sirko 7941d52ec3 Merge pull request #3 from ddivad195/esirko/validate-paths
validate paths
2024-01-26 22:03:26 -05:00
Edwin Sirko b9af78dd4e fix 2024-01-26 21:54:49 -05:00
Edwin Sirko 10fbfab203 fix 2024-01-26 21:54:16 -05:00
Edwin Sirko d7d99939bd lint 2024-01-26 21:42:28 -05:00
Edwin Sirko 1219afee65 dist 2024-01-26 21:07:36 -05:00
Edwin Sirko 7b797db603 comment 2024-01-26 21:06:07 -05:00
Edwin Sirko 35c8ddfb58 ok 2024-01-26 21:03:27 -05:00
Edwin Sirko 2052298171 fix 2024-01-26 20:52:00 -05:00
Edwin Sirko 31164a045a tests almost there 2024-01-26 20:44:17 -05:00
Edwin Sirko d7ee685291 wip 2024-01-26 18:09:48 -05:00
Edwin Sirko 3e6047108b wip 2024-01-26 17:45:38 -05:00
Edwin Sirko 6630eef689 tests 2024-01-26 16:35:51 -05:00
Edwin Sirko 6eccb75525 fix 2024-01-26 16:35:51 -05:00
Edwin Sirko 89c429cf42 refactored path calculation 2024-01-26 16:35:51 -05:00
ddivad195 814845b943 remove console log of path 2024-01-26 21:31:48 +00:00
boxofyellow 4e9f7acdee fix outputs 2024-01-26 16:29:43 -05:00
boxofyellow 1191379019 fromm running npm run all 2024-01-26 13:10:01 -08:00
boxofyellow 78e9f1d365 fix tests 2024-01-26 13:09:44 -08:00
boxofyellow ac306b4799 after npm run package 2024-01-26 12:18:00 -08:00
boxofyellow ebdbf0a34b fix lint 2024-01-26 12:14:31 -08:00
boxofyellow 332b90e4be Disable attestations 2024-01-26 14:59:41 -05:00
boxofyellow 5b27b9838a make sure to populate outputs of the composite action 2024-01-26 14:49:48 -05:00
Edwin Sirko c0eda00aa3 Merge pull request #2 from ddivad195/esirko/validation-stuff
validation stuff
2024-01-26 13:50:45 -05:00
Edwin Sirko 206ff2d4e2 order logic to fail fast appropriately 2024-01-26 13:15:36 -05:00
Edwin Sirko 7120405e17 auto changes 2024-01-26 13:09:08 -05:00
Edwin Sirko 4ed2e10e92 isActionRepo() 2024-01-26 13:07:57 -05:00
Edwin Sirko 55e582b23e resolved a TODO item about semver 2024-01-26 13:07:11 -05:00
Edwin Sirko d264ea0899 now properly getting the CR URL 2024-01-26 13:05:35 -05:00
Edwin Sirko f0a3f907d9 generate-new-version fixes 2024-01-26 13:03:16 -05:00
David Daly 20f2765392 cleanup action steps 2024-01-26 16:55:53 +00:00
boxofyellow 9189d9269d Remove the problematic test for really-realls 2024-01-26 16:34:43 +00:00
boxofyellow 70f1827fe7 remove new test to see if the CI will pass 2024-01-26 16:34:40 +00:00
boxofyellow 6ca3985d5d after running npm run all 2024-01-26 16:34:40 +00:00
boxofyellow ba9590c184 fix up string interpolation 2024-01-26 16:34:40 +00:00
boxofyellow 748d779644 PR feed back 2024-01-26 16:34:40 +00:00
boxofyellow c773bf210d fix lint errors 2024-01-26 16:34:40 +00:00
boxofyellow c8ad6774d7 after running npm run all 2024-01-26 16:34:40 +00:00
boxofyellow dbec321cd4 this was already done 2024-01-26 16:34:40 +00:00
boxofyellow c06282fe29 make main.test.ts test data more varied 2024-01-26 16:34:40 +00:00
boxofyellow c88b93864f make ghcr-client.test.ts test data more varied 2024-01-26 16:34:40 +00:00
boxofyellow 8abbcf0a68 make oci-container.test.ts test data more varied 2024-01-26 16:34:40 +00:00
boxofyellow 54571c1488 fix copy past error 2024-01-26 16:34:40 +00:00
boxofyellow 67b4eb38f3 add notes about console.log 2024-01-26 16:34:40 +00:00
boxofyellow 0ba67aaf7a leave shared object unchanged 2024-01-26 16:34:40 +00:00
boxofyellow df5639170c add test for **_some_** of the layer to already be there 2024-01-26 16:34:40 +00:00
boxofyellow 84a94880b6 test bundleFilesintoDirectory can handle sub directories 2024-01-26 16:34:40 +00:00
boxofyellow e144688ccc Add negative test for createArchives 2024-01-26 16:34:40 +00:00
Edwin Sirko 983c5e7554 v0.0.67: revert that stuff 2024-01-26 16:34:40 +00:00
Edwin Sirko 7269b88cd7 v0.0.66: testing something dumb 2024-01-26 16:34:39 +00:00
Edwin Sirko be346d67f7 v0.0.65: documentation update 2024-01-26 16:34:39 +00:00
Edwin Sirko 2016180627 v0.0.64: got rid of registry input 2024-01-26 16:34:39 +00:00
Edwin Sirko 61a702939a v0.0.63: fixed sed 2024-01-26 16:34:39 +00:00
Edwin Sirko 94d1a57d98 v0.0.62: created generate-new-version.sh 2024-01-26 16:34:39 +00:00
Edwin Sirko 624fc6877e v0.0.61: fixed version 2024-01-26 16:34:39 +00:00
Edwin Sirko 4772dbfcc0 v0.0.60: why didn't it print 2024-01-26 16:34:39 +00:00
Edwin Sirko d5e6f39e19 v0.0.59: try this 2024-01-26 16:34:39 +00:00
Edwin Sirko 5d36b1908c v0.0.58: try this 2024-01-26 16:34:38 +00:00
Edwin Sirko 475c9db7d7 v0.0.57: this probably won't work 2024-01-26 16:34:38 +00:00
Edwin Sirko 86588e3791 v0.0.56: style 2024-01-26 16:34:38 +00:00
Edwin Sirko 995b9bd0a1 v0.0.55: get rid of an extraneous await 2024-01-26 16:34:38 +00:00
Edwin Sirko 120663b080 v0.0.53 2024-01-26 16:34:38 +00:00
Edwin Sirko 9853dad78c v0.0.53: attempted fix 2024-01-26 16:34:38 +00:00
Edwin Sirko bb84d03b7e v0.0.52: fetch CR URL 2024-01-26 16:34:37 +00:00
David Daly 79314de299 Refactor into a composite action and integrate with generate-build-provenance action
- Refactor the action from a 'node' action to a 'composite' action
- Create and output a sha256 hash of the image manifest
- Update action inputs & outputs
- Integrate with the 'generate-build-provenance' action as part of the composite action
2024-01-26 16:34:37 +00:00
dependabot[bot] 19c74b87f0 Bump eslint-plugin-jsonc from 2.10.0 to 2.13.0
Bumps [eslint-plugin-jsonc](https://github.com/ota-meshi/eslint-plugin-jsonc) from 2.10.0 to 2.13.0.
- [Release notes](https://github.com/ota-meshi/eslint-plugin-jsonc/releases)
- [Changelog](https://github.com/ota-meshi/eslint-plugin-jsonc/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ota-meshi/eslint-plugin-jsonc/compare/v2.10.0...v2.13.0)

---
updated-dependencies:
- dependency-name: eslint-plugin-jsonc
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-23 22:22:01 +00:00
dependabot[bot] 56df88d82e Bump prettier from 3.1.0 to 3.2.4
Bumps [prettier](https://github.com/prettier/prettier) from 3.1.0 to 3.2.4.
- [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.1.0...3.2.4)

---
updated-dependencies:
- dependency-name: prettier
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-17 22:57:32 +00:00
David Daly 7ef2d24f3b Merge pull request #42 from immutable-actions/dependabot/npm_and_yarn/typescript-eslint/parser-6.19.0
Bump @typescript-eslint/parser from 6.12.0 to 6.19.0
2024-01-16 14:11:18 +00:00
David Daly 9644d0366d Merge pull request #43 from immutable-actions/dependabot/npm_and_yarn/types/node-20.11.4
Bump @types/node from 20.9.4 to 20.11.4
2024-01-16 14:11:08 +00:00
dependabot[bot] d7c62e85e3 Bump @types/node from 20.9.4 to 20.11.4
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.9.4 to 20.11.4.
- [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-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-16 14:10:19 +00:00
dependabot[bot] 7de850349b Bump @typescript-eslint/parser from 6.12.0 to 6.19.0
Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 6.12.0 to 6.19.0.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v6.19.0/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-16 14:10:14 +00:00
David Daly b1add807f0 Merge pull request #30 from immutable-actions/dependabot/github_actions/actions/upload-artifact-4
Bump actions/upload-artifact from 3 to 4
2024-01-16 14:10:02 +00:00
David Daly 7d8d590e92 Merge pull request #29 from immutable-actions/dependabot/github_actions/github/codeql-action-3
Bump github/codeql-action from 2 to 3
2024-01-16 14:09:53 +00:00
David Daly 05d737b2f0 Merge pull request #26 from immutable-actions/dependabot/npm_and_yarn/types/jest-29.5.11
Bump @types/jest from 29.5.8 to 29.5.11
2024-01-16 14:09:42 +00:00
David Daly 2ef6e4fb4b Merge pull request #16 from immutable-actions/dependabot/npm_and_yarn/types/archiver-6.0.2
Bump @types/archiver from 6.0.1 to 6.0.2
2024-01-16 14:09:31 +00:00
David Daly 462d5b2421 Merge pull request #15 from immutable-actions/dependabot/npm_and_yarn/types/tar-6.1.10
Bump @types/tar from 6.1.9 to 6.1.10
2024-01-16 14:09:21 +00:00
David Daly 4a28d7b07e Merge pull request #14 from immutable-actions/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2024-01-16 14:09:08 +00:00
dependabot[bot] aec16d01be Bump actions/upload-artifact from 3 to 4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-14 22:39:10 +00:00
dependabot[bot] adccae446d Bump github/codeql-action from 2 to 3
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 2 to 3.
- [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/v2...v3)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-13 22:40:55 +00:00
dependabot[bot] 519446eefa Bump @types/jest from 29.5.8 to 29.5.11
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 29.5.8 to 29.5.11.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

---
updated-dependencies:
- dependency-name: "@types/jest"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-06 22:29:35 +00:00
Conor Sloan 6c4739c768 Sanitize repo name when including in a layer title
These titles were previously including the filename as {org}/{repo}_{version} which means when clients download the layer they think it needs to be unwrapped into a folder called {org}.
This is not what we want, so we should instead rename the file as {org}-{repo}_{version}. e.g. from myorg/myrepo_myversion.zip to myorg-myrepo_myversion.zip
2023-11-23 14:37:29 +00:00
dependabot[bot] 18773447f5 Bump @types/archiver from 6.0.1 to 6.0.2
Bumps [@types/archiver](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/archiver) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/archiver)

---
updated-dependencies:
- dependency-name: "@types/archiver"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-22 22:50:34 +00:00
dependabot[bot] 2b8d285933 Bump @types/tar from 6.1.9 to 6.1.10
Bumps [@types/tar](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/tar) from 6.1.9 to 6.1.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/tar)

---
updated-dependencies:
- dependency-name: "@types/tar"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-22 22:50:16 +00:00
dependabot[bot] 5dc2808a23 Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-22 22:10:33 +00:00
Conor Sloan 0461881066 fix readme linting 2023-11-22 16:06:33 +00:00
Conor Sloan c2bb735a45 add support for multiple paths in path input variable
This allows users to provide multiple filepaths to include in their action, whether they're files or folders.
2023-11-22 16:02:01 +00:00
Conor Sloan 56b7ee3ceb Update README.md 2023-11-22 14:49:13 +00:00
Conor Sloan 7dcdd9d45c Update README.md 2023-11-22 14:40:03 +00:00
Conor Sloan 09e610e36c Add a TODO list to the readme
This should help instruct the person who picks up this task what to implement before testing commences.
2023-11-22 14:32:17 +00:00
Conor Sloan a45c53b2f2 add test case for semver formats 2023-11-22 14:25:27 +00:00
Conor Sloan cceef5869f fix markdown linting 2023-11-22 13:36:22 +00:00
Conor Sloan 1cd46a724e Ask linter to ignore licenses file. 2023-11-22 13:31:06 +00:00
Conor Sloan 665d8f05cc clean up markdowns 2023-11-22 13:17:51 +00:00
Conor Sloan 6babec0772 upgrade deps 2023-11-22 13:10:58 +00:00
Conor Sloan f574209690 Create release.yml 2023-11-22 13:06:42 +00:00
Conor Sloan bf7467ab03 update dist 2023-11-22 13:05:25 +00:00
Conor Sloan 7c7ef462b7 fix or disable linting in fs-helper
There is a bit of a catch-22 bere where the createArchives function should not use .then and .catch in favour of async/await according to the linter, but can't use async/await because it's discouraged to make new promise executor functions async. There is a hacky workaround to make this work, but it seems like it isn't worth making the code more fragmented, so i just disabled the linter for these lines.
2023-11-22 13:03:09 +00:00
Conor Sloan ffca16d01f fix low hanging fruit linter errors 2023-11-22 12:41:53 +00:00
Conor Sloan 4c296f98cf Remove test-action from ci.yml
we don't need this as it pertains to the template js action. We'll find other ways to test the action.
2023-11-22 12:18:03 +00:00
Conor Sloan e39e2c9775 Disable codeql analysis 2023-11-22 12:14:23 +00:00
Conor Sloan 61de8fadb6 fix linter 2023-11-17 21:38:11 +00:00
Conor Sloan 1e8c61d3f5 update deps 2023-11-17 21:31:12 +00:00
Conor Sloan 306ad46422 Merge pull request #1 from immutable-actions/dependabot/npm_and_yarn/types/tar-6.1.9
Bump @types/tar from 6.1.8 to 6.1.9
2023-11-17 21:27:55 +00:00
Conor Sloan 2904fe0ac2 split readme and contributing 2023-11-17 20:08:25 +00:00
dependabot[bot] 54a9cc0dae Bump @types/tar from 6.1.8 to 6.1.9
Bumps [@types/tar](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/tar) from 6.1.8 to 6.1.9.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/tar)

---
updated-dependencies:
- dependency-name: "@types/tar"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-11-17 20:05:26 +00:00
Conor Sloan 8f0926c56a initial mvp version 2023-11-17 20:04:42 +00:00
42 changed files with 21988 additions and 74839 deletions
+4
View File
@@ -0,0 +1,4 @@
# Repository CODEOWNERS
* @actions/actions-runtime
* @ncalteen
-1
View File
@@ -17,7 +17,6 @@ on:
permissions:
contents: read
packages: read
jobs:
check-dist:
+60
View File
@@ -0,0 +1,60 @@
name: E2E Test
on:
workflow_dispatch:
permissions: {}
jobs:
e2e-test:
name: E2E Integration Test
runs-on: ubuntu-latest
steps:
- name: Send message to consumer to publish
id: send-message
run: |
echo "SHA: ${{ github.sha }}"
curl -s -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.PAT }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/immutable-actions/test-publish-internal-ts-action/dispatches \
-d '{"event_type":"e2e-test","client_payload":{"unit":false,"integration":true,"sha":"${{ github.sha }}"}}'
- name: Wait for successful publish
id: wait-for-successful-publish
run: |
START_TIME="$SECONDS"
TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
while (( SECONDS - START_TIME < 60 )); do
echo "Polling for workflow created after $TIMESTAMP"
RESULT=$(curl -s -L -H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.PAT }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/immutable-actions/test-publish-internal-ts-action/actions/runs?created=>$TIMESTAMP" \
| jq '.workflow_runs[] | select(.name=="Publish Actions Package" and (.display_title | contains("${{ github.sha }}"))) | .status, .conclusion')
# split the RESULT into an array
mapfile -t RESULT <<< "$RESULT"
STATUS=$(echo "${RESULT[0]}" | sed -e 's/^"//' -e 's/"$//')
CONCLUSION=$(echo "${RESULT[1]}" | sed -e 's/^"//' -e 's/"$//')
if [ -z "$STATUS" ]; then
echo "No workflow found yet"
else
echo "Workflow status: $STATUS"
echo "Workflow conclusion: $CONCLUSION"
if [ "$STATUS" = "completed" ]; then
if [ "$CONCLUSION" = "success" ]; then
echo "workflow succeeded"
exit 0
elif [ "$CONCLUSION" = "failure" ]; then
echo "workflow failed"
exit 1
fi
fi
fi
sleep 1
done
exit 2
+1 -2
View File
@@ -9,7 +9,6 @@ on:
permissions:
contents: read
packages: read
jobs:
test-typescript:
@@ -43,4 +42,4 @@ jobs:
- name: Test
id: npm-ci-test
run: npm run ci-test
+6 -5
View File
@@ -12,16 +12,17 @@ on:
# schedule:
# - cron: '31 7 * * 3'
permissions:
actions: read
contents: read
security-events: write
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
checks: write
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
+2 -6
View File
@@ -11,7 +11,6 @@ on:
permissions:
contents: read
statuses: write
packages: read
jobs:
lint:
@@ -22,9 +21,6 @@ jobs:
- name: Checkout
id: checkout
uses: actions/checkout@v4
# this is necessary based on https://github.com/super-linter/super-linter?tab=readme-ov-file#get-started
with:
fetch-depth: 0
- name: Setup Node.js
id: setup-node
@@ -39,10 +35,10 @@ jobs:
- name: Lint Code Base
id: super-linter
uses: super-linter/super-linter/slim@v6
uses: super-linter/super-linter/slim@v5
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VALIDATE_TYPESCRIPT_STANDARD: false
TYPESCRIPT_DEFAULT_STYLE: prettier
VALIDATE_JSCPD: false
FILTER_REGEX_EXCLUDE: .*/licenses\.txt$
@@ -1,23 +0,0 @@
name: Release new action version
on:
release:
types: [released]
env:
TAG_NAME: ${{ github.event.release.tag_name }}
permissions:
contents: write
jobs:
update_tag:
name: Update the major tag to include the ${{ github.event.release.tag_name }} changes
environment:
name: releaseNewActionVersion
runs-on: ubuntu-latest
steps:
- name: Update the ${{ env.TAG_NAME }} tag
id: update-major-tag
uses: actions/publish-action@v0.3.0
with:
source-tag: ${{ env.TAG_NAME }}
slack-webhook: ${{ secrets.SLACK_WEBHOOK }}
+7 -9
View File
@@ -1,18 +1,16 @@
# Package and publish the action when a new release is published
# Since this is the publishing action itself, we can use the current checkout as the action
name: 'Publish Immutable Action Version'
on:
name: 'release'
on: # rebuild any PRs and main branch changes
release:
types: [published]
types: [created]
permissions:
contents: read
id-token: write
contents: write
packages: write
jobs:
package-and-publish:
package-and-publish:
runs-on: ubuntu-latest
steps:
- name: Check out repository
- name: Checking out!
uses: actions/checkout@v4
- name: Publish Immutable Action Version
- name: Publish action package
uses: ./
-2
View File
@@ -1,8 +1,6 @@
# Dependency directory
node_modules
.npmrc
# Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
# Logs
logs
+1 -1
View File
@@ -1 +1 @@
24.4.0
20.6.0
-3
View File
@@ -1,3 +0,0 @@
# Repository CODEOWNERS
* @actions/actions-sudo
+14 -43
View File
@@ -1,57 +1,28 @@
# Publish Immutable Action
# Publish Action Package
> [!IMPORTANT]
> This action is **not ready for public use**. It is part of an upcoming public roadmap item (see [GitHub Actions: Immutable actions publishing](https://github.com/github/roadmap/issues/592)).
> Attempts to use this action to upload an OCI artifact will not work until this feature has been fully released to the public. Please do not attempt to use it until that time.
_This action_ packages _your action_ as OCI artifacts and publishes it to the [GitHub Container registry](ghcr.io).
This action packages _your action_ as an [OCI container](https://opencontainers.org/) and publishes it to the [GitHub Container registry](https://ghcr.io).
This allows your action to be consumed as an _immutable_ package if a [SemVer](https://semver.org/) is specified in the consumer's workflow file.
This allows your action to be consumed as an _immutable_ package even if a [SemVer](https://semver.org/) is specified in the consumer's workflow file.
Your workflow can be triggered by any [event](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows) which has a `GITHUB_REF` that points to a Git tag.
Some examples of these events are:
- [`release`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release) (uses tag associated with release)
- [`push`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push) (only applies to pushed tags)
- [`workflow_dispatch`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch) (only applies if subject of dispatch is a tag)
The associated tag must follow [semantic versioning](https://semver.org/) - this tag value will be used to create a package version.
Consumers of your action will then be able to specify that version to consume your action from the package, e.g.
- `- uses: your-name/your-action@v1.2.3`
- `- uses: your-name/your-action@v1`
Such packages will come with stronger security guarantees for consumers than existing git-based action resolution, such as:
- Provenance attestations generated using the [`@actions/attest`](https://github.com/actions/toolkit/tree/main/packages/attest) package
- Tag immutability - it will not be possible to overwrite tags once published, ensuring versions of an action can't change once in use
- Namespace immutability - it will not be possible to delete and recreate the package with different content; this would undermine tag immutability
Your action workflow must be triggered on `release` as in the following example. The release's title must follow [semantic versioning](https://semver.org/).
Then consumers of your action will then be able to specify the version, e.g., `- uses: your-name/your-action@v1.2.3` or even `- uses: your-name/your-action@v1`.
## Usage
An actions workflow file like the following should be placed in your action repository:
<!-- start usage -->
```yaml
name: "Publish Immutable Action Version"
on:
release:
types: [published]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
steps:
- name: Check out repo
uses: actions/checkout@v4
- name: Publish
id: publish
uses: actions/publish-immutable-action@0.0.3
- uses: immutable-actions/publish-action-package@v1
with:
# Relative path of the working directory of the repository to be tar archived
# and uploaded as OCI Artifact layer. You can mention multiple files/folders
# by mentioning relative paths as space separated values.
#
# This defaults to the entire action repository contents if not explicitly defined.
# Default: '.'
path: 'src/ action.yml dist/'
```
<!-- end usage -->
+12 -54
View File
@@ -3,9 +3,6 @@ import {
getContainerRegistryURL
} from '../src/api-client'
const url = 'https://registry.example.com'
const test_token = 'test_token'
let fetchMock: jest.SpyInstance
beforeEach(() => {
@@ -19,45 +16,22 @@ afterEach(() => {
describe('getRepositoryMetadata', () => {
it('returns repository metadata when the fetch response is ok', async () => {
fetchMock.mockResolvedValueOnce(
new Response(
JSON.stringify({
id: '123',
owner: { id: '456' },
visibility: 'public'
})
)
)
const result = await getRepositoryMetadata(url, 'repository', test_token)
expect(result).toEqual({
repoId: '123',
ownerId: '456',
visibility: 'public'
})
expect(fetchMock).toHaveBeenCalledWith(
'https://registry.example.com/repos/repository',
{
method: 'GET',
headers: {
Authorization: `Bearer ${test_token}`,
Accept: 'application/vnd.github.v3+json'
}
}
new Response(JSON.stringify({ id: '123', owner: { id: '456' } }))
)
const result = await getRepositoryMetadata('repository', 'token')
expect(result).toEqual({ repoId: '123', ownerId: '456' })
})
it('throws an error when the fetch errors', async () => {
fetchMock.mockRejectedValueOnce(new Error('API is down'))
await expect(
getRepositoryMetadata(url, 'repository', 'token')
).rejects.toThrow('API is down')
await expect(getRepositoryMetadata('repository', 'token')).rejects.toThrow(
'API is down'
)
})
it('throws an error when the response status is not ok', async () => {
fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 }))
await expect(
getRepositoryMetadata(url, 'repository', 'token')
).rejects.toThrow(
await expect(getRepositoryMetadata('repository', 'token')).rejects.toThrow(
'Failed to fetch repository metadata due to bad status code: 500'
)
})
@@ -66,9 +40,7 @@ describe('getRepositoryMetadata', () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ wrong: 'format' }))
)
await expect(
getRepositoryMetadata(url, 'repository', 'token')
).rejects.toThrow(
await expect(getRepositoryMetadata('repository', 'token')).rejects.toThrow(
'Failed to fetch repository metadata: unexpected response format'
)
})
@@ -79,32 +51,18 @@ describe('getContainerRegistryURL', () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ url: 'https://registry.example.com' }))
)
const result = await getContainerRegistryURL(url, test_token)
const result = await getContainerRegistryURL()
expect(result).toEqual(new URL('https://registry.example.com'))
expect(fetchMock).toHaveBeenCalledWith(
'https://registry.example.com/packages/container-registry-url',
{
method: 'GET',
headers: {
Authorization: `Bearer ${test_token}`,
Accept: 'application/vnd.github.v3+json'
}
}
)
})
it('throws an error when the fetch errors', async () => {
fetchMock.mockRejectedValueOnce(new Error('API is down'))
await expect(getContainerRegistryURL(url, test_token)).rejects.toThrow(
'API is down'
)
await expect(getContainerRegistryURL()).rejects.toThrow('API is down')
})
it('throws an error when the response status is not ok', async () => {
fetchMock.mockResolvedValueOnce(new Response(null, { status: 500 }))
await expect(getContainerRegistryURL(url, test_token)).rejects.toThrow(
await expect(getContainerRegistryURL()).rejects.toThrow(
'Failed to fetch container registry url due to bad status code: 500'
)
})
@@ -113,7 +71,7 @@ describe('getContainerRegistryURL', () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ wrong: 'format' }))
)
await expect(getContainerRegistryURL(url, test_token)).rejects.toThrow(
await expect(getContainerRegistryURL()).rejects.toThrow(
'Failed to fetch repository metadata: unexpected response format'
)
})
-331
View File
@@ -1,331 +0,0 @@
import * as core from '@actions/core'
import * as github from '@actions/github'
import * as cfg from '../src/config'
import * as apiClient from '../src/api-client'
let getContainerRegistryURLMock: jest.SpyInstance
let getRepositoryMetadataMock: jest.SpyInstance
let getInputMock: jest.SpyInstance
const ghcrUrl = new URL('https://ghcr.io')
describe('config.resolvePublishActionOptions', () => {
beforeEach(() => {
getContainerRegistryURLMock = jest
.spyOn(apiClient, 'getContainerRegistryURL')
.mockImplementation()
getRepositoryMetadataMock = jest
.spyOn(apiClient, 'getRepositoryMetadata')
.mockImplementation()
getInputMock = jest.spyOn(core, 'getInput').mockImplementation()
configureEventContext()
})
afterEach(() => {
jest.clearAllMocks()
clearEventContext()
})
it('throws an error when the token is not provided', async () => {
getInputMock.mockReturnValueOnce(undefined)
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find GITHUB_TOKEN.'
)
})
it('throws an error when the event is not provided', async () => {
getInputMock.mockReturnValueOnce('token')
github.context.eventName = ''
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find event name.'
)
})
it('throws an error when the ref is not provided', async () => {
getInputMock.mockReturnValueOnce('token')
github.context.ref = ''
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find GITHUB_REF.'
)
})
it('throws an error when the workspaceDir is not provided', async () => {
getInputMock.mockReturnValueOnce('token')
process.env.GITHUB_WORKSPACE = ''
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find GITHUB_WORKSPACE.'
)
})
it('throws an error when the repository is not provided', async () => {
getInputMock.mockReturnValueOnce('token')
github.context.payload.repository = undefined
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find Repository.'
)
})
it('throws an error when the apiBaseUrl is not provided', async () => {
getInputMock.mockReturnValueOnce('token')
github.context.apiUrl = ''
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find GITHUB_API_URL.'
)
})
it('throws an error when the runnerTempDir is not provided', async () => {
getInputMock.mockReturnValueOnce('token')
process.env.RUNNER_TEMP = ''
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find RUNNER_TEMP.'
)
})
it('throws an error when the sha is not provided', async () => {
getInputMock.mockReturnValueOnce('token')
github.context.sha = ''
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find GITHUB_SHA.'
)
})
it('throws an error when the githubServerUrl is not provided', async () => {
getInputMock.mockReturnValueOnce('token')
github.context.serverUrl = ''
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find GITHUB_SERVER_URL.'
)
})
it('throws an error when the repositoryId is not provided', async () => {
getInputMock.mockReturnValueOnce('token')
process.env.GITHUB_REPOSITORY_ID = ''
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find GITHUB_REPOSITORY_ID.'
)
})
it('throws an error when the repositoryOwnerId is not provided', async () => {
getInputMock.mockReturnValueOnce('token')
process.env.GITHUB_REPOSITORY_OWNER_ID = ''
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find GITHUB_REPOSITORY_OWNER_ID.'
)
})
it('throws an error when getting the container registry URL fails', async () => {
getInputMock.mockReturnValueOnce('token')
getContainerRegistryURLMock.mockRejectedValue(
new Error('Failed to get container registry URL')
)
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Failed to get container registry URL'
)
})
it('throws an error when getting the repository metadata fails', async () => {
getInputMock.mockReturnValueOnce('token')
getContainerRegistryURLMock.mockResolvedValue(ghcrUrl)
getRepositoryMetadataMock.mockRejectedValue(
new Error('Failed to get repository metadata')
)
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Failed to get repository metadata'
)
})
it('throws an error when returned repository visibility is empty', async () => {
getInputMock.mockReturnValueOnce('token')
getContainerRegistryURLMock.mockResolvedValue(ghcrUrl)
getRepositoryMetadataMock.mockResolvedValue({
visibility: ''
})
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Could not find repository visibility.'
)
})
it('throws an error when returned repository id does not match env var', async () => {
getInputMock.mockReturnValueOnce('token')
getContainerRegistryURLMock.mockResolvedValue(ghcrUrl)
getRepositoryMetadataMock.mockResolvedValue({
visibility: 'public',
ownerId: '12345',
repoId: '54321'
})
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Repository ID mismatch.'
)
})
it('throws an error when returned repository owner id does not match env var', async () => {
getInputMock.mockReturnValueOnce('token')
getContainerRegistryURLMock.mockResolvedValue(ghcrUrl)
getRepositoryMetadataMock.mockResolvedValue({
visibility: 'public',
ownerId: '123124',
repoId: 'repositoryId'
})
await expect(cfg.resolvePublishActionOptions()).rejects.toThrow(
'Repository Owner ID mismatch.'
)
})
it('returns options when all values are present', async () => {
getInputMock.mockImplementation((name: string) => {
expect(name).toBe('github-token')
return 'token'
})
getContainerRegistryURLMock.mockResolvedValue(ghcrUrl)
getRepositoryMetadataMock.mockResolvedValue({
visibility: 'public',
repoId: 'repositoryId',
ownerId: 'repositoryOwnerId'
})
const options = await cfg.resolvePublishActionOptions()
expect(options).toEqual({
nameWithOwner: 'nameWithOwner',
ref: 'ref',
workspaceDir: 'workspaceDir',
event: 'release',
apiBaseUrl: 'apiBaseUrl',
runnerTempDir: 'runnerTempDir',
sha: 'sha',
repositoryVisibility: 'public',
repositoryId: 'repositoryId',
repositoryOwnerId: 'repositoryOwnerId',
isEnterprise: false,
containerRegistryUrl: ghcrUrl,
token: 'token'
})
})
it('sets enterprise to true when the server URL is not github.com or ghe.com', async () => {
getInputMock.mockImplementation((name: string) => {
expect(name).toBe('github-token')
return 'token'
})
getContainerRegistryURLMock.mockResolvedValue(ghcrUrl)
getRepositoryMetadataMock.mockResolvedValue({
visibility: 'public',
repoId: 'repositoryId',
ownerId: 'repositoryOwnerId'
})
github.context.serverUrl = 'https://github-enterprise.com'
const options = await cfg.resolvePublishActionOptions()
expect(options).toEqual({
nameWithOwner: 'nameWithOwner',
ref: 'ref',
workspaceDir: 'workspaceDir',
event: 'release',
apiBaseUrl: 'apiBaseUrl',
runnerTempDir: 'runnerTempDir',
sha: 'sha',
repositoryId: 'repositoryId',
repositoryOwnerId: 'repositoryOwnerId',
isEnterprise: true,
containerRegistryUrl: ghcrUrl,
token: 'token',
repositoryVisibility: 'public'
})
})
})
describe('config.serializeOptions', () => {
it('serializes the options, ignoring internal keys', () => {
const options: cfg.PublishActionOptions = {
nameWithOwner: 'nameWithOwner',
ref: 'ref',
workspaceDir: 'workspaceDir',
event: 'release',
apiBaseUrl: 'apiBaseUrl',
runnerTempDir: 'runnerTempDir',
sha: 'sha',
repositoryId: 'repositoryId',
repositoryOwnerId: 'repositoryOwnerId',
isEnterprise: false,
containerRegistryUrl: ghcrUrl,
token: 'token',
repositoryVisibility: 'public'
}
const serialized = cfg.serializeOptions(options)
// Parse the JSON
const parsed = JSON.parse(serialized)
expect(parsed.nameWithOwner).toBe('nameWithOwner')
expect(parsed.ref).toBe('ref')
expect(parsed.workspaceDir).toBe('workspaceDir')
expect(parsed.event).toBe('release')
expect(parsed.apiBaseUrl).toBe('apiBaseUrl')
expect(parsed.sha).toBe('sha')
expect(parsed.isEnterprise).toBe(false)
expect(parsed.containerRegistryUrl).toBe(ghcrUrl.toString())
expect(parsed.token).toBeUndefined()
expect(parsed.repositoryId).toBeUndefined()
expect(parsed.repositoryOwnerId).toBeUndefined()
expect(parsed.runnerTempDir).toBeUndefined()
})
})
function configureEventContext(): void {
github.context.ref = 'ref'
github.context.eventName = 'release'
github.context.apiUrl = 'apiBaseUrl'
github.context.sha = 'sha'
github.context.serverUrl = 'https://github.com/'
github.context.payload = {
repository: {
full_name: 'nameWithOwner',
name: 'name',
owner: {
login: 'owner'
}
}
}
process.env.RUNNER_TEMP = 'runnerTempDir'
process.env.GITHUB_WORKSPACE = 'workspaceDir'
process.env.GITHUB_REPOSITORY_ID = 'repositoryId'
process.env.GITHUB_REPOSITORY_OWNER_ID = 'repositoryOwnerId'
}
function clearEventContext(): void {
github.context.ref = ''
github.context.eventName = ''
github.context.apiUrl = ''
github.context.sha = ''
github.context.serverUrl = ''
github.context.payload = {}
process.env.RUNNER_TEMP = ''
process.env.GITHUB_WORKSPACE = ''
process.env.GITHUB_REPOSITORY_ID = ''
process.env.GITHUB_REPOSITORY_OWNER_ID = ''
}
+41 -115
View File
@@ -4,19 +4,19 @@ import * as os from 'os'
import { execSync } from 'child_process'
const fileContent = 'This is the content of the file'
const tmpFileDir = '/tmp'
describe('stageActionFiles', () => {
let sourceDir: string
let stagingDir: string
beforeEach(() => {
sourceDir = fsHelper.createTempDir(tmpFileDir, 'source')
process.env.RUNNER_TEMP = '/tmp'
sourceDir = fsHelper.createTempDir('source')
fs.mkdirSync(`${sourceDir}/src`)
fs.writeFileSync(`${sourceDir}/src/main.js`, fileContent)
fs.writeFileSync(`${sourceDir}/src/other.js`, fileContent)
stagingDir = fsHelper.createTempDir(tmpFileDir, 'staging')
stagingDir = fsHelper.createTempDir('staging')
})
afterEach(() => {
@@ -24,7 +24,13 @@ describe('stageActionFiles', () => {
fs.rmSync(stagingDir, { recursive: true })
})
it('copies all files (excluding the .git folder) to the staging directory', () => {
it('returns an error if no action.yml file is present', () => {
expect(() => fsHelper.stageActionFiles(sourceDir, stagingDir)).toThrow(
/^No action.yml or action.yaml file found in source repository/
)
})
it('copies all non-hidden files to the staging directory', () => {
fs.writeFileSync(`${sourceDir}/action.yml`, fileContent)
fs.mkdirSync(`${sourceDir}/.git`)
@@ -38,11 +44,26 @@ describe('stageActionFiles', () => {
expect(fs.existsSync(`${stagingDir}/src/main.js`)).toBe(true)
expect(fs.existsSync(`${stagingDir}/src/other.js`)).toBe(true)
// Hidden files are copied
expect(fs.existsSync(`${stagingDir}/.github`)).toBe(true)
// .git folder is not copied
// Hidden files should not be copied
expect(fs.existsSync(`${stagingDir}/.git`)).toBe(false)
expect(fs.existsSync(`${stagingDir}/.github`)).toBe(false)
})
it('copies all non-hidden files to the staging directory, even if action.yml is in a subdirectory', () => {
fs.mkdirSync(`${sourceDir}/my-sub-action`, { recursive: true })
fs.writeFileSync(`${sourceDir}/my-sub-action/action.yml`, fileContent)
fsHelper.stageActionFiles(sourceDir, stagingDir)
expect(fs.existsSync(`${stagingDir}/src/main.js`)).toBe(true)
expect(fs.existsSync(`${stagingDir}/src/other.js`)).toBe(true)
expect(fs.existsSync(`${stagingDir}/my-sub-action/action.yml`)).toBe(true)
})
it('accepts action.yaml as a valid action file as well as action.yml', () => {
fs.writeFileSync(`${sourceDir}/action.yaml`, fileContent)
fsHelper.stageActionFiles(sourceDir, stagingDir)
expect(fs.existsSync(`${stagingDir}/action.yaml`)).toBe(true)
})
})
@@ -51,13 +72,14 @@ describe('createArchives', () => {
let archiveDir: string
beforeAll(() => {
stageDir = fsHelper.createTempDir(tmpFileDir, 'staging')
process.env.RUNNER_TEMP = '/tmp'
stageDir = fsHelper.createTempDir('staging')
fs.writeFileSync(`${stageDir}/hello.txt`, fileContent)
fs.writeFileSync(`${stageDir}/world.txt`, fileContent)
})
beforeEach(() => {
archiveDir = fsHelper.createTempDir(tmpFileDir, 'archive')
archiveDir = fsHelper.createTempDir('archive')
})
afterEach(() => {
@@ -134,17 +156,19 @@ describe('createTempDir', () => {
})
it('creates a temporary directory', () => {
const tmpDir = fsHelper.createTempDir(tmpFileDir, 'subdir')
process.env.RUNNER_TEMP = '/tmp'
const tmpDir = fsHelper.createTempDir('subdir')
expect(fs.existsSync(tmpDir)).toEqual(true)
expect(fs.statSync(tmpDir).isDirectory()).toEqual(true)
})
it('creates a unique temporary directory', () => {
const dir1 = fsHelper.createTempDir(tmpFileDir, 'dir1')
process.env.RUNNER_TEMP = '/tmp'
const dir1 = fsHelper.createTempDir('dir1')
dirs.push(dir1)
const dir2 = fsHelper.createTempDir(tmpFileDir, 'dir2')
const dir2 = fsHelper.createTempDir('dir2')
dirs.push(dir2)
expect(dir1).not.toEqual(dir2)
@@ -155,7 +179,8 @@ describe('isDirectory', () => {
let dir: string
beforeEach(() => {
dir = fsHelper.createTempDir(tmpFileDir, 'subdir')
process.env.RUNNER_TEMP = '/tmp'
dir = fsHelper.createTempDir('subdir')
})
afterEach(() => {
@@ -177,7 +202,8 @@ describe('readFileContents', () => {
let dir: string
beforeEach(() => {
dir = fsHelper.createTempDir(tmpFileDir, 'subdir')
process.env.RUNNER_TEMP = '/tmp'
dir = fsHelper.createTempDir('subdir')
})
afterEach(() => {
@@ -191,103 +217,3 @@ describe('readFileContents', () => {
expect(fsHelper.readFileContents(tempFile).toString()).toEqual(fileContent)
})
})
describe('ensureCorrectShaCheckedOut', () => {
let dir: string
let commit1: string
let commit2: string
const tag1 = 'tag1'
const tag2 = 'tag2'
beforeEach(() => {
dir = fsHelper.createTempDir(tmpFileDir, 'subdir')
// Set up a git repository
execSync('git init', { cwd: dir })
// Set user and email in this git repo (not globally)
execSync('git config user.email monalisa@github.com', { cwd: dir })
execSync('git config user.name Mona', { cwd: dir })
// Add a file to the repo
fs.writeFileSync(`${dir}/file1.txt`, fileContent)
execSync('git add .', { cwd: dir })
// Add two commits
execSync('git commit --allow-empty -m "test"', { cwd: dir })
execSync('git commit --allow-empty -m "test"', { cwd: dir })
// Grab the two commits
commit1 = execSync('git rev-parse HEAD~1', { cwd: dir }).toString().trim()
commit2 = execSync('git rev-parse HEAD', { cwd: dir }).toString().trim()
// Create a tag for each commit
execSync(`git tag ${tag1} ${commit1}`, { cwd: dir })
execSync(`git tag ${tag2} ${commit2}`, { cwd: dir })
})
afterEach(() => {
fs.rmSync(dir, { recursive: true })
})
it('does not throw an error if the correct SHA is checked out', async () => {
await expect(
fsHelper.ensureTagAndRefCheckedOut(`refs/tags/${tag2}`, commit2, dir)
).resolves.toBeUndefined()
})
it('throws an error if the correct SHA is not checked out', async () => {
await expect(
fsHelper.ensureTagAndRefCheckedOut(`refs/tags/${tag1}`, commit1, dir)
).rejects.toThrow(
'The expected commit associated with the tag refs/tags/tag1 is not checked out.'
)
})
it('throws if there is an issue getting sha for tag', async () => {
await expect(async () =>
fsHelper.ensureTagAndRefCheckedOut(
`refs/tags/some-unknown-tag`,
commit2,
dir
)
).rejects.toThrow('Error retrieving commit associated with tag')
})
it('throws an error if the sha of the tag does not match expected sha', async () => {
await expect(async () =>
fsHelper.ensureTagAndRefCheckedOut(`refs/tags/${tag1}`, commit2, dir)
).rejects.toThrow(
'The commit associated with the tag refs/tags/tag1 does not match the SHA of the commit provided by the actions context.'
)
})
it('throws if the provided ref is not a tag ref', async () => {
await expect(async () =>
fsHelper.ensureTagAndRefCheckedOut(`refs/heads/main`, commit2, dir)
).rejects.toThrow('Tag ref provided is not in expected format.')
})
it('throws if there are untracked files in the working directory', async () => {
// Add an untracked file
fs.writeFileSync(`${dir}/untracked-file.txt`, fileContent)
await expect(async () =>
fsHelper.ensureTagAndRefCheckedOut(`refs/tags/${tag2}`, commit2, dir)
).rejects.toThrow(
'The working directory has uncommitted changes. Uploading modified code from the checked out repository is not supported by this action.'
)
})
it('throws if there are uncommitted changes in the working directory', async () => {
// Add an untracked file
fs.writeFileSync(`${dir}/file1.txt`, fileContent + fileContent)
execSync('git add .', { cwd: dir })
await expect(async () =>
fsHelper.ensureTagAndRefCheckedOut(`refs/tags/${tag2}`, commit2, dir)
).rejects.toThrow(
'The working directory has uncommitted changes. Uploading modified code from the checked out repository is not supported by this action.'
)
})
})
File diff suppressed because it is too large Load Diff
+233 -563
View File
@@ -7,50 +7,33 @@
*/
import * as core from '@actions/core'
import * as attest from '@actions/attest'
import * as main from '../src/main'
import * as cfg from '../src/config'
import * as github from '@actions/github'
import * as fsHelper from '../src/fs-helper'
import * as ghcr from '../src/ghcr-client'
import * as ociContainer from '../src/oci-container'
const ghcrUrl = new URL('https://ghcr.io')
const predicateType = 'https://slsa.dev/provenance/v1'
const bundleMediaType = 'application/vnd.dev.sigstore.bundle.v0.3+json'
import * as api from '../src/api-client'
// Mock the GitHub Actions core library
let setFailedMock: jest.SpyInstance
let setOutputMock: jest.SpyInstance
// Mock the FS Helper
// Mock the filesystem helper
let createTempDirMock: jest.SpyInstance
let createArchivesMock: jest.SpyInstance
let stageActionFilesMock: jest.SpyInstance
let ensureCorrectShaCheckedOutMock: jest.SpyInstance
let readFileContentsMock: jest.SpyInstance
// Mock OCI container lib
let calculateManifestDigestMock: jest.SpyInstance
// Mock the GHCR Client
let publishOCIArtifactMock: jest.SpyInstance
// Mock GHCR client
let client: ghcr.Client
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let createGHCRClient: jest.SpyInstance
let uploadOCIImageManifestMock: jest.SpyInstance
let uploadOCIIndexManifestMock: jest.SpyInstance
// Mock the config resolution
let resolvePublishActionOptionsMock: jest.SpyInstance
// Mock generating attestation
let generateAttestationMock: jest.SpyInstance
// Mock the API Client
let getContainerRegistryURLMock: jest.SpyInstance
let getRepositoryMetadataMock: jest.SpyInstance
describe('run', () => {
beforeEach(() => {
jest.clearAllMocks()
client = new ghcr.Client('token', ghcrUrl)
// Core mocks
setFailedMock = jest.spyOn(core, 'setFailed').mockImplementation()
setOutputMock = jest.spyOn(core, 'setOutput').mockImplementation()
@@ -65,105 +48,123 @@ describe('run', () => {
stageActionFilesMock = jest
.spyOn(fsHelper, 'stageActionFiles')
.mockImplementation()
ensureCorrectShaCheckedOutMock = jest
.spyOn(fsHelper, 'ensureTagAndRefCheckedOut')
.mockImplementation()
readFileContentsMock = jest
.spyOn(fsHelper, 'readFileContents')
.mockImplementation()
// OCI Container mocks
calculateManifestDigestMock = jest
.spyOn(ociContainer, 'sha256Digest')
.mockImplementation()
// GHCR Client mocks
createGHCRClient = jest
.spyOn(ghcr, 'Client')
.mockImplementation(() => client)
uploadOCIImageManifestMock = jest
.spyOn(client, 'uploadOCIImageManifest')
.mockImplementation()
uploadOCIIndexManifestMock = jest
.spyOn(client, 'uploadOCIIndexManifest')
publishOCIArtifactMock = jest
.spyOn(ghcr, 'publishOCIArtifact')
.mockImplementation()
// Config mocks
resolvePublishActionOptionsMock = jest
.spyOn(cfg, 'resolvePublishActionOptions')
// API Client mocks
getContainerRegistryURLMock = jest
.spyOn(api, 'getContainerRegistryURL')
.mockImplementation()
// Attestation mocks
generateAttestationMock = jest
.spyOn(attest, 'attestProvenance')
getRepositoryMetadataMock = jest
.spyOn(api, 'getRepositoryMetadata')
.mockImplementation()
})
it('fails if the action ref is not a tag', async () => {
const options = baseOptions()
options.ref = 'refs/heads/main' // This is a branch, not a tag
resolvePublishActionOptionsMock.mockReturnValueOnce(options)
it('fails if no repository found', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = ''
// Run the action
await main.run()
expect(setFailedMock).toHaveBeenCalledWith(
'The ref refs/heads/main is not a valid tag reference.'
)
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Could not find Repository.')
})
it('fails if the value of the tag ref is not a valid semver', async () => {
const tags = ['test', 'v1.0', 'chicken', '111111']
it('fails if no token found', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
process.env.TOKEN = ''
for (const tag of tags) {
const options = baseOptions()
options.ref = `refs/tags/${tag}`
resolvePublishActionOptionsMock.mockReturnValueOnce(options)
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Could not find GITHUB_TOKEN.')
})
it('fails if no source commit found', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
process.env.TOKEN = 'test'
process.env.GITHUB_SHA = ''
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Could not find source commit.')
})
it('fails if trigger is not release or tag push', async () => {
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
process.env.GITHUB_SHA = 'test-sha'
process.env.TOKEN = 'token'
// TODO: If we want we can add all of these: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows
const invalidEvents = ['workflow_dispatch, pull_request, schedule']
for (const event of invalidEvents) {
github.context.eventName = event
await main.run()
expect(setFailedMock).toHaveBeenCalledWith(
`${tag} is not a valid semantic version tag, and so cannot be uploaded to the action package.`
'This action can only be triggered by release events or tag push events.'
)
}
})
it('fails if ensuring the correct SHA is checked out errors', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
it('fails if the trigger is a push, but not a tag push', async () => {
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
process.env.GITHUB_SHA = 'test-sha'
process.env.TOKEN = 'token'
github.context.eventName = 'push'
github.context.ref = 'refs/heads/main' // This is a branch, not a tag
ensureCorrectShaCheckedOutMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
expect(setFailedMock).toHaveBeenCalledWith(
'This action can only be triggered by release events or tag push events.'
)
})
it('fails if creating staging temp directory fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
it('fails if the value of the tag input is not a valid semver', async () => {
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
process.env.GITHUB_SHA = 'test-sha'
process.env.TOKEN = 'token'
github.context.eventName = 'release'
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
const tags = ['test', 'v1.0', 'chicken', '111111']
// Run the action
await main.run()
for (const tag of tags) {
github.context.payload = {
release: {
id: '123',
tag_name: tag
}
}
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
await main.run()
expect(setFailedMock).toHaveBeenCalledWith(
`${tag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`
)
}
})
it('fails if staging files fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'tmpDir/staging'
})
// Mock the environment
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
github.context.eventName = 'release'
process.env.GITHUB_SHA = 'test-sha'
process.env.TOKEN = 'token'
github.context.payload = {
release: {
id: '123',
tag_name: 'v1.2.3'
}
}
stageActionFilesMock.mockImplementation(() => {
throw new Error('Something went wrong')
@@ -176,20 +177,23 @@ describe('run', () => {
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if creating archives temp directory fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation((_, path: string) => {
if (path === 'staging') {
return 'staging'
it('fails if creating temp directory fails', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
github.context.eventName = 'release'
process.env.GITHUB_SHA = 'test-sha'
process.env.TOKEN = 'token'
github.context.payload = {
release: {
id: '123',
tag_name: 'v1.2.3'
}
}
createTempDirMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
stageActionFilesMock.mockImplementation(() => {})
// Run the action
await main.run()
@@ -198,15 +202,17 @@ describe('run', () => {
})
it('fails if creating archives fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
// Mock the environment
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
github.context.eventName = 'release'
process.env.GITHUB_SHA = 'test-sha'
process.env.TOKEN = 'token'
github.context.payload = {
release: {
id: '123',
tag_name: 'v1.2.3'
}
}
createArchivesMock.mockImplementation(() => {
throw new Error('Something went wrong')
@@ -219,20 +225,17 @@ describe('run', () => {
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if creating attestation fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
it('fails if getting container registry URL fails', async () => {
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
github.context.eventName = 'release'
process.env.GITHUB_SHA = 'test-sha'
process.env.TOKEN = 'token'
github.context.payload = {
release: {
id: '123',
tag_name: 'v1.2.3'
}
}
createArchivesMock.mockImplementation(() => {
return {
@@ -249,443 +252,128 @@ describe('run', () => {
}
})
uploadOCIImageManifestMock.mockImplementation(() => {
getRepositoryMetadataMock.mockImplementation(() => {
return { repoId: 'test', ownerId: 'test' }
})
getContainerRegistryURLMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if publishing OCI artifact fails', async () => {
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
github.context.eventName = 'release'
process.env.GITHUB_SHA = 'test-sha'
process.env.TOKEN = 'token'
github.context.payload = {
release: {
id: '123',
tag_name: 'v1.2.3'
}
}
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
getRepositoryMetadataMock.mockImplementation(() => {
return { repoId: 'test', ownerId: 'test' }
})
getContainerRegistryURLMock.mockImplementation(() => {
return new URL('https://ghcr.io')
})
publishOCIArtifactMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('uploads the artifact, returns package metadata from GHCR, and cleans up tmp dirs', async () => {
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
github.context.eventName = 'release'
process.env.GITHUB_SHA = 'test-sha'
process.env.TOKEN = 'token'
github.context.payload = {
release: {
id: '123',
tag_name: 'v1.2.3'
}
}
createTempDirMock.mockImplementation(() => '/tmp/test/subdir')
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
getRepositoryMetadataMock.mockImplementation(() => {
return { repoId: 'test', ownerId: 'test' }
})
getContainerRegistryURLMock.mockImplementation(() => {
return new URL('https://ghcr.io')
})
publishOCIArtifactMock.mockImplementation(() => {
return {
packageURL: 'https://ghcr.io/v2/test-org/test-repo:1.2.3',
publishedDigest: 'sha256:my-test-digest'
manifestDigest: 'sha256:my-test-digest'
}
})
generateAttestationMock.mockImplementation(async () => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if uploading attestation to GHCR fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
generateAttestationMock.mockImplementation(async options => {
expect(options).toHaveProperty('skipWrite', true)
return {
attestationID: 'test-attestation-id',
certificate: 'test',
bundle: {
mediaType: bundleMediaType,
verificationMaterial: {
publicKey: {
hint: 'test-hint'
}
},
dsseEnvelope: {
payload: btoa(`{"predicateType": "${predicateType}"}`)
}
}
}
})
uploadOCIImageManifestMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if uploading referrer index manifest to GHCR fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
generateAttestationMock.mockImplementation(async options => {
expect(options).toHaveProperty('skipWrite', true)
return {
attestationID: 'test-attestation-id',
certificate: 'test',
bundle: {
mediaType: bundleMediaType,
verificationMaterial: {
publicKey: {
hint: 'test-hint'
}
},
dsseEnvelope: {
payload: btoa(`{"predicateType": "${predicateType}"}`)
}
}
}
})
uploadOCIImageManifestMock.mockImplementation(() => {
return 'attestation-digest'
})
uploadOCIIndexManifestMock.mockImplementation(() => {
throw new Error('Something went wrong')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if publishing action package version fails', async () => {
resolvePublishActionOptionsMock.mockReturnValue(baseOptions())
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
readFileContentsMock.mockImplementation(() => {
return Buffer.from('test')
})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
generateAttestationMock.mockImplementation(async options => {
expect(options).toHaveProperty('skipWrite', true)
return {
attestationID: 'test-attestation-id',
certificate: 'test',
bundle: {
mediaType: bundleMediaType,
verificationMaterial: {
publicKey: {
hint: 'test-hint'
}
},
dsseEnvelope: {
payload: btoa(`{"predicateType": "${predicateType}"}`)
}
}
}
})
uploadOCIImageManifestMock.mockImplementation(
(repo, manifest, blobs, tag) => {
if (tag === undefined) {
return 'attestation-digest'
} else {
throw new Error('Something went wrong')
}
}
)
uploadOCIIndexManifestMock.mockImplementation(() => {
return 'referrer-index-digest'
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('uploads the artifact, returns package metadata from GHCR, and skips writing attestation in enterprise', async () => {
const options = baseOptions()
options.isEnterprise = true
resolvePublishActionOptionsMock.mockReturnValue(options)
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'zip',
size: 5,
sha256: '123'
},
tarFile: {
path: 'tar',
size: 52,
sha256: '1234'
}
}
})
readFileContentsMock.mockImplementation(filepath => {
return Buffer.from(`${filepath}`)
})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
uploadOCIImageManifestMock.mockImplementation(
(repository, manifest, blobs, tag) => {
expect(repository).toBe(options.nameWithOwner)
expect(tag).toBe('1.2.3')
expect(blobs.size).toBe(3)
expect(blobs.has(ociContainer.emptyConfigSha)).toBeTruthy()
expect(blobs.has('123')).toBeTruthy()
expect(blobs.has('1234')).toBeTruthy()
expect(manifest.mediaType).toBe(ociContainer.imageManifestMediaType)
expect(manifest.layers.length).toBe(2)
expect(manifest.annotations['com.github.package.type']).toBe(
ociContainer.actionPackageAnnotationValue
)
return 'sha256:my-test-digest'
}
)
// Run the action
await main.run()
// Check the results
expect(uploadOCIImageManifestMock).toHaveBeenCalledTimes(1)
// Check outputs
expect(setOutputMock).toHaveBeenCalledTimes(1)
expect(setOutputMock).toHaveBeenCalledWith(
'package-manifest-sha',
'sha256:my-test-digest'
)
})
it('uploads the artifact, returns package metadata from GHCR, and creates an attestation in non-enterprise', async () => {
const options = baseOptions()
resolvePublishActionOptionsMock.mockReturnValue(options)
ensureCorrectShaCheckedOutMock.mockImplementation(() => {})
createTempDirMock.mockImplementation(() => {
return 'stagingOrArchivesDir'
})
stageActionFilesMock.mockImplementation(() => {})
createArchivesMock.mockImplementation(() => {
return {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
readFileContentsMock.mockImplementation(() => {
return Buffer.from('test')
})
calculateManifestDigestMock.mockImplementation(() => {
return 'sha256:my-test-digest'
})
generateAttestationMock.mockImplementation(async opts => {
expect(opts).toHaveProperty('skipWrite', true)
return {
attestationID: 'test-attestation-id',
certificate: 'test',
bundle: {
mediaType: bundleMediaType,
verificationMaterial: {
publicKey: {
hint: 'test-hint'
}
},
dsseEnvelope: {
payload: btoa(`{"predicateType": "${predicateType}"}`)
}
}
}
})
uploadOCIIndexManifestMock.mockImplementation(
async (repository, manifest, tag) => {
expect(repository).toBe(options.nameWithOwner)
expect(tag).toBe('sha256-my-test-digest')
expect(manifest.mediaType).toBe(ociContainer.imageIndexMediaType)
expect(manifest.annotations['com.github.package.type']).toBe(
ociContainer.actionPackageReferrerTagAnnotationValue
)
expect(manifest.manifests.length).toBe(1)
expect(manifest.manifests[0].mediaType).toBe(
ociContainer.imageManifestMediaType
)
expect(manifest.manifests[0].artifactType).toBe(bundleMediaType)
expect(
manifest.manifests[0].annotations['dev.sigstore.bundle.predicateType']
).toBe(predicateType)
expect(
manifest.manifests[0].annotations['com.github.package.type']
).toBe(ociContainer.actionPackageAttestationAnnotationValue)
return 'sha256:referrer-index-digest'
}
)
uploadOCIImageManifestMock.mockImplementation(
(repository, manifest, blobs, tag) => {
let expectedBlobKeys: string[] = []
let expectedAnnotationValue = ''
let expectedTagValue: string | undefined = undefined
let returnValue = ''
let expectedPredicateTypeValue: string | undefined = undefined
let expectedSubjectMediaType: string | undefined = undefined
if (tag === undefined) {
expectedAnnotationValue =
ociContainer.actionPackageAttestationAnnotationValue
const sigStoreLayer = manifest.layers.find(
(layer: ociContainer.Descriptor) =>
layer.mediaType === bundleMediaType
)
expectedPredicateTypeValue = predicateType
expectedBlobKeys = [sigStoreLayer.digest, ociContainer.emptyConfigSha]
expectedSubjectMediaType = ociContainer.imageManifestMediaType
returnValue = 'sha256:attestation-digest'
} else {
expectedAnnotationValue = ociContainer.actionPackageAnnotationValue
expectedTagValue = '1.2.3'
expectedBlobKeys = ['123', '1234', ociContainer.emptyConfigSha]
returnValue = 'sha256:my-test-digest'
}
expect(repository).toBe(options.nameWithOwner)
expect(manifest.mediaType).toBe(ociContainer.imageManifestMediaType)
expect(manifest.annotations['com.github.package.type']).toBe(
expectedAnnotationValue
)
expect(manifest.annotations['dev.sigstore.bundle.predicateType']).toBe(
expectedPredicateTypeValue
)
expect(tag).toBe(expectedTagValue)
expect(manifest.subject?.mediaType).toBe(expectedSubjectMediaType)
expect(manifest.layers.length).toBe(expectedBlobKeys.length - 1) // Minus config layer
expect(blobs.size).toBe(expectedBlobKeys.length)
for (const expectedBlobKey of expectedBlobKeys) {
expect(blobs.has(expectedBlobKey)).toBeTruthy()
}
return returnValue
}
)
// Run the action
await main.run()
// Check the results
expect(uploadOCIImageManifestMock).toHaveBeenCalledTimes(2)
expect(uploadOCIIndexManifestMock).toHaveBeenCalledTimes(1)
expect(publishOCIArtifactMock).toHaveBeenCalledTimes(1)
// Check outputs
expect(setOutputMock).toHaveBeenCalledTimes(3)
expect(setOutputMock).toHaveBeenCalledWith(
'attestation-manifest-sha',
'sha256:attestation-digest'
'package-url',
'https://ghcr.io/v2/test-org/test-repo:1.2.3'
)
expect(setOutputMock).toHaveBeenCalledWith(
'referrer-index-manifest-sha',
'sha256:referrer-index-digest'
'package-manifest',
expect.any(String)
)
expect(setOutputMock).toHaveBeenCalledWith(
@@ -694,21 +382,3 @@ describe('run', () => {
)
})
})
function baseOptions(): cfg.PublishActionOptions {
return {
nameWithOwner: 'nameWithOwner',
workspaceDir: 'workspaceDir',
event: 'release',
apiBaseUrl: 'apiBaseUrl',
runnerTempDir: 'runnerTempDir',
sha: 'sha',
repositoryId: 'repositoryId',
repositoryOwnerId: 'repositoryOwnerId',
isEnterprise: false,
containerRegistryUrl: ghcrUrl,
token: 'token',
ref: 'refs/tags/v1.2.3',
repositoryVisibility: 'public'
}
}
+54 -198
View File
@@ -1,55 +1,53 @@
import {
createActionPackageManifest,
sha256Digest,
sizeInBytes,
OCIImageManifest,
createSigstoreAttestationManifest,
OCIIndexManifest,
createReferrerTagManifest
} from '../src/oci-container'
import { createActionPackageManifest } from '../src/oci-container'
import { FileMetadata } from '../src/fs-helper'
const createdTimestamp = '2021-01-01T00:00:00.000Z'
describe('sha256Digest', () => {
it('calculates the SHA256 digest of the provided manifest', () => {
const { manifest } = testActionPackageManifest()
const digest = sha256Digest(manifest)
const expectedDigest =
'sha256:1af9bf993bf068a51fbb54822471ab7507b07c553bcac09a7c91328740d8ed69'
expect(digest).toEqual(expectedDigest)
})
})
describe('size', () => {
it('returns the total size of the provided manifest', () => {
const { manifest } = testActionPackageManifest()
const size = sizeInBytes(manifest)
expect(size).toBe(991)
})
})
describe('createActionPackageManifest', () => {
it('creates a manifest containing the provided information', () => {
const { manifest, zipFile, tarFile } = testActionPackageManifest()
const date = new Date()
const repo = 'test-org/test-repo'
const sanitizedRepo = 'test-org-test-repo'
const version = '1.2.3'
const repoId = '123'
const ownerId = '456'
const sourceCommit = 'abc'
const tarFile: FileMetadata = {
path: '/test/test/test.tar.gz',
sha256: 'tarSha',
size: 123
}
const zipFile: FileMetadata = {
path: '/test/test/test.zip',
sha256: 'zipSha',
size: 456
}
const expectedJSON = `{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.github.actions.package.v1+json",
"config": {
"mediaType":"application/vnd.oci.empty.v1+json",
"size":2,
"digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
"mediaType": "application/vnd.github.actions.package.config.v1+json",
"size": 0,
"digest": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"annotations": {
"org.opencontainers.image.title":"config.json"
}
},
"layers":[
{
"mediaType":"application/vnd.github.actions.package.config.v1+json",
"size":0,
"digest":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"annotations":{
"org.opencontainers.image.title":"config.json"
}
},
{
"mediaType":"application/vnd.github.actions.package.layer.v1.tar+gzip",
"size":${tarFile.size},
"digest":"${tarFile.sha256}",
"annotations":{
"org.opencontainers.image.title":"test-org-test-repo_1.2.3.tar.gz"
"org.opencontainers.image.title":"${sanitizedRepo}_${version}.tar.gz"
}
},
{
@@ -57,12 +55,12 @@ describe('createActionPackageManifest', () => {
"size":${zipFile.size},
"digest":"${zipFile.sha256}",
"annotations":{
"org.opencontainers.image.title":"test-org-test-repo_1.2.3.zip"
"org.opencontainers.image.title":"${sanitizedRepo}_${version}.zip"
}
}
],
"annotations":{
"org.opencontainers.image.created":"${createdTimestamp}",
"org.opencontainers.image.created":"${date.toISOString()}",
"action.tar.gz.digest":"${tarFile.sha256}",
"action.zip.digest":"${zipFile.sha256}",
"com.github.package.type":"actions_oci_pkg",
@@ -73,168 +71,26 @@ describe('createActionPackageManifest', () => {
}
}`
const manifest = createActionPackageManifest(
{
path: 'test.tar.gz',
size: tarFile.size,
sha256: tarFile.sha256
},
{
path: 'test.zip',
size: zipFile.size,
sha256: zipFile.sha256
},
repo,
repoId,
ownerId,
sourceCommit,
version,
date
)
const manifestJSON = JSON.stringify(manifest)
expect(manifestJSON).toEqual(expectedJSON.replace(/\s/g, ''))
})
it('uses the current time if no created date is provided', () => {
const { manifest } = testActionPackageManifest(false)
expect(
manifest.annotations['org.opencontainers.image.created']
).toBeDefined()
})
})
describe('createSigstoreAttestationManifest', () => {
it('creates a manifest containing the provided information', () => {
const manifest = testAttestationManifest()
const expectedJSON = `{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json",
"config": {
"mediaType": "application/vnd.oci.empty.v1+json",
"size": 2,
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
},
"layers": [
{
"mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json",
"size": 10,
"digest": "bundleDigest"
}
],
"subject": {
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"size": 100,
"digest": "subjectDigest"
},
"annotations": {
"dev.sigstore.bundle.content": "dsse-envelope",
"dev.sigstore.bundle.predicateType": "https://slsa.dev/provenance/v1",
"com.github.package.type": "actions_oci_pkg_attestation",
"org.opencontainers.image.created": "2021-01-01T00:00:00.000Z"
}
}
`
const manifestJSON = JSON.stringify(manifest)
expect(manifestJSON).toEqual(expectedJSON.replace(/\s/g, ''))
})
it('uses the current time if no created date is provided', () => {
const manifest = testAttestationManifest(false)
expect(
manifest.annotations['org.opencontainers.image.created']
).toBeDefined()
})
})
describe('createReferrerIndexManifest', () => {
it('creates a manifest containing the provided information', () => {
const manifest = testReferrerIndexManifest()
const expectedJSON = `
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.dev.sigstore.bundle.v0.3+json",
"size": 100,
"digest": "attDigest",
"annotations": {
"com.github.package.type": "actions_oci_pkg_attestation",
"org.opencontainers.image.created": "2021-01-01T00:00:00.000Z",
"dev.sigstore.bundle.content": "dsse-envelope",
"dev.sigstore.bundle.predicateType": "https://slsa.dev/provenance/v1"
}
}
],
"annotations": {
"com.github.package.type": "actions_oci_pkg_referrer_index",
"org.opencontainers.image.created": "2021-01-01T00:00:00.000Z"
}
}
`
const manifestJSON = JSON.stringify(manifest)
expect(manifestJSON).toEqual(expectedJSON.replace(/\s/g, ''))
})
it('uses the current time if no created date is provided', () => {
const manifest = testReferrerIndexManifest(false)
expect(
manifest.annotations['org.opencontainers.image.created']
).toBeDefined()
})
})
function testActionPackageManifest(setCreated = true): {
manifest: OCIImageManifest
tarFile: FileMetadata
zipFile: FileMetadata
} {
const date = new Date('2021-01-01T00:00:00Z')
const repo = 'test-org/test-repo'
const version = '1.2.3'
const repoId = '123'
const ownerId = '456'
const sourceCommit = 'abc'
const tarFile: FileMetadata = {
path: '/test/test/test.tar.gz',
sha256: 'tarSha',
size: 123
}
const zipFile: FileMetadata = {
path: '/test/test/test.zip',
sha256: 'zipSha',
size: 456
}
const manifest = createActionPackageManifest(
tarFile,
zipFile,
repo,
repoId,
ownerId,
sourceCommit,
version,
setCreated ? date : undefined
)
return {
manifest,
tarFile,
zipFile
}
}
function testAttestationManifest(setCreated = true): OCIImageManifest {
const date = new Date(createdTimestamp)
return createSigstoreAttestationManifest(
10,
'bundleDigest',
'application/vnd.dev.sigstore.bundle.v0.3+json',
'https://slsa.dev/provenance/v1',
100,
'subjectDigest',
setCreated ? date : undefined
)
}
function testReferrerIndexManifest(setCreated = true): OCIIndexManifest {
const date = new Date(createdTimestamp)
return createReferrerTagManifest(
'attDigest',
100,
'application/vnd.dev.sigstore.bundle.v0.3+json',
'https://slsa.dev/provenance/v1',
date,
setCreated ? date : undefined
)
}
+33 -11
View File
@@ -6,19 +6,41 @@ branding:
icon: 'heart'
color: 'red'
inputs:
github-token:
description: 'The GitHub actions token used to authenticate with GitHub APIs'
default: ${{ github.token }}
outputs:
package-url:
description: 'The name of package published to GHCR along with semver. For example, https://ghcr.io/actions/package-action:1.0.1'
value: ${{steps.publish.outputs.package-url}}
package-manifest:
description: 'The package manifest of the published package in JSON format'
value: ${{steps.publish.outputs.package-manifest}}
package-manifest-sha:
description: 'A sha256 hash of the package manifest'
attestation-manifest-sha:
description: 'The sha256 of the provenance attestation uploaded to GHCR. This is not present if the package is not attested, e.g. in enterprise environments.'
referrer-index-manifest-sha:
description: 'The sha256 of the referrer index uploaded to GHCR. This is not present if the package is not attested, e.g. in enterprise environments.'
value: ${{steps.publish.outputs.package-manifest-sha}}
runs:
using: node24
main: dist/index.js
using: 'composite'
steps:
- name: Publish Action Package
run: 'node ${{github.action_path}}/dist/index.js'
shell: bash
id: publish
env:
TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REF: ${{ github.ref }}
GITHUB_SHA: ${{ github.sha }}
- name: Output variables
shell: bash
run: |
echo "package manifest": ${{steps.publish.outputs.package-manifest}}
echo "package manifest sha": ${{steps.publish.outputs.package-manifest-sha}}
echo "package url": ${{steps.publish.outputs.package-url}}
echo "subject name": ${{github.repository}}_${{github.ref}}
- name: Generate Provenance Attestation
uses: github-early-access/generate-build-provenance@main
id: build-provenance
if: endsWith(github.server_url, 'github.com') || endsWith(github.server_url, 'ghe.com')
with:
subject-name: ${{github.repository}}_${{github.ref}}
subject-digest: ${{steps.publish.outputs.package-manifest-sha}}
push-to-registry: false
+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 97.06%"><title>Coverage: 97.06%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">97.06%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">97.06%</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="116" height="20" role="img" aria-label="Coverage: 93.96%"><title>Coverage: 93.96%</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="116" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="63" height="20" fill="#555"/><rect x="63" width="53" height="20" fill="#4c1"/><rect width="116" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="325" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="530">Coverage</text><text x="325" y="140" transform="scale(.1)" fill="#fff" textLength="530">Coverage</text><text aria-hidden="true" x="885" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="430">93.96%</text><text x="885" y="140" transform="scale(.1)" fill="#fff" textLength="430">93.96%</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Generated Vendored
+27
View File
@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getContainerRegistryURL = exports.getRepositoryMetadata = void 0;
async function getRepositoryMetadata(repository, token) {
const response = await fetch(`${process.env.GITHUB_API_URL}/repos/${repository}`);
if (!response.ok) {
throw new Error(`Failed to fetch repository metadata: ${response.statusText}`);
}
const data = await response.json();
// Check that the response contains the expected data
if (!data.id || !data.owner.id) {
throw new Error(`Failed to fetch repository metadata: ${JSON.stringify(data)}`);
}
return { repoId: data.id, ownerId: data.owner.id };
}
exports.getRepositoryMetadata = getRepositoryMetadata;
async function getContainerRegistryURL() {
const response = await fetch(`${process.env.GITHUB_API_URL}/packages/container-registry-url`);
if (!response.ok) {
throw new Error(`Failed to fetch status page: ${response.statusText}`);
}
const data = await response.json();
const registryURL = new URL(data.url);
return registryURL;
}
exports.getContainerRegistryURL = getContainerRegistryURL;
//# sourceMappingURL=api-client.js.map
Generated Vendored
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":";;;AAGO,KAAK,UAAU,qBAAqB,CAAC,UAAkB,EAAE,KAAa;IACzE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAC1B,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,UAAU,UAAU,EAAE,CACpD,CAAA;IAED,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,wCAAwC,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;IAChF,CAAC;IAED,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;IAElC,qDAAqD;IACrD,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,wCAAwC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACjF,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAA;AACpD,CAAC;AAjBH,sDAiBG;AAEM,KAAK,UAAU,uBAAuB;IAC3C,MAAM,QAAQ,GAAG,MAAM,KAAK,CAC1B,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,kCAAkC,CAChE,CAAA;IACD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,gCAAgC,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAA;IACxE,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAA;IAClC,MAAM,WAAW,GAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAC1C,OAAO,WAAW,CAAA;AACpB,CAAC;AAVD,0DAUC"}
Generated Vendored
+146
View File
@@ -0,0 +1,146 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.stageActionFiles = exports.readFileContents = exports.isDirectory = exports.createArchives = exports.removeDir = exports.createTempDir = void 0;
const fs = __importStar(require("fs"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const path = __importStar(require("path"));
const tar = __importStar(require("tar"));
const archiver = __importStar(require("archiver"));
const crypto = __importStar(require("crypto"));
const os = __importStar(require("os"));
function createTempDir() {
const randomDirName = crypto.randomBytes(4).toString('hex');
const tempDir = path.join(os.tmpdir(), randomDirName);
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir);
}
return tempDir;
}
exports.createTempDir = createTempDir;
function removeDir(dir) {
if (fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true });
}
}
exports.removeDir = removeDir;
// Creates both a tar.gz and zip archive of the given directory and returns the paths to both archives (stored in the provided target directory)
// as well as the size/sha256 hash of each file.
async function createArchives(distPath, archiveTargetPath = createTempDir()) {
const zipPath = path.join(archiveTargetPath, `archive.zip`);
const tarPath = path.join(archiveTargetPath, `archive.tar.gz`);
const createZipPromise = new Promise((resolve, reject) => {
const output = fs.createWriteStream(zipPath);
const archive = archiver.create('zip');
output.on('error', (err) => {
reject(err);
});
archive.on('error', (err) => {
reject(err);
});
output.on('close', () => {
resolve(fileMetadata(zipPath));
});
archive.pipe(output);
archive.directory(distPath, false); // TODO: make sure this doesn't include dirs that start with ., same with below
archive.finalize();
});
const createTarPromise = new Promise((resolve, reject) => {
tar
.c({
file: tarPath,
C: distPath, // Change to the source directory for relative paths (TODO)
gzip: true
}, ['.'])
// eslint-disable-next-line github/no-then
.catch(err => {
reject(err);
})
// eslint-disable-next-line github/no-then
.then(() => {
resolve(fileMetadata(tarPath));
});
});
const [zipFile, tarFile] = await Promise.all([
createZipPromise,
createTarPromise
]);
return { zipFile, tarFile };
}
exports.createArchives = createArchives;
function isDirectory(dirPath) {
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory();
}
exports.isDirectory = isDirectory;
function readFileContents(filePath) {
return fs.readFileSync(filePath);
}
exports.readFileContents = readFileContents;
// Copy actions files from sourceDir to targetDir, excluding files and folders not relevant to the action
// Errors if the repo appears to not contain any action files, such as an action.yml file
function stageActionFiles(actionDir, targetDir) {
var actionYmlFound = false;
fs_extra_1.default.copySync(actionDir, targetDir, {
filter: (src, dest) => {
const basename = path.basename(src);
if (basename === 'action.yml' || basename === 'action.yaml') {
actionYmlFound = true;
}
// Filter out hidden folers like .git and .github
return !basename.startsWith('.');
}
});
if (!actionYmlFound) {
throw new Error(`No action.yml or action.yaml file found in source repository`);
}
}
exports.stageActionFiles = stageActionFiles;
// Converts a file path to a filemetadata object by querying the fs for relevant metadata.
async function fileMetadata(filePath) {
const stats = fs.statSync(filePath);
const size = stats.size;
const hash = crypto.createHash('sha256');
const fileStream = fs.createReadStream(filePath);
return new Promise((resolve, reject) => {
fileStream.on('data', data => {
hash.update(data);
});
fileStream.on('end', () => {
const sha256 = hash.digest('hex');
resolve({
path: filePath,
size,
sha256: `sha256:${sha256}`
});
});
fileStream.on('error', err => {
reject(err);
});
});
}
//# sourceMappingURL=fs-helper.js.map
Generated Vendored
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"fs-helper.js","sourceRoot":"","sources":["../src/fs-helper.ts"],"names":[],"mappings":"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"}
Generated Vendored
+154
View File
@@ -0,0 +1,154 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.publishOCIArtifact = void 0;
const core = __importStar(require("@actions/core"));
const axios_1 = __importDefault(require("axios"));
const fsHelper = __importStar(require("./fs-helper"));
const axios_debug_log_1 = __importDefault(require("axios-debug-log"));
// Publish the OCI artifact and return the URL where it can be downloaded
async function publishOCIArtifact(token, registry, repository, semver, zipFile, tarFile, manifest, debugRequests = false) {
if (debugRequests) {
configureRequestDebugLogging();
}
const b64Token = Buffer.from(token).toString('base64');
const checkBlobEndpoint = new URL(`v2/${repository}/blobs/`, registry).toString();
const uploadBlobEndpoint = new URL(`v2/${repository}/blobs/uploads/`, registry).toString();
const manifestEndpoint = new URL(`v2/${repository}/manifests/${semver}`, registry).toString();
core.info(`Creating GHCR package for release with semver:${semver} with path:"${zipFile.path}" and "${tarFile.path}".`);
const layerUploads = manifest.layers.map(async (layer) => {
switch (layer.mediaType) {
case 'application/vnd.github.actions.package.layer.v1.tar+gzip':
return uploadLayer(layer, tarFile, registry, checkBlobEndpoint, uploadBlobEndpoint, b64Token);
case 'application/vnd.github.actions.package.layer.v1.zip':
return uploadLayer(layer, zipFile, registry, checkBlobEndpoint, uploadBlobEndpoint, b64Token);
case 'application/vnd.github.actions.package.config.v1+json':
return uploadLayer(layer, { path: '', size: 0, sha256: layer.digest }, registry, checkBlobEndpoint, uploadBlobEndpoint, b64Token);
default:
throw new Error(`Unknown media type ${layer.mediaType}`);
}
});
await Promise.all(layerUploads);
const digest = await uploadManifest(JSON.stringify(manifest), manifestEndpoint, b64Token);
return { packageURL: new URL(`${repository}:${semver}`, registry), manifestDigest: digest };
}
exports.publishOCIArtifact = publishOCIArtifact;
async function uploadLayer(layer, file, registryURL, checkBlobEndpoint, uploadBlobEndpoint, b64Token) {
const checkExistsResponse = await axios_1.default.head(checkBlobEndpoint + layer.digest, {
headers: {
Authorization: `Bearer ${b64Token}`
},
validateStatus: () => {
return true; // Allow non 2xx responses
}
});
if (checkExistsResponse.status === 200 ||
checkExistsResponse.status === 202) {
core.info(`Layer ${layer.digest} already exists. Skipping upload.`);
return;
}
if (checkExistsResponse.status !== 404) {
throw new Error(`Unexpected response from blob check for layer ${layer.digest}: ${checkExistsResponse.status} ${checkExistsResponse.statusText}`);
}
core.info(`Uploading layer ${layer.digest}.`);
const initiateUploadResponse = await axios_1.default.post(uploadBlobEndpoint, layer, {
headers: {
Authorization: `Bearer ${b64Token}`
},
validateStatus: () => {
return true; // Allow non 2xx responses
}
});
if (initiateUploadResponse.status !== 202) {
core.error(`Unexpected response from upload post ${uploadBlobEndpoint}: ${initiateUploadResponse.status}`);
throw new Error(`Unexpected response from POST upload ${initiateUploadResponse.status}`);
}
const locationResponseHeader = initiateUploadResponse.headers['location'];
if (locationResponseHeader === undefined) {
throw new Error(`No location header in response from upload post ${uploadBlobEndpoint} for layer ${layer.digest}`);
}
const pathname = `${locationResponseHeader}?digest=${layer.digest}`;
const uploadBlobUrl = new URL(pathname, registryURL).toString();
// TODO: must we handle the empty config layer? Maybe we can just skip calling this at all
let data;
if (file.size === 0) {
data = Buffer.alloc(0);
}
else {
data = fsHelper.readFileContents(file.path);
}
const putResponse = await axios_1.default.put(uploadBlobUrl, data, {
headers: {
Authorization: `Bearer ${b64Token}`,
'Content-Type': 'application/octet-stream',
'Accept-Encoding': 'gzip', // TODO: What about for the config layer?
'Content-Length': layer.size.toString()
},
validateStatus: () => {
return true; // Allow non 2xx responses
}
});
if (putResponse.status !== 201) {
throw new Error(`Unexpected response from PUT upload ${putResponse.status} for layer ${layer.digest}`);
}
}
// Uploads the manifest and returns the digest returned by GHCR
async function uploadManifest(manifestJSON, manifestEndpoint, b64Token) {
core.info(`Uploading manifest to ${manifestEndpoint}.`);
const putResponse = await axios_1.default.put(manifestEndpoint, manifestJSON, {
headers: {
Authorization: `Bearer ${b64Token}`,
'Content-Type': 'application/vnd.oci.image.manifest.v1+json'
},
validateStatus: () => {
return true; // Allow non 2xx responses
}
});
if (putResponse.status !== 201) {
throw new Error(`Unexpected response from PUT manifest ${putResponse.status}`);
}
const digestResponseHeader = putResponse.headers['Docker-Content-Digest'];
if (digestResponseHeader === undefined) {
throw new Error(`No digest header in response from PUT manifest ${manifestEndpoint}`);
}
return digestResponseHeader;
}
function configureRequestDebugLogging() {
(0, axios_debug_log_1.default)({
request: (debug, config) => {
core.debug(`Request with ${config}`);
},
response: (debug, response) => {
core.debug(`Response with ${response}`);
},
error: (debug, error) => {
core.debug(`Error with ${error}`);
}
});
}
//# sourceMappingURL=ghcr-client.js.map
Generated Vendored
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"ghcr-client.js","sourceRoot":"","sources":["../src/ghcr-client.ts"],"names":[],"mappings":"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"}
Generated Vendored
+19409 -68669
View File
File diff suppressed because one or more lines are too long
Generated Vendored
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;AAAA;;GAEG;AACH,iCAA4B;AAC5B,wDAA+B;AAE/B,MAAM,IAAI,GAAG,IAAA,kBAAQ,EAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAA;AACxD,mEAAmE;AACnE,IAAA,UAAG,EAAC,IAAI,CAAC,CAAA"}
Generated Vendored
+327 -1883
View File
File diff suppressed because it is too large Load Diff
Generated Vendored
+115
View File
@@ -0,0 +1,115 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.run = void 0;
const core = __importStar(require("@actions/core"));
const github = __importStar(require("@actions/github"));
const fsHelper = __importStar(require("./fs-helper"));
const ociContainer = __importStar(require("./oci-container"));
const ghcr = __importStar(require("./ghcr-client"));
const api = __importStar(require("./api-client"));
const semver_1 = __importDefault(require("semver"));
/**
* The main function for the action.
* @returns {Promise<void>} Resolves when the action is complete.
*/
async function run(pathInput) {
const tmpDirs = [];
try {
const repository = process.env.GITHUB_REPOSITORY || '';
if (repository === '') {
core.setFailed(`Could not find Repository.`);
return;
}
const token = process.env.TOKEN || '';
const sourceCommit = process.env.GITHUB_SHA || '';
if (token === '') {
core.setFailed(`Could not find source commit.`);
return;
}
if (sourceCommit === '') {
core.setFailed(`Could not find source commit.`);
return;
}
const semanticVersion = parseSourceSemanticVersion();
// Create a temporary directory to stage files for packaging in archives
const stagedActionFilesDir = fsHelper.createTempDir();
tmpDirs.push(stagedActionFilesDir);
fsHelper.stageActionFiles(".", stagedActionFilesDir);
// Create a temporary directory to store the archives
const archiveDir = fsHelper.createTempDir();
tmpDirs.push(archiveDir);
const archives = await fsHelper.createArchives(stagedActionFilesDir, archiveDir);
const { repoId, ownerId } = await api.getRepositoryMetadata(repository, token);
const manifest = ociContainer.createActionPackageManifest(archives.tarFile, archives.zipFile, repository, repoId, ownerId, sourceCommit, semanticVersion.raw, new Date());
const containerRegistryURL = await api.getContainerRegistryURL();
console.log(`Container registry URL: ${containerRegistryURL}`);
const { packageURL, manifestDigest } = await ghcr.publishOCIArtifact(token, containerRegistryURL, repository, semanticVersion.raw, archives.zipFile, archives.tarFile, manifest, true);
core.setOutput('package-url', packageURL.toString());
core.setOutput('package-manifest', JSON.stringify(manifest));
core.setOutput('package-manifest-sha', `sha256:${manifestDigest}`);
}
catch (error) {
// Fail the workflow run if an error occurs
if (error instanceof Error)
core.setFailed(error.message);
}
finally {
// Clean up any temporary directories that exist
for (const tmpDir of tmpDirs) {
if (tmpDir !== '') {
fsHelper.removeDir(tmpDir);
}
}
}
}
exports.run = run;
// This action can be triggered by release events or tag push events.
// In each case, the source event should produce a Semantic Version compliant tag representing the code to be packaged.
function parseSourceSemanticVersion() {
const event = github.context.eventName;
var semverTag = '';
// Grab the raw tag
if (event === 'release')
semverTag = github.context.payload.release.tag_name;
else if (event === 'push' && github.context.ref.startsWith('refs/tags/')) {
semverTag = github.context.ref.replace(/^refs\/tags\//, '');
}
else {
throw new Error(`This action can only be triggered by release events or tag push events.`);
}
if (semverTag === '') {
throw new Error(`Could not find a Semantic Version tag in the event payload.`);
}
const semanticVersion = semver_1.default.parse(semverTag.replace(/^v/, ''));
if (!semanticVersion) {
throw new Error(`${semverTag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`);
}
return semanticVersion;
}
//# sourceMappingURL=main.js.map
Generated Vendored
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
Generated Vendored
+69
View File
@@ -0,0 +1,69 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createActionPackageManifest = void 0;
// Given a name and archive metadata, creates a manifest in the format expected by GHCR for an Actions Package.
function createActionPackageManifest(tarFile, zipFile, repository, repoId, ownerId, sourceCommit, version, created) {
const configLayer = createConfigLayer();
const sanitizedRepo = sanitizeRepository(repository);
const tarLayer = createTarLayer(tarFile, sanitizedRepo, version);
const zipLayer = createZipLayer(zipFile, sanitizedRepo, version);
const manifest = {
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
artifactType: 'application/vnd.github.actions.package.v1+json',
config: configLayer,
layers: [configLayer, tarLayer, zipLayer],
annotations: {
'org.opencontainers.image.created': created.toISOString(),
'action.tar.gz.digest': tarFile.sha256,
'action.zip.digest': zipFile.sha256,
'com.github.package.type': 'actions_oci_pkg',
'com.github.package.version': version,
'com.github.source.repo.id': repoId,
'com.github.source.repo.owner.id': ownerId,
'com.github.source.commit': sourceCommit,
}
};
return manifest;
}
exports.createActionPackageManifest = createActionPackageManifest;
// TODO: is this ok hardcoded?
function createConfigLayer() {
const configLayer = {
mediaType: 'application/vnd.github.actions.package.config.v1+json',
size: 0,
digest: 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
annotations: {
'org.opencontainers.image.title': 'config.json'
}
};
return configLayer;
}
function createZipLayer(zipFile, repository, version) {
const zipLayer = {
mediaType: 'application/vnd.github.actions.package.layer.v1.zip',
size: zipFile.size,
digest: zipFile.sha256,
annotations: {
'org.opencontainers.image.title': `${repository}_${version}.zip`
}
};
return zipLayer;
}
function createTarLayer(tarFile, repository, version) {
const tarLayer = {
mediaType: 'application/vnd.github.actions.package.layer.v1.tar+gzip',
size: tarFile.size,
digest: tarFile.sha256,
annotations: {
'org.opencontainers.image.title': `${repository}_${version}.tar.gz`
}
};
return tarLayer;
}
// Remove slashes so we can use the repository in a filename
// repository usually includes the namespace too, e.g. my-org/my-repo
function sanitizeRepository(repository) {
return repository.replace('/', '-');
}
//# sourceMappingURL=oci-container.js.map
Generated Vendored
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"oci-container.js","sourceRoot":"","sources":["../src/oci-container.ts"],"names":[],"mappings":";;;AAkBA,+GAA+G;AAC/G,SAAgB,2BAA2B,CACzC,OAAqB,EACrB,OAAqB,EACrB,UAAkB,EAClB,MAAc,EACd,OAAe,EACf,YAAoB,EACpB,OAAe,EACf,OAAa;IAEb,MAAM,WAAW,GAAG,iBAAiB,EAAE,CAAA;IACvC,MAAM,aAAa,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAA;IACpD,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,CAAA;IAChE,MAAM,QAAQ,GAAG,cAAc,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,CAAA;IAEhE,MAAM,QAAQ,GAAa;QACzB,aAAa,EAAE,CAAC;QAChB,SAAS,EAAE,4CAA4C;QACvD,YAAY,EAAE,gDAAgD;QAC9D,MAAM,EAAE,WAAW;QACnB,MAAM,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,QAAQ,CAAC;QACzC,WAAW,EAAE;YACX,kCAAkC,EAAE,OAAO,CAAC,WAAW,EAAE;YACzD,sBAAsB,EAAE,OAAO,CAAC,MAAM;YACtC,mBAAmB,EAAE,OAAO,CAAC,MAAM;YACnC,yBAAyB,EAAE,iBAAiB;YAC5C,4BAA4B,EAAE,OAAO;YACrC,2BAA2B,EAAE,MAAM;YACnC,iCAAiC,EAAE,OAAO;YAC1C,0BAA0B,EAAE,YAAY;SACzC;KACF,CAAA;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAlCD,kEAkCC;AAED,8BAA8B;AAC9B,SAAS,iBAAiB;IACxB,MAAM,WAAW,GAAU;QACzB,SAAS,EAAE,uDAAuD;QAClE,IAAI,EAAE,CAAC;QACP,MAAM,EACJ,yEAAyE;QAC3E,WAAW,EAAE;YACX,gCAAgC,EAAE,aAAa;SAChD;KACF,CAAA;IAED,OAAO,WAAW,CAAA;AACpB,CAAC;AAED,SAAS,cAAc,CACrB,OAAqB,EACrB,UAAkB,EAClB,OAAe;IAEf,MAAM,QAAQ,GAAU;QACtB,SAAS,EAAE,qDAAqD;QAChE,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,WAAW,EAAE;YACX,gCAAgC,EAAE,GAAG,UAAU,IAAI,OAAO,MAAM;SACjE;KACF,CAAA;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,SAAS,cAAc,CACrB,OAAqB,EACrB,UAAkB,EAClB,OAAe;IAEf,MAAM,QAAQ,GAAU;QACtB,SAAS,EAAE,0DAA0D;QACrE,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,WAAW,EAAE;YACX,gCAAgC,EAAE,GAAG,UAAU,IAAI,OAAO,SAAS;SACpE;KACF,CAAA;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,4DAA4D;AAC5D,qEAAqE;AACrE,SAAS,kBAAkB,CAAC,UAAkB;IAC5C,OAAO,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;AACrC,CAAC"}
+449 -1388
View File
File diff suppressed because it is too large Load Diff
+17 -19
View File
@@ -21,7 +21,7 @@
".": "./dist/index.js"
},
"engines": {
"node": ">=24"
"node": ">=20"
},
"scripts": {
"bundle": "npm run format:write && npm run package",
@@ -30,10 +30,9 @@
"format:write": "prettier --write **/*.ts",
"format:check": "prettier --check **/*.ts",
"lint": "npx eslint . -c ./.github/linters/.eslintrc.yml",
"package": "rm -rf dist && ncc build src/index.ts --license licenses.txt",
"package": "ncc build src/index.ts --license licenses.txt",
"package:watch": "npm run package -- --watch",
"test": "jest",
"start": "node dist/index.js",
"all": "npm run format:write && npm run lint && npm run test && npm run coverage && npm run package"
},
"license": "MIT",
@@ -67,38 +66,37 @@
]
},
"dependencies": {
"@actions/attest": "^1.4.0",
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@sigstore/oci": "^0.3.7",
"@types/fs-extra": "^11.0.4",
"archiver": "^7.0.1",
"archiver": "^6.0.1",
"axios": "^1.6.2",
"axios-debug-log": "^1.0.0",
"fs-extra": "^11.2.0",
"simple-git": "^3.22.0",
"tar": "^7.4.3"
"tar": "^6.2.0"
},
"devDependencies": {
"@types/archiver": "^6.0.2",
"@types/axios": "^0.14.0",
"@types/jest": "^29.5.12",
"@types/jest": "^29.5.11",
"@types/minimist": "^1.2.5",
"@types/node": "^24.1.0",
"@types/tar": "^6.1.13",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@types/node": "^20.11.13",
"@types/tar": "^6.1.11",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.19.0",
"@vercel/ncc": "^0.38.1",
"eslint": "^8.57.0",
"eslint": "^8.54.0",
"eslint-plugin-github": "^4.10.1",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-jsonc": "^2.13.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"js-yaml": "^4.1.0",
"make-coverage-badge": "^1.2.0",
"prettier": "^3.3.3",
"prettier": "^3.2.4",
"prettier-eslint": "^16.3.0",
"ts-jest": "^29.2.3",
"typescript": "^5.5.4"
"ts-jest": "^29.1.1",
"typescript": "^5.3.3"
}
}
+5 -5
View File
@@ -22,8 +22,8 @@ latest_tag=$(git describe --tags "$(git rev-list --tags --max-count=1)")
# if the latest_tag is empty, then there are no tags - let the user know
if [[ -z "$latest_tag" ]]; then
echo -e "No tags found (yet) - continue to create your first tag and push it"
latest_tag="[unknown]"
echo -e "No tags found (yet) - continue to create your first tag and push it"
latest_tag="[unknown]"
fi
echo -e "The latest release tag is: ${BLUE}${latest_tag}${OFF}"
@@ -31,10 +31,10 @@ read -r -p 'New Release Tag (vX.X.X format): ' new_tag
tag_regex='v[0-9]+\.[0-9]+\.[0-9]+$'
if echo "$new_tag" | grep -q -E "$tag_regex"; then
echo -e "Tag: ${BLUE}$new_tag${OFF} is valid"
echo -e "Tag: ${BLUE}$new_tag${OFF} is valid"
else
echo -e "Tag: ${BLUE}$new_tag${OFF} is ${RED}not valid${OFF} (must be in vX.X.X format)"
exit 1
echo -e "Tag: ${BLUE}$new_tag${OFF} is ${RED}not valid${OFF} (must be in vX.X.X format)"
exit 1
fi
git tag -a "$new_tag" -m "$new_tag Release"
+13 -25
View File
@@ -1,15 +1,17 @@
export async function getRepositoryMetadata(
githubAPIURL: string,
repository: string,
token: string
): Promise<{ repoId: string; ownerId: string; visibility: string }> {
const response = await fetch(`${githubAPIURL}/repos/${repository}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github.v3+json'
): Promise<{ repoId: string; ownerId: string }> {
const response = await fetch(
`${process.env.GITHUB_API_URL}/repos/${repository}`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github.v3+json'
}
}
})
)
if (!response.ok) {
throw new Error(
@@ -26,26 +28,12 @@ export async function getRepositoryMetadata(
)
}
return {
repoId: String(data.id),
ownerId: String(data.owner.id),
visibility: String(data.visibility)
}
return { repoId: String(data.id), ownerId: String(data.owner.id) }
}
export async function getContainerRegistryURL(
githubAPIURL: string,
token: string
): Promise<URL> {
export async function getContainerRegistryURL(): Promise<URL> {
const response = await fetch(
`${githubAPIURL}/packages/container-registry-url`,
{
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github.v3+json'
}
}
`${process.env.GITHUB_API_URL}/packages/container-registry-url`
)
if (!response.ok) {
throw new Error(
-157
View File
@@ -1,157 +0,0 @@
import * as apiClient from './api-client'
import * as core from '@actions/core'
import * as github from '@actions/github'
// All the environment options required to run the action
export interface PublishActionOptions {
// The name of the repository in the format owner/repo
nameWithOwner: string
// The GitHub token to use for API requests
token: string
// The base URL for the GitHub API
apiBaseUrl: string
// The base URL for the GitHub Container Registry
containerRegistryUrl: URL
// The directory where the action is running, used for git operations
workspaceDir: string
// The directory set up to be used for temporary files by the runner
runnerTempDir: string
// Whether this action is running in enterprise, determined from the github URL
isEnterprise: boolean
// The visibility of the action repository ("public", "internal" or "private")
repositoryVisibility: string
// The repository ID of the action repository
repositoryId: string
// The owner ID of the action repository
repositoryOwnerId: string
// The event that triggered the action
event: string
// The ref that triggered the action, associated with the event
ref: string
// The commit SHA associated with the ref that triggered the action
sha: string
}
export async function resolvePublishActionOptions(): Promise<PublishActionOptions> {
// Action Inputs
const token: string = core.getInput('github-token') || ''
if (token === '') {
throw new Error(`Could not find GITHUB_TOKEN.`)
}
// Context Inputs
const event: string = github.context.eventName
if (event === '') {
throw new Error(`Could not find event name.`)
}
const ref: string = github.context.ref || ''
if (ref === '') {
throw new Error(`Could not find GITHUB_REF.`)
}
const nameWithOwner: string =
github.context.payload.repository?.full_name || ''
if (nameWithOwner === '') {
throw new Error(`Could not find Repository.`)
}
const sha: string = github.context.sha || ''
if (sha === '') {
throw new Error(`Could not find GITHUB_SHA.`)
}
const apiBaseUrl: string = github.context.apiUrl || ''
if (apiBaseUrl === '') {
throw new Error(`Could not find GITHUB_API_URL.`)
}
const githubServerUrl = github.context.serverUrl || ''
if (githubServerUrl === '') {
throw new Error(`Could not find GITHUB_SERVER_URL.`)
}
// Environment Variables
const workspaceDir: string = process.env.GITHUB_WORKSPACE || ''
if (workspaceDir === '') {
throw new Error(`Could not find GITHUB_WORKSPACE.`)
}
const runnerTempDir: string = process.env.RUNNER_TEMP || ''
if (runnerTempDir === '') {
throw new Error(`Could not find RUNNER_TEMP.`)
}
const repositoryId = process.env.GITHUB_REPOSITORY_ID || ''
if (repositoryId === '') {
throw new Error(`Could not find GITHUB_REPOSITORY_ID.`)
}
const repositoryOwnerId = process.env.GITHUB_REPOSITORY_OWNER_ID || ''
if (repositoryOwnerId === '') {
throw new Error(`Could not find GITHUB_REPOSITORY_OWNER_ID.`)
}
// Required Values fetched from the GitHub API
const containerRegistryUrl: URL = await apiClient.getContainerRegistryURL(
apiBaseUrl,
token
)
const isEnterprise =
!githubServerUrl.includes('https://github.com') &&
!githubServerUrl.endsWith('.ghe.com')
const repoMetadata = await apiClient.getRepositoryMetadata(
apiBaseUrl,
nameWithOwner,
token
)
if (repoMetadata.visibility === '') {
throw new Error(`Could not find repository visibility.`)
}
if (repoMetadata.repoId !== repositoryId) {
throw new Error(`Repository ID mismatch.`)
}
if (repoMetadata.ownerId !== repositoryOwnerId) {
throw new Error(`Repository Owner ID mismatch.`)
}
const repositoryVisibility = repoMetadata.visibility
return {
event,
ref,
workspaceDir,
nameWithOwner,
token,
apiBaseUrl,
runnerTempDir,
sha,
containerRegistryUrl,
isEnterprise,
repositoryVisibility,
repositoryId,
repositoryOwnerId
}
}
// When printing this object, we want to hide some of them from being displayed
const internalKeys = new Set<string>([
'token',
'runnerTempDir',
'repositoryId',
'repositoryOwnerId'
])
export function serializeOptions(options: PublishActionOptions): string {
return JSON.stringify(
options,
(key: string, value: unknown) =>
internalKeys.has(key) ? undefined : value,
2 // 2 spaces for pretty-printing
)
}
+17 -65
View File
@@ -4,7 +4,6 @@ import * as path from 'path'
import * as tar from 'tar'
import * as archiver from 'archiver'
import * as crypto from 'crypto'
import * as simpleGit from 'simple-git'
export interface FileMetadata {
path: string
@@ -12,12 +11,12 @@ export interface FileMetadata {
sha256: string
}
// Simple convenience around creating subdirectories in the same base temporary directory
export function createTempDir(tmpDirPath: string, subDirName: string): string {
const tempDir = path.join(tmpDirPath, subDirName)
export function createTempDir(subDirName: string): string {
const runnerTempDir: string = process.env.RUNNER_TEMP || ''
const tempDir = path.join(runnerTempDir, subDirName)
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true })
fs.mkdirSync(tempDir)
}
return tempDir
@@ -49,7 +48,7 @@ export async function createArchives(
})
archive.pipe(output)
archive.directory(distPath, 'action')
archive.directory(distPath, false) // TODO: make sure this doesn't include dirs that start with ., same with below
archive.finalize()
})
@@ -58,9 +57,8 @@ export async function createArchives(
.c(
{
file: tarPath,
C: distPath,
gzip: true,
prefix: 'action'
C: distPath, // Change to the source directory for relative paths (TODO)
gzip: true
},
['.']
)
@@ -90,73 +88,27 @@ export function readFileContents(filePath: string): Buffer {
return fs.readFileSync(filePath)
}
// Copy actions files from sourceDir to targetDir, excluding the .git folder.
// Copy actions files from sourceDir to targetDir, excluding files and folders not relevant to the action
// Errors if the repo appears to not contain any action files, such as an action.yml file
export function stageActionFiles(actionDir: string, targetDir: string): void {
let actionYmlFound = false
fsExtra.copySync(actionDir, targetDir, {
filter: (src: string) => {
const basename = path.basename(src)
// Filter out the .git folder.
if (basename === '.git') {
return false
if (basename === 'action.yml' || basename === 'action.yaml') {
actionYmlFound = true
}
return true
// Filter out hidden folers like .git and .github
return basename === '.' || !basename.startsWith('.')
}
})
}
// Ensure the correct SHA is checked out for the tag by inspecting the git metadata in the workspace
// and comparing it to the information actions provided us.
// Provided ref should be in format refs/tags/<tagname>.
export async function ensureTagAndRefCheckedOut(
tagRef: string,
expectedSha: string,
gitDir: string
): Promise<void> {
if (!tagRef.startsWith('refs/tags/')) {
throw new Error(`Tag ref provided is not in expected format.`)
}
const git: simpleGit.SimpleGit = simpleGit.simpleGit(gitDir)
let tagCommitSha: string
try {
tagCommitSha = await git.raw(['rev-parse', '--verify', tagRef])
} catch (err) {
throw new Error(`Error retrieving commit associated with tag: ${err}`)
}
if (tagCommitSha.trim() !== expectedSha) {
if (!actionYmlFound) {
throw new Error(
`The commit associated with the tag ${tagRef} does not match the SHA of the commit provided by the actions context.`
)
}
let currentlyCheckedOutSha: string
try {
currentlyCheckedOutSha = await git.revparse(['HEAD'])
} catch (err) {
throw new Error(`Error validating checked out tag and ref: ${err}`)
}
if (currentlyCheckedOutSha.trim() !== expectedSha) {
throw new Error(
`The expected commit associated with the tag ${tagRef} is not checked out.`
)
}
// Call git status to check for any changes in the working directory
// This version of this action only supports uploading actions packages
// which contain the same content as the repository at the appropriate source commit.
let status: simpleGit.StatusResult
try {
status = await git.status()
} catch (err) {
throw new Error(`Error checking git status: ${err}`)
}
if (!status.isClean()) {
throw new Error(
`The working directory has uncommitted changes. Uploading modified code from the checked out repository is not supported by this action.`
`No action.yml or action.yaml file found in source repository`
)
}
}
+218 -349
View File
@@ -1,358 +1,227 @@
import * as core from '@actions/core'
import { FileMetadata } from './fs-helper'
import * as ociContainer from './oci-container'
import axios from 'axios'
import * as fsHelper from './fs-helper'
import axiosDebugLog from 'axios-debug-log'
const defaultRetries = 5
const defaultBackoff = 1000
const retryableStatusCodes = [408, 429, 500, 502, 503, 504]
export interface RetryOptions {
retries: number
backoff: number
}
export class Client {
private _b64Token: string
private _registry: URL
private _retryOptions: RetryOptions
constructor(
token: string,
registry: URL,
retryOptions: RetryOptions = {
retries: defaultRetries,
backoff: defaultBackoff
}
) {
this._b64Token = Buffer.from(token).toString('base64')
this._registry = registry
this._retryOptions = retryOptions
// Publish the OCI artifact and return the URL where it can be downloaded
export async function publishOCIArtifact(
token: string,
registry: URL,
repository: string,
semver: string,
zipFile: FileMetadata,
tarFile: FileMetadata,
manifest: ociContainer.Manifest,
debugRequests = false
): Promise<{ packageURL: URL; manifestDigest: string }> {
if (debugRequests) {
configureRequestDebugLogging()
}
async uploadOCIImageManifest(
repository: string,
manifest: ociContainer.OCIImageManifest,
blobs: Map<string, Buffer>,
tag?: string
): Promise<string> {
const manifestSHA = ociContainer.sha256Digest(manifest)
const b64Token = Buffer.from(token).toString('base64')
if (tag) {
core.info(
`Uploading manifest ${manifestSHA} with tag ${tag} to ${repository}.`
)
} else {
core.info(`Uploading manifest ${manifestSHA} to ${repository}.`)
}
const checkBlobEndpoint = new URL(
`v2/${repository}/blobs/`,
registry
).toString()
const uploadBlobEndpoint = new URL(
`v2/${repository}/blobs/uploads/`,
registry
).toString()
const manifestEndpoint = new URL(
`v2/${repository}/manifests/${semver}`,
registry
).toString()
// We must also upload the config layer
const layersToUpload = manifest.layers.concat(manifest.config)
const layerUploads: Promise<void>[] = layersToUpload.map(async layer => {
const blob = blobs.get(layer.digest)
if (!blob) {
throw new Error(`Blob for layer ${layer.digest} not found`)
}
return this.uploadLayer(layer, blob, repository)
})
await Promise.all(layerUploads)
const publishedDigest = await this.uploadManifest(
JSON.stringify(manifest),
manifest.mediaType,
repository,
tag || manifestSHA
)
if (publishedDigest !== manifestSHA) {
throw new Error(
`Digest mismatch. Expected ${manifestSHA}, got ${publishedDigest}.`
)
}
return manifestSHA
}
async uploadOCIIndexManifest(
repository: string,
manifest: ociContainer.OCIIndexManifest,
tag: string
): Promise<string> {
const manifestSHA = ociContainer.sha256Digest(manifest)
core.info(
`Uploading index manifest ${manifestSHA} with tag ${tag} to ${repository}.`
)
const publishedDigest = await this.uploadManifest(
JSON.stringify(manifest),
manifest.mediaType,
repository,
tag
)
if (publishedDigest !== manifestSHA) {
throw new Error(
`Digest mismatch. Expected ${manifestSHA}, got ${publishedDigest}.`
)
}
return manifestSHA
}
private async uploadLayer(
layer: ociContainer.Descriptor,
data: Buffer,
repository: string
): Promise<void> {
const checkExistsResponse = await this.fetchWithRetries(
this.checkBlobEndpoint(repository, layer.digest),
{
method: 'HEAD',
headers: {
Authorization: `Bearer ${this._b64Token}`
}
}
)
if (
checkExistsResponse.status === 200 ||
checkExistsResponse.status === 202
) {
core.info(`Layer ${layer.digest} already exists. Skipping upload.`)
return
}
if (checkExistsResponse.status !== 404) {
throw new Error(
await errorMessageForFailedRequest(
`check blob (${layer.digest}) exists`,
checkExistsResponse
)
)
}
core.info(`Uploading layer ${layer.digest}.`)
const initiateUploadBlobURL = this.uploadBlobEndpoint(repository)
const initiateUploadResponse = await this.fetchWithRetries(
initiateUploadBlobURL,
{
method: 'POST',
headers: {
Authorization: `Bearer ${this._b64Token}`
},
body: JSON.stringify(layer)
}
)
if (initiateUploadResponse.status !== 202) {
throw new Error(
await errorMessageForFailedRequest(
`initiate layer upload`,
initiateUploadResponse
)
)
}
const locationResponseHeader =
initiateUploadResponse.headers.get('location')
if (locationResponseHeader === undefined) {
throw new Error(
`No location header in response from upload post ${initiateUploadBlobURL} for layer ${layer.digest}`
)
}
const pathname = `${locationResponseHeader}?digest=${layer.digest}`
const uploadBlobUrl = new URL(pathname, this._registry).toString()
const putResponse = await this.fetchWithRetries(uploadBlobUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${this._b64Token}`,
'Content-Type': 'application/octet-stream',
'Accept-Encoding': 'gzip',
'Content-Length': layer.size.toString()
},
body: data
})
if (putResponse.status !== 201) {
throw new Error(
await errorMessageForFailedRequest(
`layer (${layer.digest}) upload`,
putResponse
)
)
}
}
// Uploads the manifest and returns the digest returned by GHCR
private async uploadManifest(
manifestJSON: string,
manifestMediaType: string,
repository: string,
version: string
): Promise<string> {
const manifestUrl = this.manifestEndpoint(repository, version)
core.info(`Uploading manifest to ${manifestUrl}.`)
const putResponse = await this.fetchWithRetries(manifestUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${this._b64Token}`,
'Content-Type': manifestMediaType
},
body: manifestJSON
})
if (putResponse.status !== 201) {
throw new Error(
await errorMessageForFailedRequest(`manifest upload`, putResponse)
)
}
const digestResponseHeader =
putResponse.headers.get('docker-content-digest') || ''
return digestResponseHeader
}
private checkBlobEndpoint(repository: string, digest: string): string {
return new URL(
`v2/${repository}/blobs/${digest}`,
this._registry
).toString()
}
private uploadBlobEndpoint(repository: string): string {
return new URL(`v2/${repository}/blobs/uploads/`, this._registry).toString()
}
private manifestEndpoint(repository: string, version: string): string {
return new URL(
`v2/${repository}/manifests/${version}`,
this._registry
).toString()
}
private async fetchWithDebug(
url: string,
config: RequestInit = {}
): Promise<Response> {
core.debug(`Request from ${url} with config: ${JSON.stringify(config)}`)
try {
const response = await fetch(url, config)
core.debug(`Response with ${JSON.stringify(response)}`)
return response
} catch (error) {
core.debug(`Error with ${error}`)
throw error
}
}
private async fetchWithRetries(
url: string,
config: RequestInit = {}
): Promise<Response> {
const allowedAttempts = this._retryOptions.retries + 1 // Initial attempt + retries
for (
let attemptNumber = 1;
attemptNumber <= allowedAttempts;
attemptNumber++
) {
let backoff = this._retryOptions.backoff
try {
const response = await this.fetchWithDebug(url, config)
// If this is the last attempt, just return it
if (attemptNumber === allowedAttempts) {
return response
}
// If the response is retryable, backoff and retry
if (retryableStatusCodes.includes(response.status)) {
const retryAfter = response.headers.get('retry-after')
if (retryAfter) {
backoff = parseInt(retryAfter) * 1000 // convert to ms
}
core.info(
`Received ${response.status} response. Retrying after ${backoff}ms...`
)
await new Promise(resolve => setTimeout(resolve, backoff))
continue
}
// Otherwise, just return the response
return response
} catch (error) {
// If this is the last attempt, throw the error
if (attemptNumber === allowedAttempts) {
throw error
}
core.info(`Encountered error: ${error}. Retrying after ${backoff}ms...`)
await new Promise(resolve => setTimeout(resolve, backoff))
}
}
// Should be unreachable
throw new Error('Exhausted retries without a successful response')
}
}
interface ghcrError {
code: string
message: string
}
// Generate an error message for a failed HTTP request
async function errorMessageForFailedRequest(
requestDescription: string,
response: Response
): Promise<string> {
const bodyText = await response.text()
// Try to parse the body as JSON and extract the expected fields returned from GHCR
// Expected format: { "errors": [{"code": "BAD_REQUEST", "message": "Something went wrong."}] }
// If the body does not match the expected format, just return the whole response body
let errorString = `Response Body: ${bodyText}.`
try {
const body = JSON.parse(bodyText)
const errors = body.errors
if (
Array.isArray(errors) &&
errors.length > 0 &&
errors.every(isGHCRError)
) {
const errorMessages = errors.map((error: ghcrError) => {
return `${error.code} - ${error.message}`
})
errorString = `Errors: ${errorMessages.join(', ')}`
}
} catch (error) {
// Ignore error
}
return `Unexpected ${response.status} ${response.statusText} response from ${requestDescription}. ${errorString}`
}
// Runtime checks that parsed JSON object is in the expected format
// {"code": "BAD_REQUEST", "message": "Something went wrong."}
function isGHCRError(obj: unknown): boolean {
return (
typeof obj === 'object' &&
obj !== null &&
'code' in obj &&
typeof (obj as { code: unknown }).code === 'string' &&
'message' in obj &&
typeof (obj as { message: unknown }).message === 'string'
core.info(
`Creating GHCR package for release with semver:${semver} with path:"${zipFile.path}" and "${tarFile.path}".`
)
const layerUploads: Promise<void>[] = manifest.layers.map(async layer => {
switch (layer.mediaType) {
case 'application/vnd.github.actions.package.layer.v1.tar+gzip':
return uploadLayer(
layer,
tarFile,
registry,
checkBlobEndpoint,
uploadBlobEndpoint,
b64Token
)
case 'application/vnd.github.actions.package.layer.v1.zip':
return uploadLayer(
layer,
zipFile,
registry,
checkBlobEndpoint,
uploadBlobEndpoint,
b64Token
)
case 'application/vnd.github.actions.package.config.v1+json':
return uploadLayer(
layer,
{ path: '', size: 0, sha256: layer.digest },
registry,
checkBlobEndpoint,
uploadBlobEndpoint,
b64Token
)
default:
throw new Error(`Unknown media type ${layer.mediaType}`)
}
})
await Promise.all(layerUploads)
const digest = await uploadManifest(
JSON.stringify(manifest),
manifestEndpoint,
b64Token
)
return {
packageURL: new URL(`${repository}:${semver}`, registry),
manifestDigest: digest
}
}
async function uploadLayer(
layer: ociContainer.Layer,
file: FileMetadata,
registryURL: URL,
checkBlobEndpoint: string,
uploadBlobEndpoint: string,
b64Token: string
): Promise<void> {
const checkExistsResponse = await axios.head(
checkBlobEndpoint + layer.digest,
{
headers: {
Authorization: `Bearer ${b64Token}`
},
validateStatus: () => {
return true // Allow non 2xx responses
}
}
)
if (
checkExistsResponse.status === 200 ||
checkExistsResponse.status === 202
) {
core.info(`Layer ${layer.digest} already exists. Skipping upload.`)
return
}
if (checkExistsResponse.status !== 404) {
throw new Error(
`Unexpected response from blob check for layer ${layer.digest}: ${checkExistsResponse.status} ${checkExistsResponse.statusText}`
)
}
core.info(`Uploading layer ${layer.digest}.`)
const initiateUploadResponse = await axios.post(uploadBlobEndpoint, layer, {
headers: {
Authorization: `Bearer ${b64Token}`
},
validateStatus: () => {
return true // Allow non 2xx responses
}
})
if (initiateUploadResponse.status !== 202) {
core.error(
`Unexpected response from upload post ${uploadBlobEndpoint}: ${initiateUploadResponse.status}`
)
throw new Error(
`Unexpected response from POST upload ${initiateUploadResponse.status}`
)
}
const locationResponseHeader = initiateUploadResponse.headers['location']
if (locationResponseHeader === undefined) {
throw new Error(
`No location header in response from upload post ${uploadBlobEndpoint} for layer ${layer.digest}`
)
}
const pathname = `${locationResponseHeader}?digest=${layer.digest}`
const uploadBlobUrl = new URL(pathname, registryURL).toString()
// TODO: must we handle the empty config layer? Maybe we can just skip calling this at all
let data: Buffer
if (file.size === 0) {
data = Buffer.alloc(0)
} else {
data = fsHelper.readFileContents(file.path)
}
const putResponse = await axios.put(uploadBlobUrl, data, {
headers: {
Authorization: `Bearer ${b64Token}`,
'Content-Type': 'application/octet-stream',
'Accept-Encoding': 'gzip', // TODO: What about for the config layer?
'Content-Length': layer.size.toString()
},
validateStatus: () => {
return true // Allow non 2xx responses
}
})
if (putResponse.status !== 201) {
throw new Error(
`Unexpected response from PUT upload ${putResponse.status} for layer ${layer.digest}`
)
}
}
// Uploads the manifest and returns the digest returned by GHCR
async function uploadManifest(
manifestJSON: string,
manifestEndpoint: string,
b64Token: string
): Promise<string> {
core.info(`Uploading manifest to ${manifestEndpoint}.`)
const putResponse = await axios.put(manifestEndpoint, manifestJSON, {
headers: {
Authorization: `Bearer ${b64Token}`,
'Content-Type': 'application/vnd.oci.image.manifest.v1+json'
},
validateStatus: () => {
return true // Allow non 2xx responses
}
})
if (putResponse.status !== 201) {
throw new Error(
`Unexpected response from PUT manifest ${putResponse.status}`
)
}
const digestResponseHeader = putResponse.headers['docker-content-digest']
if (digestResponseHeader === undefined) {
throw new Error(
`No digest header in response from PUT manifest ${manifestEndpoint}`
)
}
return digestResponseHeader
}
function configureRequestDebugLogging(): void {
axiosDebugLog({
request: (debug, config) => {
core.debug(`Request with ${config}`)
},
response: (debug, response) => {
core.debug(`Response with ${response}`)
},
error: (debug, error) => {
core.debug(`Error with ${error}`)
}
})
}
+67 -228
View File
@@ -1,11 +1,10 @@
import * as core from '@actions/core'
import semver from 'semver'
import * as github from '@actions/github'
import * as fsHelper from './fs-helper'
import * as ociContainer from './oci-container'
import * as ghcr from './ghcr-client'
import * as attest from '@actions/attest'
import * as cfg from './config'
import * as crypto from 'crypto'
import * as api from './api-client'
import semver from 'semver'
/**
* The main function for the action.
@@ -13,263 +12,103 @@ import * as crypto from 'crypto'
*/
export async function run(): Promise<void> {
try {
const options: cfg.PublishActionOptions =
await cfg.resolvePublishActionOptions()
const repository: string = process.env.GITHUB_REPOSITORY || ''
if (repository === '') {
core.setFailed(`Could not find Repository.`)
return
}
core.info(`Publishing action package version with options:`)
core.info(cfg.serializeOptions(options))
const token: string = process.env.TOKEN || ''
const sourceCommit: string = process.env.GITHUB_SHA || ''
if (token === '') {
core.setFailed(`Could not find GITHUB_TOKEN.`)
return
}
if (sourceCommit === '') {
core.setFailed(`Could not find source commit.`)
return
}
const semverTag: semver.SemVer = parseSemverTagFromRef(options)
const semanticVersion = parseSourceSemanticVersion()
// Ensure the correct SHA is checked out for the tag we're parsing, otherwise the bundled content will be incorrect.
await fsHelper.ensureTagAndRefCheckedOut(
options.ref,
options.sha,
options.workspaceDir
)
// Create a temporary directory to stage files for packaging in archives
const stagedActionFilesDir = fsHelper.createTempDir('staging')
fsHelper.stageActionFiles('.', stagedActionFilesDir)
const stagedActionFilesDir = fsHelper.createTempDir(
options.runnerTempDir,
'staging'
)
fsHelper.stageActionFiles(options.workspaceDir, stagedActionFilesDir)
const archiveDir = fsHelper.createTempDir(options.runnerTempDir, 'archives')
// Create a temporary directory to store the archives
const archiveDir = fsHelper.createTempDir('archive')
const archives = await fsHelper.createArchives(
stagedActionFilesDir,
archiveDir
)
const { repoId, ownerId } = await api.getRepositoryMetadata(
repository,
token
)
const manifest = ociContainer.createActionPackageManifest(
archives.tarFile,
archives.zipFile,
options.nameWithOwner,
options.repositoryId,
options.repositoryOwnerId,
options.sha,
semverTag.raw,
repository,
repoId,
ownerId,
sourceCommit,
semanticVersion.raw,
new Date()
)
const manifestDigest = ociContainer.sha256Digest(manifest)
const containerRegistryURL = await api.getContainerRegistryURL()
console.log(`Container registry URL: ${containerRegistryURL}`)
const ghcrClient = new ghcr.Client(
options.token,
options.containerRegistryUrl
)
// Attestations are not supported in GHES.
if (!options.isEnterprise) {
const { bundle, bundleDigest, bundleMediaType, bundlePredicateType } =
await generateAttestation(manifestDigest, semverTag.raw, options)
const attestationCreated = new Date()
const attestationManifest =
ociContainer.createSigstoreAttestationManifest(
bundle.length,
bundleDigest,
bundleMediaType,
bundlePredicateType,
ociContainer.sizeInBytes(manifest),
manifestDigest,
attestationCreated
)
const referrerIndexManifest = ociContainer.createReferrerTagManifest(
ociContainer.sha256Digest(attestationManifest),
ociContainer.sizeInBytes(attestationManifest),
bundleMediaType,
bundlePredicateType,
attestationCreated
)
const { attestationSHA, referrerIndexSHA } = await publishAttestation(
ghcrClient,
options.nameWithOwner,
bundle,
bundleDigest,
manifest,
attestationManifest,
referrerIndexManifest
)
if (attestationSHA !== undefined) {
core.info(`Uploaded attestation ${attestationSHA}`)
core.setOutput('attestation-manifest-sha', attestationSHA)
}
if (referrerIndexSHA !== undefined) {
core.info(`Uploaded referrer index ${referrerIndexSHA}`)
core.setOutput('referrer-index-manifest-sha', referrerIndexSHA)
}
}
const publishedDigest = await publishImmutableActionVersion(
ghcrClient,
options.nameWithOwner,
semverTag.raw,
const { packageURL, manifestDigest } = await ghcr.publishOCIArtifact(
token,
containerRegistryURL,
repository,
semanticVersion.raw,
archives.zipFile,
archives.tarFile,
manifest
manifest,
true
)
core.setOutput('package-manifest-sha', publishedDigest)
core.setOutput('package-url', packageURL.toString())
core.setOutput('package-manifest', JSON.stringify(manifest))
core.setOutput('package-manifest-sha', manifestDigest)
} catch (error) {
// Fail the workflow run if an error occurs
if (error instanceof Error) core.setFailed(error.message)
}
}
// This action can be triggered by any workflow that specifies a tag as its GITHUB_REF.
// This includes releases, creating or pushing tags, or workflow_dispatch.
// See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#about-events-that-trigger-workflows.
function parseSemverTagFromRef(opts: cfg.PublishActionOptions): semver.SemVer {
const ref = opts.ref
// This action can be triggered by release events or tag push events.
// In each case, the source event should produce a Semantic Version compliant tag representing the code to be packaged.
function parseSourceSemanticVersion(): semver.SemVer {
const event = github.context.eventName
let semverTag = ''
if (!ref.startsWith('refs/tags/')) {
throw new Error(`The ref ${ref} is not a valid tag reference.`)
}
const rawTag = ref.replace(/^refs\/tags\//, '')
const semverTag = semver.parse(rawTag.replace(/^v/, ''))
if (!semverTag) {
// Grab the raw tag
if (event === 'release') semverTag = github.context.payload.release.tag_name
else if (event === 'push' && github.context.ref.startsWith('refs/tags/')) {
semverTag = github.context.ref.replace(/^refs\/tags\//, '')
} else {
throw new Error(
`${rawTag} is not a valid semantic version tag, and so cannot be uploaded to the action package.`
`This action can only be triggered by release events or tag push events.`
)
}
return semverTag
}
async function publishImmutableActionVersion(
client: ghcr.Client,
nameWithOwner: string,
semverTag: string,
zipFile: fsHelper.FileMetadata,
tarFile: fsHelper.FileMetadata,
manifest: ociContainer.OCIImageManifest
): Promise<string> {
const manifestDigest = ociContainer.sha256Digest(manifest)
core.info(
`Creating GHCR package ${manifestDigest} for release with semver: ${semverTag}.`
)
const files = new Map<string, Buffer>()
files.set(zipFile.sha256, fsHelper.readFileContents(zipFile.path))
files.set(tarFile.sha256, fsHelper.readFileContents(tarFile.path))
files.set(ociContainer.emptyConfigSha, Buffer.from('{}'))
return await client.uploadOCIImageManifest(
nameWithOwner,
manifest,
files,
semverTag
)
}
async function publishAttestation(
client: ghcr.Client,
nameWithOwner: string,
bundle: Buffer,
bundleDigest: string,
subjectManifest: ociContainer.OCIImageManifest,
attestationManifest: ociContainer.OCIImageManifest,
referrerIndexManifest: ociContainer.OCIIndexManifest
): Promise<{
attestationSHA: string
referrerIndexSHA: string
}> {
const attestationManifestDigest =
ociContainer.sha256Digest(attestationManifest)
const subjectManifestDigest = ociContainer.sha256Digest(subjectManifest)
const referrerIndexManifestDigest = ociContainer.sha256Digest(
referrerIndexManifest
)
core.info(
`Publishing attestation ${attestationManifestDigest} for subject ${subjectManifestDigest}.`
)
const files = new Map<string, Buffer>()
files.set(ociContainer.emptyConfigSha, Buffer.from('{}'))
files.set(bundleDigest, bundle)
const attestationSHA = await client.uploadOCIImageManifest(
nameWithOwner,
attestationManifest,
files
)
// The referrer index is tagged with the subject's digest in format sha256-<digest>
const referrerTag = subjectManifestDigest.replace(':', '-')
core.info(
`Publishing referrer index ${referrerIndexManifestDigest} with tag ${referrerTag} for attestation ${attestationManifestDigest} and subject ${subjectManifestDigest}.`
)
const referrerIndexSHA = await client.uploadOCIIndexManifest(
nameWithOwner,
referrerIndexManifest,
referrerTag
)
return { attestationSHA, referrerIndexSHA }
}
async function generateAttestation(
manifestDigest: string,
semverTag: string,
options: cfg.PublishActionOptions
): Promise<{
bundle: Buffer
bundleDigest: string
bundleMediaType: string
bundlePredicateType: string
}> {
const subjectName = `${options.nameWithOwner}@${semverTag}`
const subjectDigest = removePrefix(manifestDigest, 'sha256:')
core.info(`Generating attestation ${subjectName} for digest ${subjectDigest}`)
const attestation = await attest.attestProvenance({
subjectName,
subjectDigest: { sha256: subjectDigest },
token: options.token,
sigstore: 'github',
skipWrite: true // We will upload attestations to GHCR
})
const bundleArtifact = Buffer.from(JSON.stringify(attestation.bundle))
const hash = crypto.createHash('sha256')
hash.update(bundleArtifact)
const bundleSHA = hash.digest('hex')
// We must base64 decode the dsse envelope to grab the predicate type
const dsseEnvelopeArtifact = attestation.bundle.dsseEnvelope
if (dsseEnvelopeArtifact === undefined) {
throw new Error('Attestation bundle is missing dsseEnvelope artifact')
if (semverTag === '') {
throw new Error(
`Could not find a Semantic Version tag in the event payload.`
)
}
const dsseEnvelope = JSON.parse(
Buffer.from(dsseEnvelopeArtifact.payload, 'base64').toString('utf-8')
)
const predicateType = dsseEnvelope.predicateType
if (predicateType === undefined) {
throw new Error('Attestation bundle is missing predicateType')
const semanticVersion = semver.parse(semverTag.replace(/^v/, ''))
if (!semanticVersion) {
throw new Error(
`${semverTag} is not a valid semantic version, and so cannot be uploaded as an Immutable Action.`
)
}
return {
bundle: bundleArtifact,
bundleDigest: `sha256:${bundleSHA}`,
bundleMediaType: attestation.bundle.mediaType,
bundlePredicateType: predicateType
}
}
function removePrefix(str: string, prefix: string): string {
if (str.startsWith(prefix)) {
return str.slice(prefix.length)
}
return str
return semanticVersion
}
+27 -149
View File
@@ -1,50 +1,19 @@
import { FileMetadata } from './fs-helper'
import * as crypto from 'crypto'
export const imageIndexMediaType = 'application/vnd.oci.image.index.v1+json'
export const imageManifestMediaType =
'application/vnd.oci.image.manifest.v1+json'
export const actionsPackageMediaType =
'application/vnd.github.actions.package.v1+json'
export const actionsPackageTarLayerMediaType =
'application/vnd.github.actions.package.layer.v1.tar+gzip'
export const actionsPackageZipLayerMediaType =
'application/vnd.github.actions.package.layer.v1.zip'
export const actionPackageAnnotationValue = 'actions_oci_pkg'
export const actionPackageAttestationAnnotationValue =
'actions_oci_pkg_attestation'
export const actionPackageReferrerTagAnnotationValue =
'actions_oci_pkg_referrer_index'
export const ociEmptyMediaType = 'application/vnd.oci.empty.v1+json'
export const emptyConfigSize = 2
export const emptyConfigSha =
'sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a'
export interface OCIImageManifest {
export interface Manifest {
schemaVersion: number
mediaType: string
artifactType: string
config: Descriptor
layers: Descriptor[]
subject?: Descriptor
config: Layer
layers: Layer[]
annotations: { [key: string]: string }
}
export interface OCIIndexManifest {
schemaVersion: number
mediaType: string
manifests: Descriptor[]
annotations: { [key: string]: string }
}
export interface Descriptor {
export interface Layer {
mediaType: string
size: number
digest: string
artifactType?: string
annotations?: { [key: string]: string }
annotations: { [key: string]: string }
}
// Given a name and archive metadata, creates a manifest in the format expected by GHCR for an Actions Package.
@@ -56,24 +25,24 @@ export function createActionPackageManifest(
ownerId: string,
sourceCommit: string,
version: string,
created: Date = new Date()
): OCIImageManifest {
const configLayer = createEmptyConfigLayer()
created: Date
): Manifest {
const configLayer = createConfigLayer()
const sanitizedRepo = sanitizeRepository(repository)
const tarLayer = createTarLayer(tarFile, sanitizedRepo, version)
const zipLayer = createZipLayer(zipFile, sanitizedRepo, version)
const manifest: OCIImageManifest = {
const manifest: Manifest = {
schemaVersion: 2,
mediaType: imageManifestMediaType,
artifactType: actionsPackageMediaType,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
artifactType: 'application/vnd.github.actions.package.v1+json',
config: configLayer,
layers: [tarLayer, zipLayer],
layers: [configLayer, tarLayer, zipLayer],
annotations: {
'org.opencontainers.image.created': created.toISOString(),
'action.tar.gz.digest': tarFile.sha256,
'action.zip.digest': zipFile.sha256,
'com.github.package.type': actionPackageAnnotationValue,
'com.github.package.type': 'actions_oci_pkg',
'com.github.package.version': version,
'com.github.source.repo.id': repoId,
'com.github.source.repo.owner.id': ownerId,
@@ -84,109 +53,18 @@ export function createActionPackageManifest(
return manifest
}
export function createSigstoreAttestationManifest(
bundleSize: number,
bundleDigest: string,
bundleMediaType: string,
bundlePredicateType: string,
subjectSize: number,
subjectDigest: string,
created: Date = new Date()
): OCIImageManifest {
const configLayer = createEmptyConfigLayer()
const sigstoreAttestationLayer: Descriptor = {
mediaType: bundleMediaType,
size: bundleSize,
digest: bundleDigest
}
const subject: Descriptor = {
mediaType: imageManifestMediaType,
size: subjectSize,
digest: subjectDigest
}
const manifest: OCIImageManifest = {
schemaVersion: 2,
mediaType: imageManifestMediaType,
artifactType: bundleMediaType,
config: configLayer,
layers: [sigstoreAttestationLayer],
subject,
// TODO: is this ok hardcoded?
function createConfigLayer(): Layer {
const configLayer: Layer = {
mediaType: 'application/vnd.github.actions.package.config.v1+json',
size: 0,
digest:
'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
annotations: {
'dev.sigstore.bundle.content': 'dsse-envelope',
'dev.sigstore.bundle.predicateType': bundlePredicateType,
'com.github.package.type': actionPackageAttestationAnnotationValue,
'org.opencontainers.image.created': created.toISOString()
'org.opencontainers.image.title': 'config.json'
}
}
return manifest
}
export function createReferrerTagManifest(
attestationDigest: string,
attestationSize: number,
bundleMediaType: string,
bundlePredicateType: string,
attestationCreated: Date,
created: Date = new Date()
): OCIIndexManifest {
const manifest: OCIIndexManifest = {
schemaVersion: 2,
mediaType: imageIndexMediaType,
manifests: [
{
mediaType: imageManifestMediaType,
artifactType: bundleMediaType,
size: attestationSize,
digest: attestationDigest,
annotations: {
'com.github.package.type': actionPackageAttestationAnnotationValue,
'org.opencontainers.image.created': attestationCreated.toISOString(),
'dev.sigstore.bundle.content': 'dsse-envelope',
'dev.sigstore.bundle.predicateType': bundlePredicateType
}
}
],
annotations: {
'com.github.package.type': actionPackageReferrerTagAnnotationValue,
'org.opencontainers.image.created': created.toISOString()
}
}
return manifest
}
// Calculate the SHA256 digest of a given manifest.
// This should match the digest which the GitHub container registry calculates for this manifest.
export function sha256Digest(
manifest: OCIImageManifest | OCIIndexManifest
): string {
const data = JSON.stringify(manifest)
const buffer = Buffer.from(data, 'utf8')
const hash = crypto.createHash('sha256')
hash.update(buffer)
const hexHash = hash.digest('hex')
return `sha256:${hexHash}`
}
export function sizeInBytes(
manifest: OCIImageManifest | OCIIndexManifest
): number {
const data = JSON.stringify(manifest)
return Buffer.byteLength(data, 'utf8')
}
export function createEmptyConfigLayer(): Descriptor {
const configLayer: Descriptor = {
mediaType: ociEmptyMediaType,
size: emptyConfigSize,
digest: emptyConfigSha
}
return configLayer
}
@@ -194,9 +72,9 @@ function createZipLayer(
zipFile: FileMetadata,
repository: string,
version: string
): Descriptor {
const zipLayer: Descriptor = {
mediaType: actionsPackageZipLayerMediaType,
): Layer {
const zipLayer: Layer = {
mediaType: 'application/vnd.github.actions.package.layer.v1.zip',
size: zipFile.size,
digest: zipFile.sha256,
annotations: {
@@ -211,9 +89,9 @@ function createTarLayer(
tarFile: FileMetadata,
repository: string,
version: string
): Descriptor {
const tarLayer: Descriptor = {
mediaType: actionsPackageTarLayerMediaType,
): Layer {
const tarLayer: Layer = {
mediaType: 'application/vnd.github.actions.package.layer.v1.tar+gzip',
size: tarFile.size,
digest: tarFile.sha256,
annotations: {