Compare commits
393 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 91d3933eb5 | |||
| a6bf8726aa | |||
| ae9272d5cb | |||
| f481b8c8dc | |||
| 59851786d4 | |||
| 37e09c586f | |||
| 12c01ac203 | |||
| bbab4bec57 | |||
| 672c88ec4b | |||
| a103f5eefe | |||
| 457303960f | |||
| 463b49d872 | |||
| a91ee0b497 | |||
| 94e340bfb1 | |||
| 599c6164e8 | |||
| b8261b0fb0 | |||
| aeb16eeca1 | |||
| 7a11743b35 | |||
| fe92749762 | |||
| 0e8edb0780 | |||
| 7d1daaf15e | |||
| 40ec298d4b | |||
| 787b2cf270 | |||
| 8e32b1fca3 | |||
| d9a2c5a9f9 | |||
| e6e7b6156f | |||
| f3de1e53d6 | |||
| 703d5ac24a | |||
| 97f21173cc | |||
| ce1bf116fc | |||
| 9ba9ae31a9 | |||
| 94ab8de5f3 | |||
| d47e0bac60 | |||
| 1f4b3fac06 | |||
| 8d92c9c903 | |||
| e45a26f771 | |||
| a3849b77ae | |||
| eb06c21794 | |||
| 0db3029fcf | |||
| e6e29846f2 | |||
| 7c15bf6f40 | |||
| bc713ab90d | |||
| a9d266bb7c | |||
| cf3dd065b8 | |||
| 6ec51745ad | |||
| 0f91c9c203 | |||
| e18b2d8a33 | |||
| 4fd425926c | |||
| 83dffb7746 | |||
| 1d1d5456e3 | |||
| 9e06993ffc | |||
| 3630ea6eed | |||
| c2d3089f83 | |||
| 652109d32c | |||
| f2aa430c9d | |||
| d2b7d85e7c | |||
| 409d616a6e | |||
| e3c2a88bbf | |||
| ea21da6993 | |||
| c6005c2a3c | |||
| 1589a5c066 | |||
| d3801d332c | |||
| 5804607845 | |||
| c26f803662 | |||
| 6c1f9eaae8 | |||
| 412417d0b0 | |||
| 71a6fceb8c | |||
| 34577b269e | |||
| 06c3c38ef2 | |||
| 03d6c2479c | |||
| 411e8fa448 | |||
| 2c09aaef3b | |||
| b2d865f180 | |||
| 56146a6713 | |||
| 74f24b41d1 | |||
| c0b323a0bb | |||
| 83db1b8e43 | |||
| 5b2351aebf | |||
| f5024e4e97 | |||
| 4ea08312c6 | |||
| 5e9bcaca7c | |||
| af2d2ff198 | |||
| 3d46598e70 | |||
| 894a0490f9 | |||
| 86fe4abd8e | |||
| b228732644 | |||
| 2a4f3544ad | |||
| c23fe4b81f | |||
| 034d154f88 | |||
| ccfa36f304 | |||
| 2afea665ed | |||
| e96dc8a69a | |||
| b8c50aa82d | |||
| 24685611e2 | |||
| 80d992795c | |||
| b9de68a590 | |||
| 1d61e5fb19 | |||
| 4abb5a2ae0 | |||
| 6b18932b86 | |||
| 56c460630a | |||
| e1a991ffb7 | |||
| cc9ec0424e | |||
| c91bdbadbf | |||
| 23811ac52f | |||
| e559a15ca6 | |||
| 816c1b3760 | |||
| 819157bf87 | |||
| 86102e88e9 | |||
| e4c071ba19 | |||
| b9d1dd898e | |||
| aaac0e6c98 | |||
| 9443e26349 | |||
| 5a0405df4e | |||
| 0e707aeabc | |||
| 7441dc5e59 | |||
| 1ddac5e02f | |||
| 55484166d8 | |||
| 7181b913f5 | |||
| 81cd5a5c2e | |||
| a735d9bcd4 | |||
| 4b348086a9 | |||
| 436cf8d6ea | |||
| e0aadb573c | |||
| 62a66a8ce9 | |||
| 0c58e4113e | |||
| 9366237c90 | |||
| b36e70495f | |||
| 8423354d7d | |||
| abcca5a0b2 | |||
| b2e1c39c92 | |||
| 8e8a93deae | |||
| 738c849e89 | |||
| 9b58167dc9 | |||
| ffb7e3e14e | |||
| ce378c4cec | |||
| 3e257b0745 | |||
| e5e4491ac5 | |||
| 1bc93f3cdf | |||
| 4fbc5c941a | |||
| ac778acad2 | |||
| 192c26f865 | |||
| 295cbcc4da | |||
| b00a9fd033 | |||
| 4df45177e4 | |||
| 33f1d64363 | |||
| ebe4ac336f | |||
| 94de2cf6d4 | |||
| 64c334f0e5 | |||
| e1bb04bace | |||
| 0388e62759 | |||
| e6257f1117 | |||
| b7db7552c9 | |||
| 2c50af36e2 | |||
| aac665d186 | |||
| bc4be50597 | |||
| 0982f1da89 | |||
| ed96e21792 | |||
| 2ae31879b7 | |||
| 14d8f65f10 | |||
| f47a9aff5e | |||
| ce68daa10e | |||
| a57a4fe011 | |||
| 6c9b023c1b | |||
| 0be752bc46 | |||
| 5c5e91f040 | |||
| 846a0af6ec | |||
| 4b6b45fe18 | |||
| 98a4069558 | |||
| 6e888c882e | |||
| c202c38407 | |||
| d543359fab | |||
| 8bd9e29d3c | |||
| 63c66cf07e | |||
| 556b1c57e7 | |||
| a4276ac40f | |||
| c7340e91af | |||
| d714ea08d6 | |||
| 3fd7f664a6 | |||
| 30995490f2 | |||
| 4beda9cbc0 | |||
| ba462956ea | |||
| cf5d2b8fac | |||
| f9d38b0015 | |||
| 23cfbb3484 | |||
| 83becb7900 | |||
| ef888588c1 | |||
| f05c04b173 | |||
| 518f480528 | |||
| 90be12a59c | |||
| fe1ee8b6b4 | |||
| c89375df9f | |||
| 7cb82599d4 | |||
| 8be69a26ed | |||
| 970264135a | |||
| e5e69a3171 | |||
| 567598fdd7 | |||
| d8b119ca22 | |||
| a438f61f94 | |||
| 388d774221 | |||
| 9b309c5a32 | |||
| 01e1ff7bc0 | |||
| 74ff60c561 | |||
| e98bae803b | |||
| dd553d68ce | |||
| 74dd6f6817 | |||
| 83bca5cb13 | |||
| 2a37ee752b | |||
| ec95a9b114 | |||
| 67cb82d99b | |||
| da6701aea9 | |||
| 593bc7061c | |||
| 120202a68c | |||
| c5278cdd08 | |||
| 46231a7da3 | |||
| 9b7bcb1567 | |||
| 00282d6145 | |||
| b5f31bb5a2 | |||
| 41c667327d | |||
| 2fd9a80bc1 | |||
| ebfda315a5 | |||
| fe93288f85 | |||
| 2d6e5ecd7c | |||
| 49cd1ccc3f | |||
| 3cf35dbd46 | |||
| 845770f824 | |||
| c1bb3fb679 | |||
| 347d2e2a35 | |||
| a78bb30ca0 | |||
| 1fc4ec3274 | |||
| 91842768bd | |||
| 1d5b16aa38 | |||
| 197f5a13a9 | |||
| 8263c4d15d | |||
| 4ee0048304 | |||
| d618dc457e | |||
| 558edc0a3b | |||
| 92b210aced | |||
| 6f6f4e7588 | |||
| 10a3934663 | |||
| 6421989639 | |||
| 07b91eafe5 | |||
| b9fefecf57 | |||
| 9aecf41d21 | |||
| da52b35800 | |||
| 1e0f6285e5 | |||
| dd4e856a4e | |||
| a70804595b | |||
| eb7ed88d77 | |||
| 500d0b42fe | |||
| 82efa3d285 | |||
| 2abc7c46f8 | |||
| e48f1d0c54 | |||
| aa676f3cc7 | |||
| 925ae6978b | |||
| e73063a93c | |||
| c4ae214c26 | |||
| 07242b37a4 | |||
| 01aceeaad6 | |||
| 3d29fb91d1 | |||
| 35e5aac523 | |||
| a3c696e88e | |||
| 91b7bf978c | |||
| b68735e060 | |||
| d5c547c19f | |||
| 9e285cc3fa | |||
| 3f95e2ea4f | |||
| fccc5ee6e6 | |||
| 3d61fe8000 | |||
| 9387bd7ded | |||
| 3e2837ddce | |||
| 3048a9d72c | |||
| 91f9153ca8 | |||
| eef3e92175 | |||
| ed87cc6ce3 | |||
| af45ad8eaa | |||
| 367a6c2423 | |||
| 8f2bd5d713 | |||
| 7654d97eb6 | |||
| 0b2505c754 | |||
| e3549a9c58 | |||
| c5d1911357 | |||
| f8a69bc473 | |||
| 03eca1b0c7 | |||
| 4b12bd3649 | |||
| 6cd8286138 | |||
| 3abbc6c24c | |||
| d594f1e4b3 | |||
| 4a2602dd58 | |||
| 745f129332 | |||
| 7e7e8d4206 | |||
| daa24d7958 | |||
| bda035c74d | |||
| 79acd5bac4 | |||
| b602df7c05 | |||
| 80a66f3298 | |||
| 7756e7c4cb | |||
| 6d774fcb59 | |||
| f05c940e43 | |||
| a71585a450 | |||
| 76ac2fcd59 | |||
| b463992869 | |||
| 39b9640642 | |||
| f0a876ab8b | |||
| 58406447b5 | |||
| 087191dabd | |||
| 862c4e9db4 | |||
| d1abf7dc74 | |||
| 475192a0c3 | |||
| c07c5fc410 | |||
| b820a0ff59 | |||
| 72dfadb0c3 | |||
| edee7cde32 | |||
| 6295f5d25b | |||
| 339dd63bec | |||
| d27bf857e6 | |||
| ec5c955c0a | |||
| 302a5b31d8 | |||
| ab2b23c50d | |||
| 70a01b86d3 | |||
| ff80a82f7c | |||
| 0fc0befe24 | |||
| 7d95d2cec9 | |||
| c42d30607b | |||
| ac58d176ba | |||
| 518ef1b79e | |||
| a502af8759 | |||
| 5905c6b5c1 | |||
| 5e37db2c2b | |||
| d496b07cc0 | |||
| 7a2eceac36 | |||
| fcb8c4ca79 | |||
| 4a793fd385 | |||
| 15e2399826 | |||
| 39a1ec60b2 | |||
| eafa9d39d3 | |||
| daf8bb0060 | |||
| 37f5a85219 | |||
| d1a6612b14 | |||
| 6fcdd6ab0d | |||
| 45a3c7bf81 | |||
| cdd4e107a6 | |||
| 88062ec473 | |||
| 4df5abb3ee | |||
| e19e4261da | |||
| e9b0746ee3 | |||
| 7932c147a0 | |||
| 45d2019161 | |||
| e2eeb0a784 | |||
| 6ce349e08c | |||
| 27f76dfe1a | |||
| 60145e408c | |||
| 8b45e1e356 | |||
| ea81280a4d | |||
| f0b00fd201 | |||
| 4564768940 | |||
| a31b7eca9e | |||
| 9167ce1f3a | |||
| 11601c0d2d | |||
| b9414eecb3 | |||
| 243a8bba07 | |||
| c5e1af5dc3 | |||
| c9af6bb1b3 | |||
| bf4ce74a0f | |||
| db21627995 | |||
| bb2f39337d | |||
| dc4b4dab1d | |||
| 8df94d9879 | |||
| c5035362ab | |||
| 439eaced07 | |||
| 51dc07a106 | |||
| 36b8c66aec | |||
| aa29345ae8 | |||
| e1a7863be6 | |||
| c507914181 | |||
| a65bca60a1 | |||
| a1b068ec31 | |||
| 6e33c78c3d | |||
| 9ac66375a0 | |||
| ddd04b6997 | |||
| 566ea66979 | |||
| 0d74e9080a | |||
| 8dc2d6eb6a | |||
| 3bd746139f | |||
| f915ace085 | |||
| 1bafbed467 | |||
| 98549fbf21 | |||
| b33912b7cc | |||
| cac7db2d19 | |||
| 1c367e0a26 | |||
| 09e59b9a5c | |||
| fecf6cdd59 | |||
| 2b97eb3192 | |||
| ed490dc20d |
+2
-1
@@ -1,3 +1,4 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
packages/*/node_modules/
|
packages/*/node_modules/
|
||||||
packages/*/lib/
|
packages/*/lib/
|
||||||
|
packages/glob/__tests__/_temp
|
||||||
+18
-6
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"plugins": ["jest", "@typescript-eslint"],
|
"plugins": ["jest", "@typescript-eslint"],
|
||||||
"extends": ["plugin:github/es6"],
|
"extends": ["plugin:github/recommended"],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 9,
|
"ecmaVersion": 9,
|
||||||
@@ -9,20 +9,34 @@
|
|||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
"eslint-comments/no-use": "off",
|
"eslint-comments/no-use": "off",
|
||||||
|
"github/no-then": "off",
|
||||||
"import/no-namespace": "off",
|
"import/no-namespace": "off",
|
||||||
|
"no-shadow": "off",
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
|
"no-undef": "off",
|
||||||
"@typescript-eslint/no-unused-vars": "error",
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
|
||||||
"@typescript-eslint/no-require-imports": "error",
|
"@typescript-eslint/no-require-imports": "error",
|
||||||
"@typescript-eslint/array-type": "error",
|
"@typescript-eslint/array-type": "error",
|
||||||
"@typescript-eslint/await-thenable": "error",
|
"@typescript-eslint/await-thenable": "error",
|
||||||
"@typescript-eslint/ban-ts-ignore": "error",
|
"@typescript-eslint/ban-ts-comment": "error",
|
||||||
"camelcase": "off",
|
"camelcase": "off",
|
||||||
"@typescript-eslint/camelcase": "off",
|
"@typescript-eslint/camelcase": "off",
|
||||||
"@typescript-eslint/class-name-casing": "error",
|
"@typescript-eslint/consistent-type-assertions": "off",
|
||||||
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
||||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
||||||
"@typescript-eslint/generic-type-naming": ["error", "^[A-Z][A-Za-z]*$"],
|
"@typescript-eslint/naming-convention": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"format": null,
|
||||||
|
"filter": {
|
||||||
|
// you can expand this regex as you find more cases that require quoting that you want to allow
|
||||||
|
"regex": "^[A-Z][A-Za-z]*$",
|
||||||
|
"match": true
|
||||||
|
},
|
||||||
|
"selector": "memberLike"
|
||||||
|
}
|
||||||
|
],
|
||||||
"@typescript-eslint/no-array-constructor": "error",
|
"@typescript-eslint/no-array-constructor": "error",
|
||||||
"@typescript-eslint/no-empty-interface": "error",
|
"@typescript-eslint/no-empty-interface": "error",
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
@@ -32,7 +46,6 @@
|
|||||||
"@typescript-eslint/no-misused-new": "error",
|
"@typescript-eslint/no-misused-new": "error",
|
||||||
"@typescript-eslint/no-namespace": "error",
|
"@typescript-eslint/no-namespace": "error",
|
||||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||||
"@typescript-eslint/no-object-literal-type-assertion": "error",
|
|
||||||
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
"@typescript-eslint/no-unnecessary-qualifier": "error",
|
||||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||||
"@typescript-eslint/no-useless-constructor": "error",
|
"@typescript-eslint/no-useless-constructor": "error",
|
||||||
@@ -40,7 +53,6 @@
|
|||||||
"@typescript-eslint/prefer-for-of": "warn",
|
"@typescript-eslint/prefer-for-of": "warn",
|
||||||
"@typescript-eslint/prefer-function-type": "warn",
|
"@typescript-eslint/prefer-function-type": "warn",
|
||||||
"@typescript-eslint/prefer-includes": "error",
|
"@typescript-eslint/prefer-includes": "error",
|
||||||
"@typescript-eslint/prefer-interface": "error",
|
|
||||||
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
"@typescript-eslint/prefer-string-starts-ends-with": "error",
|
||||||
"@typescript-eslint/promise-function-async": "error",
|
"@typescript-eslint/promise-function-async": "error",
|
||||||
"@typescript-eslint/require-array-sort-compare": "error",
|
"@typescript-eslint/require-array-sort-compare": "error",
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set Node.js 12.x
|
- name: Set Node.js 16.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 16.x
|
||||||
|
|
||||||
# In order to upload & download artifacts from a shell script, certain env variables need to be set that are only available in the
|
# In order to upload & download artifacts from a shell script, certain env variables need to be set that are only available in the
|
||||||
# node context. This runs a local action that gets and sets the necessary env variables that are needed
|
# node context. This runs a local action that gets and sets the necessary env variables that are needed
|
||||||
@@ -44,24 +44,27 @@ jobs:
|
|||||||
npm ci
|
npm ci
|
||||||
npm run tsc
|
npm run tsc
|
||||||
working-directory: packages/artifact
|
working-directory: packages/artifact
|
||||||
|
|
||||||
- name: Set artifact file contents
|
- name: Set artifact file contents
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
echo "non-gzip-artifact-content=hello" >> $GITHUB_ENV
|
echo "non-gzip-artifact-content=hello" >> $GITHUB_ENV
|
||||||
echo "gzip-artifact-content=Some large amount of text that has a compression ratio that is greater than 100%. If greater than 100%, gzip is used to upload the file" >> $GITHUB_ENV
|
echo "gzip-artifact-content=Some large amount of text that has a compression ratio that is greater than 100%. If greater than 100%, gzip is used to upload the file" >> $GITHUB_ENV
|
||||||
|
echo "empty-artifact-content=_EMPTY_" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Create files that will be uploaded
|
- name: Create files that will be uploaded
|
||||||
run: |
|
run: |
|
||||||
mkdir artifact-path
|
mkdir artifact-path
|
||||||
echo ${{ env.non-gzip-artifact-content }} > artifact-path/world.txt
|
echo '${{ env.non-gzip-artifact-content }}' > artifact-path/world.txt
|
||||||
echo ${{ env.gzip-artifact-content }} > artifact-path/gzip.txt
|
echo '${{ env.gzip-artifact-content }}' > artifact-path/gzip.txt
|
||||||
|
touch artifact-path/empty.txt
|
||||||
|
|
||||||
# We're using node -e to call the functions directly available in the @actions/artifact package
|
# We're using node -e to call the functions directly available in the @actions/artifact package
|
||||||
- name: Upload artifacts using uploadArtifact()
|
- name: Upload artifacts using uploadArtifact()
|
||||||
run: |
|
run: |
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-1',['artifact-path/world.txt'], '${{ github.workspace }}'))"
|
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-1',['artifact-path/world.txt'], process.argv[1]))" "${{ github.workspace }}"
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-2',['artifact-path/gzip.txt'], '${{ github.workspace }}'))"
|
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-2',['artifact-path/gzip.txt'], process.argv[1]))" "${{ github.workspace }}"
|
||||||
|
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-3',['artifact-path/empty.txt'], process.argv[1]))" "${{ github.workspace }}"
|
||||||
|
|
||||||
- name: Download artifacts using downloadArtifact()
|
- name: Download artifacts using downloadArtifact()
|
||||||
run: |
|
run: |
|
||||||
@@ -69,12 +72,15 @@ jobs:
|
|||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-1','artifact-1-directory'))"
|
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-1','artifact-1-directory'))"
|
||||||
mkdir artifact-2-directory
|
mkdir artifact-2-directory
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-2','artifact-2-directory'))"
|
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-2','artifact-2-directory'))"
|
||||||
|
mkdir artifact-3-directory
|
||||||
|
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadArtifact('my-artifact-3','artifact-3-directory'))"
|
||||||
|
|
||||||
- name: Verify downloadArtifact()
|
- name: Verify downloadArtifact()
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "artifact-1-directory/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
|
packages/artifact/__tests__/test-artifact-file.sh "artifact-1-directory/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "artifact-2-directory/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
|
packages/artifact/__tests__/test-artifact-file.sh "artifact-2-directory/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
|
||||||
|
packages/artifact/__tests__/test-artifact-file.sh "artifact-3-directory/artifact-path/empty.txt" "${{ env.empty-artifact-content }}"
|
||||||
|
|
||||||
- name: Download artifacts using downloadAllArtifacts()
|
- name: Download artifacts using downloadAllArtifacts()
|
||||||
run: |
|
run: |
|
||||||
@@ -85,4 +91,5 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-1/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
|
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-1/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-2/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
|
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-2/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
|
||||||
|
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-3/artifact-path/empty.txt" "${{ env.empty-artifact-content }}"
|
||||||
|
|||||||
@@ -18,12 +18,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set Node.js 12.x
|
- name: Set Node.js 16.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 16.x
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install
|
run: npm install
|
||||||
@@ -31,8 +31,8 @@ jobs:
|
|||||||
- name: Bootstrap
|
- name: Bootstrap
|
||||||
run: npm run bootstrap
|
run: npm run bootstrap
|
||||||
|
|
||||||
# - name: audit tools #disabled while we wait for https://github.com/actions/toolkit/issues/539
|
- name: audit tools (without allow-list)
|
||||||
# run: npm audit --audit-level=moderate
|
run: npm audit --audit-level=moderate
|
||||||
|
|
||||||
- name: audit packages
|
- name: audit packages
|
||||||
run: npm run audit-all
|
run: npm run audit-all
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set Node.js 12.x
|
- name: Set Node.js 16.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 16.x
|
||||||
|
|
||||||
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
|
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
|
||||||
# node context. This runs a local action that gets and sets the necessary env variables that are needed
|
# node context. This runs a local action that gets and sets the necessary env variables that are needed
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
name: cache-windows-bsd-unit-tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- '**.md'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- shell: bash
|
||||||
|
run: |
|
||||||
|
rm "C:\Program Files\Git\usr\bin\tar.exe"
|
||||||
|
|
||||||
|
- name: Set Node.js 12.x
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 12.x
|
||||||
|
|
||||||
|
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
|
||||||
|
# node context. This runs a local action that gets and sets the necessary env variables that are needed
|
||||||
|
- name: Set env variables
|
||||||
|
uses: ./packages/cache/__tests__/__fixtures__/
|
||||||
|
|
||||||
|
# Need root node_modules because certain npm packages like jest are configured for the entire repository and it won't be possible
|
||||||
|
# without these to just compile the cache package
|
||||||
|
- name: Install root npm packages
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Compile cache package
|
||||||
|
run: |
|
||||||
|
npm ci
|
||||||
|
npm run tsc
|
||||||
|
working-directory: packages/cache
|
||||||
|
|
||||||
|
- name: Generate files in working directory
|
||||||
|
shell: bash
|
||||||
|
run: packages/cache/__tests__/create-cache-files.sh ${{ runner.os }} test-cache
|
||||||
|
|
||||||
|
- name: Generate files outside working directory
|
||||||
|
shell: bash
|
||||||
|
run: packages/cache/__tests__/create-cache-files.sh ${{ runner.os }} ~/test-cache
|
||||||
|
|
||||||
|
# We're using node -e to call the functions directly available in the @actions/cache package
|
||||||
|
- name: Save cache using saveCache()
|
||||||
|
run: |
|
||||||
|
node -e "Promise.resolve(require('./packages/cache/lib/cache').saveCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}'))"
|
||||||
|
|
||||||
|
- name: Delete cache folders before restoring
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf test-cache
|
||||||
|
rm -rf ~/test-cache
|
||||||
|
|
||||||
|
- name: Restore cache using restoreCache() with http-client
|
||||||
|
run: |
|
||||||
|
node -e "Promise.resolve(require('./packages/cache/lib/cache').restoreCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}',[],{useAzureSdk: false}))"
|
||||||
|
|
||||||
|
- name: Verify cache restored with http-client
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
packages/cache/__tests__/verify-cache-files.sh ${{ runner.os }} test-cache
|
||||||
|
packages/cache/__tests__/verify-cache-files.sh ${{ runner.os }} ~/test-cache
|
||||||
|
|
||||||
|
- name: Delete cache folders before restoring
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
rm -rf test-cache
|
||||||
|
rm -rf ~/test-cache
|
||||||
|
|
||||||
|
- name: Restore cache using restoreCache() with Azure SDK
|
||||||
|
run: |
|
||||||
|
node -e "Promise.resolve(require('./packages/cache/lib/cache').restoreCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}'))"
|
||||||
|
|
||||||
|
- name: Verify cache restored with Azure SDK
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
packages/cache/__tests__/verify-cache-files.sh ${{ runner.os }} test-cache
|
||||||
|
packages/cache/__tests__/verify-cache-files.sh ${{ runner.os }} ~/test-cache
|
||||||
@@ -2,6 +2,8 @@ name: "Code Scanning - Action"
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 0'
|
- cron: '0 0 * * 0'
|
||||||
@@ -18,7 +20,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
|
|||||||
@@ -5,55 +5,60 @@ on:
|
|||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
required: true
|
required: true
|
||||||
description: 'core, artifact, cache, exec, github, glob, io, tool-cache'
|
description: 'core, artifact, cache, exec, github, glob, http-client, io, tool-cache'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: setup repo
|
- name: setup repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: verify package exists
|
- name: verify package exists
|
||||||
run: ls packages/${{ github.event.inputs.package }}
|
run: ls packages/${{ github.event.inputs.package }}
|
||||||
|
|
||||||
|
- name: Set Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|
||||||
- name: bootstrap
|
- name: bootstrap
|
||||||
run: npm run bootstrap
|
run: npm run bootstrap
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: test
|
- name: test
|
||||||
run: npm run test
|
run: npm run test
|
||||||
|
|
||||||
- name: pack
|
- name: pack
|
||||||
run: npm pack
|
run: npm pack
|
||||||
working-directory: packages/${{ github.event.inputs.package }}
|
working-directory: packages/${{ github.event.inputs.package }}
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ github.event.inputs.package }}
|
name: ${{ github.event.inputs.package }}
|
||||||
path: packages/${{ github.event.inputs.package }}/*.tgz
|
path: packages/${{ github.event.inputs.package }}/*.tgz
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
needs: test
|
needs: test
|
||||||
environment: npm-publish
|
environment: npm-publish
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: download artifact
|
- name: download artifact
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ github.event.inputs.package }}
|
name: ${{ github.event.inputs.package }}
|
||||||
|
|
||||||
- name: setup authentication
|
- name: setup authentication
|
||||||
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
|
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
|
||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ secrets.TOKEN }}
|
NPM_TOKEN: ${{ secrets.TOKEN }}
|
||||||
|
|
||||||
- name: publish
|
- name: publish
|
||||||
@@ -63,13 +68,13 @@ jobs:
|
|||||||
if: failure()
|
if: failure()
|
||||||
run: |
|
run: |
|
||||||
curl -X POST -H 'Content-type: application/json' --data '{"text":":pb__failed: Failed to publish a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
|
curl -X POST -H 'Content-type: application/json' --data '{"text":":pb__failed: Failed to publish a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
|
||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK: ${{ secrets.SLACK }}
|
SLACK_WEBHOOK: ${{ secrets.SLACK }}
|
||||||
|
|
||||||
- name: notify slack on success
|
- name: notify slack on success
|
||||||
if: success()
|
if: success()
|
||||||
run: |
|
run: |
|
||||||
curl -X POST -H 'Content-type: application/json' --data '{"text":":dance: Successfully published a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
|
curl -X POST -H 'Content-type: application/json' --data '{"text":":dance: Successfully published a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK
|
||||||
env:
|
env:
|
||||||
SLACK_WEBHOOK: ${{ secrets.SLACK }}
|
SLACK_WEBHOOK: ${{ secrets.SLACK }}
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set Node.js 12.x
|
- name: Set Node.js 16.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 16.x
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install
|
run: npm install
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: npm test
|
- name: npm test
|
||||||
run: npm test
|
run: npm test -- --runInBand
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
if: ${{ github.repository_owner == 'actions' }}
|
if: ${{ github.repository_owner == 'actions' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
- name: Update Octokit
|
- name: Update Octokit
|
||||||
working-directory: packages/github
|
working-directory: packages/github
|
||||||
run: |
|
run: |
|
||||||
@@ -30,7 +30,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Create PR
|
- name: Create PR
|
||||||
if: ${{steps.status.outputs.createPR}}
|
if: ${{steps.status.outputs.createPR}}
|
||||||
uses: actions/github-script@v2
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
* @actions/actions-runtime
|
* @actions/actions-runtime
|
||||||
|
|
||||||
/packages/artifact/ @actions/actions-service
|
/packages/artifact/ @actions/artifacts-actions
|
||||||
/packages/cache/ @actions/actions-service
|
/packages/cache/ @actions/actions-cache
|
||||||
|
|||||||
@@ -46,6 +46,15 @@ $ npm install @actions/glob
|
|||||||
```
|
```
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
|
:phone: [@actions/http-client](packages/http-client)
|
||||||
|
|
||||||
|
A lightweight HTTP client optimized for building actions. Read more [here](packages/http-client)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install @actions/http-client
|
||||||
|
```
|
||||||
|
<br/>
|
||||||
|
|
||||||
:pencil2: [@actions/io](packages/io)
|
:pencil2: [@actions/io](packages/io)
|
||||||
|
|
||||||
Provides disk i/o functions like cp, mv, rmRF, which etc. Read more [here](packages/io)
|
Provides disk i/o functions like cp, mv, rmRF, which etc. Read more [here](packages/io)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
os: [ubuntu-16.04, windows-2019]
|
os: [ubuntu-16.04, windows-2019]
|
||||||
runs-on: ${{matrix.os}}
|
runs-on: ${{matrix.os}}
|
||||||
actions:
|
actions:
|
||||||
- uses: actions/setup-node@v1
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
version: ${{matrix.node}}
|
version: ${{matrix.node}}
|
||||||
- run: |
|
- run: |
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
+15
-1
@@ -50,7 +50,18 @@ function setSecret(secret: string): void {}
|
|||||||
|
|
||||||
Now, future logs containing BAR will be masked. E.g. running `echo "Hello FOO BAR World"` will now print `Hello FOO **** World`.
|
Now, future logs containing BAR will be masked. E.g. running `echo "Hello FOO BAR World"` will now print `Hello FOO **** World`.
|
||||||
|
|
||||||
**WARNING** The add-mask and setSecret commands only support single line secrets. To register a multiline secrets you must register each line individually otherwise it will not be masked.
|
**WARNING** The add-mask and setSecret commands only support single-line
|
||||||
|
secrets or multi-line secrets that have been escaped. `@actions/core`
|
||||||
|
`setSecret` will escape the string you provide by default. When an escaped
|
||||||
|
multi-line string is provided the whole string and each of its lines
|
||||||
|
individually will be masked. For example you can mask `first\nsecond\r\nthird`
|
||||||
|
using:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
echo "::add-mask::first%0Asecond%0D%0Athird"
|
||||||
|
```
|
||||||
|
|
||||||
|
This will mask `first%0Asecond%0D%0Athird`, `first`, `second` and `third`.
|
||||||
|
|
||||||
**WARNING** Do **not** mask short values if you can avoid it, it could render your output unreadable (and future steps' output as well).
|
**WARNING** Do **not** mask short values if you can avoid it, it could render your output unreadable (and future steps' output as well).
|
||||||
For example, if you mask the letter `l`, running `echo "Hello FOO BAR World"` will now print `He*********o FOO BAR Wor****d`
|
For example, if you mask the letter `l`, running `echo "Hello FOO BAR World"` will now print `He*********o FOO BAR Wor****d`
|
||||||
@@ -100,9 +111,12 @@ There are several commands to emit different levels of log output:
|
|||||||
| log level | example usage |
|
| log level | example usage |
|
||||||
|---|---|
|
|---|---|
|
||||||
| [debug](action-debugging.md) | `echo "::debug::My debug message"` |
|
| [debug](action-debugging.md) | `echo "::debug::My debug message"` |
|
||||||
|
| notice | `echo "::notice::My notice message"` |
|
||||||
| warning | `echo "::warning::My warning message"` |
|
| warning | `echo "::warning::My warning message"` |
|
||||||
| error | `echo "::error::My error message"` |
|
| error | `echo "::error::My error message"` |
|
||||||
|
|
||||||
|
Additional syntax options are described at [the workflow command documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-a-debug-message).
|
||||||
|
|
||||||
### Command Echoing
|
### Command Echoing
|
||||||
|
|
||||||
By default, the echoing of commands to stdout only occurs if [Step Debugging is enabled](./action-debugging.md#How-to-Access-Step-Debug-Logs)
|
By default, the echoing of commands to stdout only occurs if [Step Debugging is enabled](./action-debugging.md#How-to-Access-Step-Debug-Logs)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ e.g. To use https://github.com/actions/setup-node, users will author:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
steps:
|
steps:
|
||||||
using: actions/setup-node@v1
|
using: actions/setup-node@v3
|
||||||
```
|
```
|
||||||
|
|
||||||
# Define Metadata
|
# Define Metadata
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Problem Matchers are a way to scan the output of actions for a specified regex p
|
|||||||
|
|
||||||
Currently, GitHub Actions limit the annotation count in a workflow run.
|
Currently, GitHub Actions limit the annotation count in a workflow run.
|
||||||
|
|
||||||
- 10 warning annotations and 10 error annotations per step
|
- 10 warning annotations, 10 error annotations, and 10 notice annotations per step
|
||||||
- 50 annotations per job (sum of annotations from all the steps)
|
- 50 annotations per job (sum of annotations from all the steps)
|
||||||
- 50 annotations per run (separate from the job annotations, these annotations aren’t created by users)
|
- 50 annotations per run (separate from the job annotations, these annotations aren’t created by users)
|
||||||
|
|
||||||
@@ -144,6 +144,6 @@ Use ECMAScript regular expression syntax when testing patterns.
|
|||||||
|
|
||||||
### File property getting dropped
|
### File property getting dropped
|
||||||
|
|
||||||
[Enable debug logging](https://help.github.com/en/actions/configuring-and-managing-workflows/managing-a-workflow-run#enabling-debug-logging) to determine why the file is getting dropped.
|
[Enable debug logging](https://docs.github.com/en/actions/managing-workflow-runs/enabling-debug-logging) to determine why the file is getting dropped.
|
||||||
|
|
||||||
This usually happens when the file does not exist or is not under the workflow repo.
|
This usually happens when the file does not exist or is not under the workflow repo.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Self-hosted runners [can be configured](https://help.github.com/en/actions/hosti
|
|||||||
|
|
||||||
For actions to **just work** behind a proxy server:
|
For actions to **just work** behind a proxy server:
|
||||||
|
|
||||||
1. Use [tool-cache] version >= 1.3.1
|
1. Use [tool-cache](/packages/tool-cache) version >= 1.3.1
|
||||||
2. Optionally use [actions/http-client](https://github.com/actions/http-client)
|
2. Optionally use [actions/http-client](/packages/http-client)
|
||||||
|
|
||||||
If you are using other http clients, refer to the [environment variables set by the runner](https://help.github.com/en/actions/hosting-your-own-runners/using-a-proxy-server-with-self-hosted-runners).
|
If you are using other http clients, refer to the [environment variables set by the runner](https://help.github.com/en/actions/hosting-your-own-runners/using-a-proxy-server-with-self-hosted-runners).
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ module.exports = {
|
|||||||
roots: ['<rootDir>/packages'],
|
roots: ['<rootDir>/packages'],
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
testMatch: ['**/__tests__/*.test.ts'],
|
testMatch: ['**/__tests__/*.test.ts'],
|
||||||
testRunner: 'jest-circus/runner',
|
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.ts$': 'ts-jest'
|
'^.+\\.ts$': 'ts-jest'
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+25459
-15559
File diff suppressed because it is too large
Load Diff
+12
-12
@@ -9,24 +9,24 @@
|
|||||||
"format": "prettier --write packages/**/*.ts",
|
"format": "prettier --write packages/**/*.ts",
|
||||||
"format-check": "prettier --check packages/**/*.ts",
|
"format-check": "prettier --check packages/**/*.ts",
|
||||||
"lint": "eslint packages/**/*.ts",
|
"lint": "eslint packages/**/*.ts",
|
||||||
|
"lint-fix": "eslint packages/**/*.ts --fix",
|
||||||
"new-package": "scripts/create-package",
|
"new-package": "scripts/create-package",
|
||||||
"test": "jest --testTimeout 10000"
|
"test": "jest --testTimeout 10000"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^24.0.11",
|
"@types/jest": "^27.0.2",
|
||||||
"@types/node": "^12.12.47",
|
"@types/node": "^16.18.1",
|
||||||
"@types/signale": "^1.2.1",
|
"@types/signale": "^1.4.1",
|
||||||
"@typescript-eslint/parser": "^2.2.7",
|
"@typescript-eslint/parser": "^4.0.0",
|
||||||
"concurrently": "^4.1.0",
|
"concurrently": "^6.1.0",
|
||||||
"eslint": "^5.16.0",
|
"eslint": "^7.23.0",
|
||||||
"eslint-plugin-github": "^2.0.0",
|
"eslint-plugin-github": "^4.1.3",
|
||||||
"eslint-plugin-jest": "^22.5.1",
|
"eslint-plugin-jest": "^22.21.0",
|
||||||
"flow-bin": "^0.115.0",
|
"flow-bin": "^0.115.0",
|
||||||
"jest": "^25.1.0",
|
"jest": "^27.2.5",
|
||||||
"jest-circus": "^24.7.1",
|
"lerna": "^5.4.0",
|
||||||
"lerna": "^3.18.4",
|
|
||||||
"prettier": "^1.19.1",
|
"prettier": "^1.19.1",
|
||||||
"ts-jest": "^25.4.0",
|
"ts-jest": "^27.0.5",
|
||||||
"typescript": "^3.9.9"
|
"typescript": "^3.9.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,3 +58,39 @@
|
|||||||
|
|
||||||
- Bump @actions/http-client to version 1.0.11 to fix proxy related issues during artifact upload and download
|
- Bump @actions/http-client to version 1.0.11 to fix proxy related issues during artifact upload and download
|
||||||
|
|
||||||
|
### 0.5.2
|
||||||
|
|
||||||
|
- Add HTTP 500 as a retryable status code for artifact upload and download.
|
||||||
|
|
||||||
|
### 0.6.0
|
||||||
|
|
||||||
|
- Support upload from named pipes [#748](https://github.com/actions/toolkit/pull/748)
|
||||||
|
- Fixes to percentage values being greater than 100% when downloading all artifacts [#889](https://github.com/actions/toolkit/pull/889)
|
||||||
|
- Improved logging and output during artifact upload [#949](https://github.com/actions/toolkit/pull/949)
|
||||||
|
- Improvements to client-side validation for certain invalid characters not allowed during upload: [#951](https://github.com/actions/toolkit/pull/951)
|
||||||
|
- Faster upload speeds for certain types of large files by exempting gzip compression [#956](https://github.com/actions/toolkit/pull/956)
|
||||||
|
- More detailed logging when dealing with chunked uploads [#957](https://github.com/actions/toolkit/pull/957)
|
||||||
|
|
||||||
|
### 0.6.1
|
||||||
|
|
||||||
|
- Fix for failing 0 byte file uploads on Windows [#962](https://github.com/actions/toolkit/pull/962)
|
||||||
|
|
||||||
|
### 1.0.0
|
||||||
|
|
||||||
|
- Update `lockfileVersion` to `v2` in `package-lock.json` [#1009](https://github.com/actions/toolkit/pull/1009)
|
||||||
|
|
||||||
|
### 1.0.1
|
||||||
|
|
||||||
|
- Update to v2.0.0 of `@actions/http-client`
|
||||||
|
|
||||||
|
### 1.0.2
|
||||||
|
|
||||||
|
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
|
||||||
|
|
||||||
|
### 1.1.0
|
||||||
|
|
||||||
|
- Add `x-actions-results-crc64` and `x-actions-results-md5` checksum headers on upload [#1063](https://github.com/actions/toolkit/pull/1063)
|
||||||
|
|
||||||
|
### 1.1.1
|
||||||
|
|
||||||
|
- Fixed a bug in Node16 where if an HTTP download finished too quickly (<1ms, e.g. when it's mocked) we attempt to delete a temp file that has not been created yet [#1278](https://github.com/actions/toolkit/pull/1278/commits/b9de68a590daf37c6747e38d3cb4f1dd2cfb791c)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import CRC64, {CRC64DigestEncoding} from '../src/internal/crc64'
|
||||||
|
|
||||||
|
const fixtures = {
|
||||||
|
data:
|
||||||
|
'🚀 👉😎👉 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n',
|
||||||
|
expected: {
|
||||||
|
hex: '846CE4ADAD6223ED',
|
||||||
|
base64: '7SNira3kbIQ=',
|
||||||
|
buffer: Buffer.from([0xed, 0x23, 0x62, 0xad, 0xad, 0xe4, 0x6c, 0x84])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertEncodings(crc: CRC64): void {
|
||||||
|
const encodings = Object.keys(fixtures.expected) as CRC64DigestEncoding[]
|
||||||
|
for (const encoding of encodings) {
|
||||||
|
expect(crc.digest(encoding)).toEqual(fixtures.expected[encoding])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('@actions/artifact/src/internal/crc64', () => {
|
||||||
|
it('CRC64 from string', async () => {
|
||||||
|
const crc = new CRC64()
|
||||||
|
crc.update(fixtures.data)
|
||||||
|
|
||||||
|
assertEncodings(crc)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('CRC64 from buffer', async () => {
|
||||||
|
const crc = new CRC64()
|
||||||
|
const buf = Buffer.from(fixtures.data)
|
||||||
|
crc.update(buf)
|
||||||
|
|
||||||
|
assertEncodings(crc)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('CRC64 from split data', async () => {
|
||||||
|
const crc = new CRC64()
|
||||||
|
const splits = fixtures.data.split('\n').slice(0, -1)
|
||||||
|
for (const split of splits) {
|
||||||
|
crc.update(`${split}\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEncodings(crc)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('flips 64 bits', async () => {
|
||||||
|
const tests = [
|
||||||
|
[BigInt(0), BigInt('0xffffffffffffffff')],
|
||||||
|
[BigInt('0xffffffffffffffff'), BigInt(0)],
|
||||||
|
[BigInt('0xdeadbeef'), BigInt('0xffffffff21524110')]
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const [input, expected] of tests) {
|
||||||
|
expect(CRC64.flip64Bits(input)).toEqual(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -71,7 +71,7 @@ describe('Download Tests', () => {
|
|||||||
setupFailedResponse()
|
setupFailedResponse()
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
const downloadHttpClient = new DownloadHttpClient()
|
||||||
expect(downloadHttpClient.listArtifacts()).rejects.toThrow(
|
expect(downloadHttpClient.listArtifacts()).rejects.toThrow(
|
||||||
'List Artifacts failed: Artifact service responded with 500'
|
'List Artifacts failed: Artifact service responded with 400'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ describe('Download Tests', () => {
|
|||||||
configVariables.getRuntimeUrl()
|
configVariables.getRuntimeUrl()
|
||||||
)
|
)
|
||||||
).rejects.toThrow(
|
).rejects.toThrow(
|
||||||
`Get Container Items failed: Artifact service responded with 500`
|
`Get Container Items failed: Artifact service responded with 400`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -166,7 +166,7 @@ describe('Download Tests', () => {
|
|||||||
it('Test retryable status codes during artifact download', async () => {
|
it('Test retryable status codes during artifact download', async () => {
|
||||||
// The first http response should return a retryable status call while the subsequent call should return a 200 so
|
// The first http response should return a retryable status call while the subsequent call should return a 200 so
|
||||||
// the download should successfully finish
|
// the download should successfully finish
|
||||||
const retryableStatusCodes = [429, 502, 503, 504]
|
const retryableStatusCodes = [429, 500, 502, 503, 504]
|
||||||
for (const statusCode of retryableStatusCodes) {
|
for (const statusCode of retryableStatusCodes) {
|
||||||
const fileContents = Buffer.from('try, try again\n', defaultEncoding)
|
const fileContents = Buffer.from('try, try again\n', defaultEncoding)
|
||||||
const targetPath = path.join(root, `FileC-${statusCode}.txt`)
|
const targetPath = path.join(root, `FileC-${statusCode}.txt`)
|
||||||
@@ -357,7 +357,7 @@ describe('Download Tests', () => {
|
|||||||
plaintext: Buffer | string
|
plaintext: Buffer | string
|
||||||
): Promise<Buffer> {
|
): Promise<Buffer> {
|
||||||
if (isGzip) {
|
if (isGzip) {
|
||||||
return <Buffer>await promisify(gzip)(plaintext)
|
return await promisify(gzip)(plaintext)
|
||||||
} else if (typeof plaintext === 'string') {
|
} else if (typeof plaintext === 'string') {
|
||||||
return Buffer.from(plaintext, defaultEncoding)
|
return Buffer.from(plaintext, defaultEncoding)
|
||||||
} else {
|
} else {
|
||||||
@@ -468,7 +468,7 @@ describe('Download Tests', () => {
|
|||||||
function setupFailedResponse(): void {
|
function setupFailedResponse(): void {
|
||||||
jest.spyOn(HttpClient.prototype, 'get').mockImplementationOnce(async () => {
|
jest.spyOn(HttpClient.prototype, 'get').mockImplementationOnce(async () => {
|
||||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
const mockMessage = new http.IncomingMessage(new net.Socket())
|
||||||
mockMessage.statusCode = 500
|
mockMessage.statusCode = 400
|
||||||
return new Promise<HttpClientResponse>(resolve => {
|
return new Promise<HttpClientResponse>(resolve => {
|
||||||
resolve({
|
resolve({
|
||||||
message: mockMessage,
|
message: mockMessage,
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
checkArtifactName,
|
||||||
|
checkArtifactFilePath
|
||||||
|
} from '../src/internal/path-and-artifact-name-validation'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
|
||||||
|
describe('Path and artifact name validation', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
// mock all output so that there is less noise when running tests
|
||||||
|
jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Check Artifact Name for any invalid characters', () => {
|
||||||
|
const invalidNames = [
|
||||||
|
'my\\artifact',
|
||||||
|
'my/artifact',
|
||||||
|
'my"artifact',
|
||||||
|
'my:artifact',
|
||||||
|
'my<artifact',
|
||||||
|
'my>artifact',
|
||||||
|
'my|artifact',
|
||||||
|
'my*artifact',
|
||||||
|
'my?artifact',
|
||||||
|
''
|
||||||
|
]
|
||||||
|
for (const invalidName of invalidNames) {
|
||||||
|
expect(() => {
|
||||||
|
checkArtifactName(invalidName)
|
||||||
|
}).toThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
const validNames = [
|
||||||
|
'my-normal-artifact',
|
||||||
|
'myNormalArtifact',
|
||||||
|
'm¥ñðrmålÄr†ï£å¢†'
|
||||||
|
]
|
||||||
|
for (const validName of validNames) {
|
||||||
|
expect(() => {
|
||||||
|
checkArtifactName(validName)
|
||||||
|
}).not.toThrow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Check Artifact File Path for any invalid characters', () => {
|
||||||
|
const invalidNames = [
|
||||||
|
'some/invalid"artifact/path',
|
||||||
|
'some/invalid:artifact/path',
|
||||||
|
'some/invalid<artifact/path',
|
||||||
|
'some/invalid>artifact/path',
|
||||||
|
'some/invalid|artifact/path',
|
||||||
|
'some/invalid*artifact/path',
|
||||||
|
'some/invalid?artifact/path',
|
||||||
|
'some/invalid\rartifact/path',
|
||||||
|
'some/invalid\nartifact/path',
|
||||||
|
'some/invalid\r\nartifact/path',
|
||||||
|
''
|
||||||
|
]
|
||||||
|
for (const invalidName of invalidNames) {
|
||||||
|
expect(() => {
|
||||||
|
checkArtifactFilePath(invalidName)
|
||||||
|
}).toThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
const validNames = [
|
||||||
|
'my/perfectly-normal/artifact-path',
|
||||||
|
'my/perfectly\\Normal/Artifact-path',
|
||||||
|
'm¥/ñðrmål/Är†ï£å¢†'
|
||||||
|
]
|
||||||
|
for (const validName of validNames) {
|
||||||
|
expect(() => {
|
||||||
|
checkArtifactFilePath(validName)
|
||||||
|
}).not.toThrow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,7 +3,6 @@ import * as net from 'net'
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as configVariables from '../src/internal/config-variables'
|
import * as configVariables from '../src/internal/config-variables'
|
||||||
import {retry} from '../src/internal/requestUtils'
|
import {retry} from '../src/internal/requestUtils'
|
||||||
import {IHttpClientResponse} from '@actions/http-client/interfaces'
|
|
||||||
import {HttpClientResponse} from '@actions/http-client'
|
import {HttpClientResponse} from '@actions/http-client'
|
||||||
|
|
||||||
jest.mock('../src/internal/config-variables')
|
jest.mock('../src/internal/config-variables')
|
||||||
@@ -42,7 +41,7 @@ async function testRetry(
|
|||||||
|
|
||||||
async function handleResponse(
|
async function handleResponse(
|
||||||
testResponseCode: number | undefined
|
testResponseCode: number | undefined
|
||||||
): Promise<IHttpClientResponse> {
|
): Promise<HttpClientResponse> {
|
||||||
if (!testResponseCode) {
|
if (!testResponseCode) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Test incorrectly set up. reverse.pop() was called too many times so not enough test response codes were supplied'
|
'Test incorrectly set up. reverse.pop() was called too many times so not enough test response codes were supplied'
|
||||||
@@ -72,7 +71,7 @@ async function emptyMockReadBody(): Promise<string> {
|
|||||||
|
|
||||||
async function setupSingleMockResponse(
|
async function setupSingleMockResponse(
|
||||||
statusCode: number
|
statusCode: number
|
||||||
): Promise<IHttpClientResponse> {
|
): Promise<HttpClientResponse> {
|
||||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
const mockMessage = new http.IncomingMessage(new net.Socket())
|
||||||
const mockReadBody = emptyMockReadBody
|
const mockReadBody = emptyMockReadBody
|
||||||
mockMessage.statusCode = statusCode
|
mockMessage.statusCode = statusCode
|
||||||
@@ -107,8 +106,8 @@ test('retry fails after exhausting retries', async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('retry fails after non-retryable status code', async () => {
|
test('retry fails after non-retryable status code', async () => {
|
||||||
await testRetry([500, 200], {
|
await testRetry([400, 200], {
|
||||||
responseCode: 500,
|
responseCode: 400,
|
||||||
errorMessage: 'test failed: Artifact service responded with 500'
|
errorMessage: 'test failed: Artifact service responded with 400'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ if [ ! -f "$path" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
actualContent=$(cat $path)
|
actualContent=$(cat "$path")
|
||||||
if [ "$actualContent" != "$expectedContent" ];then
|
if [ "$expectedContent" == "_EMPTY_" ] && [ ! -s "$path" ]; then
|
||||||
|
exit 0
|
||||||
|
elif [ "$actualContent" != "$expectedContent" ]; then
|
||||||
echo "File contents are not correct, expected $expectedContent, received $actualContent"
|
echo "File contents are not correct, expected $expectedContent, received $actualContent"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as tmp from 'tmp-promise'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as io from '../../io/src/io'
|
||||||
|
import {promises as fs} from 'fs'
|
||||||
|
import {createGZipFileOnDisk} from '../src/internal/upload-gzip'
|
||||||
|
|
||||||
|
const root = path.join(__dirname, '_temp', 'upload-gzip')
|
||||||
|
const tempGzFilePath = path.join(root, 'file.gz')
|
||||||
|
const tempGzipFilePath = path.join(root, 'file.gzip')
|
||||||
|
const tempTgzFilePath = path.join(root, 'file.tgz')
|
||||||
|
const tempTazFilePath = path.join(root, 'file.taz')
|
||||||
|
const tempZFilePath = path.join(root, 'file.Z')
|
||||||
|
const tempTaZFilePath = path.join(root, 'file.taZ')
|
||||||
|
const tempBz2FilePath = path.join(root, 'file.bz2')
|
||||||
|
const tempTbzFilePath = path.join(root, 'file.tbz')
|
||||||
|
const tempTbz2FilePath = path.join(root, 'file.tbz2')
|
||||||
|
const tempTz2FilePath = path.join(root, 'file.tz2')
|
||||||
|
const tempLzFilePath = path.join(root, 'file.lz')
|
||||||
|
const tempLzmaFilePath = path.join(root, 'file.lzma')
|
||||||
|
const tempTlzFilePath = path.join(root, 'file.tlz')
|
||||||
|
const tempLzoFilePath = path.join(root, 'file.lzo')
|
||||||
|
const tempXzFilePath = path.join(root, 'file.xz')
|
||||||
|
const tempTxzFilePath = path.join(root, 'file.txz')
|
||||||
|
const tempZstFilePath = path.join(root, 'file.zst')
|
||||||
|
const tempZstdFilePath = path.join(root, 'file.zstd')
|
||||||
|
const tempTzstFilePath = path.join(root, 'file.tzst')
|
||||||
|
const tempZipFilePath = path.join(root, 'file.zip')
|
||||||
|
const temp7zFilePath = path.join(root, 'file.7z')
|
||||||
|
const tempNormalFilePath = path.join(root, 'file.txt')
|
||||||
|
|
||||||
|
jest.mock('../src/internal/config-variables')
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
// mock all output so that there is less noise when running tests
|
||||||
|
jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'error').mockImplementation(() => {})
|
||||||
|
|
||||||
|
// clear temp directory and create files that will be "uploaded"
|
||||||
|
await io.rmRF(root)
|
||||||
|
await fs.mkdir(path.join(root))
|
||||||
|
await fs.writeFile(tempGzFilePath, 'a file with a .gz file extension')
|
||||||
|
await fs.writeFile(tempGzipFilePath, 'a file with a .gzip file extension')
|
||||||
|
await fs.writeFile(tempTgzFilePath, 'a file with a .tgz file extension')
|
||||||
|
await fs.writeFile(tempTazFilePath, 'a file with a .taz file extension')
|
||||||
|
await fs.writeFile(tempZFilePath, 'a file with a .Z file extension')
|
||||||
|
await fs.writeFile(tempTaZFilePath, 'a file with a .taZ file extension')
|
||||||
|
await fs.writeFile(tempBz2FilePath, 'a file with a .bz2 file extension')
|
||||||
|
await fs.writeFile(tempTbzFilePath, 'a file with a .tbz file extension')
|
||||||
|
await fs.writeFile(tempTbz2FilePath, 'a file with a .tbz2 file extension')
|
||||||
|
await fs.writeFile(tempTz2FilePath, 'a file with a .tz2 file extension')
|
||||||
|
await fs.writeFile(tempLzFilePath, 'a file with a .lz file extension')
|
||||||
|
await fs.writeFile(tempLzmaFilePath, 'a file with a .lzma file extension')
|
||||||
|
await fs.writeFile(tempTlzFilePath, 'a file with a .tlz file extension')
|
||||||
|
await fs.writeFile(tempLzoFilePath, 'a file with a .lzo file extension')
|
||||||
|
await fs.writeFile(tempXzFilePath, 'a file with a .xz file extension')
|
||||||
|
await fs.writeFile(tempTxzFilePath, 'a file with a .txz file extension')
|
||||||
|
await fs.writeFile(tempZstFilePath, 'a file with a .zst file extension')
|
||||||
|
await fs.writeFile(tempZstdFilePath, 'a file with a .zstd file extension')
|
||||||
|
await fs.writeFile(tempTzstFilePath, 'a file with a .tzst file extension')
|
||||||
|
await fs.writeFile(tempZipFilePath, 'a file with a .zip file extension')
|
||||||
|
await fs.writeFile(temp7zFilePath, 'a file with a .7z file extension')
|
||||||
|
await fs.writeFile(tempNormalFilePath, 'a file with a .txt file extension')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Number.MAX_SAFE_INTEGER is returned when an existing compressed file is used', async () => {
|
||||||
|
// create temporary file
|
||||||
|
const tempFile = await tmp.file()
|
||||||
|
|
||||||
|
expect(await createGZipFileOnDisk(tempGzFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempGzipFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempTgzFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempTazFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempZFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempTaZFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempBz2FilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempTbzFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempTbz2FilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempTz2FilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempLzFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempLzmaFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempTlzFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempLzoFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempXzFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempTxzFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempZstFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempZstdFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempTzstFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(tempZipFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(await createGZipFileOnDisk(temp7zFilePath, tempFile.path)).toEqual(
|
||||||
|
Number.MAX_SAFE_INTEGER
|
||||||
|
)
|
||||||
|
expect(
|
||||||
|
await createGZipFileOnDisk(tempNormalFilePath, tempFile.path)
|
||||||
|
).not.toEqual(Number.MAX_SAFE_INTEGER)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('gzip file on disk gets successfully created', async () => {
|
||||||
|
// create temporary file
|
||||||
|
const tempFile = await tmp.file()
|
||||||
|
|
||||||
|
const gzipFileSize = await createGZipFileOnDisk(
|
||||||
|
tempNormalFilePath,
|
||||||
|
tempFile.path
|
||||||
|
)
|
||||||
|
const fileStat = await fs.stat(tempNormalFilePath)
|
||||||
|
const totalFileSize = fileStat.size
|
||||||
|
|
||||||
|
// original file and gzip file should not be equal in size
|
||||||
|
expect(gzipFileSize).not.toEqual(totalFileSize)
|
||||||
|
})
|
||||||
@@ -2,6 +2,10 @@ import * as http from 'http'
|
|||||||
import * as io from '../../io/src/io'
|
import * as io from '../../io/src/io'
|
||||||
import * as net from 'net'
|
import * as net from 'net'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
import {mocked} from 'ts-jest/utils'
|
||||||
|
import {exec, execSync} from 'child_process'
|
||||||
|
import {createGunzip} from 'zlib'
|
||||||
|
import {promisify} from 'util'
|
||||||
import {UploadHttpClient} from '../src/internal/upload-http-client'
|
import {UploadHttpClient} from '../src/internal/upload-http-client'
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {promises as fs} from 'fs'
|
import {promises as fs} from 'fs'
|
||||||
@@ -174,6 +178,59 @@ describe('Upload Tests', () => {
|
|||||||
expect(uploadResult.uploadSize).toEqual(expectedTotalSize)
|
expect(uploadResult.uploadSize).toEqual(expectedTotalSize)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function hasMkfifo(): boolean {
|
||||||
|
try {
|
||||||
|
// make sure we drain the stdout
|
||||||
|
return (
|
||||||
|
process.platform !== 'win32' &&
|
||||||
|
execSync('which mkfifo').toString().length > 0
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const withMkfifoIt = hasMkfifo() ? it : it.skip
|
||||||
|
withMkfifoIt(
|
||||||
|
'Upload Artifact with content from named pipe - Success',
|
||||||
|
async () => {
|
||||||
|
// create a named pipe 'pipe' with content 'hello pipe'
|
||||||
|
const content = Buffer.from('hello pipe')
|
||||||
|
const pipeFilePath = path.join(root, 'pipe')
|
||||||
|
await promisify(exec)('mkfifo pipe', {cwd: root})
|
||||||
|
// don't want to await here as that would block until read
|
||||||
|
fs.writeFile(pipeFilePath, content)
|
||||||
|
|
||||||
|
const artifactName = 'successful-artifact'
|
||||||
|
const uploadSpecification: UploadSpecification[] = [
|
||||||
|
{
|
||||||
|
absoluteFilePath: pipeFilePath,
|
||||||
|
uploadFilePath: `${artifactName}/pipe`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const uploadUrl = `${getRuntimeUrl()}_apis/resources/Containers/13`
|
||||||
|
const uploadHttpClient = new UploadHttpClient()
|
||||||
|
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
||||||
|
uploadUrl,
|
||||||
|
uploadSpecification
|
||||||
|
)
|
||||||
|
|
||||||
|
// accesses the ReadableStream that was passed into sendStream
|
||||||
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
|
const stream = mocked(HttpClient.prototype.sendStream).mock.calls[0][2]
|
||||||
|
expect(stream).not.toBeNull()
|
||||||
|
// decompresses the passed stream
|
||||||
|
const data: Buffer[] = []
|
||||||
|
for await (const chunk of stream.pipe(createGunzip())) {
|
||||||
|
data.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string))
|
||||||
|
}
|
||||||
|
const uploaded = Buffer.concat(data)
|
||||||
|
|
||||||
|
expect(uploadResult.failedItems.length).toEqual(0)
|
||||||
|
expect(uploaded).toEqual(content)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
it('Upload Artifact - Failed Single File Upload', async () => {
|
it('Upload Artifact - Failed Single File Upload', async () => {
|
||||||
const uploadSpecification: UploadSpecification[] = [
|
const uploadSpecification: UploadSpecification[] = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getInitialRetryIntervalInMilliseconds,
|
getInitialRetryIntervalInMilliseconds,
|
||||||
getRetryMultiplier
|
getRetryMultiplier
|
||||||
} from '../src/internal/config-variables'
|
} from '../src/internal/config-variables'
|
||||||
|
import {Readable} from 'stream'
|
||||||
|
|
||||||
jest.mock('../src/internal/config-variables')
|
jest.mock('../src/internal/config-variables')
|
||||||
|
|
||||||
@@ -46,66 +47,6 @@ describe('Utils', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Check Artifact Name for any invalid characters', () => {
|
|
||||||
const invalidNames = [
|
|
||||||
'my\\artifact',
|
|
||||||
'my/artifact',
|
|
||||||
'my"artifact',
|
|
||||||
'my:artifact',
|
|
||||||
'my<artifact',
|
|
||||||
'my>artifact',
|
|
||||||
'my|artifact',
|
|
||||||
'my*artifact',
|
|
||||||
'my?artifact',
|
|
||||||
''
|
|
||||||
]
|
|
||||||
for (const invalidName of invalidNames) {
|
|
||||||
expect(() => {
|
|
||||||
utils.checkArtifactName(invalidName)
|
|
||||||
}).toThrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
const validNames = [
|
|
||||||
'my-normal-artifact',
|
|
||||||
'myNormalArtifact',
|
|
||||||
'm¥ñðrmålÄr†ï£å¢†'
|
|
||||||
]
|
|
||||||
for (const validName of validNames) {
|
|
||||||
expect(() => {
|
|
||||||
utils.checkArtifactName(validName)
|
|
||||||
}).not.toThrow()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Check Artifact File Path for any invalid characters', () => {
|
|
||||||
const invalidNames = [
|
|
||||||
'some/invalid"artifact/path',
|
|
||||||
'some/invalid:artifact/path',
|
|
||||||
'some/invalid<artifact/path',
|
|
||||||
'some/invalid>artifact/path',
|
|
||||||
'some/invalid|artifact/path',
|
|
||||||
'some/invalid*artifact/path',
|
|
||||||
'some/invalid?artifact/path',
|
|
||||||
''
|
|
||||||
]
|
|
||||||
for (const invalidName of invalidNames) {
|
|
||||||
expect(() => {
|
|
||||||
utils.checkArtifactFilePath(invalidName)
|
|
||||||
}).toThrow()
|
|
||||||
}
|
|
||||||
|
|
||||||
const validNames = [
|
|
||||||
'my/perfectly-normal/artifact-path',
|
|
||||||
'my/perfectly\\Normal/Artifact-path',
|
|
||||||
'm¥/ñðrmål/Är†ï£å¢†'
|
|
||||||
]
|
|
||||||
for (const validName of validNames) {
|
|
||||||
expect(() => {
|
|
||||||
utils.checkArtifactFilePath(validName)
|
|
||||||
}).not.toThrow()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test negative artifact retention throws', () => {
|
it('Test negative artifact retention throws', () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
utils.getProperRetention(-1, undefined)
|
utils.getProperRetention(-1, undefined)
|
||||||
@@ -134,15 +75,20 @@ describe('Utils', () => {
|
|||||||
const size = 24
|
const size = 24
|
||||||
const uncompressedLength = 100
|
const uncompressedLength = 100
|
||||||
const range = 'bytes 0-199/200'
|
const range = 'bytes 0-199/200'
|
||||||
|
const digest = {
|
||||||
|
crc64: 'bSzITYnW/P8=',
|
||||||
|
md5: 'Xiv1fT9AxLbfadrxk2y3ZvgyN0tPwCWafL/wbi9w8mk='
|
||||||
|
}
|
||||||
const headers = utils.getUploadHeaders(
|
const headers = utils.getUploadHeaders(
|
||||||
contentType,
|
contentType,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
uncompressedLength,
|
uncompressedLength,
|
||||||
size,
|
size,
|
||||||
range
|
range,
|
||||||
|
digest
|
||||||
)
|
)
|
||||||
expect(Object.keys(headers).length).toEqual(8)
|
expect(Object.keys(headers).length).toEqual(10)
|
||||||
expect(headers['Accept']).toEqual(
|
expect(headers['Accept']).toEqual(
|
||||||
`application/json;api-version=${utils.getApiVersion()}`
|
`application/json;api-version=${utils.getApiVersion()}`
|
||||||
)
|
)
|
||||||
@@ -153,6 +99,8 @@ describe('Utils', () => {
|
|||||||
expect(headers['x-tfs-filelength']).toEqual(uncompressedLength)
|
expect(headers['x-tfs-filelength']).toEqual(uncompressedLength)
|
||||||
expect(headers['Content-Length']).toEqual(size)
|
expect(headers['Content-Length']).toEqual(size)
|
||||||
expect(headers['Content-Range']).toEqual(range)
|
expect(headers['Content-Range']).toEqual(range)
|
||||||
|
expect(headers['x-actions-results-crc64']).toEqual(digest.crc64)
|
||||||
|
expect(headers['x-actions-results-md5']).toEqual(digest.md5)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Test constructing upload headers with only required parameter', () => {
|
it('Test constructing upload headers with only required parameter', () => {
|
||||||
@@ -279,4 +227,13 @@ describe('Utils', () => {
|
|||||||
const size2 = (await fs.promises.stat(emptyFile2)).size
|
const size2 = (await fs.promises.stat(emptyFile2)).size
|
||||||
expect(size2).toEqual(0)
|
expect(size2).toEqual(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Creates a digest from a readable stream', async () => {
|
||||||
|
const data = 'lorem ipsum'
|
||||||
|
const stream = Readable.from(data)
|
||||||
|
const digest = await utils.digestForStream(stream)
|
||||||
|
|
||||||
|
expect(digest.crc64).toBe('bSzITYnW/P8=')
|
||||||
|
expect(digest.md5).toBe('gKdR/eV3AoZAxBkADjPrpg==')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Generated
+236
-35
@@ -1,31 +1,227 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/artifact",
|
"name": "@actions/artifact",
|
||||||
"version": "0.5.1",
|
"version": "1.1.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@actions/artifact",
|
||||||
|
"version": "1.1.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/core": "^1.9.1",
|
||||||
|
"@actions/http-client": "^2.0.1",
|
||||||
|
"tmp": "^0.2.1",
|
||||||
|
"tmp-promise": "^3.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/tmp": "^0.2.1",
|
||||||
|
"typescript": "^3.8.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/core": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/http-client": "^2.0.1",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/http-client": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tunnel": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/tmp": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz",
|
||||||
|
"integrity": "sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||||
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||||
|
},
|
||||||
|
"node_modules/fs.realpath": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||||
|
},
|
||||||
|
"node_modules/glob": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.0.4",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rimraf": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^7.1.3"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"rimraf": "bin.js"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tmp": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"rimraf": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.17.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tmp-promise": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"tmp": "^0.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tunnel": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "3.9.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
|
||||||
|
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": {
|
"@actions/core": {
|
||||||
"version": "1.2.6",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz",
|
||||||
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
|
"integrity": "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==",
|
||||||
|
"requires": {
|
||||||
|
"@actions/http-client": "^2.0.1",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"@actions/http-client": {
|
"@actions/http-client": {
|
||||||
"version": "1.0.11",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
||||||
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
|
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tunnel": "0.0.6"
|
"tunnel": "^0.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/tmp": {
|
"@types/tmp": {
|
||||||
"version": "0.1.0",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz",
|
||||||
"integrity": "sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA=="
|
"integrity": "sha512-dDZH/tXzwjutnuk4UacGgFRwV+JSLaXL1ikvidfJprkb7L9Nx1njcRHHmi3Dsvt7pgqqTEeucQuOrWHPFgzVHA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||||
},
|
},
|
||||||
"brace-expansion": {
|
"brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
@@ -47,9 +243,9 @@
|
|||||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||||
},
|
},
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.6",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
|
||||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"fs.realpath": "^1.0.0",
|
"fs.realpath": "^1.0.0",
|
||||||
"inflight": "^1.0.4",
|
"inflight": "^1.0.4",
|
||||||
@@ -74,9 +270,9 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||||
},
|
},
|
||||||
"minimatch": {
|
"minimatch": {
|
||||||
"version": "3.0.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
@@ -95,27 +291,27 @@
|
|||||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||||
},
|
},
|
||||||
"rimraf": {
|
"rimraf": {
|
||||||
"version": "2.7.1",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
|
||||||
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
|
"integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"glob": "^7.1.3"
|
"glob": "^7.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tmp": {
|
"tmp": {
|
||||||
"version": "0.1.0",
|
"version": "0.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
|
||||||
"integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==",
|
"integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"rimraf": "^2.6.3"
|
"rimraf": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tmp-promise": {
|
"tmp-promise": {
|
||||||
"version": "2.0.2",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz",
|
||||||
"integrity": "sha512-zl71nFWjPKW2KXs+73gEk8RmqvtAeXPxhWDkTUoa3MSMkjq3I+9OeknjF178MQoMYsdqL730hfzvNfEkePxq9Q==",
|
"integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tmp": "0.1.0"
|
"tmp": "^0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tunnel": {
|
"tunnel": {
|
||||||
@@ -124,11 +320,16 @@
|
|||||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
||||||
},
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "3.8.3",
|
"version": "3.9.10",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
|
||||||
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
|
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||||
|
},
|
||||||
"wrappy": {
|
"wrappy": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/artifact",
|
"name": "@actions/artifact",
|
||||||
"version": "0.5.1",
|
"version": "1.1.1",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"description": "Actions artifact lib",
|
"description": "Actions artifact lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -37,13 +37,13 @@
|
|||||||
"url": "https://github.com/actions/toolkit/issues"
|
"url": "https://github.com/actions/toolkit/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.2.6",
|
"@actions/core": "^1.9.1",
|
||||||
"@actions/http-client": "^1.0.11",
|
"@actions/http-client": "^2.0.1",
|
||||||
"@types/tmp": "^0.1.0",
|
"tmp": "^0.2.1",
|
||||||
"tmp": "^0.1.0",
|
"tmp-promise": "^3.0.2"
|
||||||
"tmp-promise": "^2.0.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/tmp": "^0.2.1",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^3.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import {UploadOptions} from './upload-options'
|
|||||||
import {DownloadOptions} from './download-options'
|
import {DownloadOptions} from './download-options'
|
||||||
import {DownloadResponse} from './download-response'
|
import {DownloadResponse} from './download-response'
|
||||||
import {
|
import {
|
||||||
checkArtifactName,
|
|
||||||
createDirectoriesForArtifact,
|
createDirectoriesForArtifact,
|
||||||
createEmptyFilesForArtifact
|
createEmptyFilesForArtifact
|
||||||
} from './utils'
|
} from './utils'
|
||||||
|
import {checkArtifactName} from './path-and-artifact-name-validation'
|
||||||
import {DownloadHttpClient} from './download-http-client'
|
import {DownloadHttpClient} from './download-http-client'
|
||||||
import {getDownloadSpecification} from './download-specification'
|
import {getDownloadSpecification} from './download-specification'
|
||||||
import {getWorkSpaceDirectory} from './config-variables'
|
import {getWorkSpaceDirectory} from './config-variables'
|
||||||
@@ -72,6 +72,10 @@ export class DefaultArtifactClient implements ArtifactClient {
|
|||||||
rootDirectory: string,
|
rootDirectory: string,
|
||||||
options?: UploadOptions | undefined
|
options?: UploadOptions | undefined
|
||||||
): Promise<UploadResponse> {
|
): Promise<UploadResponse> {
|
||||||
|
core.info(
|
||||||
|
`Starting artifact upload
|
||||||
|
For more detailed logs during the artifact upload process, enable step-debugging: https://docs.github.com/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging#enabling-step-debug-logging`
|
||||||
|
)
|
||||||
checkArtifactName(name)
|
checkArtifactName(name)
|
||||||
|
|
||||||
// Get specification for the files being uploaded
|
// Get specification for the files being uploaded
|
||||||
@@ -103,7 +107,11 @@ export class DefaultArtifactClient implements ArtifactClient {
|
|||||||
'No URL provided by the Artifact Service to upload an artifact to'
|
'No URL provided by the Artifact Service to upload an artifact to'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
core.debug(`Upload Resource URL: ${response.fileContainerResourceUrl}`)
|
core.debug(`Upload Resource URL: ${response.fileContainerResourceUrl}`)
|
||||||
|
core.info(
|
||||||
|
`Container for artifact "${name}" successfully created. Starting upload of file(s)`
|
||||||
|
)
|
||||||
|
|
||||||
// Upload each of the files that were found concurrently
|
// Upload each of the files that were found concurrently
|
||||||
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
||||||
@@ -114,10 +122,27 @@ export class DefaultArtifactClient implements ArtifactClient {
|
|||||||
|
|
||||||
// Update the size of the artifact to indicate we are done uploading
|
// Update the size of the artifact to indicate we are done uploading
|
||||||
// The uncompressed size is used for display when downloading a zip of the artifact from the UI
|
// The uncompressed size is used for display when downloading a zip of the artifact from the UI
|
||||||
|
core.info(
|
||||||
|
`File upload process has finished. Finalizing the artifact upload`
|
||||||
|
)
|
||||||
await uploadHttpClient.patchArtifactSize(uploadResult.totalSize, name)
|
await uploadHttpClient.patchArtifactSize(uploadResult.totalSize, name)
|
||||||
|
|
||||||
|
if (uploadResult.failedItems.length > 0) {
|
||||||
|
core.info(
|
||||||
|
`Upload finished. There were ${uploadResult.failedItems.length} items that failed to upload`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
core.info(
|
||||||
|
`Artifact has been finalized. All files have been successfully uploaded!`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
core.info(
|
core.info(
|
||||||
`Finished uploading artifact ${name}. Reported size is ${uploadResult.uploadSize} bytes. There were ${uploadResult.failedItems.length} items that failed to upload`
|
`
|
||||||
|
The raw size of all the files that were specified for upload is ${uploadResult.totalSize} bytes
|
||||||
|
The size of all the files that were uploaded is ${uploadResult.uploadSize} bytes. This takes into account any gzip compression used to reduce the upload size, time and storage
|
||||||
|
|
||||||
|
Note: The size of downloaded zips can differ significantly from the reported size. For more information see: https://github.com/actions/upload-artifact#zipped-artifact-downloads \r\n`
|
||||||
)
|
)
|
||||||
|
|
||||||
uploadResponse.artifactItems = uploadSpecification.map(
|
uploadResponse.artifactItems = uploadSpecification.map(
|
||||||
@@ -178,7 +203,7 @@ export class DefaultArtifactClient implements ArtifactClient {
|
|||||||
await createDirectoriesForArtifact(
|
await createDirectoriesForArtifact(
|
||||||
downloadSpecification.directoryStructure
|
downloadSpecification.directoryStructure
|
||||||
)
|
)
|
||||||
core.info('Directory structure has been setup for the artifact')
|
core.info('Directory structure has been set up for the artifact')
|
||||||
await createEmptyFilesForArtifact(
|
await createEmptyFilesForArtifact(
|
||||||
downloadSpecification.emptyFilesToCreate
|
downloadSpecification.emptyFilesToCreate
|
||||||
)
|
)
|
||||||
@@ -215,6 +240,9 @@ export class DefaultArtifactClient implements ArtifactClient {
|
|||||||
while (downloadedArtifacts < artifacts.count) {
|
while (downloadedArtifacts < artifacts.count) {
|
||||||
const currentArtifactToDownload = artifacts.value[downloadedArtifacts]
|
const currentArtifactToDownload = artifacts.value[downloadedArtifacts]
|
||||||
downloadedArtifacts += 1
|
downloadedArtifacts += 1
|
||||||
|
core.info(
|
||||||
|
`starting download of artifact ${currentArtifactToDownload.name} : ${downloadedArtifacts}/${artifacts.count}`
|
||||||
|
)
|
||||||
|
|
||||||
// Get container entries for the specific artifact
|
// Get container entries for the specific artifact
|
||||||
const items = await downloadHttpClient.getContainerItems(
|
const items = await downloadHttpClient.getContainerItems(
|
||||||
|
|||||||
@@ -29,8 +29,20 @@ export interface PatchArtifactSizeSuccessResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadResults {
|
export interface UploadResults {
|
||||||
|
/**
|
||||||
|
* The size in bytes of data that was transferred during the upload process to the actions backend service. This takes into account possible
|
||||||
|
* gzip compression to reduce the amount of data that needs to be transferred
|
||||||
|
*/
|
||||||
uploadSize: number
|
uploadSize: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raw size of the files that were specified for upload
|
||||||
|
*/
|
||||||
totalSize: number
|
totalSize: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of files that failed to upload
|
||||||
|
*/
|
||||||
failedItems: string[]
|
failedItems: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
/**
|
||||||
|
* CRC64: cyclic redundancy check, 64-bits
|
||||||
|
*
|
||||||
|
* In order to validate that artifacts are not being corrupted over the wire, this redundancy check allows us to
|
||||||
|
* validate that there was no corruption during transmission. The implementation here is based on Go's hash/crc64 pkg,
|
||||||
|
* but without the slicing-by-8 optimization: https://cs.opensource.google/go/go/+/master:src/hash/crc64/crc64.go
|
||||||
|
*
|
||||||
|
* This implementation uses a pregenerated table based on 0x9A6C9329AC4BC9B5 as the polynomial, the same polynomial that
|
||||||
|
* is used for Azure Storage: https://github.com/Azure/azure-storage-net/blob/cbe605f9faa01bfc3003d75fc5a16b2eaccfe102/Lib/Common/Core/Util/Crc64.cs#L27
|
||||||
|
*/
|
||||||
|
|
||||||
|
// when transpile target is >= ES2020 (after dropping node 12) these can be changed to bigint literals - ts(2737)
|
||||||
|
const PREGEN_POLY_TABLE = [
|
||||||
|
BigInt('0x0000000000000000'),
|
||||||
|
BigInt('0x7F6EF0C830358979'),
|
||||||
|
BigInt('0xFEDDE190606B12F2'),
|
||||||
|
BigInt('0x81B31158505E9B8B'),
|
||||||
|
BigInt('0xC962E5739841B68F'),
|
||||||
|
BigInt('0xB60C15BBA8743FF6'),
|
||||||
|
BigInt('0x37BF04E3F82AA47D'),
|
||||||
|
BigInt('0x48D1F42BC81F2D04'),
|
||||||
|
BigInt('0xA61CECB46814FE75'),
|
||||||
|
BigInt('0xD9721C7C5821770C'),
|
||||||
|
BigInt('0x58C10D24087FEC87'),
|
||||||
|
BigInt('0x27AFFDEC384A65FE'),
|
||||||
|
BigInt('0x6F7E09C7F05548FA'),
|
||||||
|
BigInt('0x1010F90FC060C183'),
|
||||||
|
BigInt('0x91A3E857903E5A08'),
|
||||||
|
BigInt('0xEECD189FA00BD371'),
|
||||||
|
BigInt('0x78E0FF3B88BE6F81'),
|
||||||
|
BigInt('0x078E0FF3B88BE6F8'),
|
||||||
|
BigInt('0x863D1EABE8D57D73'),
|
||||||
|
BigInt('0xF953EE63D8E0F40A'),
|
||||||
|
BigInt('0xB1821A4810FFD90E'),
|
||||||
|
BigInt('0xCEECEA8020CA5077'),
|
||||||
|
BigInt('0x4F5FFBD87094CBFC'),
|
||||||
|
BigInt('0x30310B1040A14285'),
|
||||||
|
BigInt('0xDEFC138FE0AA91F4'),
|
||||||
|
BigInt('0xA192E347D09F188D'),
|
||||||
|
BigInt('0x2021F21F80C18306'),
|
||||||
|
BigInt('0x5F4F02D7B0F40A7F'),
|
||||||
|
BigInt('0x179EF6FC78EB277B'),
|
||||||
|
BigInt('0x68F0063448DEAE02'),
|
||||||
|
BigInt('0xE943176C18803589'),
|
||||||
|
BigInt('0x962DE7A428B5BCF0'),
|
||||||
|
BigInt('0xF1C1FE77117CDF02'),
|
||||||
|
BigInt('0x8EAF0EBF2149567B'),
|
||||||
|
BigInt('0x0F1C1FE77117CDF0'),
|
||||||
|
BigInt('0x7072EF2F41224489'),
|
||||||
|
BigInt('0x38A31B04893D698D'),
|
||||||
|
BigInt('0x47CDEBCCB908E0F4'),
|
||||||
|
BigInt('0xC67EFA94E9567B7F'),
|
||||||
|
BigInt('0xB9100A5CD963F206'),
|
||||||
|
BigInt('0x57DD12C379682177'),
|
||||||
|
BigInt('0x28B3E20B495DA80E'),
|
||||||
|
BigInt('0xA900F35319033385'),
|
||||||
|
BigInt('0xD66E039B2936BAFC'),
|
||||||
|
BigInt('0x9EBFF7B0E12997F8'),
|
||||||
|
BigInt('0xE1D10778D11C1E81'),
|
||||||
|
BigInt('0x606216208142850A'),
|
||||||
|
BigInt('0x1F0CE6E8B1770C73'),
|
||||||
|
BigInt('0x8921014C99C2B083'),
|
||||||
|
BigInt('0xF64FF184A9F739FA'),
|
||||||
|
BigInt('0x77FCE0DCF9A9A271'),
|
||||||
|
BigInt('0x08921014C99C2B08'),
|
||||||
|
BigInt('0x4043E43F0183060C'),
|
||||||
|
BigInt('0x3F2D14F731B68F75'),
|
||||||
|
BigInt('0xBE9E05AF61E814FE'),
|
||||||
|
BigInt('0xC1F0F56751DD9D87'),
|
||||||
|
BigInt('0x2F3DEDF8F1D64EF6'),
|
||||||
|
BigInt('0x50531D30C1E3C78F'),
|
||||||
|
BigInt('0xD1E00C6891BD5C04'),
|
||||||
|
BigInt('0xAE8EFCA0A188D57D'),
|
||||||
|
BigInt('0xE65F088B6997F879'),
|
||||||
|
BigInt('0x9931F84359A27100'),
|
||||||
|
BigInt('0x1882E91B09FCEA8B'),
|
||||||
|
BigInt('0x67EC19D339C963F2'),
|
||||||
|
BigInt('0xD75ADABD7A6E2D6F'),
|
||||||
|
BigInt('0xA8342A754A5BA416'),
|
||||||
|
BigInt('0x29873B2D1A053F9D'),
|
||||||
|
BigInt('0x56E9CBE52A30B6E4'),
|
||||||
|
BigInt('0x1E383FCEE22F9BE0'),
|
||||||
|
BigInt('0x6156CF06D21A1299'),
|
||||||
|
BigInt('0xE0E5DE5E82448912'),
|
||||||
|
BigInt('0x9F8B2E96B271006B'),
|
||||||
|
BigInt('0x71463609127AD31A'),
|
||||||
|
BigInt('0x0E28C6C1224F5A63'),
|
||||||
|
BigInt('0x8F9BD7997211C1E8'),
|
||||||
|
BigInt('0xF0F5275142244891'),
|
||||||
|
BigInt('0xB824D37A8A3B6595'),
|
||||||
|
BigInt('0xC74A23B2BA0EECEC'),
|
||||||
|
BigInt('0x46F932EAEA507767'),
|
||||||
|
BigInt('0x3997C222DA65FE1E'),
|
||||||
|
BigInt('0xAFBA2586F2D042EE'),
|
||||||
|
BigInt('0xD0D4D54EC2E5CB97'),
|
||||||
|
BigInt('0x5167C41692BB501C'),
|
||||||
|
BigInt('0x2E0934DEA28ED965'),
|
||||||
|
BigInt('0x66D8C0F56A91F461'),
|
||||||
|
BigInt('0x19B6303D5AA47D18'),
|
||||||
|
BigInt('0x980521650AFAE693'),
|
||||||
|
BigInt('0xE76BD1AD3ACF6FEA'),
|
||||||
|
BigInt('0x09A6C9329AC4BC9B'),
|
||||||
|
BigInt('0x76C839FAAAF135E2'),
|
||||||
|
BigInt('0xF77B28A2FAAFAE69'),
|
||||||
|
BigInt('0x8815D86ACA9A2710'),
|
||||||
|
BigInt('0xC0C42C4102850A14'),
|
||||||
|
BigInt('0xBFAADC8932B0836D'),
|
||||||
|
BigInt('0x3E19CDD162EE18E6'),
|
||||||
|
BigInt('0x41773D1952DB919F'),
|
||||||
|
BigInt('0x269B24CA6B12F26D'),
|
||||||
|
BigInt('0x59F5D4025B277B14'),
|
||||||
|
BigInt('0xD846C55A0B79E09F'),
|
||||||
|
BigInt('0xA72835923B4C69E6'),
|
||||||
|
BigInt('0xEFF9C1B9F35344E2'),
|
||||||
|
BigInt('0x90973171C366CD9B'),
|
||||||
|
BigInt('0x1124202993385610'),
|
||||||
|
BigInt('0x6E4AD0E1A30DDF69'),
|
||||||
|
BigInt('0x8087C87E03060C18'),
|
||||||
|
BigInt('0xFFE938B633338561'),
|
||||||
|
BigInt('0x7E5A29EE636D1EEA'),
|
||||||
|
BigInt('0x0134D92653589793'),
|
||||||
|
BigInt('0x49E52D0D9B47BA97'),
|
||||||
|
BigInt('0x368BDDC5AB7233EE'),
|
||||||
|
BigInt('0xB738CC9DFB2CA865'),
|
||||||
|
BigInt('0xC8563C55CB19211C'),
|
||||||
|
BigInt('0x5E7BDBF1E3AC9DEC'),
|
||||||
|
BigInt('0x21152B39D3991495'),
|
||||||
|
BigInt('0xA0A63A6183C78F1E'),
|
||||||
|
BigInt('0xDFC8CAA9B3F20667'),
|
||||||
|
BigInt('0x97193E827BED2B63'),
|
||||||
|
BigInt('0xE877CE4A4BD8A21A'),
|
||||||
|
BigInt('0x69C4DF121B863991'),
|
||||||
|
BigInt('0x16AA2FDA2BB3B0E8'),
|
||||||
|
BigInt('0xF86737458BB86399'),
|
||||||
|
BigInt('0x8709C78DBB8DEAE0'),
|
||||||
|
BigInt('0x06BAD6D5EBD3716B'),
|
||||||
|
BigInt('0x79D4261DDBE6F812'),
|
||||||
|
BigInt('0x3105D23613F9D516'),
|
||||||
|
BigInt('0x4E6B22FE23CC5C6F'),
|
||||||
|
BigInt('0xCFD833A67392C7E4'),
|
||||||
|
BigInt('0xB0B6C36E43A74E9D'),
|
||||||
|
BigInt('0x9A6C9329AC4BC9B5'),
|
||||||
|
BigInt('0xE50263E19C7E40CC'),
|
||||||
|
BigInt('0x64B172B9CC20DB47'),
|
||||||
|
BigInt('0x1BDF8271FC15523E'),
|
||||||
|
BigInt('0x530E765A340A7F3A'),
|
||||||
|
BigInt('0x2C608692043FF643'),
|
||||||
|
BigInt('0xADD397CA54616DC8'),
|
||||||
|
BigInt('0xD2BD67026454E4B1'),
|
||||||
|
BigInt('0x3C707F9DC45F37C0'),
|
||||||
|
BigInt('0x431E8F55F46ABEB9'),
|
||||||
|
BigInt('0xC2AD9E0DA4342532'),
|
||||||
|
BigInt('0xBDC36EC59401AC4B'),
|
||||||
|
BigInt('0xF5129AEE5C1E814F'),
|
||||||
|
BigInt('0x8A7C6A266C2B0836'),
|
||||||
|
BigInt('0x0BCF7B7E3C7593BD'),
|
||||||
|
BigInt('0x74A18BB60C401AC4'),
|
||||||
|
BigInt('0xE28C6C1224F5A634'),
|
||||||
|
BigInt('0x9DE29CDA14C02F4D'),
|
||||||
|
BigInt('0x1C518D82449EB4C6'),
|
||||||
|
BigInt('0x633F7D4A74AB3DBF'),
|
||||||
|
BigInt('0x2BEE8961BCB410BB'),
|
||||||
|
BigInt('0x548079A98C8199C2'),
|
||||||
|
BigInt('0xD53368F1DCDF0249'),
|
||||||
|
BigInt('0xAA5D9839ECEA8B30'),
|
||||||
|
BigInt('0x449080A64CE15841'),
|
||||||
|
BigInt('0x3BFE706E7CD4D138'),
|
||||||
|
BigInt('0xBA4D61362C8A4AB3'),
|
||||||
|
BigInt('0xC52391FE1CBFC3CA'),
|
||||||
|
BigInt('0x8DF265D5D4A0EECE'),
|
||||||
|
BigInt('0xF29C951DE49567B7'),
|
||||||
|
BigInt('0x732F8445B4CBFC3C'),
|
||||||
|
BigInt('0x0C41748D84FE7545'),
|
||||||
|
BigInt('0x6BAD6D5EBD3716B7'),
|
||||||
|
BigInt('0x14C39D968D029FCE'),
|
||||||
|
BigInt('0x95708CCEDD5C0445'),
|
||||||
|
BigInt('0xEA1E7C06ED698D3C'),
|
||||||
|
BigInt('0xA2CF882D2576A038'),
|
||||||
|
BigInt('0xDDA178E515432941'),
|
||||||
|
BigInt('0x5C1269BD451DB2CA'),
|
||||||
|
BigInt('0x237C997575283BB3'),
|
||||||
|
BigInt('0xCDB181EAD523E8C2'),
|
||||||
|
BigInt('0xB2DF7122E51661BB'),
|
||||||
|
BigInt('0x336C607AB548FA30'),
|
||||||
|
BigInt('0x4C0290B2857D7349'),
|
||||||
|
BigInt('0x04D364994D625E4D'),
|
||||||
|
BigInt('0x7BBD94517D57D734'),
|
||||||
|
BigInt('0xFA0E85092D094CBF'),
|
||||||
|
BigInt('0x856075C11D3CC5C6'),
|
||||||
|
BigInt('0x134D926535897936'),
|
||||||
|
BigInt('0x6C2362AD05BCF04F'),
|
||||||
|
BigInt('0xED9073F555E26BC4'),
|
||||||
|
BigInt('0x92FE833D65D7E2BD'),
|
||||||
|
BigInt('0xDA2F7716ADC8CFB9'),
|
||||||
|
BigInt('0xA54187DE9DFD46C0'),
|
||||||
|
BigInt('0x24F29686CDA3DD4B'),
|
||||||
|
BigInt('0x5B9C664EFD965432'),
|
||||||
|
BigInt('0xB5517ED15D9D8743'),
|
||||||
|
BigInt('0xCA3F8E196DA80E3A'),
|
||||||
|
BigInt('0x4B8C9F413DF695B1'),
|
||||||
|
BigInt('0x34E26F890DC31CC8'),
|
||||||
|
BigInt('0x7C339BA2C5DC31CC'),
|
||||||
|
BigInt('0x035D6B6AF5E9B8B5'),
|
||||||
|
BigInt('0x82EE7A32A5B7233E'),
|
||||||
|
BigInt('0xFD808AFA9582AA47'),
|
||||||
|
BigInt('0x4D364994D625E4DA'),
|
||||||
|
BigInt('0x3258B95CE6106DA3'),
|
||||||
|
BigInt('0xB3EBA804B64EF628'),
|
||||||
|
BigInt('0xCC8558CC867B7F51'),
|
||||||
|
BigInt('0x8454ACE74E645255'),
|
||||||
|
BigInt('0xFB3A5C2F7E51DB2C'),
|
||||||
|
BigInt('0x7A894D772E0F40A7'),
|
||||||
|
BigInt('0x05E7BDBF1E3AC9DE'),
|
||||||
|
BigInt('0xEB2AA520BE311AAF'),
|
||||||
|
BigInt('0x944455E88E0493D6'),
|
||||||
|
BigInt('0x15F744B0DE5A085D'),
|
||||||
|
BigInt('0x6A99B478EE6F8124'),
|
||||||
|
BigInt('0x224840532670AC20'),
|
||||||
|
BigInt('0x5D26B09B16452559'),
|
||||||
|
BigInt('0xDC95A1C3461BBED2'),
|
||||||
|
BigInt('0xA3FB510B762E37AB'),
|
||||||
|
BigInt('0x35D6B6AF5E9B8B5B'),
|
||||||
|
BigInt('0x4AB846676EAE0222'),
|
||||||
|
BigInt('0xCB0B573F3EF099A9'),
|
||||||
|
BigInt('0xB465A7F70EC510D0'),
|
||||||
|
BigInt('0xFCB453DCC6DA3DD4'),
|
||||||
|
BigInt('0x83DAA314F6EFB4AD'),
|
||||||
|
BigInt('0x0269B24CA6B12F26'),
|
||||||
|
BigInt('0x7D0742849684A65F'),
|
||||||
|
BigInt('0x93CA5A1B368F752E'),
|
||||||
|
BigInt('0xECA4AAD306BAFC57'),
|
||||||
|
BigInt('0x6D17BB8B56E467DC'),
|
||||||
|
BigInt('0x12794B4366D1EEA5'),
|
||||||
|
BigInt('0x5AA8BF68AECEC3A1'),
|
||||||
|
BigInt('0x25C64FA09EFB4AD8'),
|
||||||
|
BigInt('0xA4755EF8CEA5D153'),
|
||||||
|
BigInt('0xDB1BAE30FE90582A'),
|
||||||
|
BigInt('0xBCF7B7E3C7593BD8'),
|
||||||
|
BigInt('0xC399472BF76CB2A1'),
|
||||||
|
BigInt('0x422A5673A732292A'),
|
||||||
|
BigInt('0x3D44A6BB9707A053'),
|
||||||
|
BigInt('0x759552905F188D57'),
|
||||||
|
BigInt('0x0AFBA2586F2D042E'),
|
||||||
|
BigInt('0x8B48B3003F739FA5'),
|
||||||
|
BigInt('0xF42643C80F4616DC'),
|
||||||
|
BigInt('0x1AEB5B57AF4DC5AD'),
|
||||||
|
BigInt('0x6585AB9F9F784CD4'),
|
||||||
|
BigInt('0xE436BAC7CF26D75F'),
|
||||||
|
BigInt('0x9B584A0FFF135E26'),
|
||||||
|
BigInt('0xD389BE24370C7322'),
|
||||||
|
BigInt('0xACE74EEC0739FA5B'),
|
||||||
|
BigInt('0x2D545FB4576761D0'),
|
||||||
|
BigInt('0x523AAF7C6752E8A9'),
|
||||||
|
BigInt('0xC41748D84FE75459'),
|
||||||
|
BigInt('0xBB79B8107FD2DD20'),
|
||||||
|
BigInt('0x3ACAA9482F8C46AB'),
|
||||||
|
BigInt('0x45A459801FB9CFD2'),
|
||||||
|
BigInt('0x0D75ADABD7A6E2D6'),
|
||||||
|
BigInt('0x721B5D63E7936BAF'),
|
||||||
|
BigInt('0xF3A84C3BB7CDF024'),
|
||||||
|
BigInt('0x8CC6BCF387F8795D'),
|
||||||
|
BigInt('0x620BA46C27F3AA2C'),
|
||||||
|
BigInt('0x1D6554A417C62355'),
|
||||||
|
BigInt('0x9CD645FC4798B8DE'),
|
||||||
|
BigInt('0xE3B8B53477AD31A7'),
|
||||||
|
BigInt('0xAB69411FBFB21CA3'),
|
||||||
|
BigInt('0xD407B1D78F8795DA'),
|
||||||
|
BigInt('0x55B4A08FDFD90E51'),
|
||||||
|
BigInt('0x2ADA5047EFEC8728')
|
||||||
|
]
|
||||||
|
|
||||||
|
export type CRC64DigestEncoding = 'hex' | 'base64' | 'buffer'
|
||||||
|
|
||||||
|
class CRC64 {
|
||||||
|
private _crc: bigint
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._crc = BigInt(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data: Buffer | string): void {
|
||||||
|
const buffer = typeof data === 'string' ? Buffer.from(data) : data
|
||||||
|
let crc = CRC64.flip64Bits(this._crc)
|
||||||
|
|
||||||
|
for (const dataByte of buffer) {
|
||||||
|
const crcByte = Number(crc & BigInt(0xff))
|
||||||
|
crc = PREGEN_POLY_TABLE[crcByte ^ dataByte] ^ (crc >> BigInt(8))
|
||||||
|
}
|
||||||
|
|
||||||
|
this._crc = CRC64.flip64Bits(crc)
|
||||||
|
}
|
||||||
|
|
||||||
|
digest(encoding?: CRC64DigestEncoding): string | Buffer {
|
||||||
|
switch (encoding) {
|
||||||
|
case 'hex':
|
||||||
|
return this._crc.toString(16).toUpperCase()
|
||||||
|
case 'base64':
|
||||||
|
return this.toBuffer().toString('base64')
|
||||||
|
default:
|
||||||
|
return this.toBuffer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private toBuffer(): Buffer {
|
||||||
|
return Buffer.from(
|
||||||
|
[0, 8, 16, 24, 32, 40, 48, 56].map(s =>
|
||||||
|
Number((this._crc >> BigInt(s)) & BigInt(0xff))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static flip64Bits(n: bigint): bigint {
|
||||||
|
return (BigInt(1) << BigInt(64)) - BigInt(1) - n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CRC64
|
||||||
@@ -18,7 +18,7 @@ import {URL} from 'url'
|
|||||||
import {StatusReporter} from './status-reporter'
|
import {StatusReporter} from './status-reporter'
|
||||||
import {performance} from 'perf_hooks'
|
import {performance} from 'perf_hooks'
|
||||||
import {ListArtifactsResponse, QueryArtifactResponse} from './contracts'
|
import {ListArtifactsResponse, QueryArtifactResponse} from './contracts'
|
||||||
import {IHttpClientResponse} from '@actions/http-client/interfaces'
|
import {HttpClientResponse} from '@actions/http-client'
|
||||||
import {HttpManager} from './http-manager'
|
import {HttpManager} from './http-manager'
|
||||||
import {DownloadItem} from './download-specification'
|
import {DownloadItem} from './download-specification'
|
||||||
import {getDownloadFileConcurrency, getRetryLimit} from './config-variables'
|
import {getDownloadFileConcurrency, getRetryLimit} from './config-variables'
|
||||||
@@ -152,7 +152,7 @@ export class DownloadHttpClient {
|
|||||||
const headers = getDownloadHeaders('application/json', true, true)
|
const headers = getDownloadHeaders('application/json', true, true)
|
||||||
|
|
||||||
// a single GET request is used to download a file
|
// a single GET request is used to download a file
|
||||||
const makeDownloadRequest = async (): Promise<IHttpClientResponse> => {
|
const makeDownloadRequest = async (): Promise<HttpClientResponse> => {
|
||||||
const client = this.downloadHttpManager.getClient(httpClientIndex)
|
const client = this.downloadHttpManager.getClient(httpClientIndex)
|
||||||
return await client.get(artifactLocation, headers)
|
return await client.get(artifactLocation, headers)
|
||||||
}
|
}
|
||||||
@@ -219,18 +219,22 @@ export class DownloadHttpClient {
|
|||||||
fileDownloadPath: string
|
fileDownloadPath: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
destinationStream.close()
|
destinationStream.close()
|
||||||
|
// await until file is created at downloadpath; node15 and up fs.createWriteStream had not created a file yet
|
||||||
|
await new Promise<void>(resolve => {
|
||||||
|
destinationStream.on('close', resolve)
|
||||||
|
if (destinationStream.writableFinished) {
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
})
|
||||||
await rmFile(fileDownloadPath)
|
await rmFile(fileDownloadPath)
|
||||||
destinationStream = fs.createWriteStream(fileDownloadPath)
|
destinationStream = fs.createWriteStream(fileDownloadPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep trying to download a file until a retry limit has been reached
|
// keep trying to download a file until a retry limit has been reached
|
||||||
while (retryCount <= retryLimit) {
|
while (retryCount <= retryLimit) {
|
||||||
let response: IHttpClientResponse
|
let response: HttpClientResponse
|
||||||
try {
|
try {
|
||||||
response = await makeDownloadRequest()
|
response = await makeDownloadRequest()
|
||||||
if (core.isDebug()) {
|
|
||||||
displayHttpDiagnostics(response)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// if an error is caught, it is usually indicative of a timeout so retry the download
|
// if an error is caught, it is usually indicative of a timeout so retry the download
|
||||||
core.info('An error occurred while attempting to download a file')
|
core.info('An error occurred while attempting to download a file')
|
||||||
@@ -298,7 +302,7 @@ export class DownloadHttpClient {
|
|||||||
* @param isGzip a boolean denoting if the content is compressed using gzip and if we need to decode it
|
* @param isGzip a boolean denoting if the content is compressed using gzip and if we need to decode it
|
||||||
*/
|
*/
|
||||||
async pipeResponseToFile(
|
async pipeResponseToFile(
|
||||||
response: IHttpClientResponse,
|
response: HttpClientResponse,
|
||||||
destinationStream: fs.WriteStream,
|
destinationStream: fs.WriteStream,
|
||||||
isGzip: boolean
|
isGzip: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -307,7 +311,7 @@ export class DownloadHttpClient {
|
|||||||
const gunzip = zlib.createGunzip()
|
const gunzip = zlib.createGunzip()
|
||||||
response.message
|
response.message
|
||||||
.on('error', error => {
|
.on('error', error => {
|
||||||
core.error(
|
core.info(
|
||||||
`An error occurred while attempting to read the response stream`
|
`An error occurred while attempting to read the response stream`
|
||||||
)
|
)
|
||||||
gunzip.close()
|
gunzip.close()
|
||||||
@@ -316,7 +320,7 @@ export class DownloadHttpClient {
|
|||||||
})
|
})
|
||||||
.pipe(gunzip)
|
.pipe(gunzip)
|
||||||
.on('error', error => {
|
.on('error', error => {
|
||||||
core.error(
|
core.info(
|
||||||
`An error occurred while attempting to decompress the response stream`
|
`An error occurred while attempting to decompress the response stream`
|
||||||
)
|
)
|
||||||
destinationStream.close()
|
destinationStream.close()
|
||||||
@@ -327,7 +331,7 @@ export class DownloadHttpClient {
|
|||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
.on('error', error => {
|
.on('error', error => {
|
||||||
core.error(
|
core.info(
|
||||||
`An error occurred while writing a downloaded file to ${destinationStream.path}`
|
`An error occurred while writing a downloaded file to ${destinationStream.path}`
|
||||||
)
|
)
|
||||||
reject(error)
|
reject(error)
|
||||||
@@ -335,7 +339,7 @@ export class DownloadHttpClient {
|
|||||||
} else {
|
} else {
|
||||||
response.message
|
response.message
|
||||||
.on('error', error => {
|
.on('error', error => {
|
||||||
core.error(
|
core.info(
|
||||||
`An error occurred while attempting to read the response stream`
|
`An error occurred while attempting to read the response stream`
|
||||||
)
|
)
|
||||||
destinationStream.close()
|
destinationStream.close()
|
||||||
@@ -346,7 +350,7 @@ export class DownloadHttpClient {
|
|||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
.on('error', error => {
|
.on('error', error => {
|
||||||
core.error(
|
core.info(
|
||||||
`An error occurred while writing a downloaded file to ${destinationStream.path}`
|
`An error occurred while writing a downloaded file to ${destinationStream.path}`
|
||||||
)
|
)
|
||||||
reject(error)
|
reject(error)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {HttpClient} from '@actions/http-client/index'
|
import {HttpClient} from '@actions/http-client'
|
||||||
import {createHttpClient} from './utils'
|
import {createHttpClient} from './utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import {info} from '@actions/core'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalid characters that cannot be in the artifact name or an uploaded file. Will be rejected
|
||||||
|
* from the server if attempted to be sent over. These characters are not allowed due to limitations with certain
|
||||||
|
* file systems such as NTFS. To maintain platform-agnostic behavior, all characters that are not supported by an
|
||||||
|
* individual filesystem/platform will not be supported on all fileSystems/platforms
|
||||||
|
*
|
||||||
|
* FilePaths can include characters such as \ and / which are not permitted in the artifact name alone
|
||||||
|
*/
|
||||||
|
const invalidArtifactFilePathCharacters = new Map<string, string>([
|
||||||
|
['"', ' Double quote "'],
|
||||||
|
[':', ' Colon :'],
|
||||||
|
['<', ' Less than <'],
|
||||||
|
['>', ' Greater than >'],
|
||||||
|
['|', ' Vertical bar |'],
|
||||||
|
['*', ' Asterisk *'],
|
||||||
|
['?', ' Question mark ?'],
|
||||||
|
['\r', ' Carriage return \\r'],
|
||||||
|
['\n', ' Line feed \\n']
|
||||||
|
])
|
||||||
|
|
||||||
|
const invalidArtifactNameCharacters = new Map<string, string>([
|
||||||
|
...invalidArtifactFilePathCharacters,
|
||||||
|
['\\', ' Backslash \\'],
|
||||||
|
['/', ' Forward slash /']
|
||||||
|
])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans the name of the artifact to make sure there are no illegal characters
|
||||||
|
*/
|
||||||
|
export function checkArtifactName(name: string): void {
|
||||||
|
if (!name) {
|
||||||
|
throw new Error(`Artifact name: ${name}, is incorrectly provided`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [
|
||||||
|
invalidCharacterKey,
|
||||||
|
errorMessageForCharacter
|
||||||
|
] of invalidArtifactNameCharacters) {
|
||||||
|
if (name.includes(invalidCharacterKey)) {
|
||||||
|
throw new Error(
|
||||||
|
`Artifact name is not valid: ${name}. Contains the following character: ${errorMessageForCharacter}
|
||||||
|
|
||||||
|
Invalid characters include: ${Array.from(
|
||||||
|
invalidArtifactNameCharacters.values()
|
||||||
|
).toString()}
|
||||||
|
|
||||||
|
These characters are not allowed in the artifact name due to limitations with certain file systems such as NTFS. To maintain file system agnostic behavior, these characters are intentionally not allowed to prevent potential problems with downloads on different file systems.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info(`Artifact name is valid!`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scans the name of the filePath used to make sure there are no illegal characters
|
||||||
|
*/
|
||||||
|
export function checkArtifactFilePath(path: string): void {
|
||||||
|
if (!path) {
|
||||||
|
throw new Error(`Artifact path: ${path}, is incorrectly provided`)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [
|
||||||
|
invalidCharacterKey,
|
||||||
|
errorMessageForCharacter
|
||||||
|
] of invalidArtifactFilePathCharacters) {
|
||||||
|
if (path.includes(invalidCharacterKey)) {
|
||||||
|
throw new Error(
|
||||||
|
`Artifact path is not valid: ${path}. Contains the following character: ${errorMessageForCharacter}
|
||||||
|
|
||||||
|
Invalid characters include: ${Array.from(
|
||||||
|
invalidArtifactFilePathCharacters.values()
|
||||||
|
).toString()}
|
||||||
|
|
||||||
|
The following characters are not allowed in files that are uploaded due to limitations with certain file systems such as NTFS. To maintain file system agnostic behavior, these characters are intentionally not allowed to prevent potential problems with downloads on different file systems.
|
||||||
|
`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {IHttpClientResponse} from '@actions/http-client/interfaces'
|
import {HttpClientResponse} from '@actions/http-client'
|
||||||
import {
|
import {
|
||||||
isRetryableStatusCode,
|
isRetryableStatusCode,
|
||||||
isSuccessStatusCode,
|
isSuccessStatusCode,
|
||||||
@@ -11,11 +11,11 @@ import {getRetryLimit} from './config-variables'
|
|||||||
|
|
||||||
export async function retry(
|
export async function retry(
|
||||||
name: string,
|
name: string,
|
||||||
operation: () => Promise<IHttpClientResponse>,
|
operation: () => Promise<HttpClientResponse>,
|
||||||
customErrorMessages: Map<number, string>,
|
customErrorMessages: Map<number, string>,
|
||||||
maxAttempts: number
|
maxAttempts: number
|
||||||
): Promise<IHttpClientResponse> {
|
): Promise<HttpClientResponse> {
|
||||||
let response: IHttpClientResponse | undefined = undefined
|
let response: HttpClientResponse | undefined = undefined
|
||||||
let statusCode: number | undefined = undefined
|
let statusCode: number | undefined = undefined
|
||||||
let isRetryable = false
|
let isRetryable = false
|
||||||
let errorMessage = ''
|
let errorMessage = ''
|
||||||
@@ -69,11 +69,11 @@ export async function retry(
|
|||||||
throw Error(`${name} failed: ${errorMessage}`)
|
throw Error(`${name} failed: ${errorMessage}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function retryHttpClientRequest<T>(
|
export async function retryHttpClientRequest(
|
||||||
name: string,
|
name: string,
|
||||||
method: () => Promise<IHttpClientResponse>,
|
method: () => Promise<HttpClientResponse>,
|
||||||
customErrorMessages: Map<number, string> = new Map(),
|
customErrorMessages: Map<number, string> = new Map(),
|
||||||
maxAttempts = getRetryLimit()
|
maxAttempts = getRetryLimit()
|
||||||
): Promise<IHttpClientResponse> {
|
): Promise<HttpClientResponse> {
|
||||||
return await retry(name, method, customErrorMessages, maxAttempts)
|
return await retry(name, method, customErrorMessages, maxAttempts)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,16 +14,15 @@ export class StatusReporter {
|
|||||||
private displayFrequencyInMilliseconds: number
|
private displayFrequencyInMilliseconds: number
|
||||||
private largeFiles = new Map<string, string>()
|
private largeFiles = new Map<string, string>()
|
||||||
private totalFileStatus: NodeJS.Timeout | undefined
|
private totalFileStatus: NodeJS.Timeout | undefined
|
||||||
private largeFileStatus: NodeJS.Timeout | undefined
|
|
||||||
|
|
||||||
constructor(displayFrequencyInMilliseconds: number) {
|
constructor(displayFrequencyInMilliseconds: number) {
|
||||||
this.totalFileStatus = undefined
|
this.totalFileStatus = undefined
|
||||||
this.largeFileStatus = undefined
|
|
||||||
this.displayFrequencyInMilliseconds = displayFrequencyInMilliseconds
|
this.displayFrequencyInMilliseconds = displayFrequencyInMilliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
setTotalNumberOfFilesToProcess(fileTotal: number): void {
|
setTotalNumberOfFilesToProcess(fileTotal: number): void {
|
||||||
this.totalNumberOfFilesToProcess = fileTotal
|
this.totalNumberOfFilesToProcess = fileTotal
|
||||||
|
this.processedCount = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
start(): void {
|
start(): void {
|
||||||
@@ -43,42 +42,29 @@ export class StatusReporter {
|
|||||||
)}%)`
|
)}%)`
|
||||||
)
|
)
|
||||||
}, this.displayFrequencyInMilliseconds)
|
}, this.displayFrequencyInMilliseconds)
|
||||||
|
|
||||||
// displays extra information about any large files that take a significant amount of time to upload or download every 1 second
|
|
||||||
this.largeFileStatus = setInterval(() => {
|
|
||||||
for (const value of Array.from(this.largeFiles.values())) {
|
|
||||||
info(value)
|
|
||||||
}
|
|
||||||
// delete all entries in the map after displaying the information so it will not be displayed again unless explicitly added
|
|
||||||
this.largeFiles.clear()
|
|
||||||
}, 1000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if there is a large file that is being uploaded in chunks, this is used to display extra information about the status of the upload
|
// if there is a large file that is being uploaded in chunks, this is used to display extra information about the status of the upload
|
||||||
updateLargeFileStatus(
|
updateLargeFileStatus(
|
||||||
fileName: string,
|
fileName: string,
|
||||||
numerator: number,
|
chunkStartIndex: number,
|
||||||
denominator: number
|
chunkEndIndex: number,
|
||||||
|
totalUploadFileSize: number
|
||||||
): void {
|
): void {
|
||||||
// display 1 decimal place without any rounding
|
// display 1 decimal place without any rounding
|
||||||
const percentage = this.formatPercentage(numerator, denominator)
|
const percentage = this.formatPercentage(chunkEndIndex, totalUploadFileSize)
|
||||||
const displayInformation = `Uploading ${fileName} (${percentage.slice(
|
info(
|
||||||
0,
|
`Uploaded ${fileName} (${percentage.slice(
|
||||||
percentage.indexOf('.') + 2
|
0,
|
||||||
)}%)`
|
percentage.indexOf('.') + 2
|
||||||
|
)}%) bytes ${chunkStartIndex}:${chunkEndIndex}`
|
||||||
// any previously added display information should be overwritten for the specific large file because a map is being used
|
)
|
||||||
this.largeFiles.set(fileName, displayInformation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stop(): void {
|
stop(): void {
|
||||||
if (this.totalFileStatus) {
|
if (this.totalFileStatus) {
|
||||||
clearInterval(this.totalFileStatus)
|
clearInterval(this.totalFileStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.largeFileStatus) {
|
|
||||||
clearInterval(this.largeFileStatus)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
incrementProcessedCount(): void {
|
incrementProcessedCount(): void {
|
||||||
|
|||||||
@@ -3,6 +3,35 @@ import * as zlib from 'zlib'
|
|||||||
import {promisify} from 'util'
|
import {promisify} from 'util'
|
||||||
const stat = promisify(fs.stat)
|
const stat = promisify(fs.stat)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GZipping certain files that are already compressed will likely not yield further size reductions. Creating large temporary gzip
|
||||||
|
* files then will just waste a lot of time before ultimately being discarded (especially for very large files).
|
||||||
|
* If any of these types of files are encountered then on-disk gzip creation will be skipped and the original file will be uploaded as-is
|
||||||
|
*/
|
||||||
|
const gzipExemptFileExtensions = [
|
||||||
|
'.gz', // GZIP
|
||||||
|
'.gzip', // GZIP
|
||||||
|
'.tgz', // GZIP
|
||||||
|
'.taz', // GZIP
|
||||||
|
'.Z', // COMPRESS
|
||||||
|
'.taZ', // COMPRESS
|
||||||
|
'.bz2', // BZIP2
|
||||||
|
'.tbz', // BZIP2
|
||||||
|
'.tbz2', // BZIP2
|
||||||
|
'.tz2', // BZIP2
|
||||||
|
'.lz', // LZIP
|
||||||
|
'.lzma', // LZMA
|
||||||
|
'.tlz', // LZMA
|
||||||
|
'.lzo', // LZOP
|
||||||
|
'.xz', // XZ
|
||||||
|
'.txz', // XZ
|
||||||
|
'.zst', // ZSTD
|
||||||
|
'.zstd', // ZSTD
|
||||||
|
'.tzst', // ZSTD
|
||||||
|
'.zip', // ZIP
|
||||||
|
'.7z' // 7ZIP
|
||||||
|
]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Gzip compressed file of an original file at the provided temporary filepath location
|
* Creates a Gzip compressed file of an original file at the provided temporary filepath location
|
||||||
* @param {string} originalFilePath filepath of whatever will be compressed. The original file will be unmodified
|
* @param {string} originalFilePath filepath of whatever will be compressed. The original file will be unmodified
|
||||||
@@ -13,6 +42,13 @@ export async function createGZipFileOnDisk(
|
|||||||
originalFilePath: string,
|
originalFilePath: string,
|
||||||
tempFilePath: string
|
tempFilePath: string
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
|
for (const gzipExemptExtension of gzipExemptFileExtensions) {
|
||||||
|
if (originalFilePath.endsWith(gzipExemptExtension)) {
|
||||||
|
// return a really large number so that the original file gets uploaded
|
||||||
|
return Number.MAX_SAFE_INTEGER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const inputStream = fs.createReadStream(originalFilePath)
|
const inputStream = fs.createReadStream(originalFilePath)
|
||||||
const gzip = zlib.createGzip()
|
const gzip = zlib.createGzip()
|
||||||
@@ -26,7 +62,7 @@ export async function createGZipFileOnDisk(
|
|||||||
outputStream.on('error', error => {
|
outputStream.on('error', error => {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(error)
|
console.log(error)
|
||||||
reject
|
reject(error)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
UploadResults
|
UploadResults
|
||||||
} from './contracts'
|
} from './contracts'
|
||||||
import {
|
import {
|
||||||
|
digestForStream,
|
||||||
getArtifactUrl,
|
getArtifactUrl,
|
||||||
getContentRange,
|
getContentRange,
|
||||||
getUploadHeaders,
|
getUploadHeaders,
|
||||||
@@ -31,8 +32,7 @@ import {promisify} from 'util'
|
|||||||
import {URL} from 'url'
|
import {URL} from 'url'
|
||||||
import {performance} from 'perf_hooks'
|
import {performance} from 'perf_hooks'
|
||||||
import {StatusReporter} from './status-reporter'
|
import {StatusReporter} from './status-reporter'
|
||||||
import {HttpCodes} from '@actions/http-client'
|
import {HttpCodes, HttpClientResponse} from '@actions/http-client'
|
||||||
import {IHttpClientResponse} from '@actions/http-client/interfaces'
|
|
||||||
import {HttpManager} from './http-manager'
|
import {HttpManager} from './http-manager'
|
||||||
import {UploadSpecification} from './upload-specification'
|
import {UploadSpecification} from './upload-specification'
|
||||||
import {UploadOptions} from './upload-options'
|
import {UploadOptions} from './upload-options'
|
||||||
@@ -219,29 +219,41 @@ export class UploadHttpClient {
|
|||||||
httpClientIndex: number,
|
httpClientIndex: number,
|
||||||
parameters: UploadFileParameters
|
parameters: UploadFileParameters
|
||||||
): Promise<UploadFileResult> {
|
): Promise<UploadFileResult> {
|
||||||
const totalFileSize: number = (await stat(parameters.file)).size
|
const fileStat: fs.Stats = await stat(parameters.file)
|
||||||
|
const totalFileSize = fileStat.size
|
||||||
|
const isFIFO = fileStat.isFIFO()
|
||||||
let offset = 0
|
let offset = 0
|
||||||
let isUploadSuccessful = true
|
let isUploadSuccessful = true
|
||||||
let failedChunkSizes = 0
|
let failedChunkSizes = 0
|
||||||
let uploadFileSize = 0
|
let uploadFileSize = 0
|
||||||
let isGzip = true
|
let isGzip = true
|
||||||
|
|
||||||
// the file that is being uploaded is less than 64k in size, to increase throughput and to minimize disk I/O
|
// the file that is being uploaded is less than 64k in size to increase throughput and to minimize disk I/O
|
||||||
// for creating a new GZip file, an in-memory buffer is used for compression
|
// for creating a new GZip file, an in-memory buffer is used for compression
|
||||||
if (totalFileSize < 65536) {
|
// with named pipes the file size is reported as zero in that case don't read the file in memory
|
||||||
|
if (!isFIFO && totalFileSize < 65536) {
|
||||||
|
core.debug(
|
||||||
|
`${parameters.file} is less than 64k in size. Creating a gzip file in-memory to potentially reduce the upload size`
|
||||||
|
)
|
||||||
const buffer = await createGZipFileInBuffer(parameters.file)
|
const buffer = await createGZipFileInBuffer(parameters.file)
|
||||||
|
|
||||||
//An open stream is needed in the event of a failure and we need to retry. If a NodeJS.ReadableStream is directly passed in,
|
// An open stream is needed in the event of a failure and we need to retry. If a NodeJS.ReadableStream is directly passed in,
|
||||||
// it will not properly get reset to the start of the stream if a chunk upload needs to be retried
|
// it will not properly get reset to the start of the stream if a chunk upload needs to be retried
|
||||||
let openUploadStream: () => NodeJS.ReadableStream
|
let openUploadStream: () => NodeJS.ReadableStream
|
||||||
|
|
||||||
if (totalFileSize < buffer.byteLength) {
|
if (totalFileSize < buffer.byteLength) {
|
||||||
// compression did not help with reducing the size, use a readable stream from the original file for upload
|
// compression did not help with reducing the size, use a readable stream from the original file for upload
|
||||||
|
core.debug(
|
||||||
|
`The gzip file created for ${parameters.file} did not help with reducing the size of the file. The original file will be uploaded as-is`
|
||||||
|
)
|
||||||
openUploadStream = () => fs.createReadStream(parameters.file)
|
openUploadStream = () => fs.createReadStream(parameters.file)
|
||||||
isGzip = false
|
isGzip = false
|
||||||
uploadFileSize = totalFileSize
|
uploadFileSize = totalFileSize
|
||||||
} else {
|
} else {
|
||||||
// create a readable stream using a PassThrough stream that is both readable and writable
|
// create a readable stream using a PassThrough stream that is both readable and writable
|
||||||
|
core.debug(
|
||||||
|
`A gzip file created for ${parameters.file} helped with reducing the size of the original file. The file will be uploaded using gzip.`
|
||||||
|
)
|
||||||
openUploadStream = () => {
|
openUploadStream = () => {
|
||||||
const passThrough = new stream.PassThrough()
|
const passThrough = new stream.PassThrough()
|
||||||
passThrough.end(buffer)
|
passThrough.end(buffer)
|
||||||
@@ -277,6 +289,9 @@ export class UploadHttpClient {
|
|||||||
// the file that is being uploaded is greater than 64k in size, a temporary file gets created on disk using the
|
// the file that is being uploaded is greater than 64k in size, a temporary file gets created on disk using the
|
||||||
// npm tmp-promise package and this file gets used to create a GZipped file
|
// npm tmp-promise package and this file gets used to create a GZipped file
|
||||||
const tempFile = await tmp.file()
|
const tempFile = await tmp.file()
|
||||||
|
core.debug(
|
||||||
|
`${parameters.file} is greater than 64k in size. Creating a gzip file on-disk ${tempFile.path} to potentially reduce the upload size`
|
||||||
|
)
|
||||||
|
|
||||||
// create a GZip file of the original file being uploaded, the original file should not be modified in any way
|
// create a GZip file of the original file being uploaded, the original file should not be modified in any way
|
||||||
uploadFileSize = await createGZipFileOnDisk(
|
uploadFileSize = await createGZipFileOnDisk(
|
||||||
@@ -287,10 +302,18 @@ export class UploadHttpClient {
|
|||||||
let uploadFilePath = tempFile.path
|
let uploadFilePath = tempFile.path
|
||||||
|
|
||||||
// compression did not help with size reduction, use the original file for upload and delete the temp GZip file
|
// compression did not help with size reduction, use the original file for upload and delete the temp GZip file
|
||||||
if (totalFileSize < uploadFileSize) {
|
// for named pipes totalFileSize is zero, this assumes compression did help
|
||||||
|
if (!isFIFO && totalFileSize < uploadFileSize) {
|
||||||
|
core.debug(
|
||||||
|
`The gzip file created for ${parameters.file} did not help with reducing the size of the file. The original file will be uploaded as-is`
|
||||||
|
)
|
||||||
uploadFileSize = totalFileSize
|
uploadFileSize = totalFileSize
|
||||||
uploadFilePath = parameters.file
|
uploadFilePath = parameters.file
|
||||||
isGzip = false
|
isGzip = false
|
||||||
|
} else {
|
||||||
|
core.debug(
|
||||||
|
`The gzip file created for ${parameters.file} is smaller than the original file. The file will be uploaded using gzip.`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let abortFileUpload = false
|
let abortFileUpload = false
|
||||||
@@ -301,17 +324,8 @@ export class UploadHttpClient {
|
|||||||
parameters.maxChunkSize
|
parameters.maxChunkSize
|
||||||
)
|
)
|
||||||
|
|
||||||
// if an individual file is greater than 100MB (1024*1024*100) in size, display extra information about the upload status
|
const startChunkIndex = offset
|
||||||
if (uploadFileSize > 104857600) {
|
const endChunkIndex = offset + chunkSize - 1
|
||||||
this.statusReporter.updateLargeFileStatus(
|
|
||||||
parameters.file,
|
|
||||||
offset,
|
|
||||||
uploadFileSize
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = offset
|
|
||||||
const end = offset + chunkSize - 1
|
|
||||||
offset += parameters.maxChunkSize
|
offset += parameters.maxChunkSize
|
||||||
|
|
||||||
if (abortFileUpload) {
|
if (abortFileUpload) {
|
||||||
@@ -325,12 +339,12 @@ export class UploadHttpClient {
|
|||||||
parameters.resourceUrl,
|
parameters.resourceUrl,
|
||||||
() =>
|
() =>
|
||||||
fs.createReadStream(uploadFilePath, {
|
fs.createReadStream(uploadFilePath, {
|
||||||
start,
|
start: startChunkIndex,
|
||||||
end,
|
end: endChunkIndex,
|
||||||
autoClose: false
|
autoClose: false
|
||||||
}),
|
}),
|
||||||
start,
|
startChunkIndex,
|
||||||
end,
|
endChunkIndex,
|
||||||
uploadFileSize,
|
uploadFileSize,
|
||||||
isGzip,
|
isGzip,
|
||||||
totalFileSize
|
totalFileSize
|
||||||
@@ -343,11 +357,22 @@ export class UploadHttpClient {
|
|||||||
failedChunkSizes += chunkSize
|
failedChunkSizes += chunkSize
|
||||||
core.warning(`Aborting upload for ${parameters.file} due to failure`)
|
core.warning(`Aborting upload for ${parameters.file} due to failure`)
|
||||||
abortFileUpload = true
|
abortFileUpload = true
|
||||||
|
} else {
|
||||||
|
// if an individual file is greater than 8MB (1024*1024*8) in size, display extra information about the upload status
|
||||||
|
if (uploadFileSize > 8388608) {
|
||||||
|
this.statusReporter.updateLargeFileStatus(
|
||||||
|
parameters.file,
|
||||||
|
startChunkIndex,
|
||||||
|
endChunkIndex,
|
||||||
|
uploadFileSize
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the temporary file that was created as part of the upload. If the temp file does not get manually deleted by
|
// Delete the temporary file that was created as part of the upload. If the temp file does not get manually deleted by
|
||||||
// calling cleanup, it gets removed when the node process exits. For more info see: https://www.npmjs.com/package/tmp-promise#about
|
// calling cleanup, it gets removed when the node process exits. For more info see: https://www.npmjs.com/package/tmp-promise#about
|
||||||
|
core.debug(`deleting temporary gzip file ${tempFile.path}`)
|
||||||
await tempFile.cleanup()
|
await tempFile.cleanup()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -381,6 +406,9 @@ export class UploadHttpClient {
|
|||||||
isGzip: boolean,
|
isGzip: boolean,
|
||||||
totalFileSize: number
|
totalFileSize: number
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
|
// open a new stream and read it to compute the digest
|
||||||
|
const digest = await digestForStream(openStream())
|
||||||
|
|
||||||
// prepare all the necessary headers before making any http call
|
// prepare all the necessary headers before making any http call
|
||||||
const headers = getUploadHeaders(
|
const headers = getUploadHeaders(
|
||||||
'application/octet-stream',
|
'application/octet-stream',
|
||||||
@@ -388,10 +416,11 @@ export class UploadHttpClient {
|
|||||||
isGzip,
|
isGzip,
|
||||||
totalFileSize,
|
totalFileSize,
|
||||||
end - start + 1,
|
end - start + 1,
|
||||||
getContentRange(start, end, uploadFileSize)
|
getContentRange(start, end, uploadFileSize),
|
||||||
|
digest
|
||||||
)
|
)
|
||||||
|
|
||||||
const uploadChunkRequest = async (): Promise<IHttpClientResponse> => {
|
const uploadChunkRequest = async (): Promise<HttpClientResponse> => {
|
||||||
const client = this.uploadHttpManager.getClient(httpClientIndex)
|
const client = this.uploadHttpManager.getClient(httpClientIndex)
|
||||||
return await client.sendStream('PUT', resourceUrl, openStream(), headers)
|
return await client.sendStream('PUT', resourceUrl, openStream(), headers)
|
||||||
}
|
}
|
||||||
@@ -402,7 +431,7 @@ export class UploadHttpClient {
|
|||||||
// Increments the current retry count and then checks if the retry limit has been reached
|
// Increments the current retry count and then checks if the retry limit has been reached
|
||||||
// If there have been too many retries, fail so the download stops
|
// If there have been too many retries, fail so the download stops
|
||||||
const incrementAndCheckRetryLimit = (
|
const incrementAndCheckRetryLimit = (
|
||||||
response?: IHttpClientResponse
|
response?: HttpClientResponse
|
||||||
): boolean => {
|
): boolean => {
|
||||||
retryCount++
|
retryCount++
|
||||||
if (retryCount > retryLimit) {
|
if (retryCount > retryLimit) {
|
||||||
@@ -439,7 +468,7 @@ export class UploadHttpClient {
|
|||||||
|
|
||||||
// allow for failed chunks to be retried multiple times
|
// allow for failed chunks to be retried multiple times
|
||||||
while (retryCount <= retryLimit) {
|
while (retryCount <= retryLimit) {
|
||||||
let response: IHttpClientResponse
|
let response: HttpClientResponse
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response = await uploadChunkRequest()
|
response = await uploadChunkRequest()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import {debug} from '@actions/core'
|
import {debug} from '@actions/core'
|
||||||
import {join, normalize, resolve} from 'path'
|
import {join, normalize, resolve} from 'path'
|
||||||
import {checkArtifactName, checkArtifactFilePath} from './utils'
|
import {checkArtifactFilePath} from './path-and-artifact-name-validation'
|
||||||
|
|
||||||
export interface UploadSpecification {
|
export interface UploadSpecification {
|
||||||
absoluteFilePath: string
|
absoluteFilePath: string
|
||||||
@@ -19,14 +19,13 @@ export function getUploadSpecification(
|
|||||||
rootDirectory: string,
|
rootDirectory: string,
|
||||||
artifactFiles: string[]
|
artifactFiles: string[]
|
||||||
): UploadSpecification[] {
|
): UploadSpecification[] {
|
||||||
checkArtifactName(artifactName)
|
// artifact name was checked earlier on, no need to check again
|
||||||
|
|
||||||
const specifications: UploadSpecification[] = []
|
const specifications: UploadSpecification[] = []
|
||||||
|
|
||||||
if (!fs.existsSync(rootDirectory)) {
|
if (!fs.existsSync(rootDirectory)) {
|
||||||
throw new Error(`Provided rootDirectory ${rootDirectory} does not exist`)
|
throw new Error(`Provided rootDirectory ${rootDirectory} does not exist`)
|
||||||
}
|
}
|
||||||
if (!fs.lstatSync(rootDirectory).isDirectory()) {
|
if (!fs.statSync(rootDirectory).isDirectory()) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Provided rootDirectory ${rootDirectory} is not a valid directory`
|
`Provided rootDirectory ${rootDirectory} is not a valid directory`
|
||||||
)
|
)
|
||||||
@@ -58,7 +57,7 @@ export function getUploadSpecification(
|
|||||||
if (!fs.existsSync(file)) {
|
if (!fs.existsSync(file)) {
|
||||||
throw new Error(`File ${file} does not exist`)
|
throw new Error(`File ${file} does not exist`)
|
||||||
}
|
}
|
||||||
if (!fs.lstatSync(file).isDirectory()) {
|
if (!fs.statSync(file).isDirectory()) {
|
||||||
// Normalize and resolve, this allows for either absolute or relative paths to be used
|
// Normalize and resolve, this allows for either absolute or relative paths to be used
|
||||||
file = normalize(file)
|
file = normalize(file)
|
||||||
file = resolve(file)
|
file = resolve(file)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {debug, info, warning} from '@actions/core'
|
import crypto from 'crypto'
|
||||||
import {promises as fs} from 'fs'
|
import {promises as fs} from 'fs'
|
||||||
import {HttpCodes, HttpClient} from '@actions/http-client'
|
import {IncomingHttpHeaders, OutgoingHttpHeaders} from 'http'
|
||||||
import {BearerCredentialHandler} from '@actions/http-client/auth'
|
import {debug, info, warning} from '@actions/core'
|
||||||
import {IHeaders, IHttpClientResponse} from '@actions/http-client/interfaces'
|
import {HttpCodes, HttpClient, HttpClientResponse} from '@actions/http-client'
|
||||||
import {IncomingHttpHeaders} from 'http'
|
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
|
||||||
import {
|
import {
|
||||||
getRuntimeToken,
|
getRuntimeToken,
|
||||||
getRuntimeUrl,
|
getRuntimeUrl,
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
getRetryMultiplier,
|
getRetryMultiplier,
|
||||||
getInitialRetryIntervalInMilliseconds
|
getInitialRetryIntervalInMilliseconds
|
||||||
} from './config-variables'
|
} from './config-variables'
|
||||||
|
import CRC64 from './crc64'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a retry time in milliseconds that exponentially gets larger
|
* Returns a retry time in milliseconds that exponentially gets larger
|
||||||
@@ -30,7 +31,7 @@ export function getExponentialRetryTimeInMilliseconds(
|
|||||||
const maxTime = minTime * getRetryMultiplier()
|
const maxTime = minTime * getRetryMultiplier()
|
||||||
|
|
||||||
// returns a random number between the minTime (inclusive) and the maxTime (exclusive)
|
// returns a random number between the minTime (inclusive) and the maxTime (exclusive)
|
||||||
return Math.random() * (maxTime - minTime) + minTime
|
return Math.trunc(Math.random() * (maxTime - minTime) + minTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,8 +73,9 @@ export function isRetryableStatusCode(statusCode: number | undefined): boolean {
|
|||||||
|
|
||||||
const retryableStatusCodes = [
|
const retryableStatusCodes = [
|
||||||
HttpCodes.BadGateway,
|
HttpCodes.BadGateway,
|
||||||
HttpCodes.ServiceUnavailable,
|
|
||||||
HttpCodes.GatewayTimeout,
|
HttpCodes.GatewayTimeout,
|
||||||
|
HttpCodes.InternalServerError,
|
||||||
|
HttpCodes.ServiceUnavailable,
|
||||||
HttpCodes.TooManyRequests,
|
HttpCodes.TooManyRequests,
|
||||||
413 // Payload Too Large
|
413 // Payload Too Large
|
||||||
]
|
]
|
||||||
@@ -138,8 +140,8 @@ export function getDownloadHeaders(
|
|||||||
contentType: string,
|
contentType: string,
|
||||||
isKeepAlive?: boolean,
|
isKeepAlive?: boolean,
|
||||||
acceptGzip?: boolean
|
acceptGzip?: boolean
|
||||||
): IHeaders {
|
): OutgoingHttpHeaders {
|
||||||
const requestOptions: IHeaders = {}
|
const requestOptions: OutgoingHttpHeaders = {}
|
||||||
|
|
||||||
if (contentType) {
|
if (contentType) {
|
||||||
requestOptions['Content-Type'] = contentType
|
requestOptions['Content-Type'] = contentType
|
||||||
@@ -179,9 +181,10 @@ export function getUploadHeaders(
|
|||||||
isGzip?: boolean,
|
isGzip?: boolean,
|
||||||
uncompressedLength?: number,
|
uncompressedLength?: number,
|
||||||
contentLength?: number,
|
contentLength?: number,
|
||||||
contentRange?: string
|
contentRange?: string,
|
||||||
): IHeaders {
|
digest?: StreamDigest
|
||||||
const requestOptions: IHeaders = {}
|
): OutgoingHttpHeaders {
|
||||||
|
const requestOptions: OutgoingHttpHeaders = {}
|
||||||
requestOptions['Accept'] = `application/json;api-version=${getApiVersion()}`
|
requestOptions['Accept'] = `application/json;api-version=${getApiVersion()}`
|
||||||
if (contentType) {
|
if (contentType) {
|
||||||
requestOptions['Content-Type'] = contentType
|
requestOptions['Content-Type'] = contentType
|
||||||
@@ -201,6 +204,10 @@ export function getUploadHeaders(
|
|||||||
if (contentRange) {
|
if (contentRange) {
|
||||||
requestOptions['Content-Range'] = contentRange
|
requestOptions['Content-Range'] = contentRange
|
||||||
}
|
}
|
||||||
|
if (digest) {
|
||||||
|
requestOptions['x-actions-results-crc64'] = digest.crc64
|
||||||
|
requestOptions['x-actions-results-md5'] = digest.md5
|
||||||
|
}
|
||||||
|
|
||||||
return requestOptions
|
return requestOptions
|
||||||
}
|
}
|
||||||
@@ -226,7 +233,7 @@ export function getArtifactUrl(): string {
|
|||||||
* Certain information such as the TLSSocket and the Readable state are not really useful for diagnostic purposes so they can be avoided.
|
* Certain information such as the TLSSocket and the Readable state are not really useful for diagnostic purposes so they can be avoided.
|
||||||
* Other information such as the headers, the response code and message might be useful, so this is displayed.
|
* Other information such as the headers, the response code and message might be useful, so this is displayed.
|
||||||
*/
|
*/
|
||||||
export function displayHttpDiagnostics(response: IHttpClientResponse): void {
|
export function displayHttpDiagnostics(response: HttpClientResponse): void {
|
||||||
info(
|
info(
|
||||||
`##### Begin Diagnostic HTTP information #####
|
`##### Begin Diagnostic HTTP information #####
|
||||||
Status Code: ${response.message.statusCode}
|
Status Code: ${response.message.statusCode}
|
||||||
@@ -236,55 +243,6 @@ Header Information: ${JSON.stringify(response.message.headers, undefined, 2)}
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalid characters that cannot be in the artifact name or an uploaded file. Will be rejected
|
|
||||||
* from the server if attempted to be sent over. These characters are not allowed due to limitations with certain
|
|
||||||
* file systems such as NTFS. To maintain platform-agnostic behavior, all characters that are not supported by an
|
|
||||||
* individual filesystem/platform will not be supported on all fileSystems/platforms
|
|
||||||
*
|
|
||||||
* FilePaths can include characters such as \ and / which are not permitted in the artifact name alone
|
|
||||||
*/
|
|
||||||
const invalidArtifactFilePathCharacters = ['"', ':', '<', '>', '|', '*', '?']
|
|
||||||
const invalidArtifactNameCharacters = [
|
|
||||||
...invalidArtifactFilePathCharacters,
|
|
||||||
'\\',
|
|
||||||
'/'
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scans the name of the artifact to make sure there are no illegal characters
|
|
||||||
*/
|
|
||||||
export function checkArtifactName(name: string): void {
|
|
||||||
if (!name) {
|
|
||||||
throw new Error(`Artifact name: ${name}, is incorrectly provided`)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const invalidChar of invalidArtifactNameCharacters) {
|
|
||||||
if (name.includes(invalidChar)) {
|
|
||||||
throw new Error(
|
|
||||||
`Artifact name is not valid: ${name}. Contains character: "${invalidChar}". Invalid artifact name characters include: ${invalidArtifactNameCharacters.toString()}.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Scans the name of the filePath used to make sure there are no illegal characters
|
|
||||||
*/
|
|
||||||
export function checkArtifactFilePath(path: string): void {
|
|
||||||
if (!path) {
|
|
||||||
throw new Error(`Artifact path: ${path}, is incorrectly provided`)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const invalidChar of invalidArtifactFilePathCharacters) {
|
|
||||||
if (path.includes(invalidChar)) {
|
|
||||||
throw new Error(
|
|
||||||
`Artifact path is not valid: ${path}. Contains character: "${invalidChar}". Invalid characters include: ${invalidArtifactFilePathCharacters.toString()}.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createDirectoriesForArtifact(
|
export async function createDirectoriesForArtifact(
|
||||||
directories: string[]
|
directories: string[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -339,3 +297,29 @@ export function getProperRetention(
|
|||||||
export async function sleep(milliseconds: number): Promise<void> {
|
export async function sleep(milliseconds: number): Promise<void> {
|
||||||
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
return new Promise(resolve => setTimeout(resolve, milliseconds))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StreamDigest {
|
||||||
|
crc64: string
|
||||||
|
md5: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function digestForStream(
|
||||||
|
stream: NodeJS.ReadableStream
|
||||||
|
): Promise<StreamDigest> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const crc64 = new CRC64()
|
||||||
|
const md5 = crypto.createHash('md5')
|
||||||
|
stream
|
||||||
|
.on('data', data => {
|
||||||
|
crc64.update(data)
|
||||||
|
md5.update(data)
|
||||||
|
})
|
||||||
|
.on('end', () =>
|
||||||
|
resolve({
|
||||||
|
crc64: crc64.digest('base64') as string,
|
||||||
|
md5: md5.digest('base64')
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+12
-5
@@ -2,17 +2,17 @@
|
|||||||
|
|
||||||
> Functions necessary for caching dependencies and build outputs to improve workflow execution time.
|
> Functions necessary for caching dependencies and build outputs to improve workflow execution time.
|
||||||
|
|
||||||
See ["Caching dependencies to speed up workflows"](https://help.github.com/github/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows) for how caching works.
|
See ["Caching dependencies to speed up workflows"](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) for how caching works.
|
||||||
|
|
||||||
Note that GitHub will remove any cache entries that have not been accessed in over 7 days. There is no limit on the number of caches you can store, but the total size of all caches in a repository is limited to 5 GB. If you exceed this limit, GitHub will save your cache but will begin evicting caches until the total size is less than 5 GB.
|
Note that GitHub will remove any cache entries that have not been accessed in over 7 days. There is no limit on the number of caches you can store, but the total size of all caches in a repository is limited to 10 GB. If you exceed this limit, GitHub will save your cache but will begin evicting caches until the total size is less than 10 GB.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
This package is used by the v2+ versions of our first party cache action. You can find an example implementation in the cache repo [here](https://github.com/actions/cache).
|
This package is used by the v2+ versions of our first party cache action. You can find an example implementation in the cache repo [here](https://github.com/actions/cache).
|
||||||
|
|
||||||
#### Save Cache
|
#### Save Cache
|
||||||
|
|
||||||
Saves a cache containing the files in `paths` using the `key` provided. The files would be compressed using zstandard compression algorithm if zstd is installed, otherwise gzip is used. Function returns the cache id if the cache was saved succesfully and throws an error if cache upload fails.
|
Saves a cache containing the files in `paths` using the `key` provided. The files would be compressed using zstandard compression algorithm if zstd is installed, otherwise gzip is used. Function returns the cache id if the cache was saved succesfully and throws an error if cache upload fails.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const cache = require('@actions/cache');
|
const cache = require('@actions/cache');
|
||||||
@@ -26,7 +26,7 @@ const cacheId = await cache.saveCache(paths, key)
|
|||||||
|
|
||||||
#### Restore Cache
|
#### Restore Cache
|
||||||
|
|
||||||
Restores a cache based on `key` and `restoreKeys` to the `paths` provided. Function returns the cache key for cache hit and returns undefined if cache not found.
|
Restores a cache based on `key` and `restoreKeys` to the `paths` provided. Function returns the cache key for cache hit and returns undefined if cache not found.
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const cache = require('@actions/cache');
|
const cache = require('@actions/cache');
|
||||||
@@ -42,3 +42,10 @@ const restoreKeys = [
|
|||||||
const cacheKey = await cache.restoreCache(paths, key, restoreKeys)
|
const cacheKey = await cache.restoreCache(paths, key, restoreKeys)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### Cache segment restore timeout
|
||||||
|
|
||||||
|
A cache gets downloaded in multiple segments of fixed sizes (now `128MB` to fail-fast, previously `1GB` for a `32-bit` runner and `2GB` for a `64-bit` runner were used). Sometimes, a segment download gets stuck which causes the workflow job to be stuck forever and fail. Version `v3.0.4` of cache package introduces a segment download timeout. The segment download timeout will allow the segment download to get aborted and hence allow the job to proceed with a cache miss.
|
||||||
|
|
||||||
|
Default value of this timeout is 10 minutes (starting `v3.2.1` and higher, previously 60 minutes in versions between `v.3.0.4` and `v3.2.0`, both included) and can be customized by specifying an [environment variable](https://docs.github.com/en/actions/learn-github-actions/environment-variables) named `SEGMENT_DOWNLOAD_TIMEOUT_MINS` with timeout value in minutes.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Vendored
+120
@@ -5,38 +5,158 @@
|
|||||||
- Initial release
|
- Initial release
|
||||||
|
|
||||||
### 0.2.0
|
### 0.2.0
|
||||||
|
|
||||||
- Fixes issues with the zstd compression algorithm on Windows and Ubuntu 16.04 [#469](https://github.com/actions/toolkit/pull/469)
|
- Fixes issues with the zstd compression algorithm on Windows and Ubuntu 16.04 [#469](https://github.com/actions/toolkit/pull/469)
|
||||||
|
|
||||||
### 0.2.1
|
### 0.2.1
|
||||||
|
|
||||||
- Fix to await async function getCompressionMethod
|
- Fix to await async function getCompressionMethod
|
||||||
|
|
||||||
### 1.0.0
|
### 1.0.0
|
||||||
|
|
||||||
- Downloads Azure-hosted caches using the Azure SDK for speed and reliability
|
- Downloads Azure-hosted caches using the Azure SDK for speed and reliability
|
||||||
- Displays download progress
|
- Displays download progress
|
||||||
- Includes changes that break compatibility with earlier versions, including:
|
- Includes changes that break compatibility with earlier versions, including:
|
||||||
- `retry`, `retryTypedResponse`, and `retryHttpClientResponse` moved from `cacheHttpClient` to `requestUtils`
|
- `retry`, `retryTypedResponse`, and `retryHttpClientResponse` moved from `cacheHttpClient` to `requestUtils`
|
||||||
|
|
||||||
### 1.0.1
|
### 1.0.1
|
||||||
|
|
||||||
- Fix bug in downloading large files (> 2 GBs) with the Azure SDK
|
- Fix bug in downloading large files (> 2 GBs) with the Azure SDK
|
||||||
|
|
||||||
### 1.0.2
|
### 1.0.2
|
||||||
|
|
||||||
- Use posix archive format to add support for some tools
|
- Use posix archive format to add support for some tools
|
||||||
|
|
||||||
### 1.0.3
|
### 1.0.3
|
||||||
|
|
||||||
- Use http-client v1.0.9
|
- Use http-client v1.0.9
|
||||||
- Fixes error handling so retries are not attempted on non-retryable errors (409 Conflict, for example)
|
- Fixes error handling so retries are not attempted on non-retryable errors (409 Conflict, for example)
|
||||||
- Adds 5 second delay between retry attempts
|
- Adds 5 second delay between retry attempts
|
||||||
|
|
||||||
### 1.0.4
|
### 1.0.4
|
||||||
|
|
||||||
- Use @actions/core v1.2.6
|
- Use @actions/core v1.2.6
|
||||||
- Fixes uploadChunk to throw an error if any unsuccessful response code is received
|
- Fixes uploadChunk to throw an error if any unsuccessful response code is received
|
||||||
|
|
||||||
### 1.0.5
|
### 1.0.5
|
||||||
|
|
||||||
- Fix to ensure Windows cache paths get resolved correctly
|
- Fix to ensure Windows cache paths get resolved correctly
|
||||||
|
|
||||||
### 1.0.6
|
### 1.0.6
|
||||||
|
|
||||||
- Make caching more verbose [#650](https://github.com/actions/toolkit/pull/650)
|
- Make caching more verbose [#650](https://github.com/actions/toolkit/pull/650)
|
||||||
- Use GNU tar on macOS if available [#701](https://github.com/actions/toolkit/pull/701)
|
- Use GNU tar on macOS if available [#701](https://github.com/actions/toolkit/pull/701)
|
||||||
|
|
||||||
### 1.0.7
|
### 1.0.7
|
||||||
|
|
||||||
- Fixes permissions issue extracting archives with GNU tar on macOS ([issue](https://github.com/actions/cache/issues/527))
|
- Fixes permissions issue extracting archives with GNU tar on macOS ([issue](https://github.com/actions/cache/issues/527))
|
||||||
|
|
||||||
|
### 1.0.8
|
||||||
|
|
||||||
|
- Increase the allowed artifact cache size from 5GB to 10GB ([issue](https://github.com/actions/cache/discussions/497))
|
||||||
|
|
||||||
|
### 1.0.9
|
||||||
|
|
||||||
|
- Use @azure/ms-rest-js v2.6.0
|
||||||
|
- Use @azure/storage-blob v12.8.0
|
||||||
|
|
||||||
|
### 1.0.10
|
||||||
|
|
||||||
|
- Update `lockfileVersion` to `v2` in `package-lock.json [#1022](https://github.com/actions/toolkit/pull/1022)
|
||||||
|
|
||||||
|
### 1.0.11
|
||||||
|
|
||||||
|
- Fix file downloads > 2GB([issue](https://github.com/actions/cache/issues/773))
|
||||||
|
|
||||||
|
### 2.0.0
|
||||||
|
|
||||||
|
- Added support to check if Actions cache service feature is available or not [#1028](https://github.com/actions/toolkit/pull/1028)
|
||||||
|
|
||||||
|
### 2.0.3
|
||||||
|
|
||||||
|
- Update to v2.0.0 of `@actions/http-client`
|
||||||
|
|
||||||
|
### 2.0.4
|
||||||
|
|
||||||
|
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
|
||||||
|
|
||||||
|
### 2.0.5
|
||||||
|
|
||||||
|
- Fix to avoid saving empty cache when no files are available for caching. ([issue](https://github.com/actions/cache/issues/624))
|
||||||
|
|
||||||
|
### 2.0.6
|
||||||
|
|
||||||
|
- Fix `Tar failed with error: The process '/usr/bin/tar' failed with exit code 1` issue when temp directory where tar is getting created is actually the subdirectory of the path mentioned by the user for caching. ([issue](https://github.com/actions/cache/issues/689))
|
||||||
|
|
||||||
|
### 3.0.0
|
||||||
|
|
||||||
|
- Updated actions/cache to suppress Actions cache server error and log warning for those error [#1122](https://github.com/actions/toolkit/pull/1122)
|
||||||
|
|
||||||
|
### 3.0.1
|
||||||
|
|
||||||
|
- Fix [#833](https://github.com/actions/cache/issues/833) - cache doesn't work with github workspace directory.
|
||||||
|
- Fix [#809](https://github.com/actions/cache/issues/809) `zstd -d: no such file or directory` error on AWS self-hosted runners.
|
||||||
|
|
||||||
|
### 3.0.2
|
||||||
|
|
||||||
|
- Added 1 hour timeout for the download stuck issue [#810](https://github.com/actions/cache/issues/810).
|
||||||
|
|
||||||
|
### 3.0.3
|
||||||
|
|
||||||
|
- Bug fixes for download stuck issue [#810](https://github.com/actions/cache/issues/810).
|
||||||
|
|
||||||
|
### 3.0.4
|
||||||
|
|
||||||
|
- Fix zstd not working for windows on gnu tar in issues [#888](https://github.com/actions/cache/issues/888) and [#891](https://github.com/actions/cache/issues/891).
|
||||||
|
- Allowing users to provide a custom timeout as input for aborting download of a cache segment using an environment variable `SEGMENT_DOWNLOAD_TIMEOUT_MINS`. Default is 60 minutes.
|
||||||
|
|
||||||
|
### 3.0.5
|
||||||
|
|
||||||
|
- Update `@actions/cache` to use `@actions/core@^1.10.0`
|
||||||
|
|
||||||
|
### 3.0.6
|
||||||
|
|
||||||
|
- Added `@azure/abort-controller` to dependencies to fix compatibility issue with ESM [#1208](https://github.com/actions/toolkit/issues/1208)
|
||||||
|
|
||||||
|
### 3.1.0-beta.1
|
||||||
|
|
||||||
|
- Update actions/cache on windows to use gnu tar and zstd by default and fallback to bsdtar and zstd if gnu tar is not available. ([issue](https://github.com/actions/cache/issues/984))
|
||||||
|
|
||||||
|
### 3.1.0-beta.2
|
||||||
|
|
||||||
|
- Added support for fallback to gzip to restore old caches on windows.
|
||||||
|
|
||||||
|
### 3.1.0-beta.3
|
||||||
|
|
||||||
|
- Bug Fixes for fallback to gzip to restore old caches on windows and bsdtar if gnutar is not available.
|
||||||
|
|
||||||
|
### 3.1.0
|
||||||
|
|
||||||
|
- Update actions/cache on windows to use gnu tar and zstd by default
|
||||||
|
- Update actions/cache on windows to fallback to bsdtar and zstd if gnu tar is not available.
|
||||||
|
- Added support for fallback to gzip to restore old caches on windows.
|
||||||
|
|
||||||
|
### 3.1.1
|
||||||
|
|
||||||
|
- Reverted changes in 3.1.0 to fix issue with symlink restoration on windows.
|
||||||
|
- Added support for verbose logging about cache version during cache miss.
|
||||||
|
|
||||||
|
### 3.1.2
|
||||||
|
|
||||||
|
- Fix issue with symlink restoration on windows.
|
||||||
|
|
||||||
|
### 3.1.3
|
||||||
|
|
||||||
|
- Fix to prevent from setting MYSYS environement variable globally [#1329](https://github.com/actions/toolkit/pull/1329).
|
||||||
|
|
||||||
|
### 3.1.4
|
||||||
|
|
||||||
|
- Fix zstd not being used due to `zstd --version` output change in zstd 1.5.4 release. See [#1353](https://github.com/actions/toolkit/pull/1353).
|
||||||
|
|
||||||
|
### 3.2.0
|
||||||
|
|
||||||
|
- Add `lookupOnly` to cache restore `DownloadOptions`.
|
||||||
|
|
||||||
|
### 3.2.1
|
||||||
|
|
||||||
|
- Updated @azure/storage-blob to `v12.13.0`
|
||||||
|
|||||||
+2
-2
@@ -3,10 +3,10 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const filePath = process.env[`GITHUB_ENV`]
|
const filePath = process.env[`GITHUB_ENV`]
|
||||||
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_URL=${process.env.ACTIONS_RUNTIME_URL}${os.EOL}`, {
|
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_TOKEN=${process.env.ACTIONS_RUNTIME_TOKEN}${os.EOL}`, {
|
||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
})
|
})
|
||||||
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_TOKEN=${process.env.ACTIONS_RUNTIME_TOKEN}${os.EOL}`, {
|
fs.appendFileSync(filePath, `ACTIONS_CACHE_URL=${process.env.ACTIONS_CACHE_URL}${os.EOL}`, {
|
||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
})
|
})
|
||||||
fs.appendFileSync(filePath, `GITHUB_RUN_ID=${process.env.GITHUB_RUN_ID}${os.EOL}`, {
|
fs.appendFileSync(filePath, `GITHUB_RUN_ID=${process.env.GITHUB_RUN_ID}${os.EOL}`, {
|
||||||
|
|||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
import * as cache from '../src/cache'
|
||||||
|
|
||||||
|
test('isFeatureAvailable returns true if server url is set', () => {
|
||||||
|
try {
|
||||||
|
process.env['ACTIONS_CACHE_URL'] = 'http://cache.com'
|
||||||
|
expect(cache.isFeatureAvailable()).toBe(true)
|
||||||
|
} finally {
|
||||||
|
delete process.env['ACTIONS_CACHE_URL']
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('isFeatureAvailable returns false if server url is not set', () => {
|
||||||
|
expect(cache.isFeatureAvailable()).toBe(false)
|
||||||
|
})
|
||||||
+17
-6
@@ -7,7 +7,7 @@ jest.mock('../src/internal/downloadUtils')
|
|||||||
|
|
||||||
test('getCacheVersion with one path returns version', async () => {
|
test('getCacheVersion with one path returns version', async () => {
|
||||||
const paths = ['node_modules']
|
const paths = ['node_modules']
|
||||||
const result = getCacheVersion(paths)
|
const result = getCacheVersion(paths, undefined, true)
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
'b3e0c6cb5ecf32614eeb2997d905b9c297046d7cbf69062698f25b14b4cb0985'
|
'b3e0c6cb5ecf32614eeb2997d905b9c297046d7cbf69062698f25b14b4cb0985'
|
||||||
)
|
)
|
||||||
@@ -15,7 +15,7 @@ test('getCacheVersion with one path returns version', async () => {
|
|||||||
|
|
||||||
test('getCacheVersion with multiple paths returns version', async () => {
|
test('getCacheVersion with multiple paths returns version', async () => {
|
||||||
const paths = ['node_modules', 'dist']
|
const paths = ['node_modules', 'dist']
|
||||||
const result = getCacheVersion(paths)
|
const result = getCacheVersion(paths, undefined, true)
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
'165c3053bc646bf0d4fac17b1f5731caca6fe38e0e464715c0c3c6b6318bf436'
|
'165c3053bc646bf0d4fac17b1f5731caca6fe38e0e464715c0c3c6b6318bf436'
|
||||||
)
|
)
|
||||||
@@ -23,22 +23,33 @@ test('getCacheVersion with multiple paths returns version', async () => {
|
|||||||
|
|
||||||
test('getCacheVersion with zstd compression returns version', async () => {
|
test('getCacheVersion with zstd compression returns version', async () => {
|
||||||
const paths = ['node_modules']
|
const paths = ['node_modules']
|
||||||
const result = getCacheVersion(paths, CompressionMethod.Zstd)
|
const result = getCacheVersion(paths, CompressionMethod.Zstd, true)
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
'273877e14fd65d270b87a198edbfa2db5a43de567c9a548d2a2505b408befe24'
|
'273877e14fd65d270b87a198edbfa2db5a43de567c9a548d2a2505b408befe24'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('getCacheVersion with gzip compression does not change vesion', async () => {
|
test('getCacheVersion with gzip compression returns version', async () => {
|
||||||
const paths = ['node_modules']
|
const paths = ['node_modules']
|
||||||
const result = getCacheVersion(paths, CompressionMethod.Gzip)
|
const result = getCacheVersion(paths, CompressionMethod.Gzip, true)
|
||||||
|
|
||||||
expect(result).toEqual(
|
expect(result).toEqual(
|
||||||
'b3e0c6cb5ecf32614eeb2997d905b9c297046d7cbf69062698f25b14b4cb0985'
|
'470e252814dbffc9524891b17cf4e5749b26c1b5026e63dd3f00972db2393117'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('getCacheVersion with enableCrossOsArchive as false returns version on windows', async () => {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const paths = ['node_modules']
|
||||||
|
const result = getCacheVersion(paths)
|
||||||
|
|
||||||
|
expect(result).toEqual(
|
||||||
|
'2db19d6596dc34f51f0043120148827a264863f5c6ac857569c2af7119bad14e'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('downloadCache uses http-client for non-Azure URLs', async () => {
|
test('downloadCache uses http-client for non-Azure URLs', async () => {
|
||||||
const downloadCacheHttpClientMock = jest.spyOn(
|
const downloadCacheHttpClientMock = jest.spyOn(
|
||||||
downloadUtils,
|
downloadUtils,
|
||||||
|
|||||||
+6
@@ -32,3 +32,9 @@ test('assertDefined throws if undefined', () => {
|
|||||||
test('assertDefined returns value', () => {
|
test('assertDefined returns value', () => {
|
||||||
expect(cacheUtils.assertDefined('test', 5)).toBe(5)
|
expect(cacheUtils.assertDefined('test', 5)).toBe(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('resolvePaths works on github workspace directory', async () => {
|
||||||
|
const workspace = process.env['GITHUB_WORKSPACE'] ?? '.'
|
||||||
|
const paths = await cacheUtils.resolvePaths([workspace])
|
||||||
|
expect(paths.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|||||||
+4
-3
@@ -87,7 +87,7 @@ test('download progress tracked correctly', () => {
|
|||||||
expect(progress.isDone()).toBe(true)
|
expect(progress.isDone()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('display timer works correctly', () => {
|
test('display timer works correctly', done => {
|
||||||
const progress = new DownloadProgress(1000)
|
const progress = new DownloadProgress(1000)
|
||||||
|
|
||||||
const infoMock = jest.spyOn(core, 'info')
|
const infoMock = jest.spyOn(core, 'info')
|
||||||
@@ -103,6 +103,7 @@ test('display timer works correctly', () => {
|
|||||||
const test2 = (): void => {
|
const test2 = (): void => {
|
||||||
check()
|
check()
|
||||||
expect(progress.timeoutHandle).toBeUndefined()
|
expect(progress.timeoutHandle).toBeUndefined()
|
||||||
|
done()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the progress is displayed, stop the timer, and call test2.
|
// Validate the progress is displayed, stop the timer, and call test2.
|
||||||
@@ -112,7 +113,7 @@ test('display timer works correctly', () => {
|
|||||||
progress.stopDisplayTimer()
|
progress.stopDisplayTimer()
|
||||||
progress.setReceivedBytes(1000)
|
progress.setReceivedBytes(1000)
|
||||||
|
|
||||||
setTimeout(() => test2(), 100)
|
setTimeout(() => test2(), 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start the timer, update the received bytes, and call test1.
|
// Start the timer, update the received bytes, and call test1.
|
||||||
@@ -122,7 +123,7 @@ test('display timer works correctly', () => {
|
|||||||
|
|
||||||
progress.setReceivedBytes(500)
|
progress.setReceivedBytes(500)
|
||||||
|
|
||||||
setTimeout(() => test1(), 100)
|
setTimeout(() => test1(), 500)
|
||||||
}
|
}
|
||||||
|
|
||||||
start()
|
start()
|
||||||
|
|||||||
+28
-2
@@ -8,6 +8,8 @@ import {
|
|||||||
const useAzureSdk = true
|
const useAzureSdk = true
|
||||||
const downloadConcurrency = 8
|
const downloadConcurrency = 8
|
||||||
const timeoutInMs = 30000
|
const timeoutInMs = 30000
|
||||||
|
const segmentTimeoutInMs = 600000
|
||||||
|
const lookupOnly = false
|
||||||
const uploadConcurrency = 4
|
const uploadConcurrency = 4
|
||||||
const uploadChunkSize = 32 * 1024 * 1024
|
const uploadChunkSize = 32 * 1024 * 1024
|
||||||
|
|
||||||
@@ -17,7 +19,9 @@ test('getDownloadOptions sets defaults', async () => {
|
|||||||
expect(actualOptions).toEqual({
|
expect(actualOptions).toEqual({
|
||||||
useAzureSdk,
|
useAzureSdk,
|
||||||
downloadConcurrency,
|
downloadConcurrency,
|
||||||
timeoutInMs
|
timeoutInMs,
|
||||||
|
segmentTimeoutInMs,
|
||||||
|
lookupOnly
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -25,7 +29,9 @@ test('getDownloadOptions overrides all settings', async () => {
|
|||||||
const expectedOptions: DownloadOptions = {
|
const expectedOptions: DownloadOptions = {
|
||||||
useAzureSdk: false,
|
useAzureSdk: false,
|
||||||
downloadConcurrency: 14,
|
downloadConcurrency: 14,
|
||||||
timeoutInMs: 20000
|
timeoutInMs: 20000,
|
||||||
|
segmentTimeoutInMs: 3600000,
|
||||||
|
lookupOnly: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const actualOptions = getDownloadOptions(expectedOptions)
|
const actualOptions = getDownloadOptions(expectedOptions)
|
||||||
@@ -52,3 +58,23 @@ test('getUploadOptions overrides all settings', async () => {
|
|||||||
|
|
||||||
expect(actualOptions).toEqual(expectedOptions)
|
expect(actualOptions).toEqual(expectedOptions)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('getDownloadOptions overrides download timeout minutes', async () => {
|
||||||
|
const expectedOptions: DownloadOptions = {
|
||||||
|
useAzureSdk: false,
|
||||||
|
downloadConcurrency: 14,
|
||||||
|
timeoutInMs: 20000,
|
||||||
|
segmentTimeoutInMs: 3600000,
|
||||||
|
lookupOnly: true
|
||||||
|
}
|
||||||
|
process.env.SEGMENT_DOWNLOAD_TIMEOUT_MINS = '10'
|
||||||
|
const actualOptions = getDownloadOptions(expectedOptions)
|
||||||
|
|
||||||
|
expect(actualOptions.useAzureSdk).toEqual(expectedOptions.useAzureSdk)
|
||||||
|
expect(actualOptions.downloadConcurrency).toEqual(
|
||||||
|
expectedOptions.downloadConcurrency
|
||||||
|
)
|
||||||
|
expect(actualOptions.timeoutInMs).toEqual(expectedOptions.timeoutInMs)
|
||||||
|
expect(actualOptions.segmentTimeoutInMs).toEqual(600000)
|
||||||
|
expect(actualOptions.lookupOnly).toEqual(expectedOptions.lookupOnly)
|
||||||
|
})
|
||||||
|
|||||||
+33
-2
@@ -1,5 +1,6 @@
|
|||||||
import {retry} from '../src/internal/requestUtils'
|
import {retry, retryTypedResponse} from '../src/internal/requestUtils'
|
||||||
import {HttpClientError} from '@actions/http-client'
|
import {HttpClientError} from '@actions/http-client'
|
||||||
|
import * as requestUtils from '../src/internal/requestUtils'
|
||||||
|
|
||||||
interface ITestResponse {
|
interface ITestResponse {
|
||||||
statusCode: number
|
statusCode: number
|
||||||
@@ -30,7 +31,6 @@ async function handleResponse(
|
|||||||
response: ITestResponse | undefined
|
response: ITestResponse | undefined
|
||||||
): Promise<ITestResponse> {
|
): Promise<ITestResponse> {
|
||||||
if (!response) {
|
if (!response) {
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
fail('Retry method called too many times')
|
fail('Retry method called too many times')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,3 +146,34 @@ test('retry converts errors to response object', async () => {
|
|||||||
null
|
null
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('retryTypedResponse gives an error with error message', async () => {
|
||||||
|
const httpClientError = new HttpClientError(
|
||||||
|
'The cache filesize must be between 0 and 10 * 1024 * 1024 bytes',
|
||||||
|
400
|
||||||
|
)
|
||||||
|
jest.spyOn(requestUtils, 'retry').mockReturnValue(
|
||||||
|
new Promise(resolve => {
|
||||||
|
resolve(httpClientError)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
await retryTypedResponse<string>(
|
||||||
|
'reserveCache',
|
||||||
|
async () =>
|
||||||
|
new Promise(resolve => {
|
||||||
|
resolve({
|
||||||
|
statusCode: 400,
|
||||||
|
result: '',
|
||||||
|
headers: {},
|
||||||
|
error: httpClientError
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toHaveProperty(
|
||||||
|
'message',
|
||||||
|
'The cache filesize must be between 0 and 10 * 1024 * 1024 bytes'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
+48
-6
@@ -18,7 +18,6 @@ beforeAll(() => {
|
|||||||
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||||
jest.spyOn(core, 'error').mockImplementation(() => {})
|
jest.spyOn(core, 'error').mockImplementation(() => {})
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
||||||
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
|
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
|
||||||
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
|
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
|
||||||
return actualUtils.getCacheFileName(cm)
|
return actualUtils.getCacheFileName(cm)
|
||||||
@@ -74,13 +73,17 @@ test('restore with no cache found', async () => {
|
|||||||
test('restore with server error should fail', async () => {
|
test('restore with server error should fail', async () => {
|
||||||
const paths = ['node_modules']
|
const paths = ['node_modules']
|
||||||
const key = 'node-test'
|
const key = 'node-test'
|
||||||
|
const logWarningMock = jest.spyOn(core, 'warning')
|
||||||
|
|
||||||
jest.spyOn(cacheHttpClient, 'getCacheEntry').mockImplementation(() => {
|
jest.spyOn(cacheHttpClient, 'getCacheEntry').mockImplementation(() => {
|
||||||
throw new Error('HTTP Error Occurred')
|
throw new Error('HTTP Error Occurred')
|
||||||
})
|
})
|
||||||
|
|
||||||
await expect(restoreCache(paths, key)).rejects.toThrowError(
|
const cacheKey = await restoreCache(paths, key)
|
||||||
'HTTP Error Occurred'
|
expect(cacheKey).toBe(undefined)
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
'Failed to restore: HTTP Error Occurred'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -139,7 +142,8 @@ test('restore with gzip compressed cache found', async () => {
|
|||||||
|
|
||||||
expect(cacheKey).toBe(key)
|
expect(cacheKey).toBe(key)
|
||||||
expect(getCacheMock).toHaveBeenCalledWith([key], paths, {
|
expect(getCacheMock).toHaveBeenCalledWith([key], paths, {
|
||||||
compressionMethod: compression
|
compressionMethod: compression,
|
||||||
|
enableCrossOsArchive: false
|
||||||
})
|
})
|
||||||
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1)
|
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1)
|
||||||
expect(downloadCacheMock).toHaveBeenCalledWith(
|
expect(downloadCacheMock).toHaveBeenCalledWith(
|
||||||
@@ -198,7 +202,8 @@ test('restore with zstd compressed cache found', async () => {
|
|||||||
|
|
||||||
expect(cacheKey).toBe(key)
|
expect(cacheKey).toBe(key)
|
||||||
expect(getCacheMock).toHaveBeenCalledWith([key], paths, {
|
expect(getCacheMock).toHaveBeenCalledWith([key], paths, {
|
||||||
compressionMethod: compression
|
compressionMethod: compression,
|
||||||
|
enableCrossOsArchive: false
|
||||||
})
|
})
|
||||||
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1)
|
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1)
|
||||||
expect(downloadCacheMock).toHaveBeenCalledWith(
|
expect(downloadCacheMock).toHaveBeenCalledWith(
|
||||||
@@ -255,7 +260,8 @@ test('restore with cache found for restore key', async () => {
|
|||||||
|
|
||||||
expect(cacheKey).toBe(restoreKey)
|
expect(cacheKey).toBe(restoreKey)
|
||||||
expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey], paths, {
|
expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey], paths, {
|
||||||
compressionMethod: compression
|
compressionMethod: compression,
|
||||||
|
enableCrossOsArchive: false
|
||||||
})
|
})
|
||||||
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1)
|
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1)
|
||||||
expect(downloadCacheMock).toHaveBeenCalledWith(
|
expect(downloadCacheMock).toHaveBeenCalledWith(
|
||||||
@@ -270,3 +276,39 @@ test('restore with cache found for restore key', async () => {
|
|||||||
expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression)
|
expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression)
|
||||||
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('restore with dry run', async () => {
|
||||||
|
const paths = ['node_modules']
|
||||||
|
const key = 'node-test'
|
||||||
|
const options = {lookupOnly: true}
|
||||||
|
|
||||||
|
const cacheEntry: ArtifactCacheEntry = {
|
||||||
|
cacheKey: key,
|
||||||
|
scope: 'refs/heads/main',
|
||||||
|
archiveLocation: 'www.actionscache.test/download'
|
||||||
|
}
|
||||||
|
const getCacheMock = jest.spyOn(cacheHttpClient, 'getCacheEntry')
|
||||||
|
getCacheMock.mockImplementation(async () => {
|
||||||
|
return Promise.resolve(cacheEntry)
|
||||||
|
})
|
||||||
|
|
||||||
|
const createTempDirectoryMock = jest.spyOn(cacheUtils, 'createTempDirectory')
|
||||||
|
const downloadCacheMock = jest.spyOn(cacheHttpClient, 'downloadCache')
|
||||||
|
|
||||||
|
const compression = CompressionMethod.Gzip
|
||||||
|
const getCompressionMock = jest
|
||||||
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
||||||
|
.mockReturnValue(Promise.resolve(compression))
|
||||||
|
|
||||||
|
const cacheKey = await restoreCache(paths, key, undefined, options)
|
||||||
|
|
||||||
|
expect(cacheKey).toBe(key)
|
||||||
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(getCacheMock).toHaveBeenCalledWith([key], paths, {
|
||||||
|
compressionMethod: compression,
|
||||||
|
enableCrossOsArchive: false
|
||||||
|
})
|
||||||
|
// creating a tempDir and downloading the cache are skipped
|
||||||
|
expect(createTempDirectoryMock).toHaveBeenCalledTimes(0)
|
||||||
|
expect(downloadCacheMock).toHaveBeenCalledTimes(0)
|
||||||
|
})
|
||||||
|
|||||||
+167
-27
@@ -5,6 +5,12 @@ import * as cacheHttpClient from '../src/internal/cacheHttpClient'
|
|||||||
import * as cacheUtils from '../src/internal/cacheUtils'
|
import * as cacheUtils from '../src/internal/cacheUtils'
|
||||||
import {CacheFilename, CompressionMethod} from '../src/internal/constants'
|
import {CacheFilename, CompressionMethod} from '../src/internal/constants'
|
||||||
import * as tar from '../src/internal/tar'
|
import * as tar from '../src/internal/tar'
|
||||||
|
import {TypedResponse} from '@actions/http-client/lib/interfaces'
|
||||||
|
import {
|
||||||
|
ReserveCacheResponse,
|
||||||
|
ITypedResponseWithError
|
||||||
|
} from '../src/internal/contracts'
|
||||||
|
import {HttpClientError} from '@actions/http-client'
|
||||||
|
|
||||||
jest.mock('../src/internal/cacheHttpClient')
|
jest.mock('../src/internal/cacheHttpClient')
|
||||||
jest.mock('../src/internal/cacheUtils')
|
jest.mock('../src/internal/cacheUtils')
|
||||||
@@ -16,17 +22,13 @@ beforeAll(() => {
|
|||||||
jest.spyOn(core, 'info').mockImplementation(() => {})
|
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||||
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||||
jest.spyOn(core, 'error').mockImplementation(() => {})
|
jest.spyOn(core, 'error').mockImplementation(() => {})
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/promise-function-async
|
|
||||||
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
|
jest.spyOn(cacheUtils, 'getCacheFileName').mockImplementation(cm => {
|
||||||
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
|
const actualUtils = jest.requireActual('../src/internal/cacheUtils')
|
||||||
return actualUtils.getCacheFileName(cm)
|
return actualUtils.getCacheFileName(cm)
|
||||||
})
|
})
|
||||||
|
|
||||||
jest.spyOn(cacheUtils, 'resolvePaths').mockImplementation(async filePaths => {
|
jest.spyOn(cacheUtils, 'resolvePaths').mockImplementation(async filePaths => {
|
||||||
return filePaths.map(x => path.resolve(x))
|
return filePaths.map(x => path.resolve(x))
|
||||||
})
|
})
|
||||||
|
|
||||||
jest.spyOn(cacheUtils, 'createTempDirectory').mockImplementation(async () => {
|
jest.spyOn(cacheUtils, 'createTempDirectory').mockImplementation(async () => {
|
||||||
return Promise.resolve('/foo/bar')
|
return Promise.resolve('/foo/bar')
|
||||||
})
|
})
|
||||||
@@ -46,8 +48,9 @@ test('save with large cache outputs should fail', async () => {
|
|||||||
const cachePaths = [path.resolve(filePath)]
|
const cachePaths = [path.resolve(filePath)]
|
||||||
|
|
||||||
const createTarMock = jest.spyOn(tar, 'createTar')
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
||||||
|
const logWarningMock = jest.spyOn(core, 'warning')
|
||||||
|
|
||||||
const cacheSize = 6 * 1024 * 1024 * 1024 //~6GB, over the 5GB limit
|
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
|
||||||
jest
|
jest
|
||||||
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||||
.mockReturnValueOnce(cacheSize)
|
.mockReturnValueOnce(cacheSize)
|
||||||
@@ -56,8 +59,11 @@ test('save with large cache outputs should fail', async () => {
|
|||||||
.spyOn(cacheUtils, 'getCompressionMethod')
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
||||||
.mockReturnValueOnce(Promise.resolve(compression))
|
.mockReturnValueOnce(Promise.resolve(compression))
|
||||||
|
|
||||||
await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
|
const cacheId = await saveCache([filePath], primaryKey)
|
||||||
'Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache.'
|
expect(cacheId).toBe(-1)
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
'Failed to save: Cache size of ~11264 MB (11811160064 B) is over the 10GB limit, not saving cache.'
|
||||||
)
|
)
|
||||||
|
|
||||||
const archiveFolder = '/foo/bar'
|
const archiveFolder = '/foo/bar'
|
||||||
@@ -71,14 +77,120 @@ test('save with large cache outputs should fail', async () => {
|
|||||||
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('save with reserve cache failure should fail', async () => {
|
test('save with large cache outputs should fail in GHES with error message', async () => {
|
||||||
const paths = ['node_modules']
|
const filePath = 'node_modules'
|
||||||
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
||||||
|
const cachePaths = [path.resolve(filePath)]
|
||||||
|
|
||||||
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
||||||
|
const logWarningMock = jest.spyOn(core, 'warning')
|
||||||
|
|
||||||
|
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
|
||||||
|
jest
|
||||||
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||||
|
.mockReturnValueOnce(cacheSize)
|
||||||
|
const compression = CompressionMethod.Gzip
|
||||||
|
const getCompressionMock = jest
|
||||||
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
||||||
|
.mockReturnValueOnce(Promise.resolve(compression))
|
||||||
|
|
||||||
|
jest.spyOn(cacheUtils, 'isGhes').mockReturnValueOnce(true)
|
||||||
|
|
||||||
const reserveCacheMock = jest
|
const reserveCacheMock = jest
|
||||||
.spyOn(cacheHttpClient, 'reserveCache')
|
.spyOn(cacheHttpClient, 'reserveCache')
|
||||||
.mockImplementation(async () => {
|
.mockImplementation(async () => {
|
||||||
return -1
|
const response: ITypedResponseWithError<ReserveCacheResponse> = {
|
||||||
|
statusCode: 400,
|
||||||
|
result: null,
|
||||||
|
headers: {},
|
||||||
|
error: new HttpClientError(
|
||||||
|
'The cache filesize must be between 0 and 1073741824 bytes',
|
||||||
|
400
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
|
||||||
|
const cacheId = await saveCache([filePath], primaryKey)
|
||||||
|
expect(cacheId).toBe(-1)
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
'Failed to save: The cache filesize must be between 0 and 1073741824 bytes'
|
||||||
|
)
|
||||||
|
|
||||||
|
const archiveFolder = '/foo/bar'
|
||||||
|
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(createTarMock).toHaveBeenCalledWith(
|
||||||
|
archiveFolder,
|
||||||
|
cachePaths,
|
||||||
|
compression
|
||||||
|
)
|
||||||
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('save with large cache outputs should fail in GHES without error message', async () => {
|
||||||
|
const filePath = 'node_modules'
|
||||||
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
||||||
|
const cachePaths = [path.resolve(filePath)]
|
||||||
|
|
||||||
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
||||||
|
const logWarningMock = jest.spyOn(core, 'warning')
|
||||||
|
|
||||||
|
const cacheSize = 11 * 1024 * 1024 * 1024 //~11GB, over the 10GB limit
|
||||||
|
jest
|
||||||
|
.spyOn(cacheUtils, 'getArchiveFileSizeInBytes')
|
||||||
|
.mockReturnValueOnce(cacheSize)
|
||||||
|
const compression = CompressionMethod.Gzip
|
||||||
|
const getCompressionMock = jest
|
||||||
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
||||||
|
.mockReturnValueOnce(Promise.resolve(compression))
|
||||||
|
|
||||||
|
jest.spyOn(cacheUtils, 'isGhes').mockReturnValueOnce(true)
|
||||||
|
|
||||||
|
const reserveCacheMock = jest
|
||||||
|
.spyOn(cacheHttpClient, 'reserveCache')
|
||||||
|
.mockImplementation(async () => {
|
||||||
|
const response: ITypedResponseWithError<ReserveCacheResponse> = {
|
||||||
|
statusCode: 400,
|
||||||
|
result: null,
|
||||||
|
headers: {}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
})
|
||||||
|
|
||||||
|
const cacheId = await saveCache([filePath], primaryKey)
|
||||||
|
expect(cacheId).toBe(-1)
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
'Failed to save: Cache size of ~11264 MB (11811160064 B) is over the data cap limit, not saving cache.'
|
||||||
|
)
|
||||||
|
|
||||||
|
const archiveFolder = '/foo/bar'
|
||||||
|
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(createTarMock).toHaveBeenCalledWith(
|
||||||
|
archiveFolder,
|
||||||
|
cachePaths,
|
||||||
|
compression
|
||||||
|
)
|
||||||
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('save with reserve cache failure should fail', async () => {
|
||||||
|
const paths = ['node_modules']
|
||||||
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
||||||
|
const logInfoMock = jest.spyOn(core, 'info')
|
||||||
|
|
||||||
|
const reserveCacheMock = jest
|
||||||
|
.spyOn(cacheHttpClient, 'reserveCache')
|
||||||
|
.mockImplementation(async () => {
|
||||||
|
const response: TypedResponse<ReserveCacheResponse> = {
|
||||||
|
statusCode: 500,
|
||||||
|
result: null,
|
||||||
|
headers: {}
|
||||||
|
}
|
||||||
|
return response
|
||||||
})
|
})
|
||||||
|
|
||||||
const createTarMock = jest.spyOn(tar, 'createTar')
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
||||||
@@ -88,14 +200,20 @@ test('save with reserve cache failure should fail', async () => {
|
|||||||
.spyOn(cacheUtils, 'getCompressionMethod')
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
||||||
.mockReturnValueOnce(Promise.resolve(compression))
|
.mockReturnValueOnce(Promise.resolve(compression))
|
||||||
|
|
||||||
await expect(saveCache(paths, primaryKey)).rejects.toThrowError(
|
const cacheId = await saveCache(paths, primaryKey)
|
||||||
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
expect(cacheId).toBe(-1)
|
||||||
|
expect(logInfoMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(logInfoMock).toHaveBeenCalledWith(
|
||||||
|
`Failed to save: Unable to reserve cache with key ${primaryKey}, another job may be creating this cache. More details: undefined`
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
||||||
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, paths, {
|
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, paths, {
|
||||||
compressionMethod: compression
|
cacheSize: undefined,
|
||||||
|
compressionMethod: compression,
|
||||||
|
enableCrossOsArchive: false
|
||||||
})
|
})
|
||||||
expect(createTarMock).toHaveBeenCalledTimes(0)
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0)
|
expect(saveCacheMock).toHaveBeenCalledTimes(0)
|
||||||
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
@@ -104,12 +222,17 @@ test('save with server error should fail', async () => {
|
|||||||
const filePath = 'node_modules'
|
const filePath = 'node_modules'
|
||||||
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
||||||
const cachePaths = [path.resolve(filePath)]
|
const cachePaths = [path.resolve(filePath)]
|
||||||
|
const logWarningMock = jest.spyOn(core, 'warning')
|
||||||
const cacheId = 4
|
const cacheId = 4
|
||||||
const reserveCacheMock = jest
|
const reserveCacheMock = jest
|
||||||
.spyOn(cacheHttpClient, 'reserveCache')
|
.spyOn(cacheHttpClient, 'reserveCache')
|
||||||
.mockImplementation(async () => {
|
.mockImplementation(async () => {
|
||||||
return cacheId
|
const response: TypedResponse<ReserveCacheResponse> = {
|
||||||
|
statusCode: 500,
|
||||||
|
result: {cacheId},
|
||||||
|
headers: {}
|
||||||
|
}
|
||||||
|
return response
|
||||||
})
|
})
|
||||||
|
|
||||||
const createTarMock = jest.spyOn(tar, 'createTar')
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
||||||
@@ -124,24 +247,26 @@ test('save with server error should fail', async () => {
|
|||||||
.spyOn(cacheUtils, 'getCompressionMethod')
|
.spyOn(cacheUtils, 'getCompressionMethod')
|
||||||
.mockReturnValueOnce(Promise.resolve(compression))
|
.mockReturnValueOnce(Promise.resolve(compression))
|
||||||
|
|
||||||
await expect(saveCache([filePath], primaryKey)).rejects.toThrowError(
|
await saveCache([filePath], primaryKey)
|
||||||
'HTTP Error Occurred'
|
expect(logWarningMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
'Failed to save: HTTP Error Occurred'
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
||||||
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
|
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
|
||||||
compressionMethod: compression
|
cacheSize: undefined,
|
||||||
|
compressionMethod: compression,
|
||||||
|
enableCrossOsArchive: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const archiveFolder = '/foo/bar'
|
const archiveFolder = '/foo/bar'
|
||||||
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
|
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
|
||||||
|
|
||||||
expect(createTarMock).toHaveBeenCalledTimes(1)
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
||||||
expect(createTarMock).toHaveBeenCalledWith(
|
expect(createTarMock).toHaveBeenCalledWith(
|
||||||
archiveFolder,
|
archiveFolder,
|
||||||
cachePaths,
|
cachePaths,
|
||||||
compression
|
compression
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1)
|
expect(saveCacheMock).toHaveBeenCalledTimes(1)
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile, undefined)
|
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile, undefined)
|
||||||
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||||
@@ -156,7 +281,12 @@ test('save with valid inputs uploads a cache', async () => {
|
|||||||
const reserveCacheMock = jest
|
const reserveCacheMock = jest
|
||||||
.spyOn(cacheHttpClient, 'reserveCache')
|
.spyOn(cacheHttpClient, 'reserveCache')
|
||||||
.mockImplementation(async () => {
|
.mockImplementation(async () => {
|
||||||
return cacheId
|
const response: TypedResponse<ReserveCacheResponse> = {
|
||||||
|
statusCode: 500,
|
||||||
|
result: {cacheId},
|
||||||
|
headers: {}
|
||||||
|
}
|
||||||
|
return response
|
||||||
})
|
})
|
||||||
const createTarMock = jest.spyOn(tar, 'createTar')
|
const createTarMock = jest.spyOn(tar, 'createTar')
|
||||||
|
|
||||||
@@ -170,20 +300,30 @@ test('save with valid inputs uploads a cache', async () => {
|
|||||||
|
|
||||||
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
||||||
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
|
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, [filePath], {
|
||||||
compressionMethod: compression
|
cacheSize: undefined,
|
||||||
|
compressionMethod: compression,
|
||||||
|
enableCrossOsArchive: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const archiveFolder = '/foo/bar'
|
const archiveFolder = '/foo/bar'
|
||||||
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
|
const archiveFile = path.join(archiveFolder, CacheFilename.Zstd)
|
||||||
|
|
||||||
expect(createTarMock).toHaveBeenCalledTimes(1)
|
expect(createTarMock).toHaveBeenCalledTimes(1)
|
||||||
expect(createTarMock).toHaveBeenCalledWith(
|
expect(createTarMock).toHaveBeenCalledWith(
|
||||||
archiveFolder,
|
archiveFolder,
|
||||||
cachePaths,
|
cachePaths,
|
||||||
compression
|
compression
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1)
|
expect(saveCacheMock).toHaveBeenCalledTimes(1)
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile, undefined)
|
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile, undefined)
|
||||||
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
expect(getCompressionMock).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('save with non existing path should not save cache', async () => {
|
||||||
|
const path = 'node_modules'
|
||||||
|
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
||||||
|
jest.spyOn(cacheUtils, 'resolvePaths').mockImplementation(async () => {
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
await expect(saveCache([path], primaryKey)).rejects.toThrowError(
|
||||||
|
`Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
Vendored
+272
-57
@@ -1,7 +1,14 @@
|
|||||||
import * as exec from '@actions/exec'
|
import * as exec from '@actions/exec'
|
||||||
import * as io from '@actions/io'
|
import * as io from '@actions/io'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {CacheFilename, CompressionMethod} from '../src/internal/constants'
|
import {
|
||||||
|
CacheFilename,
|
||||||
|
CompressionMethod,
|
||||||
|
GnuTarPathOnWindows,
|
||||||
|
ManifestFilename,
|
||||||
|
SystemTarPathOnWindows,
|
||||||
|
TarFilename
|
||||||
|
} from '../src/internal/constants'
|
||||||
import * as tar from '../src/internal/tar'
|
import * as tar from '../src/internal/tar'
|
||||||
import * as utils from '../src/internal/cacheUtils'
|
import * as utils from '../src/internal/cacheUtils'
|
||||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
@@ -13,7 +20,9 @@ jest.mock('@actions/io')
|
|||||||
const IS_WINDOWS = process.platform === 'win32'
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
const IS_MAC = process.platform === 'darwin'
|
const IS_MAC = process.platform === 'darwin'
|
||||||
|
|
||||||
const defaultTarPath = process.platform === 'darwin' ? 'gtar' : 'tar'
|
const defaultTarPath = IS_MAC ? 'gtar' : 'tar'
|
||||||
|
|
||||||
|
const defaultEnv = {MSYS: 'winsymlinks:nativestrict'}
|
||||||
|
|
||||||
function getTempDir(): string {
|
function getTempDir(): string {
|
||||||
return path.join(__dirname, '_temp', 'tar')
|
return path.join(__dirname, '_temp', 'tar')
|
||||||
@@ -28,6 +37,10 @@ beforeAll(async () => {
|
|||||||
await jest.requireActual('@actions/io').rmRF(getTempDir())
|
await jest.requireActual('@actions/io').rmRF(getTempDir())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
delete process.env['GITHUB_WORKSPACE']
|
delete process.env['GITHUB_WORKSPACE']
|
||||||
await jest.requireActual('@actions/io').rmRF(getTempDir())
|
await jest.requireActual('@actions/io').rmRF(getTempDir())
|
||||||
@@ -41,16 +54,15 @@ test('zstd extract tar', async () => {
|
|||||||
? `${process.env['windir']}\\fakepath\\cache.tar`
|
? `${process.env['windir']}\\fakepath\\cache.tar`
|
||||||
: 'cache.tar'
|
: 'cache.tar'
|
||||||
const workspace = process.env['GITHUB_WORKSPACE']
|
const workspace = process.env['GITHUB_WORKSPACE']
|
||||||
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
||||||
|
|
||||||
await tar.extractTar(archivePath, CompressionMethod.Zstd)
|
await tar.extractTar(archivePath, CompressionMethod.Zstd)
|
||||||
|
|
||||||
expect(mkdirMock).toHaveBeenCalledWith(workspace)
|
expect(mkdirMock).toHaveBeenCalledWith(workspace)
|
||||||
expect(execMock).toHaveBeenCalledTimes(1)
|
expect(execMock).toHaveBeenCalledTimes(1)
|
||||||
expect(execMock).toHaveBeenCalledWith(
|
expect(execMock).toHaveBeenCalledWith(
|
||||||
`"${defaultTarPath}"`,
|
|
||||||
[
|
[
|
||||||
'--use-compress-program',
|
`"${tarPath}"`,
|
||||||
'zstd -d --long=30',
|
|
||||||
'-xf',
|
'-xf',
|
||||||
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
||||||
'-P',
|
'-P',
|
||||||
@@ -58,11 +70,70 @@ test('zstd extract tar', async () => {
|
|||||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
|
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
|
||||||
]
|
]
|
||||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
||||||
{cwd: undefined}
|
.concat([
|
||||||
|
'--use-compress-program',
|
||||||
|
IS_WINDOWS ? '"zstd -d --long=30"' : 'unzstd --long=30'
|
||||||
|
])
|
||||||
|
.join(' '),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
cwd: undefined,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('zstd extract tar with windows BSDtar', async () => {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const mkdirMock = jest.spyOn(io, 'mkdirP')
|
||||||
|
const execMock = jest.spyOn(exec, 'exec')
|
||||||
|
jest
|
||||||
|
.spyOn(utils, 'getGnuTarPathOnWindows')
|
||||||
|
.mockReturnValue(Promise.resolve(''))
|
||||||
|
|
||||||
|
const archivePath = `${process.env['windir']}\\fakepath\\cache.tar`
|
||||||
|
const workspace = process.env['GITHUB_WORKSPACE']
|
||||||
|
const tarPath = SystemTarPathOnWindows
|
||||||
|
|
||||||
|
await tar.extractTar(archivePath, CompressionMethod.Zstd)
|
||||||
|
|
||||||
|
expect(mkdirMock).toHaveBeenCalledWith(workspace)
|
||||||
|
expect(execMock).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
|
expect(execMock).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
[
|
||||||
|
'zstd -d --long=30 --force -o',
|
||||||
|
TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||||
|
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
|
||||||
|
].join(' '),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
cwd: undefined,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(execMock).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
[
|
||||||
|
`"${tarPath}"`,
|
||||||
|
'-xf',
|
||||||
|
TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||||
|
'-P',
|
||||||
|
'-C',
|
||||||
|
workspace?.replace(/\\/g, '/')
|
||||||
|
].join(' '),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
cwd: undefined,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('gzip extract tar', async () => {
|
test('gzip extract tar', async () => {
|
||||||
const mkdirMock = jest.spyOn(io, 'mkdirP')
|
const mkdirMock = jest.spyOn(io, 'mkdirP')
|
||||||
const execMock = jest.spyOn(exec, 'exec')
|
const execMock = jest.spyOn(exec, 'exec')
|
||||||
@@ -74,51 +145,58 @@ test('gzip extract tar', async () => {
|
|||||||
await tar.extractTar(archivePath, CompressionMethod.Gzip)
|
await tar.extractTar(archivePath, CompressionMethod.Gzip)
|
||||||
|
|
||||||
expect(mkdirMock).toHaveBeenCalledWith(workspace)
|
expect(mkdirMock).toHaveBeenCalledWith(workspace)
|
||||||
const tarPath = IS_WINDOWS
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
||||||
? `${process.env['windir']}\\System32\\tar.exe`
|
|
||||||
: defaultTarPath
|
|
||||||
expect(execMock).toHaveBeenCalledTimes(1)
|
expect(execMock).toHaveBeenCalledTimes(1)
|
||||||
expect(execMock).toHaveBeenCalledWith(
|
expect(execMock).toHaveBeenCalledWith(
|
||||||
`"${tarPath}"`,
|
|
||||||
[
|
[
|
||||||
'-z',
|
`"${tarPath}"`,
|
||||||
'-xf',
|
'-xf',
|
||||||
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
||||||
'-P',
|
'-P',
|
||||||
'-C',
|
'-C',
|
||||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
|
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace
|
||||||
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
]
|
||||||
{cwd: undefined}
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||||
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
||||||
|
.concat(['-z'])
|
||||||
|
.join(' '),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
cwd: undefined,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('gzip extract GNU tar on windows', async () => {
|
test('gzip extract GNU tar on windows with GNUtar in path', async () => {
|
||||||
if (IS_WINDOWS) {
|
if (IS_WINDOWS) {
|
||||||
jest.spyOn(fs, 'existsSync').mockReturnValueOnce(false)
|
// GNU tar present in path but not at default location
|
||||||
|
jest
|
||||||
const isGnuMock = jest
|
.spyOn(utils, 'getGnuTarPathOnWindows')
|
||||||
.spyOn(utils, 'isGnuTarInstalled')
|
.mockReturnValue(Promise.resolve('tar'))
|
||||||
.mockReturnValue(Promise.resolve(true))
|
|
||||||
const execMock = jest.spyOn(exec, 'exec')
|
const execMock = jest.spyOn(exec, 'exec')
|
||||||
const archivePath = `${process.env['windir']}\\fakepath\\cache.tar`
|
const archivePath = `${process.env['windir']}\\fakepath\\cache.tar`
|
||||||
const workspace = process.env['GITHUB_WORKSPACE']
|
const workspace = process.env['GITHUB_WORKSPACE']
|
||||||
|
|
||||||
await tar.extractTar(archivePath, CompressionMethod.Gzip)
|
await tar.extractTar(archivePath, CompressionMethod.Gzip)
|
||||||
|
|
||||||
expect(isGnuMock).toHaveBeenCalledTimes(1)
|
|
||||||
expect(execMock).toHaveBeenCalledTimes(1)
|
expect(execMock).toHaveBeenCalledTimes(1)
|
||||||
expect(execMock).toHaveBeenCalledWith(
|
expect(execMock).toHaveBeenCalledWith(
|
||||||
`"tar"`,
|
|
||||||
[
|
[
|
||||||
'-z',
|
`"tar"`,
|
||||||
'-xf',
|
'-xf',
|
||||||
archivePath.replace(/\\/g, '/'),
|
archivePath.replace(/\\/g, '/'),
|
||||||
'-P',
|
'-P',
|
||||||
'-C',
|
'-C',
|
||||||
workspace?.replace(/\\/g, '/'),
|
workspace?.replace(/\\/g, '/'),
|
||||||
'--force-local'
|
'--force-local',
|
||||||
],
|
'-z'
|
||||||
{cwd: undefined}
|
].join(' '),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
cwd: undefined,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -134,29 +212,99 @@ test('zstd create tar', async () => {
|
|||||||
|
|
||||||
await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Zstd)
|
await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Zstd)
|
||||||
|
|
||||||
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
||||||
|
|
||||||
expect(execMock).toHaveBeenCalledTimes(1)
|
expect(execMock).toHaveBeenCalledTimes(1)
|
||||||
expect(execMock).toHaveBeenCalledWith(
|
expect(execMock).toHaveBeenCalledWith(
|
||||||
`"${defaultTarPath}"`,
|
|
||||||
[
|
[
|
||||||
|
`"${tarPath}"`,
|
||||||
'--posix',
|
'--posix',
|
||||||
'--use-compress-program',
|
|
||||||
'zstd -T0 --long=30',
|
|
||||||
'-cf',
|
'-cf',
|
||||||
IS_WINDOWS ? CacheFilename.Zstd.replace(/\\/g, '/') : CacheFilename.Zstd,
|
IS_WINDOWS ? CacheFilename.Zstd.replace(/\\/g, '/') : CacheFilename.Zstd,
|
||||||
|
'--exclude',
|
||||||
|
IS_WINDOWS ? CacheFilename.Zstd.replace(/\\/g, '/') : CacheFilename.Zstd,
|
||||||
'-P',
|
'-P',
|
||||||
'-C',
|
'-C',
|
||||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
|
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
|
||||||
'--files-from',
|
'--files-from',
|
||||||
'manifest.txt'
|
ManifestFilename
|
||||||
]
|
]
|
||||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
||||||
|
.concat([
|
||||||
|
'--use-compress-program',
|
||||||
|
IS_WINDOWS ? '"zstd -T0 --long=30"' : 'zstdmt --long=30'
|
||||||
|
])
|
||||||
|
.join(' '),
|
||||||
|
undefined, // args
|
||||||
{
|
{
|
||||||
cwd: archiveFolder
|
cwd: archiveFolder,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('zstd create tar with windows BSDtar', async () => {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const execMock = jest.spyOn(exec, 'exec')
|
||||||
|
jest
|
||||||
|
.spyOn(utils, 'getGnuTarPathOnWindows')
|
||||||
|
.mockReturnValue(Promise.resolve(''))
|
||||||
|
|
||||||
|
const archiveFolder = getTempDir()
|
||||||
|
const workspace = process.env['GITHUB_WORKSPACE']
|
||||||
|
const sourceDirectories = ['~/.npm/cache', `${workspace}/dist`]
|
||||||
|
|
||||||
|
await fs.promises.mkdir(archiveFolder, {recursive: true})
|
||||||
|
|
||||||
|
await tar.createTar(
|
||||||
|
archiveFolder,
|
||||||
|
sourceDirectories,
|
||||||
|
CompressionMethod.Zstd
|
||||||
|
)
|
||||||
|
|
||||||
|
const tarPath = SystemTarPathOnWindows
|
||||||
|
|
||||||
|
expect(execMock).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
|
expect(execMock).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
[
|
||||||
|
`"${tarPath}"`,
|
||||||
|
'--posix',
|
||||||
|
'-cf',
|
||||||
|
TarFilename.replace(/\\/g, '/'),
|
||||||
|
'--exclude',
|
||||||
|
TarFilename.replace(/\\/g, '/'),
|
||||||
|
'-P',
|
||||||
|
'-C',
|
||||||
|
workspace?.replace(/\\/g, '/'),
|
||||||
|
'--files-from',
|
||||||
|
ManifestFilename
|
||||||
|
].join(' '),
|
||||||
|
undefined, // args
|
||||||
|
{
|
||||||
|
cwd: archiveFolder,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(execMock).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
[
|
||||||
|
'zstd -T0 --long=30 --force -o',
|
||||||
|
CacheFilename.Zstd.replace(/\\/g, '/'),
|
||||||
|
TarFilename.replace(/\\/g, '/')
|
||||||
|
].join(' '),
|
||||||
|
undefined, // args
|
||||||
|
{
|
||||||
|
cwd: archiveFolder,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('gzip create tar', async () => {
|
test('gzip create tar', async () => {
|
||||||
const execMock = jest.spyOn(exec, 'exec')
|
const execMock = jest.spyOn(exec, 'exec')
|
||||||
|
|
||||||
@@ -168,26 +316,31 @@ test('gzip create tar', async () => {
|
|||||||
|
|
||||||
await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Gzip)
|
await tar.createTar(archiveFolder, sourceDirectories, CompressionMethod.Gzip)
|
||||||
|
|
||||||
const tarPath = IS_WINDOWS
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
||||||
? `${process.env['windir']}\\System32\\tar.exe`
|
|
||||||
: defaultTarPath
|
|
||||||
|
|
||||||
expect(execMock).toHaveBeenCalledTimes(1)
|
expect(execMock).toHaveBeenCalledTimes(1)
|
||||||
expect(execMock).toHaveBeenCalledWith(
|
expect(execMock).toHaveBeenCalledWith(
|
||||||
`"${tarPath}"`,
|
|
||||||
[
|
[
|
||||||
|
`"${tarPath}"`,
|
||||||
'--posix',
|
'--posix',
|
||||||
'-z',
|
|
||||||
'-cf',
|
'-cf',
|
||||||
IS_WINDOWS ? CacheFilename.Gzip.replace(/\\/g, '/') : CacheFilename.Gzip,
|
IS_WINDOWS ? CacheFilename.Gzip.replace(/\\/g, '/') : CacheFilename.Gzip,
|
||||||
|
'--exclude',
|
||||||
|
IS_WINDOWS ? CacheFilename.Gzip.replace(/\\/g, '/') : CacheFilename.Gzip,
|
||||||
'-P',
|
'-P',
|
||||||
'-C',
|
'-C',
|
||||||
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
|
IS_WINDOWS ? workspace?.replace(/\\/g, '/') : workspace,
|
||||||
'--files-from',
|
'--files-from',
|
||||||
'manifest.txt'
|
ManifestFilename
|
||||||
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
]
|
||||||
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||||
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
||||||
|
.concat(['-z'])
|
||||||
|
.join(' '),
|
||||||
|
undefined, // args
|
||||||
{
|
{
|
||||||
cwd: archiveFolder
|
cwd: archiveFolder,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@@ -201,22 +354,74 @@ test('zstd list tar', async () => {
|
|||||||
|
|
||||||
await tar.listTar(archivePath, CompressionMethod.Zstd)
|
await tar.listTar(archivePath, CompressionMethod.Zstd)
|
||||||
|
|
||||||
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
||||||
expect(execMock).toHaveBeenCalledTimes(1)
|
expect(execMock).toHaveBeenCalledTimes(1)
|
||||||
expect(execMock).toHaveBeenCalledWith(
|
expect(execMock).toHaveBeenCalledWith(
|
||||||
`"${defaultTarPath}"`,
|
|
||||||
[
|
[
|
||||||
'--use-compress-program',
|
`"${tarPath}"`,
|
||||||
'zstd -d --long=30',
|
|
||||||
'-tf',
|
'-tf',
|
||||||
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
||||||
'-P'
|
'-P'
|
||||||
]
|
]
|
||||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
||||||
{cwd: undefined}
|
.concat([
|
||||||
|
'--use-compress-program',
|
||||||
|
IS_WINDOWS ? '"zstd -d --long=30"' : 'unzstd --long=30'
|
||||||
|
])
|
||||||
|
.join(' '),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
cwd: undefined,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('zstd list tar with windows BSDtar', async () => {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const execMock = jest.spyOn(exec, 'exec')
|
||||||
|
jest
|
||||||
|
.spyOn(utils, 'getGnuTarPathOnWindows')
|
||||||
|
.mockReturnValue(Promise.resolve(''))
|
||||||
|
const archivePath = `${process.env['windir']}\\fakepath\\cache.tar`
|
||||||
|
|
||||||
|
await tar.listTar(archivePath, CompressionMethod.Zstd)
|
||||||
|
|
||||||
|
const tarPath = SystemTarPathOnWindows
|
||||||
|
expect(execMock).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
|
expect(execMock).toHaveBeenNthCalledWith(
|
||||||
|
1,
|
||||||
|
[
|
||||||
|
'zstd -d --long=30 --force -o',
|
||||||
|
TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||||
|
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
|
||||||
|
].join(' '),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
cwd: undefined,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(execMock).toHaveBeenNthCalledWith(
|
||||||
|
2,
|
||||||
|
[
|
||||||
|
`"${tarPath}"`,
|
||||||
|
'-tf',
|
||||||
|
TarFilename.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||||
|
'-P'
|
||||||
|
].join(' '),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
cwd: undefined,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test('zstdWithoutLong list tar', async () => {
|
test('zstdWithoutLong list tar', async () => {
|
||||||
const execMock = jest.spyOn(exec, 'exec')
|
const execMock = jest.spyOn(exec, 'exec')
|
||||||
|
|
||||||
@@ -226,19 +431,24 @@ test('zstdWithoutLong list tar', async () => {
|
|||||||
|
|
||||||
await tar.listTar(archivePath, CompressionMethod.ZstdWithoutLong)
|
await tar.listTar(archivePath, CompressionMethod.ZstdWithoutLong)
|
||||||
|
|
||||||
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
||||||
expect(execMock).toHaveBeenCalledTimes(1)
|
expect(execMock).toHaveBeenCalledTimes(1)
|
||||||
expect(execMock).toHaveBeenCalledWith(
|
expect(execMock).toHaveBeenCalledWith(
|
||||||
`"${defaultTarPath}"`,
|
|
||||||
[
|
[
|
||||||
'--use-compress-program',
|
`"${tarPath}"`,
|
||||||
'zstd -d',
|
|
||||||
'-tf',
|
'-tf',
|
||||||
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
||||||
'-P'
|
'-P'
|
||||||
]
|
]
|
||||||
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||||
.concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
||||||
{cwd: undefined}
|
.concat(['--use-compress-program', IS_WINDOWS ? '"zstd -d"' : 'unzstd'])
|
||||||
|
.join(' '),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
cwd: undefined,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -250,18 +460,23 @@ test('gzip list tar', async () => {
|
|||||||
|
|
||||||
await tar.listTar(archivePath, CompressionMethod.Gzip)
|
await tar.listTar(archivePath, CompressionMethod.Gzip)
|
||||||
|
|
||||||
const tarPath = IS_WINDOWS
|
const tarPath = IS_WINDOWS ? GnuTarPathOnWindows : defaultTarPath
|
||||||
? `${process.env['windir']}\\System32\\tar.exe`
|
|
||||||
: defaultTarPath
|
|
||||||
expect(execMock).toHaveBeenCalledTimes(1)
|
expect(execMock).toHaveBeenCalledTimes(1)
|
||||||
expect(execMock).toHaveBeenCalledWith(
|
expect(execMock).toHaveBeenCalledWith(
|
||||||
`"${tarPath}"`,
|
|
||||||
[
|
[
|
||||||
'-z',
|
`"${tarPath}"`,
|
||||||
'-tf',
|
'-tf',
|
||||||
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
IS_WINDOWS ? archivePath.replace(/\\/g, '/') : archivePath,
|
||||||
'-P'
|
'-P'
|
||||||
].concat(IS_MAC ? ['--delay-directory-restore'] : []),
|
]
|
||||||
{cwd: undefined}
|
.concat(IS_WINDOWS ? ['--force-local'] : [])
|
||||||
|
.concat(IS_MAC ? ['--delay-directory-restore'] : [])
|
||||||
|
.concat(['-z'])
|
||||||
|
.join(' '),
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
cwd: undefined,
|
||||||
|
env: expect.objectContaining(defaultEnv)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
+857
-4355
File diff suppressed because it is too large
Load Diff
Vendored
+8
-7
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/cache",
|
"name": "@actions/cache",
|
||||||
"version": "1.0.7",
|
"version": "3.2.1",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"description": "Actions cache lib",
|
"description": "Actions cache lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -37,19 +37,20 @@
|
|||||||
"url": "https://github.com/actions/toolkit/issues"
|
"url": "https://github.com/actions/toolkit/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.2.6",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/exec": "^1.0.1",
|
"@actions/exec": "^1.0.1",
|
||||||
"@actions/glob": "^0.1.0",
|
"@actions/glob": "^0.1.0",
|
||||||
"@actions/http-client": "^1.0.9",
|
"@actions/http-client": "^2.0.1",
|
||||||
"@actions/io": "^1.0.1",
|
"@actions/io": "^1.0.1",
|
||||||
"@azure/ms-rest-js": "^2.0.7",
|
"@azure/abort-controller": "^1.1.0",
|
||||||
"@azure/storage-blob": "^12.1.2",
|
"@azure/ms-rest-js": "^2.6.0",
|
||||||
|
"@azure/storage-blob": "^12.13.0",
|
||||||
"semver": "^6.1.0",
|
"semver": "^6.1.0",
|
||||||
"uuid": "^3.3.3"
|
"uuid": "^3.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^3.8.3",
|
|
||||||
"@types/semver": "^6.0.0",
|
"@types/semver": "^6.0.0",
|
||||||
"@types/uuid": "^3.4.5"
|
"@types/uuid": "^3.4.5",
|
||||||
|
"typescript": "^4.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+116
-45
@@ -43,6 +43,16 @@ function checkKey(key: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* isFeatureAvailable to check the presence of Actions cache service
|
||||||
|
*
|
||||||
|
* @returns boolean return true if Actions cache service feature is available, otherwise false
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function isFeatureAvailable(): boolean {
|
||||||
|
return !!process.env['ACTIONS_CACHE_URL']
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Restores cache from keys
|
* Restores cache from keys
|
||||||
*
|
*
|
||||||
@@ -50,13 +60,15 @@ function checkKey(key: string): void {
|
|||||||
* @param primaryKey an explicit key for restoring the cache
|
* @param primaryKey an explicit key for restoring the cache
|
||||||
* @param restoreKeys an optional ordered list of keys to use for restoring the cache if no cache hit occurred for key
|
* @param restoreKeys an optional ordered list of keys to use for restoring the cache if no cache hit occurred for key
|
||||||
* @param downloadOptions cache download options
|
* @param downloadOptions cache download options
|
||||||
|
* @param enableCrossOsArchive an optional boolean enabled to restore on windows any cache created on any platform
|
||||||
* @returns string returns the key for the cache hit, otherwise returns undefined
|
* @returns string returns the key for the cache hit, otherwise returns undefined
|
||||||
*/
|
*/
|
||||||
export async function restoreCache(
|
export async function restoreCache(
|
||||||
paths: string[],
|
paths: string[],
|
||||||
primaryKey: string,
|
primaryKey: string,
|
||||||
restoreKeys?: string[],
|
restoreKeys?: string[],
|
||||||
options?: DownloadOptions
|
options?: DownloadOptions,
|
||||||
|
enableCrossOsArchive = false
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
checkPaths(paths)
|
checkPaths(paths)
|
||||||
|
|
||||||
@@ -76,23 +88,29 @@ export async function restoreCache(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const compressionMethod = await utils.getCompressionMethod()
|
const compressionMethod = await utils.getCompressionMethod()
|
||||||
|
let archivePath = ''
|
||||||
// path are needed to compute version
|
|
||||||
const cacheEntry = await cacheHttpClient.getCacheEntry(keys, paths, {
|
|
||||||
compressionMethod
|
|
||||||
})
|
|
||||||
if (!cacheEntry?.archiveLocation) {
|
|
||||||
// Cache not found
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const archivePath = path.join(
|
|
||||||
await utils.createTempDirectory(),
|
|
||||||
utils.getCacheFileName(compressionMethod)
|
|
||||||
)
|
|
||||||
core.debug(`Archive Path: ${archivePath}`)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// path are needed to compute version
|
||||||
|
const cacheEntry = await cacheHttpClient.getCacheEntry(keys, paths, {
|
||||||
|
compressionMethod,
|
||||||
|
enableCrossOsArchive
|
||||||
|
})
|
||||||
|
if (!cacheEntry?.archiveLocation) {
|
||||||
|
// Cache not found
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.lookupOnly) {
|
||||||
|
core.info('Lookup only - skipping download')
|
||||||
|
return cacheEntry.cacheKey
|
||||||
|
}
|
||||||
|
|
||||||
|
archivePath = path.join(
|
||||||
|
await utils.createTempDirectory(),
|
||||||
|
utils.getCacheFileName(compressionMethod)
|
||||||
|
)
|
||||||
|
core.debug(`Archive Path: ${archivePath}`)
|
||||||
|
|
||||||
// Download the cache from the cache entry
|
// Download the cache from the cache entry
|
||||||
await cacheHttpClient.downloadCache(
|
await cacheHttpClient.downloadCache(
|
||||||
cacheEntry.archiveLocation,
|
cacheEntry.archiveLocation,
|
||||||
@@ -113,6 +131,16 @@ export async function restoreCache(
|
|||||||
|
|
||||||
await extractTar(archivePath, compressionMethod)
|
await extractTar(archivePath, compressionMethod)
|
||||||
core.info('Cache restored successfully')
|
core.info('Cache restored successfully')
|
||||||
|
|
||||||
|
return cacheEntry.cacheKey
|
||||||
|
} catch (error) {
|
||||||
|
const typedError = error as Error
|
||||||
|
if (typedError.name === ValidationError.name) {
|
||||||
|
throw error
|
||||||
|
} else {
|
||||||
|
// Supress all non-validation cache related errors because caching should be optional
|
||||||
|
core.warning(`Failed to restore: ${(error as Error).message}`)
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// Try to delete the archive to save space
|
// Try to delete the archive to save space
|
||||||
try {
|
try {
|
||||||
@@ -122,7 +150,7 @@ export async function restoreCache(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cacheEntry.cacheKey
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,34 +158,32 @@ export async function restoreCache(
|
|||||||
*
|
*
|
||||||
* @param paths a list of file paths to be cached
|
* @param paths a list of file paths to be cached
|
||||||
* @param key an explicit key for restoring the cache
|
* @param key an explicit key for restoring the cache
|
||||||
|
* @param enableCrossOsArchive an optional boolean enabled to save cache on windows which could be restored on any platform
|
||||||
* @param options cache upload options
|
* @param options cache upload options
|
||||||
* @returns number returns cacheId if the cache was saved successfully and throws an error if save fails
|
* @returns number returns cacheId if the cache was saved successfully and throws an error if save fails
|
||||||
*/
|
*/
|
||||||
export async function saveCache(
|
export async function saveCache(
|
||||||
paths: string[],
|
paths: string[],
|
||||||
key: string,
|
key: string,
|
||||||
options?: UploadOptions
|
options?: UploadOptions,
|
||||||
|
enableCrossOsArchive = false
|
||||||
): Promise<number> {
|
): Promise<number> {
|
||||||
checkPaths(paths)
|
checkPaths(paths)
|
||||||
checkKey(key)
|
checkKey(key)
|
||||||
|
|
||||||
const compressionMethod = await utils.getCompressionMethod()
|
const compressionMethod = await utils.getCompressionMethod()
|
||||||
|
let cacheId = -1
|
||||||
core.debug('Reserving Cache')
|
|
||||||
const cacheId = await cacheHttpClient.reserveCache(key, paths, {
|
|
||||||
compressionMethod
|
|
||||||
})
|
|
||||||
if (cacheId === -1) {
|
|
||||||
throw new ReserveCacheError(
|
|
||||||
`Unable to reserve cache with key ${key}, another job may be creating this cache.`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
core.debug(`Cache ID: ${cacheId}`)
|
|
||||||
|
|
||||||
const cachePaths = await utils.resolvePaths(paths)
|
const cachePaths = await utils.resolvePaths(paths)
|
||||||
core.debug('Cache Paths:')
|
core.debug('Cache Paths:')
|
||||||
core.debug(`${JSON.stringify(cachePaths)}`)
|
core.debug(`${JSON.stringify(cachePaths)}`)
|
||||||
|
|
||||||
|
if (cachePaths.length === 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Path Validation Error: Path(s) specified in the action for caching do(es) not exist, hence no cache is being saved.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const archiveFolder = await utils.createTempDirectory()
|
const archiveFolder = await utils.createTempDirectory()
|
||||||
const archivePath = path.join(
|
const archivePath = path.join(
|
||||||
archiveFolder,
|
archiveFolder,
|
||||||
@@ -166,24 +192,69 @@ export async function saveCache(
|
|||||||
|
|
||||||
core.debug(`Archive Path: ${archivePath}`)
|
core.debug(`Archive Path: ${archivePath}`)
|
||||||
|
|
||||||
await createTar(archiveFolder, cachePaths, compressionMethod)
|
try {
|
||||||
if (core.isDebug()) {
|
await createTar(archiveFolder, cachePaths, compressionMethod)
|
||||||
await listTar(archivePath, compressionMethod)
|
if (core.isDebug()) {
|
||||||
}
|
await listTar(archivePath, compressionMethod)
|
||||||
|
}
|
||||||
|
const fileSizeLimit = 10 * 1024 * 1024 * 1024 // 10GB per repo limit
|
||||||
|
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
|
||||||
|
core.debug(`File Size: ${archiveFileSize}`)
|
||||||
|
|
||||||
const fileSizeLimit = 5 * 1024 * 1024 * 1024 // 5GB per repo limit
|
// For GHES, this check will take place in ReserveCache API with enterprise file size limit
|
||||||
const archiveFileSize = utils.getArchiveFileSizeInBytes(archivePath)
|
if (archiveFileSize > fileSizeLimit && !utils.isGhes()) {
|
||||||
core.debug(`File Size: ${archiveFileSize}`)
|
throw new Error(
|
||||||
if (archiveFileSize > fileSizeLimit) {
|
`Cache size of ~${Math.round(
|
||||||
throw new Error(
|
archiveFileSize / (1024 * 1024)
|
||||||
`Cache size of ~${Math.round(
|
)} MB (${archiveFileSize} B) is over the 10GB limit, not saving cache.`
|
||||||
archiveFileSize / (1024 * 1024)
|
)
|
||||||
)} MB (${archiveFileSize} B) is over the 5GB limit, not saving cache.`
|
}
|
||||||
|
|
||||||
|
core.debug('Reserving Cache')
|
||||||
|
const reserveCacheResponse = await cacheHttpClient.reserveCache(
|
||||||
|
key,
|
||||||
|
paths,
|
||||||
|
{
|
||||||
|
compressionMethod,
|
||||||
|
enableCrossOsArchive,
|
||||||
|
cacheSize: archiveFileSize
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
core.debug(`Saving Cache (ID: ${cacheId})`)
|
if (reserveCacheResponse?.result?.cacheId) {
|
||||||
await cacheHttpClient.saveCache(cacheId, archivePath, options)
|
cacheId = reserveCacheResponse?.result?.cacheId
|
||||||
|
} else if (reserveCacheResponse?.statusCode === 400) {
|
||||||
|
throw new Error(
|
||||||
|
reserveCacheResponse?.error?.message ??
|
||||||
|
`Cache size of ~${Math.round(
|
||||||
|
archiveFileSize / (1024 * 1024)
|
||||||
|
)} MB (${archiveFileSize} B) is over the data cap limit, not saving cache.`
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw new ReserveCacheError(
|
||||||
|
`Unable to reserve cache with key ${key}, another job may be creating this cache. More details: ${reserveCacheResponse?.error?.message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
core.debug(`Saving Cache (ID: ${cacheId})`)
|
||||||
|
await cacheHttpClient.saveCache(cacheId, archivePath, options)
|
||||||
|
} catch (error) {
|
||||||
|
const typedError = error as Error
|
||||||
|
if (typedError.name === ValidationError.name) {
|
||||||
|
throw error
|
||||||
|
} else if (typedError.name === ReserveCacheError.name) {
|
||||||
|
core.info(`Failed to save: ${typedError.message}`)
|
||||||
|
} else {
|
||||||
|
core.warning(`Failed to save: ${typedError.message}`)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Try to delete the archive to save space
|
||||||
|
try {
|
||||||
|
await utils.unlinkFile(archivePath)
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(`Failed to delete archive: ${error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return cacheId
|
return cacheId
|
||||||
}
|
}
|
||||||
|
|||||||
+71
-23
@@ -1,7 +1,10 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {HttpClient} from '@actions/http-client'
|
import {HttpClient} from '@actions/http-client'
|
||||||
import {BearerCredentialHandler} from '@actions/http-client/auth'
|
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
|
||||||
import {IRequestOptions, ITypedResponse} from '@actions/http-client/interfaces'
|
import {
|
||||||
|
RequestOptions,
|
||||||
|
TypedResponse
|
||||||
|
} from '@actions/http-client/lib/interfaces'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import {URL} from 'url'
|
import {URL} from 'url'
|
||||||
@@ -13,7 +16,9 @@ import {
|
|||||||
InternalCacheOptions,
|
InternalCacheOptions,
|
||||||
CommitCacheRequest,
|
CommitCacheRequest,
|
||||||
ReserveCacheRequest,
|
ReserveCacheRequest,
|
||||||
ReserveCacheResponse
|
ReserveCacheResponse,
|
||||||
|
ITypedResponseWithError,
|
||||||
|
ArtifactCacheList
|
||||||
} from './contracts'
|
} from './contracts'
|
||||||
import {downloadCacheHttpClient, downloadCacheStorageSDK} from './downloadUtils'
|
import {downloadCacheHttpClient, downloadCacheStorageSDK} from './downloadUtils'
|
||||||
import {
|
import {
|
||||||
@@ -31,12 +36,7 @@ import {
|
|||||||
const versionSalt = '1.0'
|
const versionSalt = '1.0'
|
||||||
|
|
||||||
function getCacheApiUrl(resource: string): string {
|
function getCacheApiUrl(resource: string): string {
|
||||||
// Ideally we just use ACTIONS_CACHE_URL
|
const baseUrl: string = process.env['ACTIONS_CACHE_URL'] || ''
|
||||||
const baseUrl: string = (
|
|
||||||
process.env['ACTIONS_CACHE_URL'] ||
|
|
||||||
process.env['ACTIONS_RUNTIME_URL'] ||
|
|
||||||
''
|
|
||||||
).replace('pipelines', 'artifactcache')
|
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
throw new Error('Cache Service Url not found, unable to restore cache.')
|
throw new Error('Cache Service Url not found, unable to restore cache.')
|
||||||
}
|
}
|
||||||
@@ -50,8 +50,8 @@ function createAcceptHeader(type: string, apiVersion: string): string {
|
|||||||
return `${type};api-version=${apiVersion}`
|
return `${type};api-version=${apiVersion}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRequestOptions(): IRequestOptions {
|
function getRequestOptions(): RequestOptions {
|
||||||
const requestOptions: IRequestOptions = {
|
const requestOptions: RequestOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
Accept: createAcceptHeader('application/json', '6.0-preview.1')
|
Accept: createAcceptHeader('application/json', '6.0-preview.1')
|
||||||
}
|
}
|
||||||
@@ -73,13 +73,21 @@ function createHttpClient(): HttpClient {
|
|||||||
|
|
||||||
export function getCacheVersion(
|
export function getCacheVersion(
|
||||||
paths: string[],
|
paths: string[],
|
||||||
compressionMethod?: CompressionMethod
|
compressionMethod?: CompressionMethod,
|
||||||
|
enableCrossOsArchive = false
|
||||||
): string {
|
): string {
|
||||||
const components = paths.concat(
|
const components = paths
|
||||||
!compressionMethod || compressionMethod === CompressionMethod.Gzip
|
|
||||||
? []
|
// Add compression method to cache version to restore
|
||||||
: [compressionMethod]
|
// compressed cache as per compression method
|
||||||
)
|
if (compressionMethod) {
|
||||||
|
components.push(compressionMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only check for windows platforms if enableCrossOsArchive is false
|
||||||
|
if (process.platform === 'win32' && !enableCrossOsArchive) {
|
||||||
|
components.push('windows-only')
|
||||||
|
}
|
||||||
|
|
||||||
// Add salt to cache version to support breaking changes in cache entry
|
// Add salt to cache version to support breaking changes in cache entry
|
||||||
components.push(versionSalt)
|
components.push(versionSalt)
|
||||||
@@ -96,7 +104,11 @@ export async function getCacheEntry(
|
|||||||
options?: InternalCacheOptions
|
options?: InternalCacheOptions
|
||||||
): Promise<ArtifactCacheEntry | null> {
|
): Promise<ArtifactCacheEntry | null> {
|
||||||
const httpClient = createHttpClient()
|
const httpClient = createHttpClient()
|
||||||
const version = getCacheVersion(paths, options?.compressionMethod)
|
const version = getCacheVersion(
|
||||||
|
paths,
|
||||||
|
options?.compressionMethod,
|
||||||
|
options?.enableCrossOsArchive
|
||||||
|
)
|
||||||
const resource = `cache?keys=${encodeURIComponent(
|
const resource = `cache?keys=${encodeURIComponent(
|
||||||
keys.join(',')
|
keys.join(',')
|
||||||
)}&version=${version}`
|
)}&version=${version}`
|
||||||
@@ -104,7 +116,12 @@ export async function getCacheEntry(
|
|||||||
const response = await retryTypedResponse('getCacheEntry', async () =>
|
const response = await retryTypedResponse('getCacheEntry', async () =>
|
||||||
httpClient.getJson<ArtifactCacheEntry>(getCacheApiUrl(resource))
|
httpClient.getJson<ArtifactCacheEntry>(getCacheApiUrl(resource))
|
||||||
)
|
)
|
||||||
|
// Cache not found
|
||||||
if (response.statusCode === 204) {
|
if (response.statusCode === 204) {
|
||||||
|
// List cache for primary key only if cache miss occurs
|
||||||
|
if (core.isDebug()) {
|
||||||
|
await printCachesListForDiagnostics(keys[0], httpClient, version)
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (!isSuccessStatusCode(response.statusCode)) {
|
if (!isSuccessStatusCode(response.statusCode)) {
|
||||||
@@ -114,6 +131,7 @@ export async function getCacheEntry(
|
|||||||
const cacheResult = response.result
|
const cacheResult = response.result
|
||||||
const cacheDownloadUrl = cacheResult?.archiveLocation
|
const cacheDownloadUrl = cacheResult?.archiveLocation
|
||||||
if (!cacheDownloadUrl) {
|
if (!cacheDownloadUrl) {
|
||||||
|
// Cache achiveLocation not found. This should never happen, and hence bail out.
|
||||||
throw new Error('Cache not found.')
|
throw new Error('Cache not found.')
|
||||||
}
|
}
|
||||||
core.setSecret(cacheDownloadUrl)
|
core.setSecret(cacheDownloadUrl)
|
||||||
@@ -123,6 +141,31 @@ export async function getCacheEntry(
|
|||||||
return cacheResult
|
return cacheResult
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function printCachesListForDiagnostics(
|
||||||
|
key: string,
|
||||||
|
httpClient: HttpClient,
|
||||||
|
version: string
|
||||||
|
): Promise<void> {
|
||||||
|
const resource = `caches?key=${encodeURIComponent(key)}`
|
||||||
|
const response = await retryTypedResponse('listCache', async () =>
|
||||||
|
httpClient.getJson<ArtifactCacheList>(getCacheApiUrl(resource))
|
||||||
|
)
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
const cacheListResult = response.result
|
||||||
|
const totalCount = cacheListResult?.totalCount
|
||||||
|
if (totalCount && totalCount > 0) {
|
||||||
|
core.debug(
|
||||||
|
`No matching cache found for cache key '${key}', version '${version} and scope ${process.env['GITHUB_REF']}. There exist one or more cache(s) with similar key but they have different version or scope. See more info on cache matching here: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key \nOther caches with similar key:`
|
||||||
|
)
|
||||||
|
for (const cacheEntry of cacheListResult?.artifactCaches || []) {
|
||||||
|
core.debug(
|
||||||
|
`Cache Key: ${cacheEntry?.cacheKey}, Cache Version: ${cacheEntry?.cacheVersion}, Cache Scope: ${cacheEntry?.scope}, Cache Created: ${cacheEntry?.creationTime}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function downloadCache(
|
export async function downloadCache(
|
||||||
archiveLocation: string,
|
archiveLocation: string,
|
||||||
archivePath: string,
|
archivePath: string,
|
||||||
@@ -148,13 +191,18 @@ export async function reserveCache(
|
|||||||
key: string,
|
key: string,
|
||||||
paths: string[],
|
paths: string[],
|
||||||
options?: InternalCacheOptions
|
options?: InternalCacheOptions
|
||||||
): Promise<number> {
|
): Promise<ITypedResponseWithError<ReserveCacheResponse>> {
|
||||||
const httpClient = createHttpClient()
|
const httpClient = createHttpClient()
|
||||||
const version = getCacheVersion(paths, options?.compressionMethod)
|
const version = getCacheVersion(
|
||||||
|
paths,
|
||||||
|
options?.compressionMethod,
|
||||||
|
options?.enableCrossOsArchive
|
||||||
|
)
|
||||||
|
|
||||||
const reserveCacheRequest: ReserveCacheRequest = {
|
const reserveCacheRequest: ReserveCacheRequest = {
|
||||||
key,
|
key,
|
||||||
version
|
version,
|
||||||
|
cacheSize: options?.cacheSize
|
||||||
}
|
}
|
||||||
const response = await retryTypedResponse('reserveCache', async () =>
|
const response = await retryTypedResponse('reserveCache', async () =>
|
||||||
httpClient.postJson<ReserveCacheResponse>(
|
httpClient.postJson<ReserveCacheResponse>(
|
||||||
@@ -162,7 +210,7 @@ export async function reserveCache(
|
|||||||
reserveCacheRequest
|
reserveCacheRequest
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return response?.result?.cacheId ?? -1
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContentRange(start: number, end: number): string {
|
function getContentRange(start: number, end: number): string {
|
||||||
@@ -278,7 +326,7 @@ async function commitCache(
|
|||||||
httpClient: HttpClient,
|
httpClient: HttpClient,
|
||||||
cacheId: number,
|
cacheId: number,
|
||||||
filesize: number
|
filesize: number
|
||||||
): Promise<ITypedResponse<null>> {
|
): Promise<TypedResponse<null>> {
|
||||||
const commitCacheRequest: CommitCacheRequest = {size: filesize}
|
const commitCacheRequest: CommitCacheRequest = {size: filesize}
|
||||||
return await retryTypedResponse('commitCache', async () =>
|
return await retryTypedResponse('commitCache', async () =>
|
||||||
httpClient.postJson<null>(
|
httpClient.postJson<null>(
|
||||||
|
|||||||
+34
-20
@@ -7,7 +7,11 @@ import * as path from 'path'
|
|||||||
import * as semver from 'semver'
|
import * as semver from 'semver'
|
||||||
import * as util from 'util'
|
import * as util from 'util'
|
||||||
import {v4 as uuidV4} from 'uuid'
|
import {v4 as uuidV4} from 'uuid'
|
||||||
import {CacheFilename, CompressionMethod} from './constants'
|
import {
|
||||||
|
CacheFilename,
|
||||||
|
CompressionMethod,
|
||||||
|
GnuTarPathOnWindows
|
||||||
|
} from './constants'
|
||||||
|
|
||||||
// From https://github.com/actions/toolkit/blob/main/packages/tool-cache/src/tool-cache.ts#L23
|
// From https://github.com/actions/toolkit/blob/main/packages/tool-cache/src/tool-cache.ts#L23
|
||||||
export async function createTempDirectory(): Promise<string> {
|
export async function createTempDirectory(): Promise<string> {
|
||||||
@@ -52,7 +56,12 @@ export async function resolvePaths(patterns: string[]): Promise<string[]> {
|
|||||||
.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
|
.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
|
||||||
core.debug(`Matched: ${relativeFile}`)
|
core.debug(`Matched: ${relativeFile}`)
|
||||||
// Paths are made relative so the tar entries are all relative to the root of the workspace.
|
// Paths are made relative so the tar entries are all relative to the root of the workspace.
|
||||||
paths.push(`${relativeFile}`)
|
if (relativeFile === '') {
|
||||||
|
// path.relative returns empty string if workspace and file are equal
|
||||||
|
paths.push('.')
|
||||||
|
} else {
|
||||||
|
paths.push(`${relativeFile}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return paths
|
return paths
|
||||||
@@ -62,11 +71,15 @@ export async function unlinkFile(filePath: fs.PathLike): Promise<void> {
|
|||||||
return util.promisify(fs.unlink)(filePath)
|
return util.promisify(fs.unlink)(filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVersion(app: string): Promise<string> {
|
async function getVersion(
|
||||||
core.debug(`Checking ${app} --version`)
|
app: string,
|
||||||
|
additionalArgs: string[] = []
|
||||||
|
): Promise<string> {
|
||||||
let versionOutput = ''
|
let versionOutput = ''
|
||||||
|
additionalArgs.push('--version')
|
||||||
|
core.debug(`Checking ${app} ${additionalArgs.join(' ')}`)
|
||||||
try {
|
try {
|
||||||
await exec.exec(`${app} --version`, [], {
|
await exec.exec(`${app}`, additionalArgs, {
|
||||||
ignoreReturnCode: true,
|
ignoreReturnCode: true,
|
||||||
silent: true,
|
silent: true,
|
||||||
listeners: {
|
listeners: {
|
||||||
@@ -85,23 +98,14 @@ async function getVersion(app: string): Promise<string> {
|
|||||||
|
|
||||||
// Use zstandard if possible to maximize cache performance
|
// Use zstandard if possible to maximize cache performance
|
||||||
export async function getCompressionMethod(): Promise<CompressionMethod> {
|
export async function getCompressionMethod(): Promise<CompressionMethod> {
|
||||||
if (process.platform === 'win32' && !(await isGnuTarInstalled())) {
|
const versionOutput = await getVersion('zstd', ['--quiet'])
|
||||||
// Disable zstd due to bug https://github.com/actions/cache/issues/301
|
|
||||||
return CompressionMethod.Gzip
|
|
||||||
}
|
|
||||||
|
|
||||||
const versionOutput = await getVersion('zstd')
|
|
||||||
const version = semver.clean(versionOutput)
|
const version = semver.clean(versionOutput)
|
||||||
|
core.debug(`zstd version: ${version}`)
|
||||||
|
|
||||||
if (!versionOutput.toLowerCase().includes('zstd command line interface')) {
|
if (versionOutput === '') {
|
||||||
// zstd is not installed
|
|
||||||
return CompressionMethod.Gzip
|
return CompressionMethod.Gzip
|
||||||
} else if (!version || semver.lt(version, 'v1.3.2')) {
|
|
||||||
// zstd is installed but using a version earlier than v1.3.2
|
|
||||||
// v1.3.2 is required to use the `--long` options in zstd
|
|
||||||
return CompressionMethod.ZstdWithoutLong
|
|
||||||
} else {
|
} else {
|
||||||
return CompressionMethod.Zstd
|
return CompressionMethod.ZstdWithoutLong
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,9 +115,12 @@ export function getCacheFileName(compressionMethod: CompressionMethod): string {
|
|||||||
: CacheFilename.Zstd
|
: CacheFilename.Zstd
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isGnuTarInstalled(): Promise<boolean> {
|
export async function getGnuTarPathOnWindows(): Promise<string> {
|
||||||
|
if (fs.existsSync(GnuTarPathOnWindows)) {
|
||||||
|
return GnuTarPathOnWindows
|
||||||
|
}
|
||||||
const versionOutput = await getVersion('tar')
|
const versionOutput = await getVersion('tar')
|
||||||
return versionOutput.toLowerCase().includes('gnu tar')
|
return versionOutput.toLowerCase().includes('gnu tar') ? io.which('tar') : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assertDefined<T>(name: string, value?: T): T {
|
export function assertDefined<T>(name: string, value?: T): T {
|
||||||
@@ -123,3 +130,10 @@ export function assertDefined<T>(name: string, value?: T): T {
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isGhes(): boolean {
|
||||||
|
const ghUrl = new URL(
|
||||||
|
process.env['GITHUB_SERVER_URL'] || 'https://github.com'
|
||||||
|
)
|
||||||
|
return ghUrl.hostname.toUpperCase() !== 'GITHUB.COM'
|
||||||
|
}
|
||||||
|
|||||||
+15
@@ -11,6 +11,11 @@ export enum CompressionMethod {
|
|||||||
Zstd = 'zstd'
|
Zstd = 'zstd'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ArchiveToolType {
|
||||||
|
GNU = 'gnu',
|
||||||
|
BSD = 'bsd'
|
||||||
|
}
|
||||||
|
|
||||||
// The default number of retry attempts.
|
// The default number of retry attempts.
|
||||||
export const DefaultRetryAttempts = 2
|
export const DefaultRetryAttempts = 2
|
||||||
|
|
||||||
@@ -21,3 +26,13 @@ export const DefaultRetryDelay = 5000
|
|||||||
// over the socket during this period, the socket is destroyed and the download
|
// over the socket during this period, the socket is destroyed and the download
|
||||||
// is aborted.
|
// is aborted.
|
||||||
export const SocketTimeout = 5000
|
export const SocketTimeout = 5000
|
||||||
|
|
||||||
|
// The default path of GNUtar on hosted Windows runners
|
||||||
|
export const GnuTarPathOnWindows = `${process.env['PROGRAMFILES']}\\Git\\usr\\bin\\tar.exe`
|
||||||
|
|
||||||
|
// The default path of BSDtar on hosted Windows runners
|
||||||
|
export const SystemTarPathOnWindows = `${process.env['SYSTEMDRIVE']}\\Windows\\System32\\tar.exe`
|
||||||
|
|
||||||
|
export const TarFilename = 'cache.tar'
|
||||||
|
|
||||||
|
export const ManifestFilename = 'manifest.txt'
|
||||||
|
|||||||
+20
@@ -1,12 +1,24 @@
|
|||||||
import {CompressionMethod} from './constants'
|
import {CompressionMethod} from './constants'
|
||||||
|
import {TypedResponse} from '@actions/http-client/lib/interfaces'
|
||||||
|
import {HttpClientError} from '@actions/http-client'
|
||||||
|
|
||||||
|
export interface ITypedResponseWithError<T> extends TypedResponse<T> {
|
||||||
|
error?: HttpClientError
|
||||||
|
}
|
||||||
|
|
||||||
export interface ArtifactCacheEntry {
|
export interface ArtifactCacheEntry {
|
||||||
cacheKey?: string
|
cacheKey?: string
|
||||||
scope?: string
|
scope?: string
|
||||||
|
cacheVersion?: string
|
||||||
creationTime?: string
|
creationTime?: string
|
||||||
archiveLocation?: string
|
archiveLocation?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ArtifactCacheList {
|
||||||
|
totalCount: number
|
||||||
|
artifactCaches?: ArtifactCacheEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface CommitCacheRequest {
|
export interface CommitCacheRequest {
|
||||||
size: number
|
size: number
|
||||||
}
|
}
|
||||||
@@ -14,6 +26,7 @@ export interface CommitCacheRequest {
|
|||||||
export interface ReserveCacheRequest {
|
export interface ReserveCacheRequest {
|
||||||
key: string
|
key: string
|
||||||
version?: string
|
version?: string
|
||||||
|
cacheSize?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReserveCacheResponse {
|
export interface ReserveCacheResponse {
|
||||||
@@ -22,4 +35,11 @@ export interface ReserveCacheResponse {
|
|||||||
|
|
||||||
export interface InternalCacheOptions {
|
export interface InternalCacheOptions {
|
||||||
compressionMethod?: CompressionMethod
|
compressionMethod?: CompressionMethod
|
||||||
|
enableCrossOsArchive?: boolean
|
||||||
|
cacheSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArchiveTool {
|
||||||
|
path: string
|
||||||
|
type: string
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-14
@@ -1,6 +1,5 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {HttpClient} from '@actions/http-client'
|
import {HttpClient, HttpClientResponse} from '@actions/http-client'
|
||||||
import {IHttpClientResponse} from '@actions/http-client/interfaces'
|
|
||||||
import {BlockBlobClient} from '@azure/storage-blob'
|
import {BlockBlobClient} from '@azure/storage-blob'
|
||||||
import {TransferProgressEvent} from '@azure/ms-rest-js'
|
import {TransferProgressEvent} from '@azure/ms-rest-js'
|
||||||
import * as buffer from 'buffer'
|
import * as buffer from 'buffer'
|
||||||
@@ -13,6 +12,8 @@ import {SocketTimeout} from './constants'
|
|||||||
import {DownloadOptions} from '../options'
|
import {DownloadOptions} from '../options'
|
||||||
import {retryHttpClientResponse} from './requestUtils'
|
import {retryHttpClientResponse} from './requestUtils'
|
||||||
|
|
||||||
|
import {AbortController} from '@azure/abort-controller'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pipes the body of a HTTP response to a stream
|
* Pipes the body of a HTTP response to a stream
|
||||||
*
|
*
|
||||||
@@ -20,7 +21,7 @@ import {retryHttpClientResponse} from './requestUtils'
|
|||||||
* @param output the writable stream
|
* @param output the writable stream
|
||||||
*/
|
*/
|
||||||
async function pipeResponseToStream(
|
async function pipeResponseToStream(
|
||||||
response: IHttpClientResponse,
|
response: HttpClientResponse,
|
||||||
output: NodeJS.WritableStream
|
output: NodeJS.WritableStream
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const pipeline = util.promisify(stream.pipeline)
|
const pipeline = util.promisify(stream.pipeline)
|
||||||
@@ -133,7 +134,7 @@ export class DownloadProgress {
|
|||||||
*
|
*
|
||||||
* @param delayInMs the delay between each write
|
* @param delayInMs the delay between each write
|
||||||
*/
|
*/
|
||||||
startDisplayTimer(delayInMs: number = 1000): void {
|
startDisplayTimer(delayInMs = 1000): void {
|
||||||
const displayCallback = (): void => {
|
const displayCallback = (): void => {
|
||||||
this.display()
|
this.display()
|
||||||
|
|
||||||
@@ -240,14 +241,18 @@ export async function downloadCacheStorageSDK(
|
|||||||
//
|
//
|
||||||
// If the file exceeds the buffer maximum length (~1 GB on 32-bit systems and ~2 GB
|
// If the file exceeds the buffer maximum length (~1 GB on 32-bit systems and ~2 GB
|
||||||
// on 64-bit systems), split the download into multiple segments
|
// on 64-bit systems), split the download into multiple segments
|
||||||
const maxSegmentSize = buffer.constants.MAX_LENGTH
|
// ~2 GB = 2147483647, beyond this, we start getting out of range error. So, capping it accordingly.
|
||||||
|
|
||||||
|
// Updated segment size to 128MB = 134217728 bytes, to complete a segment faster and fail fast
|
||||||
|
const maxSegmentSize = Math.min(134217728, buffer.constants.MAX_LENGTH)
|
||||||
const downloadProgress = new DownloadProgress(contentLength)
|
const downloadProgress = new DownloadProgress(contentLength)
|
||||||
|
|
||||||
const fd = fs.openSync(archivePath, 'w')
|
const fd = fs.openSync(archivePath, 'w')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
downloadProgress.startDisplayTimer()
|
downloadProgress.startDisplayTimer()
|
||||||
|
const controller = new AbortController()
|
||||||
|
const abortSignal = controller.signal
|
||||||
while (!downloadProgress.isDone()) {
|
while (!downloadProgress.isDone()) {
|
||||||
const segmentStart =
|
const segmentStart =
|
||||||
downloadProgress.segmentOffset + downloadProgress.segmentSize
|
downloadProgress.segmentOffset + downloadProgress.segmentSize
|
||||||
@@ -258,17 +263,22 @@ export async function downloadCacheStorageSDK(
|
|||||||
)
|
)
|
||||||
|
|
||||||
downloadProgress.nextSegment(segmentSize)
|
downloadProgress.nextSegment(segmentSize)
|
||||||
|
const result = await promiseWithTimeout(
|
||||||
const result = await client.downloadToBuffer(
|
options.segmentTimeoutInMs || 3600000,
|
||||||
segmentStart,
|
client.downloadToBuffer(segmentStart, segmentSize, {
|
||||||
segmentSize,
|
abortSignal,
|
||||||
{
|
|
||||||
concurrency: options.downloadConcurrency,
|
concurrency: options.downloadConcurrency,
|
||||||
onProgress: downloadProgress.onProgress()
|
onProgress: downloadProgress.onProgress()
|
||||||
}
|
})
|
||||||
)
|
)
|
||||||
|
if (result === 'timeout') {
|
||||||
fs.writeFileSync(fd, result)
|
controller.abort()
|
||||||
|
throw new Error(
|
||||||
|
'Aborting cache download as the download time exceeded the timeout.'
|
||||||
|
)
|
||||||
|
} else if (Buffer.isBuffer(result)) {
|
||||||
|
fs.writeFileSync(fd, result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
downloadProgress.stopDisplayTimer()
|
downloadProgress.stopDisplayTimer()
|
||||||
@@ -276,3 +286,18 @@ export async function downloadCacheStorageSDK(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const promiseWithTimeout = async (
|
||||||
|
timeoutMs: number,
|
||||||
|
promise: Promise<Buffer>
|
||||||
|
): Promise<unknown> => {
|
||||||
|
let timeoutHandle: NodeJS.Timeout
|
||||||
|
const timeoutPromise = new Promise(resolve => {
|
||||||
|
timeoutHandle = setTimeout(() => resolve('timeout'), timeoutMs)
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.race([promise, timeoutPromise]).then(result => {
|
||||||
|
clearTimeout(timeoutHandle)
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
+15
-13
@@ -1,10 +1,11 @@
|
|||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {HttpCodes, HttpClientError} from '@actions/http-client'
|
|
||||||
import {
|
import {
|
||||||
IHttpClientResponse,
|
HttpCodes,
|
||||||
ITypedResponse
|
HttpClientError,
|
||||||
} from '@actions/http-client/interfaces'
|
HttpClientResponse
|
||||||
|
} from '@actions/http-client'
|
||||||
import {DefaultRetryDelay, DefaultRetryAttempts} from './constants'
|
import {DefaultRetryDelay, DefaultRetryAttempts} from './constants'
|
||||||
|
import {ITypedResponseWithError} from './contracts'
|
||||||
|
|
||||||
export function isSuccessStatusCode(statusCode?: number): boolean {
|
export function isSuccessStatusCode(statusCode?: number): boolean {
|
||||||
if (!statusCode) {
|
if (!statusCode) {
|
||||||
@@ -94,24 +95,25 @@ export async function retry<T>(
|
|||||||
|
|
||||||
export async function retryTypedResponse<T>(
|
export async function retryTypedResponse<T>(
|
||||||
name: string,
|
name: string,
|
||||||
method: () => Promise<ITypedResponse<T>>,
|
method: () => Promise<ITypedResponseWithError<T>>,
|
||||||
maxAttempts = DefaultRetryAttempts,
|
maxAttempts = DefaultRetryAttempts,
|
||||||
delay = DefaultRetryDelay
|
delay = DefaultRetryDelay
|
||||||
): Promise<ITypedResponse<T>> {
|
): Promise<ITypedResponseWithError<T>> {
|
||||||
return await retry(
|
return await retry(
|
||||||
name,
|
name,
|
||||||
method,
|
method,
|
||||||
(response: ITypedResponse<T>) => response.statusCode,
|
(response: ITypedResponseWithError<T>) => response.statusCode,
|
||||||
maxAttempts,
|
maxAttempts,
|
||||||
delay,
|
delay,
|
||||||
// If the error object contains the statusCode property, extract it and return
|
// If the error object contains the statusCode property, extract it and return
|
||||||
// an ITypedResponse<T> so it can be processed by the retry logic.
|
// an TypedResponse<T> so it can be processed by the retry logic.
|
||||||
(error: Error) => {
|
(error: Error) => {
|
||||||
if (error instanceof HttpClientError) {
|
if (error instanceof HttpClientError) {
|
||||||
return {
|
return {
|
||||||
statusCode: error.statusCode,
|
statusCode: error.statusCode,
|
||||||
result: null,
|
result: null,
|
||||||
headers: {}
|
headers: {},
|
||||||
|
error
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return undefined
|
return undefined
|
||||||
@@ -120,16 +122,16 @@ export async function retryTypedResponse<T>(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function retryHttpClientResponse<T>(
|
export async function retryHttpClientResponse(
|
||||||
name: string,
|
name: string,
|
||||||
method: () => Promise<IHttpClientResponse>,
|
method: () => Promise<HttpClientResponse>,
|
||||||
maxAttempts = DefaultRetryAttempts,
|
maxAttempts = DefaultRetryAttempts,
|
||||||
delay = DefaultRetryDelay
|
delay = DefaultRetryDelay
|
||||||
): Promise<IHttpClientResponse> {
|
): Promise<HttpClientResponse> {
|
||||||
return await retry(
|
return await retry(
|
||||||
name,
|
name,
|
||||||
method,
|
method,
|
||||||
(response: IHttpClientResponse) => response.message.statusCode,
|
(response: HttpClientResponse) => response.message.statusCode,
|
||||||
maxAttempts,
|
maxAttempts,
|
||||||
delay
|
delay
|
||||||
)
|
)
|
||||||
|
|||||||
Vendored
+247
-105
@@ -3,23 +3,28 @@ import * as io from '@actions/io'
|
|||||||
import {existsSync, writeFileSync} from 'fs'
|
import {existsSync, writeFileSync} from 'fs'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as utils from './cacheUtils'
|
import * as utils from './cacheUtils'
|
||||||
import {CompressionMethod} from './constants'
|
import {ArchiveTool} from './contracts'
|
||||||
|
import {
|
||||||
|
CompressionMethod,
|
||||||
|
SystemTarPathOnWindows,
|
||||||
|
ArchiveToolType,
|
||||||
|
TarFilename,
|
||||||
|
ManifestFilename
|
||||||
|
} from './constants'
|
||||||
|
|
||||||
async function getTarPath(
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
args: string[],
|
|
||||||
compressionMethod: CompressionMethod
|
// Returns tar path and type: BSD or GNU
|
||||||
): Promise<string> {
|
async function getTarPath(): Promise<ArchiveTool> {
|
||||||
switch (process.platform) {
|
switch (process.platform) {
|
||||||
case 'win32': {
|
case 'win32': {
|
||||||
const systemTar = `${process.env['windir']}\\System32\\tar.exe`
|
const gnuTar = await utils.getGnuTarPathOnWindows()
|
||||||
if (compressionMethod !== CompressionMethod.Gzip) {
|
const systemTar = SystemTarPathOnWindows
|
||||||
// We only use zstandard compression on windows when gnu tar is installed due to
|
if (gnuTar) {
|
||||||
// a bug with compressing large files with bsdtar + zstd
|
// Use GNUtar as default on windows
|
||||||
args.push('--force-local')
|
return <ArchiveTool>{path: gnuTar, type: ArchiveToolType.GNU}
|
||||||
} else if (existsSync(systemTar)) {
|
} else if (existsSync(systemTar)) {
|
||||||
return systemTar
|
return <ArchiveTool>{path: systemTar, type: ArchiveToolType.BSD}
|
||||||
} else if (await utils.isGnuTarInstalled()) {
|
|
||||||
args.push('--force-local')
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -27,33 +32,244 @@ async function getTarPath(
|
|||||||
const gnuTar = await io.which('gtar', false)
|
const gnuTar = await io.which('gtar', false)
|
||||||
if (gnuTar) {
|
if (gnuTar) {
|
||||||
// fix permission denied errors when extracting BSD tar archive with GNU tar - https://github.com/actions/cache/issues/527
|
// fix permission denied errors when extracting BSD tar archive with GNU tar - https://github.com/actions/cache/issues/527
|
||||||
args.push('--delay-directory-restore')
|
return <ArchiveTool>{path: gnuTar, type: ArchiveToolType.GNU}
|
||||||
return gnuTar
|
} else {
|
||||||
|
return <ArchiveTool>{
|
||||||
|
path: await io.which('tar', true),
|
||||||
|
type: ArchiveToolType.BSD
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return await io.which('tar', true)
|
// Default assumption is GNU tar is present in path
|
||||||
|
return <ArchiveTool>{
|
||||||
|
path: await io.which('tar', true),
|
||||||
|
type: ArchiveToolType.GNU
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function execTar(
|
// Return arguments for tar as per tarPath, compressionMethod, method type and os
|
||||||
args: string[],
|
async function getTarArgs(
|
||||||
|
tarPath: ArchiveTool,
|
||||||
compressionMethod: CompressionMethod,
|
compressionMethod: CompressionMethod,
|
||||||
cwd?: string
|
type: string,
|
||||||
): Promise<void> {
|
archivePath = ''
|
||||||
try {
|
): Promise<string[]> {
|
||||||
await exec(`"${await getTarPath(args, compressionMethod)}"`, args, {cwd})
|
const args = [`"${tarPath.path}"`]
|
||||||
} catch (error) {
|
const cacheFileName = utils.getCacheFileName(compressionMethod)
|
||||||
throw new Error(`Tar failed with error: ${error?.message}`)
|
const tarFile = 'cache.tar'
|
||||||
|
const workingDirectory = getWorkingDirectory()
|
||||||
|
// Speficic args for BSD tar on windows for workaround
|
||||||
|
const BSD_TAR_ZSTD =
|
||||||
|
tarPath.type === ArchiveToolType.BSD &&
|
||||||
|
compressionMethod !== CompressionMethod.Gzip &&
|
||||||
|
IS_WINDOWS
|
||||||
|
|
||||||
|
// Method specific args
|
||||||
|
switch (type) {
|
||||||
|
case 'create':
|
||||||
|
args.push(
|
||||||
|
'--posix',
|
||||||
|
'-cf',
|
||||||
|
BSD_TAR_ZSTD
|
||||||
|
? tarFile
|
||||||
|
: cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||||
|
'--exclude',
|
||||||
|
BSD_TAR_ZSTD
|
||||||
|
? tarFile
|
||||||
|
: cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||||
|
'-P',
|
||||||
|
'-C',
|
||||||
|
workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||||
|
'--files-from',
|
||||||
|
ManifestFilename
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'extract':
|
||||||
|
args.push(
|
||||||
|
'-xf',
|
||||||
|
BSD_TAR_ZSTD
|
||||||
|
? tarFile
|
||||||
|
: archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||||
|
'-P',
|
||||||
|
'-C',
|
||||||
|
workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'list':
|
||||||
|
args.push(
|
||||||
|
'-tf',
|
||||||
|
BSD_TAR_ZSTD
|
||||||
|
? tarFile
|
||||||
|
: archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||||
|
'-P'
|
||||||
|
)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Platform specific args
|
||||||
|
if (tarPath.type === ArchiveToolType.GNU) {
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'win32':
|
||||||
|
args.push('--force-local')
|
||||||
|
break
|
||||||
|
case 'darwin':
|
||||||
|
args.push('--delay-directory-restore')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns commands to run tar and compression program
|
||||||
|
async function getCommands(
|
||||||
|
compressionMethod: CompressionMethod,
|
||||||
|
type: string,
|
||||||
|
archivePath = ''
|
||||||
|
): Promise<string[]> {
|
||||||
|
let args
|
||||||
|
|
||||||
|
const tarPath = await getTarPath()
|
||||||
|
const tarArgs = await getTarArgs(
|
||||||
|
tarPath,
|
||||||
|
compressionMethod,
|
||||||
|
type,
|
||||||
|
archivePath
|
||||||
|
)
|
||||||
|
const compressionArgs =
|
||||||
|
type !== 'create'
|
||||||
|
? await getDecompressionProgram(tarPath, compressionMethod, archivePath)
|
||||||
|
: await getCompressionProgram(tarPath, compressionMethod)
|
||||||
|
const BSD_TAR_ZSTD =
|
||||||
|
tarPath.type === ArchiveToolType.BSD &&
|
||||||
|
compressionMethod !== CompressionMethod.Gzip &&
|
||||||
|
IS_WINDOWS
|
||||||
|
|
||||||
|
if (BSD_TAR_ZSTD && type !== 'create') {
|
||||||
|
args = [[...compressionArgs].join(' '), [...tarArgs].join(' ')]
|
||||||
|
} else {
|
||||||
|
args = [[...tarArgs].join(' '), [...compressionArgs].join(' ')]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BSD_TAR_ZSTD) {
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
return [args.join(' ')]
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWorkingDirectory(): string {
|
function getWorkingDirectory(): string {
|
||||||
return process.env['GITHUB_WORKSPACE'] ?? process.cwd()
|
return process.env['GITHUB_WORKSPACE'] ?? process.cwd()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Common function for extractTar and listTar to get the compression method
|
||||||
|
async function getDecompressionProgram(
|
||||||
|
tarPath: ArchiveTool,
|
||||||
|
compressionMethod: CompressionMethod,
|
||||||
|
archivePath: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
// -d: Decompress.
|
||||||
|
// unzstd is equivalent to 'zstd -d'
|
||||||
|
// --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
|
||||||
|
// Using 30 here because we also support 32-bit self-hosted runners.
|
||||||
|
const BSD_TAR_ZSTD =
|
||||||
|
tarPath.type === ArchiveToolType.BSD &&
|
||||||
|
compressionMethod !== CompressionMethod.Gzip &&
|
||||||
|
IS_WINDOWS
|
||||||
|
switch (compressionMethod) {
|
||||||
|
case CompressionMethod.Zstd:
|
||||||
|
return BSD_TAR_ZSTD
|
||||||
|
? [
|
||||||
|
'zstd -d --long=30 --force -o',
|
||||||
|
TarFilename,
|
||||||
|
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'--use-compress-program',
|
||||||
|
IS_WINDOWS ? '"zstd -d --long=30"' : 'unzstd --long=30'
|
||||||
|
]
|
||||||
|
case CompressionMethod.ZstdWithoutLong:
|
||||||
|
return BSD_TAR_ZSTD
|
||||||
|
? [
|
||||||
|
'zstd -d --force -o',
|
||||||
|
TarFilename,
|
||||||
|
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
|
||||||
|
]
|
||||||
|
: ['--use-compress-program', IS_WINDOWS ? '"zstd -d"' : 'unzstd']
|
||||||
|
default:
|
||||||
|
return ['-z']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for creating the archive
|
||||||
|
// -T#: Compress using # working thread. If # is 0, attempt to detect and use the number of physical CPU cores.
|
||||||
|
// zstdmt is equivalent to 'zstd -T0'
|
||||||
|
// --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
|
||||||
|
// Using 30 here because we also support 32-bit self-hosted runners.
|
||||||
|
// Long range mode is added to zstd in v1.3.2 release, so we will not use --long in older version of zstd.
|
||||||
|
async function getCompressionProgram(
|
||||||
|
tarPath: ArchiveTool,
|
||||||
|
compressionMethod: CompressionMethod
|
||||||
|
): Promise<string[]> {
|
||||||
|
const cacheFileName = utils.getCacheFileName(compressionMethod)
|
||||||
|
const BSD_TAR_ZSTD =
|
||||||
|
tarPath.type === ArchiveToolType.BSD &&
|
||||||
|
compressionMethod !== CompressionMethod.Gzip &&
|
||||||
|
IS_WINDOWS
|
||||||
|
switch (compressionMethod) {
|
||||||
|
case CompressionMethod.Zstd:
|
||||||
|
return BSD_TAR_ZSTD
|
||||||
|
? [
|
||||||
|
'zstd -T0 --long=30 --force -o',
|
||||||
|
cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||||
|
TarFilename
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'--use-compress-program',
|
||||||
|
IS_WINDOWS ? '"zstd -T0 --long=30"' : 'zstdmt --long=30'
|
||||||
|
]
|
||||||
|
case CompressionMethod.ZstdWithoutLong:
|
||||||
|
return BSD_TAR_ZSTD
|
||||||
|
? [
|
||||||
|
'zstd -T0 --force -o',
|
||||||
|
cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
||||||
|
TarFilename
|
||||||
|
]
|
||||||
|
: ['--use-compress-program', IS_WINDOWS ? '"zstd -T0"' : 'zstdmt']
|
||||||
|
default:
|
||||||
|
return ['-z']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executes all commands as separate processes
|
||||||
|
async function execCommands(commands: string[], cwd?: string): Promise<void> {
|
||||||
|
for (const command of commands) {
|
||||||
|
try {
|
||||||
|
await exec(command, undefined, {
|
||||||
|
cwd,
|
||||||
|
env: {...(process.env as object), MSYS: 'winsymlinks:nativestrict'}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`${command.split(' ')[0]} failed with error: ${error?.message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// List the contents of a tar
|
||||||
|
export async function listTar(
|
||||||
|
archivePath: string,
|
||||||
|
compressionMethod: CompressionMethod
|
||||||
|
): Promise<void> {
|
||||||
|
const commands = await getCommands(compressionMethod, 'list', archivePath)
|
||||||
|
await execCommands(commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract a tar
|
||||||
export async function extractTar(
|
export async function extractTar(
|
||||||
archivePath: string,
|
archivePath: string,
|
||||||
compressionMethod: CompressionMethod
|
compressionMethod: CompressionMethod
|
||||||
@@ -61,95 +277,21 @@ export async function extractTar(
|
|||||||
// Create directory to extract tar into
|
// Create directory to extract tar into
|
||||||
const workingDirectory = getWorkingDirectory()
|
const workingDirectory = getWorkingDirectory()
|
||||||
await io.mkdirP(workingDirectory)
|
await io.mkdirP(workingDirectory)
|
||||||
// --d: Decompress.
|
const commands = await getCommands(compressionMethod, 'extract', archivePath)
|
||||||
// --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
|
await execCommands(commands)
|
||||||
// Using 30 here because we also support 32-bit self-hosted runners.
|
|
||||||
function getCompressionProgram(): string[] {
|
|
||||||
switch (compressionMethod) {
|
|
||||||
case CompressionMethod.Zstd:
|
|
||||||
return ['--use-compress-program', 'zstd -d --long=30']
|
|
||||||
case CompressionMethod.ZstdWithoutLong:
|
|
||||||
return ['--use-compress-program', 'zstd -d']
|
|
||||||
default:
|
|
||||||
return ['-z']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const args = [
|
|
||||||
...getCompressionProgram(),
|
|
||||||
'-xf',
|
|
||||||
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
|
||||||
'-P',
|
|
||||||
'-C',
|
|
||||||
workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/')
|
|
||||||
]
|
|
||||||
await execTar(args, compressionMethod)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a tar
|
||||||
export async function createTar(
|
export async function createTar(
|
||||||
archiveFolder: string,
|
archiveFolder: string,
|
||||||
sourceDirectories: string[],
|
sourceDirectories: string[],
|
||||||
compressionMethod: CompressionMethod
|
compressionMethod: CompressionMethod
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Write source directories to manifest.txt to avoid command length limits
|
// Write source directories to manifest.txt to avoid command length limits
|
||||||
const manifestFilename = 'manifest.txt'
|
|
||||||
const cacheFileName = utils.getCacheFileName(compressionMethod)
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
path.join(archiveFolder, manifestFilename),
|
path.join(archiveFolder, ManifestFilename),
|
||||||
sourceDirectories.join('\n')
|
sourceDirectories.join('\n')
|
||||||
)
|
)
|
||||||
const workingDirectory = getWorkingDirectory()
|
const commands = await getCommands(compressionMethod, 'create')
|
||||||
|
await execCommands(commands, archiveFolder)
|
||||||
// -T#: Compress using # working thread. If # is 0, attempt to detect and use the number of physical CPU cores.
|
|
||||||
// --long=#: Enables long distance matching with # bits. Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
|
|
||||||
// Using 30 here because we also support 32-bit self-hosted runners.
|
|
||||||
// Long range mode is added to zstd in v1.3.2 release, so we will not use --long in older version of zstd.
|
|
||||||
function getCompressionProgram(): string[] {
|
|
||||||
switch (compressionMethod) {
|
|
||||||
case CompressionMethod.Zstd:
|
|
||||||
return ['--use-compress-program', 'zstd -T0 --long=30']
|
|
||||||
case CompressionMethod.ZstdWithoutLong:
|
|
||||||
return ['--use-compress-program', 'zstd -T0']
|
|
||||||
default:
|
|
||||||
return ['-z']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const args = [
|
|
||||||
'--posix',
|
|
||||||
...getCompressionProgram(),
|
|
||||||
'-cf',
|
|
||||||
cacheFileName.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
|
||||||
'-P',
|
|
||||||
'-C',
|
|
||||||
workingDirectory.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
|
||||||
'--files-from',
|
|
||||||
manifestFilename
|
|
||||||
]
|
|
||||||
await execTar(args, compressionMethod, archiveFolder)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listTar(
|
|
||||||
archivePath: string,
|
|
||||||
compressionMethod: CompressionMethod
|
|
||||||
): Promise<void> {
|
|
||||||
// --d: Decompress.
|
|
||||||
// --long=#: Enables long distance matching with # bits.
|
|
||||||
// Maximum is 30 (1GB) on 32-bit OS and 31 (2GB) on 64-bit.
|
|
||||||
// Using 30 here because we also support 32-bit self-hosted runners.
|
|
||||||
function getCompressionProgram(): string[] {
|
|
||||||
switch (compressionMethod) {
|
|
||||||
case CompressionMethod.Zstd:
|
|
||||||
return ['--use-compress-program', 'zstd -d --long=30']
|
|
||||||
case CompressionMethod.ZstdWithoutLong:
|
|
||||||
return ['--use-compress-program', 'zstd -d']
|
|
||||||
default:
|
|
||||||
return ['-z']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const args = [
|
|
||||||
...getCompressionProgram(),
|
|
||||||
'-tf',
|
|
||||||
archivePath.replace(new RegExp(`\\${path.sep}`, 'g'), '/'),
|
|
||||||
'-P'
|
|
||||||
]
|
|
||||||
await execTar(args, compressionMethod)
|
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+42
-2
@@ -46,6 +46,22 @@ export interface DownloadOptions {
|
|||||||
* @default 30000
|
* @default 30000
|
||||||
*/
|
*/
|
||||||
timeoutInMs?: number
|
timeoutInMs?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time after which a segment download should be aborted if stuck
|
||||||
|
*
|
||||||
|
* @default 3600000
|
||||||
|
*/
|
||||||
|
segmentTimeoutInMs?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weather to skip downloading the cache entry.
|
||||||
|
* If lookupOnly is set to true, the restore function will only check if
|
||||||
|
* a matching cache entry exists and return the cache key if it does.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
lookupOnly?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,7 +100,9 @@ export function getDownloadOptions(copy?: DownloadOptions): DownloadOptions {
|
|||||||
const result: DownloadOptions = {
|
const result: DownloadOptions = {
|
||||||
useAzureSdk: true,
|
useAzureSdk: true,
|
||||||
downloadConcurrency: 8,
|
downloadConcurrency: 8,
|
||||||
timeoutInMs: 30000
|
timeoutInMs: 30000,
|
||||||
|
segmentTimeoutInMs: 600000,
|
||||||
|
lookupOnly: false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (copy) {
|
if (copy) {
|
||||||
@@ -99,11 +117,33 @@ export function getDownloadOptions(copy?: DownloadOptions): DownloadOptions {
|
|||||||
if (typeof copy.timeoutInMs === 'number') {
|
if (typeof copy.timeoutInMs === 'number') {
|
||||||
result.timeoutInMs = copy.timeoutInMs
|
result.timeoutInMs = copy.timeoutInMs
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if (typeof copy.segmentTimeoutInMs === 'number') {
|
||||||
|
result.segmentTimeoutInMs = copy.segmentTimeoutInMs
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof copy.lookupOnly === 'boolean') {
|
||||||
|
result.lookupOnly = copy.lookupOnly
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const segmentDownloadTimeoutMins =
|
||||||
|
process.env['SEGMENT_DOWNLOAD_TIMEOUT_MINS']
|
||||||
|
|
||||||
|
if (
|
||||||
|
segmentDownloadTimeoutMins &&
|
||||||
|
!isNaN(Number(segmentDownloadTimeoutMins)) &&
|
||||||
|
isFinite(Number(segmentDownloadTimeoutMins))
|
||||||
|
) {
|
||||||
|
result.segmentTimeoutInMs = Number(segmentDownloadTimeoutMins) * 60 * 1000
|
||||||
|
}
|
||||||
core.debug(`Use Azure SDK: ${result.useAzureSdk}`)
|
core.debug(`Use Azure SDK: ${result.useAzureSdk}`)
|
||||||
core.debug(`Download concurrency: ${result.downloadConcurrency}`)
|
core.debug(`Download concurrency: ${result.downloadConcurrency}`)
|
||||||
core.debug(`Request timeout (ms): ${result.timeoutInMs}`)
|
core.debug(`Request timeout (ms): ${result.timeoutInMs}`)
|
||||||
|
core.debug(
|
||||||
|
`Cache segment download timeout mins env var: ${process.env['SEGMENT_DOWNLOAD_TIMEOUT_MINS']}`
|
||||||
|
)
|
||||||
|
core.debug(`Segment download timeout (ms): ${result.segmentTimeoutInMs}`)
|
||||||
|
core.debug(`Lookup only: ${result.lookupOnly}`)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+2
-1
@@ -4,7 +4,8 @@
|
|||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"outDir": "./lib",
|
"outDir": "./lib",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"lib": ["es6", "dom"]
|
"lib": ["es6", "dom"],
|
||||||
|
"useUnknownInCatchVariables": false
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./src"
|
"./src"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ Outputs can be set with `setOutput` which makes them available to be mapped into
|
|||||||
```js
|
```js
|
||||||
const myInput = core.getInput('inputName', { required: true });
|
const myInput = core.getInput('inputName', { required: true });
|
||||||
const myBooleanInput = core.getBooleanInput('booleanInputName', { required: true });
|
const myBooleanInput = core.getBooleanInput('booleanInputName', { required: true });
|
||||||
|
const myMultilineInput = core.getMultilineInput('multilineInputName', { required: true });
|
||||||
core.setOutput('outputKey', 'outputVal');
|
core.setOutput('outputKey', 'outputVal');
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -91,6 +92,8 @@ try {
|
|||||||
|
|
||||||
// Do stuff
|
// Do stuff
|
||||||
core.info('Output to the actions build log')
|
core.info('Output to the actions build log')
|
||||||
|
|
||||||
|
core.notice('This is a message that will also emit an annotation')
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
core.error(`Error ${err}, action may still succeed though`);
|
core.error(`Error ${err}, action may still succeed though`);
|
||||||
@@ -114,6 +117,59 @@ const result = await core.group('Do something async', async () => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Annotations
|
||||||
|
|
||||||
|
This library has 3 methods that will produce [annotations](https://docs.github.com/en/rest/reference/checks#create-a-check-run).
|
||||||
|
```js
|
||||||
|
core.error('This is a bad error, action may still succeed though.')
|
||||||
|
|
||||||
|
core.warning('Something went wrong, but it\'s not bad enough to fail the build.')
|
||||||
|
|
||||||
|
core.notice('Something happened that you might want to know about.')
|
||||||
|
```
|
||||||
|
|
||||||
|
These will surface to the UI in the Actions page and on Pull Requests. They look something like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
These annotations can also be attached to particular lines and columns of your source files to show exactly where a problem is occuring.
|
||||||
|
|
||||||
|
These options are:
|
||||||
|
```typescript
|
||||||
|
export interface AnnotationProperties {
|
||||||
|
/**
|
||||||
|
* A title for the annotation.
|
||||||
|
*/
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of the file for which the annotation should be created.
|
||||||
|
*/
|
||||||
|
file?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The start line for the annotation.
|
||||||
|
*/
|
||||||
|
startLine?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The end line for the annotation. Defaults to `startLine` when `startLine` is provided.
|
||||||
|
*/
|
||||||
|
endLine?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||||
|
*/
|
||||||
|
startColumn?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The end column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||||
|
* Defaults to `startColumn` when `startColumn` is provided.
|
||||||
|
*/
|
||||||
|
endColumn?: number
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
#### Styling output
|
#### Styling output
|
||||||
|
|
||||||
Colored output is supported in the Action logs via standard [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). 3/4 bit, 8 bit and 24 bit colors are all supported.
|
Colored output is supported in the Action logs via standard [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code). 3/4 bit, 8 bit and 24 bit colors are all supported.
|
||||||
@@ -206,3 +262,74 @@ var pid = core.getState("pidToKill");
|
|||||||
|
|
||||||
process.kill(pid);
|
process.kill(pid);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### OIDC Token
|
||||||
|
|
||||||
|
You can use these methods to interact with the GitHub OIDC provider and get a JWT ID token which would help to get access token from third party cloud providers.
|
||||||
|
|
||||||
|
**Method Name**: getIDToken()
|
||||||
|
|
||||||
|
**Inputs**
|
||||||
|
|
||||||
|
audience : optional
|
||||||
|
|
||||||
|
**Outputs**
|
||||||
|
|
||||||
|
A [JWT](https://jwt.io/) ID Token
|
||||||
|
|
||||||
|
In action's `main.ts`:
|
||||||
|
```js
|
||||||
|
const core = require('@actions/core');
|
||||||
|
async function getIDTokenAction(): Promise<void> {
|
||||||
|
|
||||||
|
const audience = core.getInput('audience', {required: false})
|
||||||
|
|
||||||
|
const id_token1 = await core.getIDToken() // ID Token with default audience
|
||||||
|
const id_token2 = await core.getIDToken(audience) // ID token with custom audience
|
||||||
|
|
||||||
|
// this id_token can be used to get access token from third party cloud providers
|
||||||
|
}
|
||||||
|
getIDTokenAction()
|
||||||
|
```
|
||||||
|
|
||||||
|
In action's `actions.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: 'GetIDToken'
|
||||||
|
description: 'Get ID token from Github OIDC provider'
|
||||||
|
inputs:
|
||||||
|
audience:
|
||||||
|
description: 'Audience for which the ID token is intended for'
|
||||||
|
required: false
|
||||||
|
outputs:
|
||||||
|
id_token1:
|
||||||
|
description: 'ID token obtained from OIDC provider'
|
||||||
|
id_token2:
|
||||||
|
description: 'ID token obtained from OIDC provider'
|
||||||
|
runs:
|
||||||
|
using: 'node12'
|
||||||
|
main: 'dist/index.js'
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Filesystem path helpers
|
||||||
|
|
||||||
|
You can use these methods to manipulate file paths across operating systems.
|
||||||
|
|
||||||
|
The `toPosixPath` function converts input paths to Posix-style (Linux) paths.
|
||||||
|
The `toWin32Path` function converts input paths to Windows-style paths. These
|
||||||
|
functions work independently of the underlying runner operating system.
|
||||||
|
|
||||||
|
```js
|
||||||
|
toPosixPath('\\foo\\bar') // => /foo/bar
|
||||||
|
toWin32Path('/foo/bar') // => \foo\bar
|
||||||
|
```
|
||||||
|
|
||||||
|
The `toPlatformPath` function converts input paths to the expected value on the runner's operating system.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// On a Windows runner.
|
||||||
|
toPlatformPath('/foo/bar') // => \foo\bar
|
||||||
|
|
||||||
|
// On a Linux runner.
|
||||||
|
toPlatformPath('\\foo\\bar') // => /foo/bar
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,5 +1,43 @@
|
|||||||
# @actions/core Releases
|
# @actions/core Releases
|
||||||
|
|
||||||
|
### 1.10.0
|
||||||
|
- `saveState` and `setOutput` now use environment files if available [#1178](https://github.com/actions/toolkit/pull/1178)
|
||||||
|
- `getMultilineInput` now correctly trims whitespace by default [#1185](https://github.com/actions/toolkit/pull/1185)
|
||||||
|
|
||||||
|
### 1.9.1
|
||||||
|
- Randomize delimiter when calling `core.exportVariable`
|
||||||
|
|
||||||
|
### 1.9.0
|
||||||
|
- Added `toPosixPath`, `toWin32Path` and `toPlatformPath` utilities [#1102](https://github.com/actions/toolkit/pull/1102)
|
||||||
|
|
||||||
|
### 1.8.2
|
||||||
|
- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
|
||||||
|
|
||||||
|
### 1.8.1
|
||||||
|
- Update to v2.0.0 of `@actions/http-client`
|
||||||
|
|
||||||
|
### 1.8.0
|
||||||
|
- Deprecate `markdownSummary` extension export in favor of `summary`
|
||||||
|
- https://github.com/actions/toolkit/pull/1072
|
||||||
|
- https://github.com/actions/toolkit/pull/1073
|
||||||
|
|
||||||
|
### 1.7.0
|
||||||
|
- [Added `markdownSummary` extension](https://github.com/actions/toolkit/pull/1014)
|
||||||
|
|
||||||
|
### 1.6.0
|
||||||
|
- [Added OIDC Client function `getIDToken`](https://github.com/actions/toolkit/pull/919)
|
||||||
|
- [Added `file` parameter to `AnnotationProperties`](https://github.com/actions/toolkit/pull/896)
|
||||||
|
|
||||||
|
### 1.5.0
|
||||||
|
- [Added support for notice annotations and more annotation fields](https://github.com/actions/toolkit/pull/855)
|
||||||
|
|
||||||
|
### 1.4.0
|
||||||
|
- [Added the `getMultilineInput` function](https://github.com/actions/toolkit/pull/829)
|
||||||
|
|
||||||
|
### 1.3.0
|
||||||
|
- [Added the trimWhitespace option to getInput](https://github.com/actions/toolkit/pull/802)
|
||||||
|
- [Added the getBooleanInput function](https://github.com/actions/toolkit/pull/725)
|
||||||
|
|
||||||
### 1.2.7
|
### 1.2.7
|
||||||
- [Prepend newline for set-output](https://github.com/actions/toolkit/pull/772)
|
- [Prepend newline for set-output](https://github.com/actions/toolkit/pull/772)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ import * as fs from 'fs'
|
|||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as core from '../src/core'
|
import * as core from '../src/core'
|
||||||
|
import {HttpClient} from '@actions/http-client'
|
||||||
|
import {toCommandProperties} from '../src/utils'
|
||||||
|
import * as uuid from 'uuid'
|
||||||
|
|
||||||
|
jest.mock('uuid')
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/unbound-method */
|
/* eslint-disable @typescript-eslint/unbound-method */
|
||||||
|
|
||||||
@@ -27,15 +32,23 @@ const testEnvVars = {
|
|||||||
INPUT_BOOLEAN_INPUT_FALSE2: 'False',
|
INPUT_BOOLEAN_INPUT_FALSE2: 'False',
|
||||||
INPUT_BOOLEAN_INPUT_FALSE3: 'FALSE',
|
INPUT_BOOLEAN_INPUT_FALSE3: 'FALSE',
|
||||||
INPUT_WRONG_BOOLEAN_INPUT: 'wrong',
|
INPUT_WRONG_BOOLEAN_INPUT: 'wrong',
|
||||||
|
INPUT_WITH_TRAILING_WHITESPACE: ' some val ',
|
||||||
|
INPUT_MY_INPUT_LIST: 'val1\nval2\nval3',
|
||||||
|
INPUT_LIST_WITH_TRAILING_WHITESPACE: ' val1 \n val2 \n ',
|
||||||
|
|
||||||
// Save inputs
|
// Save inputs
|
||||||
STATE_TEST_1: 'state_val',
|
STATE_TEST_1: 'state_val',
|
||||||
|
|
||||||
// File Commands
|
// File Commands
|
||||||
GITHUB_PATH: '',
|
GITHUB_PATH: '',
|
||||||
GITHUB_ENV: ''
|
GITHUB_ENV: '',
|
||||||
|
GITHUB_OUTPUT: '',
|
||||||
|
GITHUB_STATE: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UUID = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'
|
||||||
|
const DELIMITER = `ghadelimiter_${UUID}`
|
||||||
|
|
||||||
describe('@actions/core', () => {
|
describe('@actions/core', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
const filePath = path.join(__dirname, `test`)
|
const filePath = path.join(__dirname, `test`)
|
||||||
@@ -49,6 +62,14 @@ describe('@actions/core', () => {
|
|||||||
process.env[key] = testEnvVars[key as keyof typeof testEnvVars]
|
process.env[key] = testEnvVars[key as keyof typeof testEnvVars]
|
||||||
}
|
}
|
||||||
process.stdout.write = jest.fn()
|
process.stdout.write = jest.fn()
|
||||||
|
|
||||||
|
jest.spyOn(uuid, 'v4').mockImplementation(() => {
|
||||||
|
return UUID
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('legacy exportVariable produces the correct command and sets the env', () => {
|
it('legacy exportVariable produces the correct command and sets the env', () => {
|
||||||
@@ -86,7 +107,7 @@ describe('@actions/core', () => {
|
|||||||
core.exportVariable('my var', 'var val')
|
core.exportVariable('my var', 'var val')
|
||||||
verifyFileCommand(
|
verifyFileCommand(
|
||||||
command,
|
command,
|
||||||
`my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}var val${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
|
`my var<<${DELIMITER}${os.EOL}var val${os.EOL}${DELIMITER}${os.EOL}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -96,7 +117,7 @@ describe('@actions/core', () => {
|
|||||||
core.exportVariable('my var', true)
|
core.exportVariable('my var', true)
|
||||||
verifyFileCommand(
|
verifyFileCommand(
|
||||||
command,
|
command,
|
||||||
`my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}true${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
|
`my var<<${DELIMITER}${os.EOL}true${os.EOL}${DELIMITER}${os.EOL}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -106,13 +127,45 @@ describe('@actions/core', () => {
|
|||||||
core.exportVariable('my var', 5)
|
core.exportVariable('my var', 5)
|
||||||
verifyFileCommand(
|
verifyFileCommand(
|
||||||
command,
|
command,
|
||||||
`my var<<_GitHubActionsFileCommandDelimeter_${os.EOL}5${os.EOL}_GitHubActionsFileCommandDelimeter_${os.EOL}`
|
`my var<<${DELIMITER}${os.EOL}5${os.EOL}${DELIMITER}${os.EOL}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('exportVariable does not allow delimiter as value', () => {
|
||||||
|
const command = 'ENV'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
core.exportVariable('my var', `good stuff ${DELIMITER} bad stuff`)
|
||||||
|
}).toThrow(
|
||||||
|
`Unexpected input: value should not contain the delimiter "${DELIMITER}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
const filePath = path.join(__dirname, `test/${command}`)
|
||||||
|
fs.unlinkSync(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('exportVariable does not allow delimiter as name', () => {
|
||||||
|
const command = 'ENV'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
core.exportVariable(`good stuff ${DELIMITER} bad stuff`, 'test')
|
||||||
|
}).toThrow(
|
||||||
|
`Unexpected input: name should not contain the delimiter "${DELIMITER}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
const filePath = path.join(__dirname, `test/${command}`)
|
||||||
|
fs.unlinkSync(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
it('setSecret produces the correct command', () => {
|
it('setSecret produces the correct command', () => {
|
||||||
core.setSecret('secret val')
|
core.setSecret('secret val')
|
||||||
assertWriteCalls([`::add-mask::secret val${os.EOL}`])
|
core.setSecret('multi\nline\r\nsecret')
|
||||||
|
assertWriteCalls([
|
||||||
|
`::add-mask::secret val${os.EOL}`,
|
||||||
|
`::add-mask::multi%0Aline%0D%0Asecret${os.EOL}`
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('prependPath produces the correct commands and sets the env', () => {
|
it('prependPath produces the correct commands and sets the env', () => {
|
||||||
@@ -165,6 +218,22 @@ describe('@actions/core', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('getInput trims whitespace by default', () => {
|
||||||
|
expect(core.getInput('with trailing whitespace')).toBe('some val')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getInput trims whitespace when option is explicitly true', () => {
|
||||||
|
expect(
|
||||||
|
core.getInput('with trailing whitespace', {trimWhitespace: true})
|
||||||
|
).toBe('some val')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getInput does not trim whitespace when option is false', () => {
|
||||||
|
expect(
|
||||||
|
core.getInput('with trailing whitespace', {trimWhitespace: false})
|
||||||
|
).toBe(' some val ')
|
||||||
|
})
|
||||||
|
|
||||||
it('getInput gets non-required boolean input', () => {
|
it('getInput gets non-required boolean input', () => {
|
||||||
expect(core.getBooleanInput('boolean input')).toBe(true)
|
expect(core.getBooleanInput('boolean input')).toBe(true)
|
||||||
})
|
})
|
||||||
@@ -189,7 +258,38 @@ describe('@actions/core', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('setOutput produces the correct command', () => {
|
it('getMultilineInput works', () => {
|
||||||
|
expect(core.getMultilineInput('my input list')).toEqual([
|
||||||
|
'val1',
|
||||||
|
'val2',
|
||||||
|
'val3'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getMultilineInput trims whitespace by default', () => {
|
||||||
|
expect(core.getMultilineInput('list with trailing whitespace')).toEqual([
|
||||||
|
'val1',
|
||||||
|
'val2'
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getMultilineInput trims whitespace when option is explicitly true', () => {
|
||||||
|
expect(
|
||||||
|
core.getMultilineInput('list with trailing whitespace', {
|
||||||
|
trimWhitespace: true
|
||||||
|
})
|
||||||
|
).toEqual(['val1', 'val2'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getMultilineInput does not trim whitespace when option is false', () => {
|
||||||
|
expect(
|
||||||
|
core.getMultilineInput('list with trailing whitespace', {
|
||||||
|
trimWhitespace: false
|
||||||
|
})
|
||||||
|
).toEqual([' val1 ', ' val2 ', ' '])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('legacy setOutput produces the correct command', () => {
|
||||||
core.setOutput('some output', 'some value')
|
core.setOutput('some output', 'some value')
|
||||||
assertWriteCalls([
|
assertWriteCalls([
|
||||||
os.EOL,
|
os.EOL,
|
||||||
@@ -197,16 +297,74 @@ describe('@actions/core', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('setOutput handles bools', () => {
|
it('legacy setOutput handles bools', () => {
|
||||||
core.setOutput('some output', false)
|
core.setOutput('some output', false)
|
||||||
assertWriteCalls([os.EOL, `::set-output name=some output::false${os.EOL}`])
|
assertWriteCalls([os.EOL, `::set-output name=some output::false${os.EOL}`])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('setOutput handles numbers', () => {
|
it('legacy setOutput handles numbers', () => {
|
||||||
core.setOutput('some output', 1.01)
|
core.setOutput('some output', 1.01)
|
||||||
assertWriteCalls([os.EOL, `::set-output name=some output::1.01${os.EOL}`])
|
assertWriteCalls([os.EOL, `::set-output name=some output::1.01${os.EOL}`])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('setOutput produces the correct command and sets the output', () => {
|
||||||
|
const command = 'OUTPUT'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
core.setOutput('my out', 'out val')
|
||||||
|
verifyFileCommand(
|
||||||
|
command,
|
||||||
|
`my out<<${DELIMITER}${os.EOL}out val${os.EOL}${DELIMITER}${os.EOL}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('setOutput handles boolean inputs', () => {
|
||||||
|
const command = 'OUTPUT'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
core.setOutput('my out', true)
|
||||||
|
verifyFileCommand(
|
||||||
|
command,
|
||||||
|
`my out<<${DELIMITER}${os.EOL}true${os.EOL}${DELIMITER}${os.EOL}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('setOutput handles number inputs', () => {
|
||||||
|
const command = 'OUTPUT'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
core.setOutput('my out', 5)
|
||||||
|
verifyFileCommand(
|
||||||
|
command,
|
||||||
|
`my out<<${DELIMITER}${os.EOL}5${os.EOL}${DELIMITER}${os.EOL}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('setOutput does not allow delimiter as value', () => {
|
||||||
|
const command = 'OUTPUT'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
core.setOutput('my out', `good stuff ${DELIMITER} bad stuff`)
|
||||||
|
}).toThrow(
|
||||||
|
`Unexpected input: value should not contain the delimiter "${DELIMITER}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
const filePath = path.join(__dirname, `test/${command}`)
|
||||||
|
fs.unlinkSync(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('setOutput does not allow delimiter as name', () => {
|
||||||
|
const command = 'OUTPUT'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
core.setOutput(`good stuff ${DELIMITER} bad stuff`, 'test')
|
||||||
|
}).toThrow(
|
||||||
|
`Unexpected input: name should not contain the delimiter "${DELIMITER}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
const filePath = path.join(__dirname, `test/${command}`)
|
||||||
|
fs.unlinkSync(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
it('setFailed sets the correct exit code and failure message', () => {
|
it('setFailed sets the correct exit code and failure message', () => {
|
||||||
core.setFailed('Failure message')
|
core.setFailed('Failure message')
|
||||||
expect(process.exitCode).toBe(core.ExitCode.Failure)
|
expect(process.exitCode).toBe(core.ExitCode.Failure)
|
||||||
@@ -242,6 +400,21 @@ describe('@actions/core', () => {
|
|||||||
assertWriteCalls([`::error::Error: ${message}${os.EOL}`])
|
assertWriteCalls([`::error::Error: ${message}${os.EOL}`])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('error handles parameters correctly', () => {
|
||||||
|
const message = 'this is my error message'
|
||||||
|
core.error(new Error(message), {
|
||||||
|
title: 'A title',
|
||||||
|
file: 'root/test.txt',
|
||||||
|
startColumn: 1,
|
||||||
|
endColumn: 2,
|
||||||
|
startLine: 5,
|
||||||
|
endLine: 5
|
||||||
|
})
|
||||||
|
assertWriteCalls([
|
||||||
|
`::error title=A title,file=root/test.txt,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
it('warning sets the correct message', () => {
|
it('warning sets the correct message', () => {
|
||||||
core.warning('Warning')
|
core.warning('Warning')
|
||||||
assertWriteCalls([`::warning::Warning${os.EOL}`])
|
assertWriteCalls([`::warning::Warning${os.EOL}`])
|
||||||
@@ -258,6 +431,72 @@ describe('@actions/core', () => {
|
|||||||
assertWriteCalls([`::warning::Error: ${message}${os.EOL}`])
|
assertWriteCalls([`::warning::Error: ${message}${os.EOL}`])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('warning handles parameters correctly', () => {
|
||||||
|
const message = 'this is my error message'
|
||||||
|
core.warning(new Error(message), {
|
||||||
|
title: 'A title',
|
||||||
|
file: 'root/test.txt',
|
||||||
|
startColumn: 1,
|
||||||
|
endColumn: 2,
|
||||||
|
startLine: 5,
|
||||||
|
endLine: 5
|
||||||
|
})
|
||||||
|
assertWriteCalls([
|
||||||
|
`::warning title=A title,file=root/test.txt,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('notice sets the correct message', () => {
|
||||||
|
core.notice('Notice')
|
||||||
|
assertWriteCalls([`::notice::Notice${os.EOL}`])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('notice escapes the message', () => {
|
||||||
|
core.notice('\r\nnotice\n')
|
||||||
|
assertWriteCalls([`::notice::%0D%0Anotice%0A${os.EOL}`])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('notice handles an error object', () => {
|
||||||
|
const message = 'this is my error message'
|
||||||
|
core.notice(new Error(message))
|
||||||
|
assertWriteCalls([`::notice::Error: ${message}${os.EOL}`])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('notice handles parameters correctly', () => {
|
||||||
|
const message = 'this is my error message'
|
||||||
|
core.notice(new Error(message), {
|
||||||
|
title: 'A title',
|
||||||
|
file: 'root/test.txt',
|
||||||
|
startColumn: 1,
|
||||||
|
endColumn: 2,
|
||||||
|
startLine: 5,
|
||||||
|
endLine: 5
|
||||||
|
})
|
||||||
|
assertWriteCalls([
|
||||||
|
`::notice title=A title,file=root/test.txt,line=5,endLine=5,col=1,endColumn=2::Error: ${message}${os.EOL}`
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('annotations map field names correctly', () => {
|
||||||
|
const commandProperties = toCommandProperties({
|
||||||
|
title: 'A title',
|
||||||
|
file: 'root/test.txt',
|
||||||
|
startColumn: 1,
|
||||||
|
endColumn: 2,
|
||||||
|
startLine: 5,
|
||||||
|
endLine: 5
|
||||||
|
})
|
||||||
|
expect(commandProperties.title).toBe('A title')
|
||||||
|
expect(commandProperties.file).toBe('root/test.txt')
|
||||||
|
expect(commandProperties.col).toBe(1)
|
||||||
|
expect(commandProperties.endColumn).toBe(2)
|
||||||
|
expect(commandProperties.line).toBe(5)
|
||||||
|
expect(commandProperties.endLine).toBe(5)
|
||||||
|
|
||||||
|
expect(commandProperties.startColumn).toBeUndefined()
|
||||||
|
expect(commandProperties.startLine).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('startGroup starts a new group', () => {
|
it('startGroup starts a new group', () => {
|
||||||
core.startGroup('my-group')
|
core.startGroup('my-group')
|
||||||
assertWriteCalls([`::group::my-group${os.EOL}`])
|
assertWriteCalls([`::group::my-group${os.EOL}`])
|
||||||
@@ -291,21 +530,79 @@ describe('@actions/core', () => {
|
|||||||
assertWriteCalls([`::debug::%0D%0Adebug%0A${os.EOL}`])
|
assertWriteCalls([`::debug::%0D%0Adebug%0A${os.EOL}`])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saveState produces the correct command', () => {
|
it('legacy saveState produces the correct command', () => {
|
||||||
core.saveState('state_1', 'some value')
|
core.saveState('state_1', 'some value')
|
||||||
assertWriteCalls([`::save-state name=state_1::some value${os.EOL}`])
|
assertWriteCalls([`::save-state name=state_1::some value${os.EOL}`])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saveState handles numbers', () => {
|
it('legacy saveState handles numbers', () => {
|
||||||
core.saveState('state_1', 1)
|
core.saveState('state_1', 1)
|
||||||
assertWriteCalls([`::save-state name=state_1::1${os.EOL}`])
|
assertWriteCalls([`::save-state name=state_1::1${os.EOL}`])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('saveState handles bools', () => {
|
it('legacy saveState handles bools', () => {
|
||||||
core.saveState('state_1', true)
|
core.saveState('state_1', true)
|
||||||
assertWriteCalls([`::save-state name=state_1::true${os.EOL}`])
|
assertWriteCalls([`::save-state name=state_1::true${os.EOL}`])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('saveState produces the correct command and saves the state', () => {
|
||||||
|
const command = 'STATE'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
core.saveState('my state', 'out val')
|
||||||
|
verifyFileCommand(
|
||||||
|
command,
|
||||||
|
`my state<<${DELIMITER}${os.EOL}out val${os.EOL}${DELIMITER}${os.EOL}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('saveState handles boolean inputs', () => {
|
||||||
|
const command = 'STATE'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
core.saveState('my state', true)
|
||||||
|
verifyFileCommand(
|
||||||
|
command,
|
||||||
|
`my state<<${DELIMITER}${os.EOL}true${os.EOL}${DELIMITER}${os.EOL}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('saveState handles number inputs', () => {
|
||||||
|
const command = 'STATE'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
core.saveState('my state', 5)
|
||||||
|
verifyFileCommand(
|
||||||
|
command,
|
||||||
|
`my state<<${DELIMITER}${os.EOL}5${os.EOL}${DELIMITER}${os.EOL}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('saveState does not allow delimiter as value', () => {
|
||||||
|
const command = 'STATE'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
core.saveState('my state', `good stuff ${DELIMITER} bad stuff`)
|
||||||
|
}).toThrow(
|
||||||
|
`Unexpected input: value should not contain the delimiter "${DELIMITER}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
const filePath = path.join(__dirname, `test/${command}`)
|
||||||
|
fs.unlinkSync(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('saveState does not allow delimiter as name', () => {
|
||||||
|
const command = 'STATE'
|
||||||
|
createFileCommandFile(command)
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
core.saveState(`good stuff ${DELIMITER} bad stuff`, 'test')
|
||||||
|
}).toThrow(
|
||||||
|
`Unexpected input: name should not contain the delimiter "${DELIMITER}"`
|
||||||
|
)
|
||||||
|
|
||||||
|
const filePath = path.join(__dirname, `test/${command}`)
|
||||||
|
fs.unlinkSync(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
it('getState gets wrapper action state', () => {
|
it('getState gets wrapper action state', () => {
|
||||||
expect(core.getState('TEST_1')).toBe('state_val')
|
expect(core.getState('TEST_1')).toBe('state_val')
|
||||||
})
|
})
|
||||||
@@ -360,3 +657,20 @@ function verifyFileCommand(command: string, expectedContents: string): void {
|
|||||||
fs.unlinkSync(filePath)
|
fs.unlinkSync(filePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTokenEndPoint(): string {
|
||||||
|
return 'https://vstoken.actions.githubusercontent.com/.well-known/openid-configuration'
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('oidc-client-tests', () => {
|
||||||
|
it('Get Http Client', async () => {
|
||||||
|
const http = new HttpClient('actions/oidc-client')
|
||||||
|
expect(http).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('HTTP get request to get token endpoint', async () => {
|
||||||
|
const http = new HttpClient('actions/oidc-client')
|
||||||
|
const res = await http.get(getTokenEndPoint())
|
||||||
|
expect(res.message.statusCode).toBe(200)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
import {toPlatformPath, toPosixPath, toWin32Path} from '../src/path-utils'
|
||||||
|
|
||||||
|
describe('#toPosixPath', () => {
|
||||||
|
const cases: {
|
||||||
|
only?: boolean
|
||||||
|
name: string
|
||||||
|
input: string
|
||||||
|
expected: string
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
name: 'empty string',
|
||||||
|
input: '',
|
||||||
|
expected: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'single value',
|
||||||
|
input: 'foo',
|
||||||
|
expected: 'foo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with posix relative',
|
||||||
|
input: 'foo/bar/baz',
|
||||||
|
expected: 'foo/bar/baz'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with posix absolute',
|
||||||
|
input: '/foo/bar/baz',
|
||||||
|
expected: '/foo/bar/baz'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with win32 relative',
|
||||||
|
input: 'foo\\bar\\baz',
|
||||||
|
expected: 'foo/bar/baz'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with win32 absolute',
|
||||||
|
input: '\\foo\\bar\\baz',
|
||||||
|
expected: '/foo/bar/baz'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with a mix',
|
||||||
|
input: '\\foo/bar/baz',
|
||||||
|
expected: '/foo/bar/baz'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const tc of cases) {
|
||||||
|
const fn = tc.only ? it.only : it
|
||||||
|
fn(tc.name, () => {
|
||||||
|
const result = toPosixPath(tc.input)
|
||||||
|
expect(result).toEqual(tc.expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#toWin32Path', () => {
|
||||||
|
const cases: {
|
||||||
|
only?: boolean
|
||||||
|
name: string
|
||||||
|
input: string
|
||||||
|
expected: string
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
name: 'empty string',
|
||||||
|
input: '',
|
||||||
|
expected: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'single value',
|
||||||
|
input: 'foo',
|
||||||
|
expected: 'foo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with posix relative',
|
||||||
|
input: 'foo/bar/baz',
|
||||||
|
expected: 'foo\\bar\\baz'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with posix absolute',
|
||||||
|
input: '/foo/bar/baz',
|
||||||
|
expected: '\\foo\\bar\\baz'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with win32 relative',
|
||||||
|
input: 'foo\\bar\\baz',
|
||||||
|
expected: 'foo\\bar\\baz'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with win32 absolute',
|
||||||
|
input: '\\foo\\bar\\baz',
|
||||||
|
expected: '\\foo\\bar\\baz'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with a mix',
|
||||||
|
input: '\\foo/bar\\baz',
|
||||||
|
expected: '\\foo\\bar\\baz'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const tc of cases) {
|
||||||
|
const fn = tc.only ? it.only : it
|
||||||
|
fn(tc.name, () => {
|
||||||
|
const result = toWin32Path(tc.input)
|
||||||
|
expect(result).toEqual(tc.expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('#toPlatformPath', () => {
|
||||||
|
const cases: {
|
||||||
|
only?: boolean
|
||||||
|
name: string
|
||||||
|
input: string
|
||||||
|
expected: string
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
name: 'empty string',
|
||||||
|
input: '',
|
||||||
|
expected: ''
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'single value',
|
||||||
|
input: 'foo',
|
||||||
|
expected: 'foo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with posix relative',
|
||||||
|
input: 'foo/bar/baz',
|
||||||
|
expected: path.join('foo', 'bar', 'baz')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with posix absolute',
|
||||||
|
input: '/foo/bar/baz',
|
||||||
|
expected: path.join(path.sep, 'foo', 'bar', 'baz')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with win32 relative',
|
||||||
|
input: 'foo\\bar\\baz',
|
||||||
|
expected: path.join('foo', 'bar', 'baz')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with win32 absolute',
|
||||||
|
input: '\\foo\\bar\\baz',
|
||||||
|
expected: path.join(path.sep, 'foo', 'bar', 'baz')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'with a mix',
|
||||||
|
input: '\\foo/bar\\baz',
|
||||||
|
expected: path.join(path.sep, 'foo', 'bar', 'baz')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
for (const tc of cases) {
|
||||||
|
const fn = tc.only ? it.only : it
|
||||||
|
fn(tc.name, () => {
|
||||||
|
const result = toPlatformPath(tc.input)
|
||||||
|
expect(result).toEqual(tc.expected)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
import * as fs from 'fs'
|
||||||
|
import * as os from 'os'
|
||||||
|
import path from 'path'
|
||||||
|
import {summary, SUMMARY_ENV_VAR} from '../src/summary'
|
||||||
|
|
||||||
|
const testDirectoryPath = path.join(__dirname, 'test')
|
||||||
|
const testFilePath = path.join(testDirectoryPath, 'test-summary.md')
|
||||||
|
|
||||||
|
async function assertSummary(expected: string): Promise<void> {
|
||||||
|
const file = await fs.promises.readFile(testFilePath, {encoding: 'utf8'})
|
||||||
|
expect(file).toEqual(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixtures = {
|
||||||
|
text: 'hello world 🌎',
|
||||||
|
code: `func fork() {
|
||||||
|
for {
|
||||||
|
go fork()
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
list: ['foo', 'bar', 'baz', '💣'],
|
||||||
|
table: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
data: 'foo',
|
||||||
|
header: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'bar',
|
||||||
|
header: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'baz',
|
||||||
|
header: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
data: 'tall',
|
||||||
|
rowspan: '3'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
['one', 'two', 'three'],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
data: 'wide',
|
||||||
|
colspan: '3'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
],
|
||||||
|
details: {
|
||||||
|
label: 'open me',
|
||||||
|
content: '🎉 surprise'
|
||||||
|
},
|
||||||
|
img: {
|
||||||
|
src: 'https://github.com/actions.png',
|
||||||
|
alt: 'actions logo',
|
||||||
|
options: {
|
||||||
|
width: '32',
|
||||||
|
height: '32'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
quote: {
|
||||||
|
text: 'Where the world builds software',
|
||||||
|
cite: 'https://github.com/about'
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
text: 'GitHub',
|
||||||
|
href: 'https://github.com/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('@actions/core/src/summary', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
process.env[SUMMARY_ENV_VAR] = testFilePath
|
||||||
|
await fs.promises.mkdir(testDirectoryPath, {recursive: true})
|
||||||
|
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
|
||||||
|
summary.emptyBuffer()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await fs.promises.unlink(testFilePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws if summary env var is undefined', async () => {
|
||||||
|
process.env[SUMMARY_ENV_VAR] = undefined
|
||||||
|
const write = summary.addRaw(fixtures.text).write()
|
||||||
|
|
||||||
|
await expect(write).rejects.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws if summary file does not exist', async () => {
|
||||||
|
await fs.promises.unlink(testFilePath)
|
||||||
|
const write = summary.addRaw(fixtures.text).write()
|
||||||
|
|
||||||
|
await expect(write).rejects.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('appends text to summary file', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'})
|
||||||
|
await summary.addRaw(fixtures.text).write()
|
||||||
|
await assertSummary(`# ${fixtures.text}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('overwrites text to summary file', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, 'overwrite', {encoding: 'utf8'})
|
||||||
|
await summary.addRaw(fixtures.text).write({overwrite: true})
|
||||||
|
await assertSummary(fixtures.text)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('appends text with EOL to summary file', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'})
|
||||||
|
await summary.addRaw(fixtures.text, true).write()
|
||||||
|
await assertSummary(`# ${fixtures.text}${os.EOL}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('chains appends text to summary file', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
|
||||||
|
await summary
|
||||||
|
.addRaw(fixtures.text)
|
||||||
|
.addRaw(fixtures.text)
|
||||||
|
.addRaw(fixtures.text)
|
||||||
|
.write()
|
||||||
|
await assertSummary([fixtures.text, fixtures.text, fixtures.text].join(''))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('empties buffer after write', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'})
|
||||||
|
await summary.addRaw(fixtures.text).write()
|
||||||
|
await assertSummary(fixtures.text)
|
||||||
|
expect(summary.isEmptyBuffer()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns summary buffer as string', () => {
|
||||||
|
summary.addRaw(fixtures.text)
|
||||||
|
expect(summary.stringify()).toEqual(fixtures.text)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('return correct values for isEmptyBuffer', () => {
|
||||||
|
summary.addRaw(fixtures.text)
|
||||||
|
expect(summary.isEmptyBuffer()).toBe(false)
|
||||||
|
|
||||||
|
summary.emptyBuffer()
|
||||||
|
expect(summary.isEmptyBuffer()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears a buffer and summary file', async () => {
|
||||||
|
await fs.promises.writeFile(testFilePath, 'content', {encoding: 'utf8'})
|
||||||
|
await summary.clear()
|
||||||
|
await assertSummary('')
|
||||||
|
expect(summary.isEmptyBuffer()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds EOL', async () => {
|
||||||
|
await summary
|
||||||
|
.addRaw(fixtures.text)
|
||||||
|
.addEOL()
|
||||||
|
.write()
|
||||||
|
await assertSummary(fixtures.text + os.EOL)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a code block without language', async () => {
|
||||||
|
await summary.addCodeBlock(fixtures.code).write()
|
||||||
|
const expected = `<pre><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a code block with a language', async () => {
|
||||||
|
await summary.addCodeBlock(fixtures.code, 'go').write()
|
||||||
|
const expected = `<pre lang="go"><code>func fork() {\n for {\n go fork()\n }\n}</code></pre>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an unordered list', async () => {
|
||||||
|
await summary.addList(fixtures.list).write()
|
||||||
|
const expected = `<ul><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ul>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an ordered list', async () => {
|
||||||
|
await summary.addList(fixtures.list, true).write()
|
||||||
|
const expected = `<ol><li>foo</li><li>bar</li><li>baz</li><li>💣</li></ol>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a table', async () => {
|
||||||
|
await summary.addTable(fixtures.table).write()
|
||||||
|
const expected = `<table><tr><th>foo</th><th>bar</th><th>baz</th><td rowspan="3">tall</td></tr><tr><td>one</td><td>two</td><td>three</td></tr><tr><td colspan="3">wide</td></tr></table>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a details element', async () => {
|
||||||
|
await summary
|
||||||
|
.addDetails(fixtures.details.label, fixtures.details.content)
|
||||||
|
.write()
|
||||||
|
const expected = `<details><summary>open me</summary>🎉 surprise</details>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an image with alt text', async () => {
|
||||||
|
await summary.addImage(fixtures.img.src, fixtures.img.alt).write()
|
||||||
|
const expected = `<img src="https://github.com/actions.png" alt="actions logo">${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an image with custom dimensions', async () => {
|
||||||
|
await summary
|
||||||
|
.addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options)
|
||||||
|
.write()
|
||||||
|
const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds an image with custom dimensions', async () => {
|
||||||
|
await summary
|
||||||
|
.addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options)
|
||||||
|
.write()
|
||||||
|
const expected = `<img src="https://github.com/actions.png" alt="actions logo" width="32" height="32">${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds headings h1...h6', async () => {
|
||||||
|
for (const i of [1, 2, 3, 4, 5, 6]) {
|
||||||
|
summary.addHeading('heading', i)
|
||||||
|
}
|
||||||
|
await summary.write()
|
||||||
|
const expected = `<h1>heading</h1>${os.EOL}<h2>heading</h2>${os.EOL}<h3>heading</h3>${os.EOL}<h4>heading</h4>${os.EOL}<h5>heading</h5>${os.EOL}<h6>heading</h6>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds h1 if heading level not specified', async () => {
|
||||||
|
await summary.addHeading('heading').write()
|
||||||
|
const expected = `<h1>heading</h1>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses h1 if heading level is garbage or out of range', async () => {
|
||||||
|
await summary
|
||||||
|
.addHeading('heading', 'foobar')
|
||||||
|
.addHeading('heading', 1337)
|
||||||
|
.addHeading('heading', -1)
|
||||||
|
.addHeading('heading', Infinity)
|
||||||
|
.write()
|
||||||
|
const expected = `<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}<h1>heading</h1>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a separator', async () => {
|
||||||
|
await summary.addSeparator().write()
|
||||||
|
const expected = `<hr>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a break', async () => {
|
||||||
|
await summary.addBreak().write()
|
||||||
|
const expected = `<br>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a quote', async () => {
|
||||||
|
await summary.addQuote(fixtures.quote.text).write()
|
||||||
|
const expected = `<blockquote>Where the world builds software</blockquote>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a quote with citation', async () => {
|
||||||
|
await summary.addQuote(fixtures.quote.text, fixtures.quote.cite).write()
|
||||||
|
const expected = `<blockquote cite="https://github.com/about">Where the world builds software</blockquote>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('adds a link with href', async () => {
|
||||||
|
await summary.addLink(fixtures.link.text, fixtures.link.href).write()
|
||||||
|
const expected = `<a href="https://github.com/">GitHub</a>${os.EOL}`
|
||||||
|
await assertSummary(expected)
|
||||||
|
})
|
||||||
|
})
|
||||||
Generated
+77
-2
@@ -1,14 +1,89 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/core",
|
"name": "@actions/core",
|
||||||
"version": "1.2.7",
|
"version": "1.10.0",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@actions/core",
|
||||||
|
"version": "1.10.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/http-client": "^2.0.1",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^12.0.2",
|
||||||
|
"@types/uuid": "^8.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/http-client": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tunnel": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "12.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
|
||||||
|
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/uuid": {
|
||||||
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/tunnel": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@actions/http-client": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==",
|
||||||
|
"requires": {
|
||||||
|
"tunnel": "^0.0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "12.0.2",
|
"version": "12.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz",
|
||||||
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
|
"integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
|
},
|
||||||
|
"@types/uuid": {
|
||||||
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"tunnel": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
||||||
|
},
|
||||||
|
"uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/core",
|
"name": "@actions/core",
|
||||||
"version": "1.2.7",
|
"version": "1.10.0",
|
||||||
"description": "Actions core lib",
|
"description": "Actions core lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"github",
|
"github",
|
||||||
@@ -35,7 +35,12 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/actions/toolkit/issues"
|
"url": "https://github.com/actions/toolkit/issues"
|
||||||
},
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/http-client": "^2.0.1",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^12.0.2"
|
"@types/node": "^12.0.2",
|
||||||
|
"@types/uuid": "^8.3.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {toCommandValue} from './utils'
|
|||||||
// We use any as a valid input type
|
// We use any as a valid input type
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
interface CommandProperties {
|
export interface CommandProperties {
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ export function issueCommand(
|
|||||||
process.stdout.write(cmd.toString() + os.EOL)
|
process.stdout.write(cmd.toString() + os.EOL)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function issue(name: string, message: string = ''): void {
|
export function issue(name: string, message = ''): void {
|
||||||
issueCommand(name, {}, message)
|
issueCommand(name, {}, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+145
-15
@@ -1,16 +1,21 @@
|
|||||||
import {issue, issueCommand} from './command'
|
import {issue, issueCommand} from './command'
|
||||||
import {issueCommand as issueFileCommand} from './file-command'
|
import {issueFileCommand, prepareKeyValueMessage} from './file-command'
|
||||||
import {toCommandValue} from './utils'
|
import {toCommandProperties, toCommandValue} from './utils'
|
||||||
|
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
|
|
||||||
|
import {OidcClient} from './oidc-utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for getInput options
|
* Interface for getInput options
|
||||||
*/
|
*/
|
||||||
export interface InputOptions {
|
export interface InputOptions {
|
||||||
/** Optional. Whether the input is required. If required and not present, will throw. Defaults to false */
|
/** Optional. Whether the input is required. If required and not present, will throw. Defaults to false */
|
||||||
required?: boolean
|
required?: boolean
|
||||||
|
|
||||||
|
/** Optional. Whether leading/trailing whitespace will be trimmed for the input. Defaults to true */
|
||||||
|
trimWhitespace?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +33,43 @@ export enum ExitCode {
|
|||||||
Failure = 1
|
Failure = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional properties that can be sent with annotation commands (notice, error, and warning)
|
||||||
|
* See: https://docs.github.com/en/rest/reference/checks#create-a-check-run for more information about annotations.
|
||||||
|
*/
|
||||||
|
export interface AnnotationProperties {
|
||||||
|
/**
|
||||||
|
* A title for the annotation.
|
||||||
|
*/
|
||||||
|
title?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path of the file for which the annotation should be created.
|
||||||
|
*/
|
||||||
|
file?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The start line for the annotation.
|
||||||
|
*/
|
||||||
|
startLine?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The end line for the annotation. Defaults to `startLine` when `startLine` is provided.
|
||||||
|
*/
|
||||||
|
endLine?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The start column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||||
|
*/
|
||||||
|
startColumn?: number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The end column for the annotation. Cannot be sent when `startLine` and `endLine` are different values.
|
||||||
|
* Defaults to `startColumn` when `startColumn` is provided.
|
||||||
|
*/
|
||||||
|
endColumn?: number
|
||||||
|
}
|
||||||
|
|
||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
// Variables
|
// Variables
|
||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
@@ -44,12 +86,10 @@ export function exportVariable(name: string, val: any): void {
|
|||||||
|
|
||||||
const filePath = process.env['GITHUB_ENV'] || ''
|
const filePath = process.env['GITHUB_ENV'] || ''
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
const delimiter = '_GitHubActionsFileCommandDelimeter_'
|
return issueFileCommand('ENV', prepareKeyValueMessage(name, val))
|
||||||
const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`
|
|
||||||
issueFileCommand('ENV', commandValue)
|
|
||||||
} else {
|
|
||||||
issueCommand('set-env', {name}, convertedVal)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
issueCommand('set-env', {name}, convertedVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +115,9 @@ export function addPath(inputPath: string): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the value of an input. The value is also trimmed.
|
* Gets the value of an input.
|
||||||
|
* Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
|
||||||
|
* Returns an empty string if the value is not defined.
|
||||||
*
|
*
|
||||||
* @param name name of the input to get
|
* @param name name of the input to get
|
||||||
* @param options optional. See InputOptions.
|
* @param options optional. See InputOptions.
|
||||||
@@ -88,9 +130,36 @@ export function getInput(name: string, options?: InputOptions): string {
|
|||||||
throw new Error(`Input required and not supplied: ${name}`)
|
throw new Error(`Input required and not supplied: ${name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options && options.trimWhitespace === false) {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
return val.trim()
|
return val.trim()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the values of an multiline input. Each value is also trimmed.
|
||||||
|
*
|
||||||
|
* @param name name of the input to get
|
||||||
|
* @param options optional. See InputOptions.
|
||||||
|
* @returns string[]
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function getMultilineInput(
|
||||||
|
name: string,
|
||||||
|
options?: InputOptions
|
||||||
|
): string[] {
|
||||||
|
const inputs: string[] = getInput(name, options)
|
||||||
|
.split('\n')
|
||||||
|
.filter(x => x !== '')
|
||||||
|
|
||||||
|
if (options && options.trimWhitespace === false) {
|
||||||
|
return inputs
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputs.map(input => input.trim())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
|
* Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
|
||||||
* Support boolean input list: `true | True | TRUE | false | False | FALSE` .
|
* Support boolean input list: `true | True | TRUE | false | False | FALSE` .
|
||||||
@@ -121,8 +190,13 @@ export function getBooleanInput(name: string, options?: InputOptions): boolean {
|
|||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function setOutput(name: string, value: any): void {
|
export function setOutput(name: string, value: any): void {
|
||||||
|
const filePath = process.env['GITHUB_OUTPUT'] || ''
|
||||||
|
if (filePath) {
|
||||||
|
return issueFileCommand('OUTPUT', prepareKeyValueMessage(name, value))
|
||||||
|
}
|
||||||
|
|
||||||
process.stdout.write(os.EOL)
|
process.stdout.write(os.EOL)
|
||||||
issueCommand('set-output', {name}, value)
|
issueCommand('set-output', {name}, toCommandValue(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -171,17 +245,49 @@ export function debug(message: string): void {
|
|||||||
/**
|
/**
|
||||||
* Adds an error issue
|
* Adds an error issue
|
||||||
* @param message error issue message. Errors will be converted to string via toString()
|
* @param message error issue message. Errors will be converted to string via toString()
|
||||||
|
* @param properties optional properties to add to the annotation.
|
||||||
*/
|
*/
|
||||||
export function error(message: string | Error): void {
|
export function error(
|
||||||
issue('error', message instanceof Error ? message.toString() : message)
|
message: string | Error,
|
||||||
|
properties: AnnotationProperties = {}
|
||||||
|
): void {
|
||||||
|
issueCommand(
|
||||||
|
'error',
|
||||||
|
toCommandProperties(properties),
|
||||||
|
message instanceof Error ? message.toString() : message
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds an warning issue
|
* Adds a warning issue
|
||||||
* @param message warning issue message. Errors will be converted to string via toString()
|
* @param message warning issue message. Errors will be converted to string via toString()
|
||||||
|
* @param properties optional properties to add to the annotation.
|
||||||
*/
|
*/
|
||||||
export function warning(message: string | Error): void {
|
export function warning(
|
||||||
issue('warning', message instanceof Error ? message.toString() : message)
|
message: string | Error,
|
||||||
|
properties: AnnotationProperties = {}
|
||||||
|
): void {
|
||||||
|
issueCommand(
|
||||||
|
'warning',
|
||||||
|
toCommandProperties(properties),
|
||||||
|
message instanceof Error ? message.toString() : message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a notice issue
|
||||||
|
* @param message notice issue message. Errors will be converted to string via toString()
|
||||||
|
* @param properties optional properties to add to the annotation.
|
||||||
|
*/
|
||||||
|
export function notice(
|
||||||
|
message: string | Error,
|
||||||
|
properties: AnnotationProperties = {}
|
||||||
|
): void {
|
||||||
|
issueCommand(
|
||||||
|
'notice',
|
||||||
|
toCommandProperties(properties),
|
||||||
|
message instanceof Error ? message.toString() : message
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -244,7 +350,12 @@ export async function group<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
|||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function saveState(name: string, value: any): void {
|
export function saveState(name: string, value: any): void {
|
||||||
issueCommand('save-state', {name}, value)
|
const filePath = process.env['GITHUB_STATE'] || ''
|
||||||
|
if (filePath) {
|
||||||
|
return issueFileCommand('STATE', prepareKeyValueMessage(name, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
issueCommand('save-state', {name}, toCommandValue(value))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -256,3 +367,22 @@ export function saveState(name: string, value: any): void {
|
|||||||
export function getState(name: string): string {
|
export function getState(name: string): string {
|
||||||
return process.env[`STATE_${name}`] || ''
|
return process.env[`STATE_${name}`] || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getIDToken(aud?: string): Promise<string> {
|
||||||
|
return await OidcClient.getIDToken(aud)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summary exports
|
||||||
|
*/
|
||||||
|
export {summary} from './summary'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use core.summary
|
||||||
|
*/
|
||||||
|
export {markdownSummary} from './summary'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path exports
|
||||||
|
*/
|
||||||
|
export {toPosixPath, toWin32Path, toPlatformPath} from './path-utils'
|
||||||
|
|||||||
@@ -5,9 +5,10 @@
|
|||||||
|
|
||||||
import * as fs from 'fs'
|
import * as fs from 'fs'
|
||||||
import * as os from 'os'
|
import * as os from 'os'
|
||||||
|
import {v4 as uuidv4} from 'uuid'
|
||||||
import {toCommandValue} from './utils'
|
import {toCommandValue} from './utils'
|
||||||
|
|
||||||
export function issueCommand(command: string, message: any): void {
|
export function issueFileCommand(command: string, message: any): void {
|
||||||
const filePath = process.env[`GITHUB_${command}`]
|
const filePath = process.env[`GITHUB_${command}`]
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -22,3 +23,25 @@ export function issueCommand(command: string, message: any): void {
|
|||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function prepareKeyValueMessage(key: string, value: any): string {
|
||||||
|
const delimiter = `ghadelimiter_${uuidv4()}`
|
||||||
|
const convertedValue = toCommandValue(value)
|
||||||
|
|
||||||
|
// These should realistically never happen, but just in case someone finds a
|
||||||
|
// way to exploit uuid generation let's not allow keys or values that contain
|
||||||
|
// the delimiter.
|
||||||
|
if (key.includes(delimiter)) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected input: name should not contain the delimiter "${delimiter}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (convertedValue.includes(delimiter)) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected input: value should not contain the delimiter "${delimiter}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-extraneous-class */
|
||||||
|
import * as actions_http_client from '@actions/http-client'
|
||||||
|
import {RequestOptions} from '@actions/http-client/lib/interfaces'
|
||||||
|
import {HttpClient} from '@actions/http-client'
|
||||||
|
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
|
||||||
|
import {debug, setSecret} from './core'
|
||||||
|
interface TokenResponse {
|
||||||
|
value?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OidcClient {
|
||||||
|
private static createHttpClient(
|
||||||
|
allowRetry = true,
|
||||||
|
maxRetry = 10
|
||||||
|
): actions_http_client.HttpClient {
|
||||||
|
const requestOptions: RequestOptions = {
|
||||||
|
allowRetries: allowRetry,
|
||||||
|
maxRetries: maxRetry
|
||||||
|
}
|
||||||
|
|
||||||
|
return new HttpClient(
|
||||||
|
'actions/oidc-client',
|
||||||
|
[new BearerCredentialHandler(OidcClient.getRequestToken())],
|
||||||
|
requestOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getRequestToken(): string {
|
||||||
|
const token = process.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']
|
||||||
|
if (!token) {
|
||||||
|
throw new Error(
|
||||||
|
'Unable to get ACTIONS_ID_TOKEN_REQUEST_TOKEN env variable'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getIDTokenUrl(): string {
|
||||||
|
const runtimeUrl = process.env['ACTIONS_ID_TOKEN_REQUEST_URL']
|
||||||
|
if (!runtimeUrl) {
|
||||||
|
throw new Error('Unable to get ACTIONS_ID_TOKEN_REQUEST_URL env variable')
|
||||||
|
}
|
||||||
|
return runtimeUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async getCall(id_token_url: string): Promise<string> {
|
||||||
|
const httpclient = OidcClient.createHttpClient()
|
||||||
|
|
||||||
|
const res = await httpclient
|
||||||
|
.getJson<TokenResponse>(id_token_url)
|
||||||
|
.catch(error => {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to get ID Token. \n
|
||||||
|
Error Code : ${error.statusCode}\n
|
||||||
|
Error Message: ${error.result.message}`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const id_token = res.result?.value
|
||||||
|
if (!id_token) {
|
||||||
|
throw new Error('Response json body do not have ID Token field')
|
||||||
|
}
|
||||||
|
return id_token
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getIDToken(audience?: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
// New ID Token is requested from action service
|
||||||
|
let id_token_url: string = OidcClient.getIDTokenUrl()
|
||||||
|
if (audience) {
|
||||||
|
const encodedAudience = encodeURIComponent(audience)
|
||||||
|
id_token_url = `${id_token_url}&audience=${encodedAudience}`
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`ID token url is ${id_token_url}`)
|
||||||
|
|
||||||
|
const id_token = await OidcClient.getCall(id_token_url)
|
||||||
|
setSecret(id_token)
|
||||||
|
return id_token
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Error message: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* toPosixPath converts the given path to the posix form. On Windows, \\ will be
|
||||||
|
* replaced with /.
|
||||||
|
*
|
||||||
|
* @param pth. Path to transform.
|
||||||
|
* @return string Posix path.
|
||||||
|
*/
|
||||||
|
export function toPosixPath(pth: string): string {
|
||||||
|
return pth.replace(/[\\]/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* toWin32Path converts the given path to the win32 form. On Linux, / will be
|
||||||
|
* replaced with \\.
|
||||||
|
*
|
||||||
|
* @param pth. Path to transform.
|
||||||
|
* @return string Win32 path.
|
||||||
|
*/
|
||||||
|
export function toWin32Path(pth: string): string {
|
||||||
|
return pth.replace(/[/]/g, '\\')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* toPlatformPath converts the given path to a platform-specific path. It does
|
||||||
|
* this by replacing instances of / and \ with the platform-specific path
|
||||||
|
* separator.
|
||||||
|
*
|
||||||
|
* @param pth The path to platformize.
|
||||||
|
* @return string The platform-specific path.
|
||||||
|
*/
|
||||||
|
export function toPlatformPath(pth: string): string {
|
||||||
|
return pth.replace(/[/\\]/g, path.sep)
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
import {EOL} from 'os'
|
||||||
|
import {constants, promises} from 'fs'
|
||||||
|
const {access, appendFile, writeFile} = promises
|
||||||
|
|
||||||
|
export const SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'
|
||||||
|
export const SUMMARY_DOCS_URL =
|
||||||
|
'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary'
|
||||||
|
|
||||||
|
export type SummaryTableRow = (SummaryTableCell | string)[]
|
||||||
|
|
||||||
|
export interface SummaryTableCell {
|
||||||
|
/**
|
||||||
|
* Cell content
|
||||||
|
*/
|
||||||
|
data: string
|
||||||
|
/**
|
||||||
|
* Render cell as header
|
||||||
|
* (optional) default: false
|
||||||
|
*/
|
||||||
|
header?: boolean
|
||||||
|
/**
|
||||||
|
* Number of columns the cell extends
|
||||||
|
* (optional) default: '1'
|
||||||
|
*/
|
||||||
|
colspan?: string
|
||||||
|
/**
|
||||||
|
* Number of rows the cell extends
|
||||||
|
* (optional) default: '1'
|
||||||
|
*/
|
||||||
|
rowspan?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SummaryImageOptions {
|
||||||
|
/**
|
||||||
|
* The width of the image in pixels. Must be an integer without a unit.
|
||||||
|
* (optional)
|
||||||
|
*/
|
||||||
|
width?: string
|
||||||
|
/**
|
||||||
|
* The height of the image in pixels. Must be an integer without a unit.
|
||||||
|
* (optional)
|
||||||
|
*/
|
||||||
|
height?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SummaryWriteOptions {
|
||||||
|
/**
|
||||||
|
* Replace all existing content in summary file with buffer contents
|
||||||
|
* (optional) default: false
|
||||||
|
*/
|
||||||
|
overwrite?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class Summary {
|
||||||
|
private _buffer: string
|
||||||
|
private _filePath?: string
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this._buffer = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the summary file path from the environment, rejects if env var is not found or file does not exist
|
||||||
|
* Also checks r/w permissions.
|
||||||
|
*
|
||||||
|
* @returns step summary file path
|
||||||
|
*/
|
||||||
|
private async filePath(): Promise<string> {
|
||||||
|
if (this._filePath) {
|
||||||
|
return this._filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathFromEnv = process.env[SUMMARY_ENV_VAR]
|
||||||
|
if (!pathFromEnv) {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to find environment variable for $${SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await access(pathFromEnv, constants.R_OK | constants.W_OK)
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
this._filePath = pathFromEnv
|
||||||
|
return this._filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps content in an HTML tag, adding any HTML attributes
|
||||||
|
*
|
||||||
|
* @param {string} tag HTML tag to wrap
|
||||||
|
* @param {string | null} content content within the tag
|
||||||
|
* @param {[attribute: string]: string} attrs key-value list of HTML attributes to add
|
||||||
|
*
|
||||||
|
* @returns {string} content wrapped in HTML element
|
||||||
|
*/
|
||||||
|
private wrap(
|
||||||
|
tag: string,
|
||||||
|
content: string | null,
|
||||||
|
attrs: {[attribute: string]: string} = {}
|
||||||
|
): string {
|
||||||
|
const htmlAttrs = Object.entries(attrs)
|
||||||
|
.map(([key, value]) => ` ${key}="${value}"`)
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return `<${tag}${htmlAttrs}>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<${tag}${htmlAttrs}>${content}</${tag}>`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes text in the buffer to the summary buffer file and empties buffer. Will append by default.
|
||||||
|
*
|
||||||
|
* @param {SummaryWriteOptions} [options] (optional) options for write operation
|
||||||
|
*
|
||||||
|
* @returns {Promise<Summary>} summary instance
|
||||||
|
*/
|
||||||
|
async write(options?: SummaryWriteOptions): Promise<Summary> {
|
||||||
|
const overwrite = !!options?.overwrite
|
||||||
|
const filePath = await this.filePath()
|
||||||
|
const writeFunc = overwrite ? writeFile : appendFile
|
||||||
|
await writeFunc(filePath, this._buffer, {encoding: 'utf8'})
|
||||||
|
return this.emptyBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the summary buffer and wipes the summary file
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
async clear(): Promise<Summary> {
|
||||||
|
return this.emptyBuffer().write({overwrite: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the current summary buffer as a string
|
||||||
|
*
|
||||||
|
* @returns {string} string of summary buffer
|
||||||
|
*/
|
||||||
|
stringify(): string {
|
||||||
|
return this._buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the summary buffer is empty
|
||||||
|
*
|
||||||
|
* @returns {boolen} true if the buffer is empty
|
||||||
|
*/
|
||||||
|
isEmptyBuffer(): boolean {
|
||||||
|
return this._buffer.length === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the summary buffer without writing to summary file
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
emptyBuffer(): Summary {
|
||||||
|
this._buffer = ''
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds raw text to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} text content to add
|
||||||
|
* @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false)
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addRaw(text: string, addEOL = false): Summary {
|
||||||
|
this._buffer += text
|
||||||
|
return addEOL ? this.addEOL() : this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the operating system-specific end-of-line marker to the buffer
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addEOL(): Summary {
|
||||||
|
return this.addRaw(EOL)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML codeblock to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} code content to render within fenced code block
|
||||||
|
* @param {string} lang (optional) language to syntax highlight code
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addCodeBlock(code: string, lang?: string): Summary {
|
||||||
|
const attrs = {
|
||||||
|
...(lang && {lang})
|
||||||
|
}
|
||||||
|
const element = this.wrap('pre', this.wrap('code', code), attrs)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML list to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string[]} items list of items to render
|
||||||
|
* @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false)
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addList(items: string[], ordered = false): Summary {
|
||||||
|
const tag = ordered ? 'ol' : 'ul'
|
||||||
|
const listItems = items.map(item => this.wrap('li', item)).join('')
|
||||||
|
const element = this.wrap(tag, listItems)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML table to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {SummaryTableCell[]} rows table rows
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addTable(rows: SummaryTableRow[]): Summary {
|
||||||
|
const tableBody = rows
|
||||||
|
.map(row => {
|
||||||
|
const cells = row
|
||||||
|
.map(cell => {
|
||||||
|
if (typeof cell === 'string') {
|
||||||
|
return this.wrap('td', cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {header, data, colspan, rowspan} = cell
|
||||||
|
const tag = header ? 'th' : 'td'
|
||||||
|
const attrs = {
|
||||||
|
...(colspan && {colspan}),
|
||||||
|
...(rowspan && {rowspan})
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.wrap(tag, data, attrs)
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
return this.wrap('tr', cells)
|
||||||
|
})
|
||||||
|
.join('')
|
||||||
|
|
||||||
|
const element = this.wrap('table', tableBody)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a collapsable HTML details element to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} label text for the closed state
|
||||||
|
* @param {string} content collapsable content
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addDetails(label: string, content: string): Summary {
|
||||||
|
const element = this.wrap('details', this.wrap('summary', label) + content)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML image tag to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} src path to the image you to embed
|
||||||
|
* @param {string} alt text description of the image
|
||||||
|
* @param {SummaryImageOptions} options (optional) addition image attributes
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addImage(src: string, alt: string, options?: SummaryImageOptions): Summary {
|
||||||
|
const {width, height} = options || {}
|
||||||
|
const attrs = {
|
||||||
|
...(width && {width}),
|
||||||
|
...(height && {height})
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = this.wrap('img', null, {src, alt, ...attrs})
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML section heading element
|
||||||
|
*
|
||||||
|
* @param {string} text heading text
|
||||||
|
* @param {number | string} [level=1] (optional) the heading level, default: 1
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addHeading(text: string, level?: number | string): Summary {
|
||||||
|
const tag = `h${level}`
|
||||||
|
const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)
|
||||||
|
? tag
|
||||||
|
: 'h1'
|
||||||
|
const element = this.wrap(allowedTag, text)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML thematic break (<hr>) to the summary buffer
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addSeparator(): Summary {
|
||||||
|
const element = this.wrap('hr', null)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML line break (<br>) to the summary buffer
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addBreak(): Summary {
|
||||||
|
const element = this.wrap('br', null)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML blockquote to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} text quote text
|
||||||
|
* @param {string} cite (optional) citation url
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addQuote(text: string, cite?: string): Summary {
|
||||||
|
const attrs = {
|
||||||
|
...(cite && {cite})
|
||||||
|
}
|
||||||
|
const element = this.wrap('blockquote', text, attrs)
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an HTML anchor tag to the summary buffer
|
||||||
|
*
|
||||||
|
* @param {string} text link text/content
|
||||||
|
* @param {string} href hyperlink
|
||||||
|
*
|
||||||
|
* @returns {Summary} summary instance
|
||||||
|
*/
|
||||||
|
addLink(text: string, href: string): Summary {
|
||||||
|
const element = this.wrap('a', text, {href})
|
||||||
|
return this.addRaw(element).addEOL()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _summary = new Summary()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use `core.summary`
|
||||||
|
*/
|
||||||
|
export const markdownSummary = _summary
|
||||||
|
export const summary = _summary
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
// We use any as a valid input type
|
// We use any as a valid input type
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
import {AnnotationProperties} from './core'
|
||||||
|
import {CommandProperties} from './command'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||||
* @param input input to sanitize into a string
|
* @param input input to sanitize into a string
|
||||||
@@ -13,3 +16,26 @@ export function toCommandValue(input: any): string {
|
|||||||
}
|
}
|
||||||
return JSON.stringify(input)
|
return JSON.stringify(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param annotationProperties
|
||||||
|
* @returns The command properties to send with the actual annotation command
|
||||||
|
* See IssueCommandProperties: https://github.com/actions/runner/blob/main/src/Runner.Worker/ActionCommandManager.cs#L646
|
||||||
|
*/
|
||||||
|
export function toCommandProperties(
|
||||||
|
annotationProperties: AnnotationProperties
|
||||||
|
): CommandProperties {
|
||||||
|
if (!Object.keys(annotationProperties).length) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: annotationProperties.title,
|
||||||
|
file: annotationProperties.file,
|
||||||
|
line: annotationProperties.startLine,
|
||||||
|
endLine: annotationProperties.endLine,
|
||||||
|
col: annotationProperties.startColumn,
|
||||||
|
endColumn: annotationProperties.endColumn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# @actions/exec Releases
|
# @actions/exec Releases
|
||||||
|
|
||||||
|
### 1.1.1
|
||||||
|
- Update `lockfileVersion` to `v2` in `package-lock.json [#1024](https://github.com/actions/toolkit/pull/1024)
|
||||||
|
|
||||||
|
### 1.1.0
|
||||||
|
|
||||||
|
- [Fix stdline dropping large output](https://github.com/actions/toolkit/pull/773)
|
||||||
|
- [Add getExecOutput function](https://github.com/actions/toolkit/pull/814)
|
||||||
|
- [Better error for bad cwd](https://github.com/actions/toolkit/pull/793)
|
||||||
|
|
||||||
### 1.0.3
|
### 1.0.3
|
||||||
|
|
||||||
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
|
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
|
||||||
|
|||||||
@@ -286,6 +286,31 @@ describe('@actions/exec', () => {
|
|||||||
expect(stderrCalled).toBeTruthy()
|
expect(stderrCalled).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Handles large stdline', async () => {
|
||||||
|
const stdlinePath: string = path.join(
|
||||||
|
__dirname,
|
||||||
|
'scripts',
|
||||||
|
'stdlineoutput.js'
|
||||||
|
)
|
||||||
|
const nodePath: string = await io.which('node', true)
|
||||||
|
|
||||||
|
const _testExecOptions = getExecOptions()
|
||||||
|
let largeLine = ''
|
||||||
|
_testExecOptions.listeners = {
|
||||||
|
stdline: (line: string) => {
|
||||||
|
largeLine = line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitCode = await exec.exec(
|
||||||
|
`"${nodePath}"`,
|
||||||
|
[stdlinePath],
|
||||||
|
_testExecOptions
|
||||||
|
)
|
||||||
|
expect(exitCode).toBe(0)
|
||||||
|
expect(Buffer.byteLength(largeLine)).toEqual(2 ** 16 + 1)
|
||||||
|
})
|
||||||
|
|
||||||
it('Handles stdin shell', async () => {
|
it('Handles stdin shell', async () => {
|
||||||
let command: string
|
let command: string
|
||||||
if (IS_WINDOWS) {
|
if (IS_WINDOWS) {
|
||||||
@@ -538,6 +563,22 @@ describe('@actions/exec', () => {
|
|||||||
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
|
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Exec roots throws friendly error when bad cwd is specified', async () => {
|
||||||
|
const execOptions = getExecOptions()
|
||||||
|
execOptions.cwd = 'nonexistent/path'
|
||||||
|
|
||||||
|
await expect(exec.exec('ls', ['-all'], execOptions)).rejects.toThrowError(
|
||||||
|
`The cwd: ${execOptions.cwd} does not exist!`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Exec roots does not throw when valid cwd is provided', async () => {
|
||||||
|
const execOptions = getExecOptions()
|
||||||
|
execOptions.cwd = './'
|
||||||
|
|
||||||
|
await expect(exec.exec('ls', ['-all'], execOptions)).resolves.toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
it('Exec roots relative tool path using rooted options.cwd', async () => {
|
it('Exec roots relative tool path using rooted options.cwd', async () => {
|
||||||
let command: string
|
let command: string
|
||||||
if (IS_WINDOWS) {
|
if (IS_WINDOWS) {
|
||||||
@@ -620,6 +661,165 @@ describe('@actions/exec', () => {
|
|||||||
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
|
expect(output.trim()).toBe(`args[0]: "hello"${os.EOL}args[1]: "world"`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('correctly outputs for getExecOutput', async () => {
|
||||||
|
const stdErrPath: string = path.join(
|
||||||
|
__dirname,
|
||||||
|
'scripts',
|
||||||
|
'stderroutput.js'
|
||||||
|
)
|
||||||
|
const stdOutPath: string = path.join(
|
||||||
|
__dirname,
|
||||||
|
'scripts',
|
||||||
|
'stdoutoutput.js'
|
||||||
|
)
|
||||||
|
const nodePath: string = await io.which('node', true)
|
||||||
|
|
||||||
|
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||||
|
`"${nodePath}"`,
|
||||||
|
[stdOutPath],
|
||||||
|
getExecOptions()
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(exitCodeOut).toBe(0)
|
||||||
|
expect(stdout).toBe('this is output to stdout')
|
||||||
|
|
||||||
|
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
|
||||||
|
`"${nodePath}"`,
|
||||||
|
[stdErrPath],
|
||||||
|
getExecOptions()
|
||||||
|
)
|
||||||
|
expect(exitCodeErr).toBe(0)
|
||||||
|
expect(stderr).toBe('this is output to stderr')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('correctly outputs for getExecOutput with additional listeners', async () => {
|
||||||
|
const stdErrPath: string = path.join(
|
||||||
|
__dirname,
|
||||||
|
'scripts',
|
||||||
|
'stderroutput.js'
|
||||||
|
)
|
||||||
|
const stdOutPath: string = path.join(
|
||||||
|
__dirname,
|
||||||
|
'scripts',
|
||||||
|
'stdoutoutput.js'
|
||||||
|
)
|
||||||
|
|
||||||
|
const nodePath: string = await io.which('node', true)
|
||||||
|
let listenerOut = ''
|
||||||
|
|
||||||
|
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||||
|
`"${nodePath}"`,
|
||||||
|
[stdOutPath],
|
||||||
|
{
|
||||||
|
...getExecOptions(),
|
||||||
|
listeners: {
|
||||||
|
stdout: data => {
|
||||||
|
listenerOut = data.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(exitCodeOut).toBe(0)
|
||||||
|
expect(stdout).toBe('this is output to stdout')
|
||||||
|
expect(listenerOut).toBe('this is output to stdout')
|
||||||
|
|
||||||
|
let listenerErr = ''
|
||||||
|
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
|
||||||
|
`"${nodePath}"`,
|
||||||
|
[stdErrPath],
|
||||||
|
{
|
||||||
|
...getExecOptions(),
|
||||||
|
listeners: {
|
||||||
|
stderr: data => {
|
||||||
|
listenerErr = data.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(exitCodeErr).toBe(0)
|
||||||
|
expect(stderr).toBe('this is output to stderr')
|
||||||
|
expect(listenerErr).toBe('this is output to stderr')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('correctly outputs for getExecOutput when total size exceeds buffer size', async () => {
|
||||||
|
const stdErrPath: string = path.join(
|
||||||
|
__dirname,
|
||||||
|
'scripts',
|
||||||
|
'stderroutput.js'
|
||||||
|
)
|
||||||
|
const stdOutPath: string = path.join(
|
||||||
|
__dirname,
|
||||||
|
'scripts',
|
||||||
|
'stdoutoutputlarge.js'
|
||||||
|
)
|
||||||
|
|
||||||
|
const nodePath: string = await io.which('node', true)
|
||||||
|
let listenerOut = ''
|
||||||
|
|
||||||
|
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||||
|
`"${nodePath}"`,
|
||||||
|
[stdOutPath],
|
||||||
|
{
|
||||||
|
...getExecOptions(),
|
||||||
|
listeners: {
|
||||||
|
stdout: data => {
|
||||||
|
listenerOut += data.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(exitCodeOut).toBe(0)
|
||||||
|
expect(Buffer.byteLength(stdout || '', 'utf8')).toBe(2 ** 25)
|
||||||
|
expect(Buffer.byteLength(listenerOut, 'utf8')).toBe(2 ** 25)
|
||||||
|
|
||||||
|
let listenerErr = ''
|
||||||
|
const {exitCode: exitCodeErr, stderr} = await exec.getExecOutput(
|
||||||
|
`"${nodePath}"`,
|
||||||
|
[stdErrPath],
|
||||||
|
{
|
||||||
|
...getExecOptions(),
|
||||||
|
listeners: {
|
||||||
|
stderr: data => {
|
||||||
|
listenerErr = data.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
expect(exitCodeErr).toBe(0)
|
||||||
|
expect(stderr).toBe('this is output to stderr')
|
||||||
|
expect(listenerErr).toBe('this is output to stderr')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('correctly outputs for getExecOutput with multi-byte characters', async () => {
|
||||||
|
const stdOutPath: string = path.join(
|
||||||
|
__dirname,
|
||||||
|
'scripts',
|
||||||
|
'stdoutputspecial.js'
|
||||||
|
)
|
||||||
|
|
||||||
|
const nodePath: string = await io.which('node', true)
|
||||||
|
let numStdOutBufferCalls = 0
|
||||||
|
const {exitCode: exitCodeOut, stdout} = await exec.getExecOutput(
|
||||||
|
`"${nodePath}"`,
|
||||||
|
[stdOutPath],
|
||||||
|
{
|
||||||
|
...getExecOptions(),
|
||||||
|
listeners: {
|
||||||
|
stdout: () => {
|
||||||
|
numStdOutBufferCalls += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(exitCodeOut).toBe(0)
|
||||||
|
//one call for each half of the © character, ensuring it was actually split and not sent together
|
||||||
|
expect(numStdOutBufferCalls).toBe(2)
|
||||||
|
expect(stdout).toBe('©')
|
||||||
|
})
|
||||||
|
|
||||||
if (IS_WINDOWS) {
|
if (IS_WINDOWS) {
|
||||||
it('Exec roots relative tool path using process.cwd (Windows path separator)', async () => {
|
it('Exec roots relative tool path using process.cwd (Windows path separator)', async () => {
|
||||||
let exitCode: number
|
let exitCode: number
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
//Default highWaterMark for readable stream buffers us 64K (2^16)
|
||||||
|
//so we go over that to get more than a buffer's worth
|
||||||
|
const os = require('os')
|
||||||
|
|
||||||
|
process.stdout.write('a'.repeat(2**16 + 1) + os.EOL);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
//Default highWaterMark for readable stream buffers us 64K (2^16)
|
||||||
|
//so we go over that to get more than a buffer's worth
|
||||||
|
process.stdout.write('a\n'.repeat(2**24));
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//first half of © character
|
||||||
|
process.stdout.write(Buffer.from([0xC2]), (err) => {
|
||||||
|
//write in the callback so that the second byte is sent separately
|
||||||
|
setTimeout(() => {
|
||||||
|
process.stdout.write(Buffer.from([0xA9])) //second half of © character
|
||||||
|
}, 5000)
|
||||||
|
|
||||||
|
})
|
||||||
Generated
+17
-2
@@ -1,8 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/exec",
|
"name": "@actions/exec",
|
||||||
"version": "1.0.4",
|
"version": "1.1.1",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@actions/exec",
|
||||||
|
"version": "1.1.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/io": "^1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@actions/io": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-rhq+tfZukbtaus7xyUtwKfuiCRXd1hWSfmJNEpFgBQJ4woqPEpsBw04awicjwz9tyG2/MVhAEMfVn664Cri5zA=="
|
||||||
|
}
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/io": {
|
"@actions/io": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/exec",
|
"name": "@actions/exec",
|
||||||
"version": "1.0.4",
|
"version": "1.1.1",
|
||||||
"description": "Actions exec lib",
|
"description": "Actions exec lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"github",
|
"github",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import {ExecOptions} from './interfaces'
|
import {StringDecoder} from 'string_decoder'
|
||||||
|
import {ExecOptions, ExecOutput, ExecListeners} from './interfaces'
|
||||||
import * as tr from './toolrunner'
|
import * as tr from './toolrunner'
|
||||||
|
|
||||||
export {ExecOptions}
|
export {ExecOptions, ExecOutput, ExecListeners}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exec a command.
|
* Exec a command.
|
||||||
@@ -28,3 +29,62 @@ export async function exec(
|
|||||||
const runner: tr.ToolRunner = new tr.ToolRunner(toolPath, args, options)
|
const runner: tr.ToolRunner = new tr.ToolRunner(toolPath, args, options)
|
||||||
return runner.exec()
|
return runner.exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exec a command and get the output.
|
||||||
|
* Output will be streamed to the live console.
|
||||||
|
* Returns promise with the exit code and collected stdout and stderr
|
||||||
|
*
|
||||||
|
* @param commandLine command to execute (can include additional args). Must be correctly escaped.
|
||||||
|
* @param args optional arguments for tool. Escaping is handled by the lib.
|
||||||
|
* @param options optional exec options. See ExecOptions
|
||||||
|
* @returns Promise<ExecOutput> exit code, stdout, and stderr
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function getExecOutput(
|
||||||
|
commandLine: string,
|
||||||
|
args?: string[],
|
||||||
|
options?: ExecOptions
|
||||||
|
): Promise<ExecOutput> {
|
||||||
|
let stdout = ''
|
||||||
|
let stderr = ''
|
||||||
|
|
||||||
|
//Using string decoder covers the case where a mult-byte character is split
|
||||||
|
const stdoutDecoder = new StringDecoder('utf8')
|
||||||
|
const stderrDecoder = new StringDecoder('utf8')
|
||||||
|
|
||||||
|
const originalStdoutListener = options?.listeners?.stdout
|
||||||
|
const originalStdErrListener = options?.listeners?.stderr
|
||||||
|
|
||||||
|
const stdErrListener = (data: Buffer): void => {
|
||||||
|
stderr += stderrDecoder.write(data)
|
||||||
|
if (originalStdErrListener) {
|
||||||
|
originalStdErrListener(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stdOutListener = (data: Buffer): void => {
|
||||||
|
stdout += stdoutDecoder.write(data)
|
||||||
|
if (originalStdoutListener) {
|
||||||
|
originalStdoutListener(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners: ExecListeners = {
|
||||||
|
...options?.listeners,
|
||||||
|
stdout: stdOutListener,
|
||||||
|
stderr: stdErrListener
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitCode = await exec(commandLine, args, {...options, listeners})
|
||||||
|
|
||||||
|
//flush any remaining characters
|
||||||
|
stdout += stdoutDecoder.end()
|
||||||
|
stderr += stderrDecoder.end()
|
||||||
|
|
||||||
|
return {
|
||||||
|
exitCode,
|
||||||
|
stdout,
|
||||||
|
stderr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,15 +34,39 @@ export interface ExecOptions {
|
|||||||
input?: Buffer
|
input?: Buffer
|
||||||
|
|
||||||
/** optional. Listeners for output. Callback functions that will be called on these events */
|
/** optional. Listeners for output. Callback functions that will be called on these events */
|
||||||
listeners?: {
|
listeners?: ExecListeners
|
||||||
stdout?: (data: Buffer) => void
|
}
|
||||||
|
|
||||||
stderr?: (data: Buffer) => void
|
/**
|
||||||
|
* Interface for the output of getExecOutput()
|
||||||
stdline?: (data: string) => void
|
*/
|
||||||
|
export interface ExecOutput {
|
||||||
errline?: (data: string) => void
|
/**The exit code of the process */
|
||||||
|
exitCode: number
|
||||||
debug?: (data: string) => void
|
|
||||||
}
|
/**The entire stdout of the process as a string */
|
||||||
|
stdout: string
|
||||||
|
|
||||||
|
/**The entire stderr of the process as a string */
|
||||||
|
stderr: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user defined listeners for an exec call
|
||||||
|
*/
|
||||||
|
export interface ExecListeners {
|
||||||
|
/** A call back for each buffer of stdout */
|
||||||
|
stdout?: (data: Buffer) => void
|
||||||
|
|
||||||
|
/** A call back for each buffer of stderr */
|
||||||
|
stderr?: (data: Buffer) => void
|
||||||
|
|
||||||
|
/** A call back for each line of stdout */
|
||||||
|
stdline?: (data: string) => void
|
||||||
|
|
||||||
|
/** A call back for each line of stderr */
|
||||||
|
errline?: (data: string) => void
|
||||||
|
|
||||||
|
/** A call back for each debug log */
|
||||||
|
debug?: (data: string) => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export class ToolRunner extends events.EventEmitter {
|
|||||||
data: Buffer,
|
data: Buffer,
|
||||||
strBuffer: string,
|
strBuffer: string,
|
||||||
onLine: (line: string) => void
|
onLine: (line: string) => void
|
||||||
): void {
|
): string {
|
||||||
try {
|
try {
|
||||||
let s = strBuffer + data.toString()
|
let s = strBuffer + data.toString()
|
||||||
let n = s.indexOf(os.EOL)
|
let n = s.indexOf(os.EOL)
|
||||||
@@ -98,10 +98,12 @@ export class ToolRunner extends events.EventEmitter {
|
|||||||
n = s.indexOf(os.EOL)
|
n = s.indexOf(os.EOL)
|
||||||
}
|
}
|
||||||
|
|
||||||
strBuffer = s
|
return s
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// streaming lines to console is best effort. Don't fail a build.
|
// streaming lines to console is best effort. Don't fail a build.
|
||||||
this._debug(`error processing line. Failed with error ${err}`)
|
this._debug(`error processing line. Failed with error ${err}`)
|
||||||
|
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +416,7 @@ export class ToolRunner extends events.EventEmitter {
|
|||||||
// otherwise verify it exists (add extension on Windows if necessary)
|
// otherwise verify it exists (add extension on Windows if necessary)
|
||||||
this.toolPath = await io.which(this.toolPath, true)
|
this.toolPath = await io.which(this.toolPath, true)
|
||||||
|
|
||||||
return new Promise<number>((resolve, reject) => {
|
return new Promise<number>(async (resolve, reject) => {
|
||||||
this._debug(`exec tool: ${this.toolPath}`)
|
this._debug(`exec tool: ${this.toolPath}`)
|
||||||
this._debug('arguments:')
|
this._debug('arguments:')
|
||||||
for (const arg of this.args) {
|
for (const arg of this.args) {
|
||||||
@@ -433,6 +435,10 @@ export class ToolRunner extends events.EventEmitter {
|
|||||||
this._debug(message)
|
this._debug(message)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (this.options.cwd && !(await ioUtil.exists(this.options.cwd))) {
|
||||||
|
return reject(new Error(`The cwd: ${this.options.cwd} does not exist!`))
|
||||||
|
}
|
||||||
|
|
||||||
const fileName = this._getSpawnFileName()
|
const fileName = this._getSpawnFileName()
|
||||||
const cp = child.spawn(
|
const cp = child.spawn(
|
||||||
fileName,
|
fileName,
|
||||||
@@ -440,7 +446,7 @@ export class ToolRunner extends events.EventEmitter {
|
|||||||
this._getSpawnOptions(this.options, fileName)
|
this._getSpawnOptions(this.options, fileName)
|
||||||
)
|
)
|
||||||
|
|
||||||
const stdbuffer = ''
|
let stdbuffer = ''
|
||||||
if (cp.stdout) {
|
if (cp.stdout) {
|
||||||
cp.stdout.on('data', (data: Buffer) => {
|
cp.stdout.on('data', (data: Buffer) => {
|
||||||
if (this.options.listeners && this.options.listeners.stdout) {
|
if (this.options.listeners && this.options.listeners.stdout) {
|
||||||
@@ -451,15 +457,19 @@ export class ToolRunner extends events.EventEmitter {
|
|||||||
optionsNonNull.outStream.write(data)
|
optionsNonNull.outStream.write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
this._processLineBuffer(data, stdbuffer, (line: string) => {
|
stdbuffer = this._processLineBuffer(
|
||||||
if (this.options.listeners && this.options.listeners.stdline) {
|
data,
|
||||||
this.options.listeners.stdline(line)
|
stdbuffer,
|
||||||
|
(line: string) => {
|
||||||
|
if (this.options.listeners && this.options.listeners.stdline) {
|
||||||
|
this.options.listeners.stdline(line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const errbuffer = ''
|
let errbuffer = ''
|
||||||
if (cp.stderr) {
|
if (cp.stderr) {
|
||||||
cp.stderr.on('data', (data: Buffer) => {
|
cp.stderr.on('data', (data: Buffer) => {
|
||||||
state.processStderr = true
|
state.processStderr = true
|
||||||
@@ -478,11 +488,15 @@ export class ToolRunner extends events.EventEmitter {
|
|||||||
s.write(data)
|
s.write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
this._processLineBuffer(data, errbuffer, (line: string) => {
|
errbuffer = this._processLineBuffer(
|
||||||
if (this.options.listeners && this.options.listeners.errline) {
|
data,
|
||||||
this.options.listeners.errline(line)
|
errbuffer,
|
||||||
|
(line: string) => {
|
||||||
|
if (this.options.listeners && this.options.listeners.errline) {
|
||||||
|
this.options.listeners.errline(line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,13 +629,13 @@ class ExecState extends events.EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processClosed: boolean = false // tracks whether the process has exited and stdio is closed
|
processClosed = false // tracks whether the process has exited and stdio is closed
|
||||||
processError: string = ''
|
processError = ''
|
||||||
processExitCode: number = 0
|
processExitCode = 0
|
||||||
processExited: boolean = false // tracks whether the process has exited
|
processExited = false // tracks whether the process has exited
|
||||||
processStderr: boolean = false // tracks whether stderr was written to
|
processStderr = false // tracks whether stderr was written to
|
||||||
private delay = 10000 // 10 seconds
|
private delay = 10000 // 10 seconds
|
||||||
private done: boolean = false
|
private done = false
|
||||||
private options: im.ExecOptions
|
private options: im.ExecOptions
|
||||||
private timeout: NodeJS.Timer | null = null
|
private timeout: NodeJS.Timer | null = null
|
||||||
private toolPath: string
|
private toolPath: string
|
||||||
|
|||||||
@@ -59,18 +59,19 @@ const newIssue = await octokit.rest.issues.create({
|
|||||||
|
|
||||||
## Webhook payload typescript definitions
|
## Webhook payload typescript definitions
|
||||||
|
|
||||||
The npm module `@octokit/webhooks` provides type definitions for the response payloads. You can cast the payload to these types for better type information.
|
The npm module `@octokit/webhooks-definitions` provides type definitions for the response payloads. You can cast the payload to these types for better type information.
|
||||||
|
|
||||||
First, install the npm module `npm install @octokit/webhooks`
|
First, install the npm module `npm install @octokit/webhooks-definitions`
|
||||||
|
|
||||||
Then, assert the type based on the eventName
|
Then, assert the type based on the eventName
|
||||||
```ts
|
```ts
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as github from '@actions/github'
|
import * as github from '@actions/github'
|
||||||
import * as Webhooks from '@octokit/webhooks'
|
import {PushEvent} from '@octokit/webhooks-definitions/schema'
|
||||||
|
|
||||||
if (github.context.eventName === 'push') {
|
if (github.context.eventName === 'push') {
|
||||||
const pushPayload = github.context.payload as Webhooks.WebhookPayloadPush
|
const pushPayload = github.context.payload as PushEvent
|
||||||
core.info(`The head commit is: ${pushPayload.head}`)
|
core.info(`The head commit is: ${pushPayload.head_commit}`)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,24 @@
|
|||||||
# @actions/github Releases
|
# @actions/github Releases
|
||||||
|
|
||||||
|
### 5.1.1
|
||||||
|
- Export default octokit options [#1188](https://github.com/actions/toolkit/pull/1188)
|
||||||
|
|
||||||
|
### 5.1.0
|
||||||
|
- Add additionalPlugins parameter to getOctokit method [#1181](https://github.com/actions/toolkit/pull/1181)
|
||||||
|
- Dependency updates [#1180](https://github.com/actions/toolkit/pull/1180)
|
||||||
|
|
||||||
|
|
||||||
|
### 5.0.3
|
||||||
|
- - Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087)
|
||||||
|
|
||||||
|
### 5.0.2
|
||||||
|
- Update to v2.0.0 of `@actions/http-client`
|
||||||
|
|
||||||
|
### 5.0.1
|
||||||
|
- [Update Octokit Dependencies](https://github.com/actions/toolkit/pull/1037)
|
||||||
### 5.0.0
|
### 5.0.0
|
||||||
- [Update @actions/github to include latest octokit definitions](https://github.com/actions/toolkit/pull/783)
|
- [Update @actions/github to include latest octokit definitions](https://github.com/actions/toolkit/pull/783)
|
||||||
|
- [Add urls to context](https://github.com/actions/toolkit/pull/794)
|
||||||
|
|
||||||
### 4.0.0
|
### 4.0.0
|
||||||
- [Add execution state information to context](https://github.com/actions/toolkit/pull/499)
|
- [Add execution state information to context](https://github.com/actions/toolkit/pull/499)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe('@actions/github', () => {
|
|||||||
proxyServer.listen(port, () => resolve())
|
proxyServer.listen(port, () => resolve())
|
||||||
})
|
})
|
||||||
proxyServer.on('connect', req => {
|
proxyServer.on('connect', req => {
|
||||||
proxyConnects.push(req.url)
|
proxyConnects.push(req.url ?? '')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe('@actions/github', () => {
|
|||||||
proxyServer.listen(port, () => resolve(null))
|
proxyServer.listen(port, () => resolve(null))
|
||||||
})
|
})
|
||||||
proxyServer.on('connect', req => {
|
proxyServer.on('connect', req => {
|
||||||
proxyConnects.push(req.url)
|
proxyConnects.push(req.url ?? '')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import * as path from 'path'
|
|||||||
import {Context} from '../src/context'
|
import {Context} from '../src/context'
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
|
||||||
describe('@actions/context', () => {
|
describe('@actions/context', () => {
|
||||||
let context: Context
|
let context: Context
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ module.exports = {
|
|||||||
moduleFileExtensions: ['js', 'ts'],
|
moduleFileExtensions: ['js', 'ts'],
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
testMatch: ['**/*.test.ts'],
|
testMatch: ['**/*.test.ts'],
|
||||||
testRunner: 'jest-circus/runner',
|
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.ts$': 'ts-jest'
|
'^.+\\.ts$': 'ts-jest'
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+490
-4655
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/github",
|
"name": "@actions/github",
|
||||||
"version": "5.0.0",
|
"version": "5.1.1",
|
||||||
"description": "Actions github lib",
|
"description": "Actions github lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"github",
|
"github",
|
||||||
@@ -38,13 +38,12 @@
|
|||||||
"url": "https://github.com/actions/toolkit/issues"
|
"url": "https://github.com/actions/toolkit/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/http-client": "^1.0.11",
|
"@actions/http-client": "^2.0.1",
|
||||||
"@octokit/core": "^3.4.0",
|
"@octokit/core": "^3.6.0",
|
||||||
"@octokit/plugin-paginate-rest": "^2.13.3",
|
"@octokit/plugin-paginate-rest": "^2.17.0",
|
||||||
"@octokit/plugin-rest-endpoint-methods": "^5.1.1"
|
"@octokit/plugin-rest-endpoint-methods": "^5.13.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^25.1.0",
|
"proxy": "^1.0.2"
|
||||||
"proxy": "^1.0.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user