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
50 changed files with 92914 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
lib/
dist/
node_modules/
coverage/
+1
View File
@@ -0,0 +1 @@
dist/** -diff linguist-generated=true
+4
View File
@@ -0,0 +1,4 @@
# Repository CODEOWNERS
* @actions/actions-runtime
* @ncalteen
+17
View File
@@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
labels:
- dependabot
- actions
schedule:
interval: daily
- package-ecosystem: npm
directory: /
labels:
- dependabot
- npm
schedule:
interval: daily
+83
View File
@@ -0,0 +1,83 @@
env:
node: true
es6: true
jest: true
globals:
Atomics: readonly
SharedArrayBuffer: readonly
ignorePatterns:
- '!.*'
- '**/node_modules/.*'
- '**/dist/.*'
- '**/coverage/.*'
- '*.json'
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 2023
sourceType: module
project:
- './.github/linters/tsconfig.json'
- './tsconfig.json'
plugins:
- jest
- '@typescript-eslint'
extends:
- eslint:recommended
- plugin:@typescript-eslint/eslint-recommended
- plugin:@typescript-eslint/recommended
- plugin:github/recommended
- plugin:jest/recommended
rules:
{
'camelcase': 'off',
'eslint-comments/no-use': 'off',
'eslint-comments/no-unused-disable': 'off',
'i18n-text/no-en': 'off',
'import/no-namespace': 'off',
'no-console': 'off',
'no-unused-vars': 'off',
'prettier/prettier': 'error',
'semi': 'off',
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
'@typescript-eslint/consistent-type-assertions': 'error',
'@typescript-eslint/explicit-member-accessibility':
['error', { 'accessibility': 'no-public' }],
'@typescript-eslint/explicit-function-return-type':
['error', { 'allowExpressions': true }],
'@typescript-eslint/func-call-spacing': ['error', 'never'],
'@typescript-eslint/no-array-constructor': 'error',
'@typescript-eslint/no-empty-interface': 'error',
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-extraneous-class': 'error',
'@typescript-eslint/no-for-in-array': 'error',
'@typescript-eslint/no-inferrable-types': 'error',
'@typescript-eslint/no-misused-new': 'error',
'@typescript-eslint/no-namespace': 'error',
'@typescript-eslint/no-non-null-assertion': 'warn',
'@typescript-eslint/no-require-imports': 'error',
'@typescript-eslint/no-unnecessary-qualifier': 'error',
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-useless-constructor': 'error',
'@typescript-eslint/no-var-requires': 'error',
'@typescript-eslint/prefer-for-of': 'warn',
'@typescript-eslint/prefer-function-type': 'warn',
'@typescript-eslint/prefer-includes': 'error',
'@typescript-eslint/prefer-string-starts-ends-with': 'error',
'@typescript-eslint/promise-function-async': 'error',
'@typescript-eslint/require-array-sort-compare': 'error',
'@typescript-eslint/restrict-plus-operands': 'error',
'@typescript-eslint/semi': ['error', 'never'],
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/type-annotation-spacing': 'error',
'@typescript-eslint/unbound-method': 'error'
}
+12
View File
@@ -0,0 +1,12 @@
# Unordered list style
MD004:
style: dash
# Increase the max line length limit
MD013:
line_length: 200
# Ordered list item prefix
MD029:
style: one
+10
View File
@@ -0,0 +1,10 @@
rules:
document-end: disable
document-start:
level: warning
present: false
line-length:
level: warning
max: 80
allow-non-breakable-words: true
allow-non-breakable-inline-mappings: true
+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["../../__tests__/**/*", "../../src/**/*"],
"exclude": ["../../dist", "../../node_modules", "../../coverage", "*.json"]
}
+60
View File
@@ -0,0 +1,60 @@
# In TypeScript actions, `dist/index.js` is a special file. When you reference
# an action with `uses:`, `dist/index.js` is the code that will be run. For this
# project, the `dist/index.js` file is generated from other source files through
# the build process. We need to make sure that the checked-in `dist/index.js`
# file matches what is expected from the build.
#
# This workflow will fail if the checked-in `dist/index.js` file does not match
# what is expected from the build.
name: Check dist/
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
check-dist:
name: Check dist/
runs-on: ubuntu-latest
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm
- name: Install Dependencies
id: install
run: npm ci
- name: Build dist/ Directory
id: build
run: npm run bundle
- name: Compare Expected and Actual Directories
id: diff
run: |
if [ "$(git diff --ignore-space-at-eol --text dist/ | wc -l)" -gt "0" ]; then
echo "Detected uncommitted changes after build. See status below:"
git diff --ignore-space-at-eol --text dist/
exit 1
fi
# If index.js was different than expected, upload the expected version as
# a workflow artifact.
- uses: actions/upload-artifact@v4
if: ${{ failure() && steps.diff.conclusion == 'failure' }}
with:
name: dist
path: 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
+45
View File
@@ -0,0 +1,45 @@
name: Continuous Integration
on:
pull_request:
push:
branches:
- main
- 'releases/*'
permissions:
contents: read
jobs:
test-typescript:
name: TypeScript Tests
runs-on: ubuntu-latest
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm
- name: Install Dependencies
id: npm-ci
run: npm ci
- name: Check Format
id: npm-format-check
run: npm run format:check
- name: Lint
id: npm-lint
run: npm run lint
- name: Test
id: npm-ci-test
run: npm run ci-test
+50
View File
@@ -0,0 +1,50 @@
name: CodeQL
on:
workflow_dispatch:
# Disable until this is a public repo since advanced security is not enabled
# push:
# branches:
# - main
# pull_request:
# branches:
# - main
# schedule:
# - cron: '31 7 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
checks: write
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language:
- TypeScript
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- name: Initialize CodeQL
id: initialize
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
source-root: src
- name: Autobuild
id: autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
id: analyze
uses: github/codeql-action/analyze@v3
+44
View File
@@ -0,0 +1,44 @@
name: Lint Code Base
on:
pull_request:
branches:
- main
push:
branches:
- main
permissions:
contents: read
statuses: write
jobs:
lint:
name: Lint Code Base
runs-on: ubuntu-latest
steps:
- name: Checkout
id: checkout
uses: actions/checkout@v4
- name: Setup Node.js
id: setup-node
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm
- name: Install Dependencies
id: install
run: npm ci
- name: Lint Code Base
id: super-linter
uses: super-linter/super-linter/slim@v5
env:
DEFAULT_BRANCH: main
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TYPESCRIPT_DEFAULT_STYLE: prettier
VALIDATE_JSCPD: false
FILTER_REGEX_EXCLUDE: .*/licenses\.txt$
+16
View File
@@ -0,0 +1,16 @@
name: 'release'
on: # rebuild any PRs and main branch changes
release:
types: [created]
permissions:
id-token: write
contents: write
packages: write
jobs:
package-and-publish:
runs-on: ubuntu-latest
steps:
- name: Checking out!
uses: actions/checkout@v4
- name: Publish action package
uses: ./
+103
View File
@@ -0,0 +1,103 @@
# Dependency directory
node_modules
# Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# OS metadata
.DS_Store
Thumbs.db
# Ignore built ts files
__tests__/runner/*
# IDE files
.idea
.vscode
*.code-workspace
+1
View File
@@ -0,0 +1 @@
20.6.0
+3
View File
@@ -0,0 +1,3 @@
dist/
node_modules/
coverage/
+16
View File
@@ -0,0 +1,16 @@
{
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": false,
"trailingComma": "none",
"bracketSpacing": true,
"bracketSameLine": true,
"arrowParens": "avoid",
"proseWrap": "always",
"htmlWhitespaceSensitivity": "css",
"endOfLine": "lf"
}
+31
View File
@@ -0,0 +1,31 @@
# Publish Action Package
_This action_ packages _your action_ as OCI artifacts and publishes it to the [GitHub Container registry](ghcr.io).
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 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
<!-- start usage -->
```yaml
on:
release:
- 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 -->
## License
The scripts and documentation in this project are released under the [MIT License](LICENSE).
+78
View File
@@ -0,0 +1,78 @@
import {
getRepositoryMetadata,
getContainerRegistryURL
} from '../src/api-client'
let fetchMock: jest.SpyInstance
beforeEach(() => {
fetchMock = jest.spyOn(global, 'fetch')
})
afterEach(() => {
fetchMock.mockRestore()
})
describe('getRepositoryMetadata', () => {
it('returns repository metadata when the fetch response is ok', async () => {
fetchMock.mockResolvedValueOnce(
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('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('repository', 'token')).rejects.toThrow(
'Failed to fetch repository metadata due to bad status code: 500'
)
})
it('throws an error when the response data is in the wrong format', async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ wrong: 'format' }))
)
await expect(getRepositoryMetadata('repository', 'token')).rejects.toThrow(
'Failed to fetch repository metadata: unexpected response format'
)
})
})
describe('getContainerRegistryURL', () => {
it('returns container registry URL when the fetch response is ok', async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ url: 'https://registry.example.com' }))
)
const result = await getContainerRegistryURL()
expect(result).toEqual(new URL('https://registry.example.com'))
})
it('throws an error when the fetch errors', async () => {
fetchMock.mockRejectedValueOnce(new Error('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()).rejects.toThrow(
'Failed to fetch container registry url due to bad status code: 500'
)
})
it('throws an error when the response data is in the wrong format', async () => {
fetchMock.mockResolvedValueOnce(
new Response(JSON.stringify({ wrong: 'format' }))
)
await expect(getContainerRegistryURL()).rejects.toThrow(
'Failed to fetch repository metadata: unexpected response format'
)
})
})
+219
View File
@@ -0,0 +1,219 @@
import * as fsHelper from '../src/fs-helper'
import * as fs from 'fs'
import * as os from 'os'
import { execSync } from 'child_process'
const fileContent = 'This is the content of the file'
describe('stageActionFiles', () => {
let sourceDir: string
let stagingDir: string
beforeEach(() => {
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('staging')
})
afterEach(() => {
fs.rmSync(sourceDir, { recursive: true })
fs.rmSync(stagingDir, { recursive: true })
})
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`)
fs.writeFileSync(`${sourceDir}/.git/HEAD`, fileContent)
fs.mkdirSync(`${sourceDir}/.github/workflows`, { recursive: true })
fs.writeFileSync(`${sourceDir}/.github/workflows/workflow.yml`, fileContent)
fsHelper.stageActionFiles(sourceDir, stagingDir)
expect(fs.existsSync(`${stagingDir}/action.yml`)).toBe(true)
expect(fs.existsSync(`${stagingDir}/src/main.js`)).toBe(true)
expect(fs.existsSync(`${stagingDir}/src/other.js`)).toBe(true)
// 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)
})
})
describe('createArchives', () => {
let stageDir: string
let archiveDir: string
beforeAll(() => {
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('archive')
})
afterEach(() => {
fs.rmSync(archiveDir, { recursive: true })
})
afterAll(() => {
fs.rmSync(stageDir, { recursive: true })
})
it('creates archives', async () => {
const { zipFile, tarFile } = await fsHelper.createArchives(
stageDir,
archiveDir
)
expect(zipFile.path).toEqual(`${archiveDir}/archive.zip`)
expect(fs.existsSync(zipFile.path)).toEqual(true)
expect(fs.statSync(zipFile.path).size).toBeGreaterThan(0)
expect(zipFile.sha256.startsWith('sha256:')).toEqual(true)
expect(tarFile.path).toEqual(`${archiveDir}/archive.tar.gz`)
expect(fs.existsSync(tarFile.path)).toEqual(true)
expect(fs.statSync(tarFile.path).size).toBeGreaterThan(0)
expect(tarFile.sha256.startsWith('sha256:')).toEqual(true)
// Validate the hashes by comparing to the output of the system's hashing utility
const zipSHA = zipFile.sha256.substring(7) // remove "sha256:" prefix
const tarSHA = tarFile.sha256.substring(7) // remove "sha256:" prefix
// sha256 hash is 64 characters long
expect(zipSHA).toHaveLength(64)
expect(tarSHA).toHaveLength(64)
let systemZipHash: string
let systemTarHash: string
if (os.platform() === 'win32') {
// Windows
systemZipHash = execSync(`CertUtil -hashfile ${zipFile.path} SHA256`)
.toString()
.split(' ')[1]
.trim()
systemTarHash = execSync(`CertUtil -hashfile ${tarFile.path} SHA256`)
.toString()
.split(' ')[1]
.trim()
} else {
// Unix-based systems
systemZipHash = execSync(`shasum -a 256 ${zipFile.path}`)
.toString()
.split(' ')[0]
systemTarHash = execSync(`shasum -a 256 ${tarFile.path}`)
.toString()
.split(' ')[0]
}
expect(zipSHA).toEqual(systemZipHash)
expect(tarSHA).toEqual(systemTarHash)
})
})
describe('createTempDir', () => {
let dirs: string[] = []
beforeEach(() => {
dirs = []
})
afterEach(() => {
for (const dir of dirs) {
fs.rmSync(dir, { recursive: true })
}
})
it('creates a temporary directory', () => {
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', () => {
process.env.RUNNER_TEMP = '/tmp'
const dir1 = fsHelper.createTempDir('dir1')
dirs.push(dir1)
const dir2 = fsHelper.createTempDir('dir2')
dirs.push(dir2)
expect(dir1).not.toEqual(dir2)
})
})
describe('isDirectory', () => {
let dir: string
beforeEach(() => {
process.env.RUNNER_TEMP = '/tmp'
dir = fsHelper.createTempDir('subdir')
})
afterEach(() => {
fs.rmSync(dir, { recursive: true })
})
it('returns true if the path is a directory', () => {
expect(fsHelper.isDirectory(dir)).toEqual(true)
})
it('returns false if the path is not a directory', () => {
const tempFile = `${dir}/file.txt`
fs.writeFileSync(tempFile, fileContent)
expect(fsHelper.isDirectory(tempFile)).toEqual(false)
})
})
describe('readFileContents', () => {
let dir: string
beforeEach(() => {
process.env.RUNNER_TEMP = '/tmp'
dir = fsHelper.createTempDir('subdir')
})
afterEach(() => {
fs.rmSync(dir, { recursive: true })
})
it('reads the contents of a file', () => {
const tempFile = `${dir}/file.txt`
fs.writeFileSync(tempFile, fileContent)
expect(fsHelper.readFileContents(tempFile).toString()).toEqual(fileContent)
})
})
+562
View File
@@ -0,0 +1,562 @@
import { publishOCIArtifact } from '../src/ghcr-client'
import axios from 'axios'
import * as fsHelper from '../src/fs-helper'
import * as ociContainer from '../src/oci-container'
// Mocks
let fsReadFileSyncMock: jest.SpyInstance
let axiosPostMock: jest.SpyInstance
let axiosPutMock: jest.SpyInstance
let axiosHeadMock: jest.SpyInstance
const token = 'test-token'
const registry = new URL('https://ghcr.io')
const repository = 'test-org/test-repo'
const semver = '1.2.3'
const genericSha = '1234567890' // We should look at using different shas here to catch bug, but that make location validation harder
const zipFile: fsHelper.FileMetadata = {
path: `test-repo-${semver}.zip`,
size: 123,
sha256: genericSha
}
const tarFile: fsHelper.FileMetadata = {
path: `test-repo-${semver}.tar.gz`,
size: 456,
sha256: genericSha
}
const testManifest: ociContainer.Manifest = {
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
artifactType: 'application/vnd.oci.image.manifest.v1+json',
config: {
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: `sha256:${tarFile.sha256}`,
annotations: {
'org.opencontainers.image.title': tarFile.path
}
},
{
mediaType: 'application/vnd.github.actions.package.layer.v1.zip',
size: zipFile.size,
digest: `sha256:${zipFile.sha256}`,
annotations: {
'org.opencontainers.image.title': zipFile.path
}
}
],
annotations: {
'org.opencontainers.image.created': '2021-01-01T00:00:00.000Z',
'action.tar.gz.digest': tarFile.sha256,
'action.zip.digest': zipFile.sha256,
'com.github.package.type': 'actions_oci_pkg'
}
}
describe('publishOCIArtifact', () => {
beforeEach(() => {
jest.clearAllMocks()
fsReadFileSyncMock = jest
.spyOn(fsHelper, 'readFileContents')
.mockImplementation()
axiosPostMock = jest.spyOn(axios, 'post').mockImplementation()
axiosPutMock = jest.spyOn(axios, 'put').mockImplementation()
axiosHeadMock = jest.spyOn(axios, 'head').mockImplementation()
})
it('publishes layer blobs & then a manifest to the provided registry', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
})
// Simulate successful reading of all the files
fsReadFileSyncMock.mockImplementation(() => {
return Buffer.from('test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(201, url, config)
if ((url as string).includes('manifest')) {
return {
status: 201,
headers: { 'docker-content-digest': '1234567678' }
}
}
return {
status: 201
}
})
await publishOCIArtifact(
token,
registry,
repository,
semver,
zipFile,
tarFile,
testManifest
)
expect(axiosHeadMock).toHaveBeenCalledTimes(3)
expect(axiosPostMock).toHaveBeenCalledTimes(3)
expect(axiosPutMock).toHaveBeenCalledTimes(4)
})
it('skips uploading all layer blobs when they all already exist', async () => {
// Simulate all blobs already existing
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(200, url, config)
return {
status: 200
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
})
// Simulate successful reading of all the files
fsReadFileSyncMock.mockImplementation(() => {
return Buffer.from('test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(201, url, config)
if ((url as string).includes('manifest')) {
return {
status: 201,
headers: { 'docker-content-digest': '1234567678' }
}
}
return {
status: 201
}
})
await publishOCIArtifact(
token,
registry,
repository,
semver,
zipFile,
tarFile,
testManifest
)
// We should only head all the blobs and then upload the manifest
expect(axiosHeadMock).toHaveBeenCalledTimes(3)
expect(axiosPostMock).toHaveBeenCalledTimes(0)
expect(axiosPutMock).toHaveBeenCalledTimes(1)
})
it('skips uploading layer blobs that already exist', async () => {
// Simulate some blobs already existing
let count = 0
axiosHeadMock.mockImplementation(async (url, config) => {
count++
if (count === 1) {
// report the first blob as being there
validateRequestConfig(200, url, config)
return {
status: 200
}
} else {
// report all others are missing
validateRequestConfig(404, url, config)
return {
status: 404
}
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
})
// Simulate successful reading of all the files
fsReadFileSyncMock.mockImplementation(() => {
return Buffer.from('test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(201, url, config)
if ((url as string).includes('manifest')) {
return {
status: 201,
headers: { 'docker-content-digest': '1234567678' }
}
}
return {
status: 201
}
})
await publishOCIArtifact(
token,
registry,
repository,
semver,
zipFile,
tarFile,
testManifest
)
// We should only head all the blobs and then upload the missing blobs and manifest
expect(axiosHeadMock).toHaveBeenCalledTimes(3)
expect(axiosPostMock).toHaveBeenCalledTimes(2)
expect(axiosPutMock).toHaveBeenCalledTimes(3)
})
it('throws an error if checking for existing blobs fails', async () => {
// Simulate failed response code
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(503, url, config)
return {
status: 503
}
})
await expect(
publishOCIArtifact(
token,
registry,
repository,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow(/^Unexpected response from blob check for layer/)
})
it('throws an error if initiating layer upload fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate failed initiation of uploads
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(503, url, config)
return {
status: 503
}
})
await expect(
publishOCIArtifact(
token,
registry,
repository,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow('Unexpected response from POST upload 503')
})
it('throws an error if the upload endpoint does not return a location', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful response code but no location header
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {}
}
})
await expect(
publishOCIArtifact(
token,
registry,
repository,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow(/^No location header in response from upload post/)
})
it('throws an error if a layer upload fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
})
// Simulate successful reading of all the files
fsReadFileSyncMock.mockImplementation(() => {
return Buffer.from('test')
})
// Simulate fails upload of all blobs & manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(500, url, config)
return {
status: 500
}
})
await expect(
publishOCIArtifact(
token,
registry,
repository,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow(/^Unexpected response from PUT upload 500/)
})
it('throws an error if a manifest upload fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
})
// Simulate successful reading of all the files
fsReadFileSyncMock.mockImplementation(() => {
return Buffer.from('test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
if (url.includes('manifest')) {
validateRequestConfig(500, url, config)
return {
status: 500
}
}
validateRequestConfig(201, url, config)
return {
status: 201
}
})
await expect(
publishOCIArtifact(
token,
registry,
repository,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow(/^Unexpected response from PUT manifest 500/)
})
it('throws an error if reading one of the files fails', async () => {
// Simulate none of the blobs existing currently
axiosHeadMock.mockImplementation(async (url, config) => {
validateRequestConfig(404, url, config)
return {
status: 404
}
})
// Simulate successful initiation of uploads for all blobs & return location
axiosPostMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(202, url, config)
return {
status: 202,
headers: {
location: `https://ghcr.io/v2/${repository}/blobs/uploads/${genericSha}`
}
}
})
// Simulate successful reading of all the files
fsReadFileSyncMock.mockImplementation(() => {
throw new Error('failed to read a file: test')
})
// Simulate successful upload of all blobs & then the manifest
axiosPutMock.mockImplementation(async (url, data, config) => {
validateRequestConfig(201, url, config)
return {
status: 201
}
})
await expect(
publishOCIArtifact(
token,
registry,
repository,
semver,
zipFile,
tarFile,
testManifest
)
).rejects.toThrow('failed to read a file: test')
})
it('throws an error if one of the layers has the wrong media type', async () => {
const modifiedTestManifest = { ...testManifest } // This is _NOT_ a deep clone
modifiedTestManifest.layers = cloneLayers(modifiedTestManifest.layers)
modifiedTestManifest.layers[0].mediaType = 'application/json'
// just checking to make sure we are not changing the shared object
expect(modifiedTestManifest.layers[0].mediaType).not.toEqual(
testManifest.layers[0].mediaType
)
await expect(
publishOCIArtifact(
token,
registry,
repository,
semver,
zipFile,
tarFile,
modifiedTestManifest
)
).rejects.toThrow('Unknown media type application/json')
})
})
// We expect all axios calls to have auth headers set and to not intercept any status codes so we can handle them.
// This function verifies that given an axios request config.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function validateRequestConfig(status: number, url: string, config: any): void {
// Basic URL checks
expect(url).toBeDefined()
if (!url.startsWith(registry.toString())) {
console.log(`${url} does not start with ${registry}`)
}
// if these expect fails, run the test again with `-- --silent=false`
// the console.log above should give a clue about which URL is failing
expect(url.startsWith(registry.toString())).toBeTruthy()
// Config checks
expect(config).toBeDefined()
expect(config.validateStatus).toBeDefined()
if (config.validateStatus) {
// Check axios will not intercept this status
expect(config.validateStatus(status)).toBe(true)
}
expect(config.headers).toBeDefined()
if (config.headers) {
// Check the auth header is set
expect(config.headers.Authorization).toBeDefined()
// Check the auth header is the base 64 encoded token
expect(config.headers.Authorization).toBe(
`Bearer ${Buffer.from(token).toString('base64')}`
)
}
}
function cloneLayers(layers: ociContainer.Layer[]): ociContainer.Layer[] {
const result: ociContainer.Layer[] = []
for (const layer of layers) {
result.push({ ...layer }) // this is _NOT_ a deep clone
}
return result
}
+17
View File
@@ -0,0 +1,17 @@
/**
* Unit tests for the action's entrypoint, src/index.ts
*/
import * as main from '../src/main'
// Mock the action's entrypoint
const runMock = jest.spyOn(main, 'run').mockImplementation()
describe('index', () => {
it('calls run when imported', async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('../src/index')
expect(runMock).toHaveBeenCalled()
})
})
+384
View File
@@ -0,0 +1,384 @@
/**
* Unit tests for the action's main functionality, src/main.ts
*
* These should be run as if the action was called from a workflow.
* Specifically, the inputs listed in `action.yml` should be set as environment
* variables following the pattern `INPUT_<INPUT_NAME>`.
*/
import * as core from '@actions/core'
import * as main from '../src/main'
import * as github from '@actions/github'
import * as fsHelper from '../src/fs-helper'
import * as ghcr from '../src/ghcr-client'
import * as api from '../src/api-client'
// Mock the GitHub Actions core library
let setFailedMock: jest.SpyInstance
let setOutputMock: jest.SpyInstance
// Mock the filesystem helper
let createTempDirMock: jest.SpyInstance
let createArchivesMock: jest.SpyInstance
let stageActionFilesMock: jest.SpyInstance
// Mock the GHCR Client
let publishOCIArtifactMock: jest.SpyInstance
// Mock the API Client
let getContainerRegistryURLMock: jest.SpyInstance
let getRepositoryMetadataMock: jest.SpyInstance
describe('run', () => {
beforeEach(() => {
jest.clearAllMocks()
// Core mocks
setFailedMock = jest.spyOn(core, 'setFailed').mockImplementation()
setOutputMock = jest.spyOn(core, 'setOutput').mockImplementation()
// FS mocks
createTempDirMock = jest
.spyOn(fsHelper, 'createTempDir')
.mockImplementation()
createArchivesMock = jest
.spyOn(fsHelper, 'createArchives')
.mockImplementation()
stageActionFilesMock = jest
.spyOn(fsHelper, 'stageActionFiles')
.mockImplementation()
// GHCR Client mocks
publishOCIArtifactMock = jest
.spyOn(ghcr, 'publishOCIArtifact')
.mockImplementation()
// API Client mocks
getContainerRegistryURLMock = jest
.spyOn(api, 'getContainerRegistryURL')
.mockImplementation()
getRepositoryMetadataMock = jest
.spyOn(api, 'getRepositoryMetadata')
.mockImplementation()
})
it('fails if no repository found', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = ''
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Could not find Repository.')
})
it('fails if no token found', async () => {
// Mock the environment
process.env.GITHUB_REPOSITORY = 'test-org/test-repo'
process.env.TOKEN = ''
// 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(
'This action can only be triggered by release events or tag push events.'
)
}
})
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
await main.run()
expect(setFailedMock).toHaveBeenCalledWith(
'This action can only be triggered by release events or tag push events.'
)
})
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'
const tags = ['test', 'v1.0', 'chicken', '111111']
for (const tag of tags) {
github.context.payload = {
release: {
id: '123',
tag_name: tag
}
}
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 () => {
// 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')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
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')
})
// Run the action
await main.run()
// Check the results
expect(setFailedMock).toHaveBeenCalledWith('Something went wrong')
})
it('fails if creating archives 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'
}
}
createArchivesMock.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 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 {
zipFile: {
path: 'test',
size: 5,
sha256: '123'
},
tarFile: {
path: 'test2',
size: 52,
sha256: '1234'
}
}
})
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',
manifestDigest: 'sha256:my-test-digest'
}
})
// Run the action
await main.run()
// Check the results
expect(publishOCIArtifactMock).toHaveBeenCalledTimes(1)
// Check outputs
expect(setOutputMock).toHaveBeenCalledTimes(3)
expect(setOutputMock).toHaveBeenCalledWith(
'package-url',
'https://ghcr.io/v2/test-org/test-repo:1.2.3'
)
expect(setOutputMock).toHaveBeenCalledWith(
'package-manifest',
expect.any(String)
)
expect(setOutputMock).toHaveBeenCalledWith(
'package-manifest-sha',
'sha256:my-test-digest'
)
})
})
+96
View File
@@ -0,0 +1,96 @@
import { createActionPackageManifest } from '../src/oci-container'
import { FileMetadata } from '../src/fs-helper'
describe('createActionPackageManifest', () => {
it('creates a manifest containing the provided information', () => {
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.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":"${sanitizedRepo}_${version}.tar.gz"
}
},
{
"mediaType":"application/vnd.github.actions.package.layer.v1.zip",
"size":${zipFile.size},
"digest":"${zipFile.sha256}",
"annotations":{
"org.opencontainers.image.title":"${sanitizedRepo}_${version}.zip"
}
}
],
"annotations":{
"org.opencontainers.image.created":"${date.toISOString()}",
"action.tar.gz.digest":"${tarFile.sha256}",
"action.zip.digest":"${zipFile.sha256}",
"com.github.package.type":"actions_oci_pkg",
"com.github.package.version":"1.2.3",
"com.github.source.repo.id":"123",
"com.github.source.repo.owner.id":"456",
"com.github.source.commit":"abc"
}
}`
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, ''))
})
})
+46
View File
@@ -0,0 +1,46 @@
name: 'Package and Publish'
description: 'Publish actions as OCI artifacts to GHCR'
# TODO: Add your action's branding here. This will appear on the GitHub Marketplace.
branding:
icon: 'heart'
color: 'red'
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'
value: ${{steps.publish.outputs.package-manifest-sha}}
runs:
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
View File
@@ -0,0 +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: 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>

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
+79506
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
+2365
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"}
+7738
View File
File diff suppressed because it is too large Load Diff
+102
View File
@@ -0,0 +1,102 @@
{
"name": "typescript-action",
"description": "GitHub Actions TypeScript template",
"version": "0.0.0",
"author": "",
"private": true,
"homepage": "https://github.com/actions/typescript-action",
"repository": {
"type": "git",
"url": "git+https://github.com/actions/typescript-action.git"
},
"bugs": {
"url": "https://github.com/actions/typescript-action/issues"
},
"keywords": [
"actions",
"node",
"setup"
],
"exports": {
".": "./dist/index.js"
},
"engines": {
"node": ">=20"
},
"scripts": {
"bundle": "npm run format:write && npm run package",
"ci-test": "jest",
"coverage": "make-coverage-badge --output-path ./badges/coverage.svg",
"format:write": "prettier --write **/*.ts",
"format:check": "prettier --check **/*.ts",
"lint": "npx eslint . -c ./.github/linters/.eslintrc.yml",
"package": "ncc build src/index.ts --license licenses.txt",
"package:watch": "npm run package -- --watch",
"test": "jest",
"all": "npm run format:write && npm run lint && npm run test && npm run coverage && npm run package"
},
"license": "MIT",
"jest": {
"preset": "ts-jest",
"verbose": true,
"clearMocks": true,
"testEnvironment": "node",
"moduleFileExtensions": [
"js",
"ts"
],
"testMatch": [
"**/*.test.ts"
],
"testPathIgnorePatterns": [
"/node_modules/",
"/dist/"
],
"transform": {
"^.+\\.ts$": "ts-jest"
},
"coverageReporters": [
"json-summary",
"text",
"lcov"
],
"collectCoverage": true,
"collectCoverageFrom": [
"./src/**"
]
},
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/exec": "^1.1.1",
"@actions/github": "^6.0.0",
"@types/fs-extra": "^11.0.4",
"archiver": "^6.0.1",
"axios": "^1.6.2",
"axios-debug-log": "^1.0.0",
"fs-extra": "^11.2.0",
"tar": "^6.2.0"
},
"devDependencies": {
"@types/archiver": "^6.0.2",
"@types/axios": "^0.14.0",
"@types/jest": "^29.5.11",
"@types/minimist": "^1.2.5",
"@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.54.0",
"eslint-plugin-github": "^4.10.1",
"eslint-plugin-jest": "^27.6.3",
"eslint-plugin-jsonc": "^2.13.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"js-yaml": "^4.1.0",
"make-coverage-badge": "^1.2.0",
"prettier": "^3.2.4",
"prettier-eslint": "^16.3.0",
"ts-jest": "^29.1.1",
"typescript": "^5.3.3"
}
}
Executable
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# About:
# This is a helper script to tag and push a new release.
# GitHub Actions use release tags to allow users to select a specific version of the action to use.
# This script will do the following:
# 1. Get the latest release tag
# 2. Prompt the user for a new release tag (while displaying the latest release tag, and a regex to validate the new tag)
# 3. Tag the new release
# 4. Push the new tag to the remote
# Usage:
# script/release
# COLORS
OFF='\033[0m'
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
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]"
fi
echo -e "The latest release tag is: ${BLUE}${latest_tag}${OFF}"
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"
else
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"
echo -e "${GREEN}OK${OFF} - Tagged: $new_tag"
git push --tags
echo -e "${GREEN}OK${OFF} - Tags pushed to remote!"
echo -e "${GREEN}DONE${OFF}"
+53
View File
@@ -0,0 +1,53 @@
export async function getRepositoryMetadata(
repository: string,
token: string
): 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(
`Failed to fetch repository metadata due to bad status code: ${response.status}`
)
}
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: unexpected response format`
)
}
return { repoId: String(data.id), ownerId: String(data.owner.id) }
}
export async function getContainerRegistryURL(): Promise<URL> {
const response = await fetch(
`${process.env.GITHUB_API_URL}/packages/container-registry-url`
)
if (!response.ok) {
throw new Error(
`Failed to fetch container registry url due to bad status code: ${response.status}`
)
}
const data = await response.json()
if (!data.url) {
throw new Error(
`Failed to fetch repository metadata: unexpected response format`
)
}
const registryURL: URL = new URL(data.url)
return registryURL
}
+138
View File
@@ -0,0 +1,138 @@
import * as fs from 'fs'
import fsExtra from 'fs-extra'
import * as path from 'path'
import * as tar from 'tar'
import * as archiver from 'archiver'
import * as crypto from 'crypto'
export interface FileMetadata {
path: string
size: number
sha256: string
}
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)
}
return tempDir
}
// 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.
export async function createArchives(
distPath: string,
archiveTargetPath: string
): Promise<{ zipFile: FileMetadata; tarFile: FileMetadata }> {
const zipPath = path.join(archiveTargetPath, `archive.zip`)
const tarPath = path.join(archiveTargetPath, `archive.tar.gz`)
const createZipPromise = new Promise<FileMetadata>((resolve, reject) => {
const output = fs.createWriteStream(zipPath)
const archive = archiver.create('zip')
output.on('error', (err: Error) => {
reject(err)
})
archive.on('error', (err: Error) => {
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<FileMetadata>((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 }
}
export function isDirectory(dirPath: string): boolean {
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory()
}
export function readFileContents(filePath: string): Buffer {
return fs.readFileSync(filePath)
}
// 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)
if (basename === 'action.yml' || basename === 'action.yaml') {
actionYmlFound = true
}
// Filter out hidden folers like .git and .github
return basename === '.' || !basename.startsWith('.')
}
})
if (!actionYmlFound) {
throw new Error(
`No action.yml or action.yaml file found in source repository`
)
}
}
// Converts a file path to a filemetadata object by querying the fs for relevant metadata.
async function fileMetadata(filePath: string): Promise<FileMetadata> {
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)
})
})
}
+227
View File
@@ -0,0 +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'
// 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()
}
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: 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}`)
}
})
}
+7
View File
@@ -0,0 +1,7 @@
/**
* The entrypoint for the action.
*/
import { run } from './main'
// eslint-disable-next-line @typescript-eslint/no-floating-promises
run()
+114
View File
@@ -0,0 +1,114 @@
import * as core from '@actions/core'
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 api from './api-client'
import semver from 'semver'
/**
* The main function for the action.
* @returns {Promise<void>} Resolves when the action is complete.
*/
export async function run(): Promise<void> {
try {
const repository: string = process.env.GITHUB_REPOSITORY || ''
if (repository === '') {
core.setFailed(`Could not find Repository.`)
return
}
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 semanticVersion = parseSourceSemanticVersion()
// Create a temporary directory to stage files for packaging in archives
const stagedActionFilesDir = fsHelper.createTempDir('staging')
fsHelper.stageActionFiles('.', stagedActionFilesDir)
// 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,
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', 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 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 = ''
// 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.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
}
+109
View File
@@ -0,0 +1,109 @@
import { FileMetadata } from './fs-helper'
export interface Manifest {
schemaVersion: number
mediaType: string
artifactType: string
config: Layer
layers: Layer[]
annotations: { [key: string]: string }
}
export interface Layer {
mediaType: string
size: number
digest: string
annotations: { [key: string]: string }
}
// Given a name and archive metadata, creates a manifest in the format expected by GHCR for an Actions Package.
export function createActionPackageManifest(
tarFile: FileMetadata,
zipFile: FileMetadata,
repository: string,
repoId: string,
ownerId: string,
sourceCommit: string,
version: string,
created: Date
): Manifest {
const configLayer = createConfigLayer()
const sanitizedRepo = sanitizeRepository(repository)
const tarLayer = createTarLayer(tarFile, sanitizedRepo, version)
const zipLayer = createZipLayer(zipFile, sanitizedRepo, version)
const manifest: 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
}
// 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: {
'org.opencontainers.image.title': 'config.json'
}
}
return configLayer
}
function createZipLayer(
zipFile: FileMetadata,
repository: string,
version: string
): Layer {
const zipLayer: Layer = {
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: FileMetadata,
repository: string,
version: string
): Layer {
const tarLayer: Layer = {
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: string): string {
return repository.replace('/', '-')
}
+19
View File
@@ -0,0 +1,19 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"rootDir": "./src",
"moduleResolution": "NodeNext",
"baseUrl": "./",
"sourceMap": true,
"outDir": "./dist",
"noImplicitAny": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"newLine": "lf"
},
"exclude": ["./dist", "./node_modules", "./__tests__", "./coverage"]
}