Compare commits
211
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71bc34b2d5 | ||
|
|
c25ba9aa99 | ||
|
|
0d63834474 | ||
|
|
8f032d304a | ||
|
|
28b09e224f | ||
|
|
111c95866e | ||
|
|
ddc9c52eb6 | ||
|
|
9f29c14937 | ||
|
|
6d37c6eb2b | ||
|
|
6477ef1460 | ||
|
|
2e5b10e3bd | ||
|
|
8c1e6a00f0 | ||
|
|
b2d5fa216f | ||
|
|
c5c786523e | ||
|
|
63c648f3c2 | ||
|
|
ce31408ff5 | ||
|
|
e26febd988 | ||
|
|
b051b4bada | ||
|
|
a08d666c78 | ||
|
|
83bb7cdeef | ||
|
|
b552972717 | ||
|
|
e3b0601228 | ||
|
|
0956e634df | ||
|
|
c171cf52fb | ||
|
|
2f1b34f165 | ||
|
|
b61854c5ca | ||
|
|
3d652d3133 | ||
|
|
c3df0928e2 | ||
|
|
9d756b2bc9 | ||
|
|
67c3b7a45c | ||
|
|
3963c722d8 | ||
|
|
3b44a4cc23 | ||
|
|
03a876f0a7 | ||
|
|
62f943c0cc | ||
|
|
291200d54f | ||
|
|
06e751600e | ||
|
|
4b6a4d80e1 | ||
|
|
b2da9aa12c | ||
|
|
88f749f686 | ||
|
|
b4f8e602b2 | ||
|
|
ced07aa89c | ||
|
|
6adf053d36 | ||
|
|
671bf1ebd5 | ||
|
|
dd26bb1149 | ||
|
|
81a802e7e0 | ||
|
|
4214a1ff24 | ||
|
|
0555a5f458 | ||
|
|
3aaff6685b | ||
|
|
9b383229c1 | ||
|
|
7b617c260d | ||
|
|
20afb1a9fc | ||
|
|
c9dab8c79d | ||
|
|
45c49b09df | ||
|
|
ab78839e86 | ||
|
|
f03b6d639f | ||
|
|
58858b5078 | ||
|
|
188abfc20b | ||
|
|
2f42c127c7 | ||
|
|
4dda3ab8a0 | ||
|
|
4b219f79f3 | ||
|
|
08d6314f7c | ||
|
|
b851b70474 | ||
|
|
e8fb71c4bb | ||
|
|
73ad88882e | ||
|
|
92695f58da | ||
|
|
760f3fd3d1 | ||
|
|
c6117995d3 | ||
|
|
24da3e2d1c | ||
|
|
deda97d5e6 | ||
|
|
cfad1451e9 | ||
|
|
c0684c5add | ||
|
|
ad9b955fe9 | ||
|
|
1718e0d97c | ||
|
|
e85cd96d85 | ||
|
|
bc24adbfd6 | ||
|
|
af1621025d | ||
|
|
6552cb9722 | ||
|
|
f74ff155bd | ||
|
|
a66e49ec8a | ||
|
|
d4c2fa4c68 | ||
|
|
8c9ab93da7 | ||
|
|
3773ef22b1 | ||
|
|
80e4680ac8 | ||
|
|
c608703ecf | ||
|
|
66ac937f2f | ||
|
|
efcab31d38 | ||
|
|
4c6d88f93a | ||
|
|
19e0016878 | ||
|
|
2820b17d9d | ||
|
|
8a5343d54a | ||
|
|
3ebee1e8b4 | ||
|
|
7da3ac6eda | ||
|
|
2461056696 | ||
|
|
3749c51d21 | ||
|
|
769c896931 | ||
|
|
c4f5ce2665 | ||
|
|
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 | ||
|
|
2afea665ed | ||
|
|
80d992795c | ||
|
|
b9de68a590 | ||
|
|
1d61e5fb19 | ||
|
|
4abb5a2ae0 | ||
|
|
6b18932b86 | ||
|
|
56c460630a | ||
|
|
e1a991ffb7 | ||
|
|
cc9ec0424e | ||
|
|
c91bdbadbf | ||
|
|
23811ac52f | ||
|
|
819157bf87 | ||
|
|
86102e88e9 | ||
|
|
e4c071ba19 | ||
|
|
9443e26349 | ||
|
|
5a0405df4e | ||
|
|
0e707aeabc | ||
|
|
7441dc5e59 | ||
|
|
1ddac5e02f | ||
|
|
55484166d8 | ||
|
|
7181b913f5 | ||
|
|
81cd5a5c2e | ||
|
|
4b348086a9 | ||
|
|
436cf8d6ea | ||
|
|
e0aadb573c | ||
|
|
62a66a8ce9 | ||
|
|
0c58e4113e | ||
|
|
9366237c90 |
@@ -2,3 +2,4 @@ node_modules/
|
|||||||
packages/*/node_modules/
|
packages/*/node_modules/
|
||||||
packages/*/lib/
|
packages/*/lib/
|
||||||
packages/glob/__tests__/_temp
|
packages/glob/__tests__/_temp
|
||||||
|
packages/*/src/generated/*/
|
||||||
|
|||||||
+44
-7
@@ -1,6 +1,13 @@
|
|||||||
{
|
{
|
||||||
"plugins": ["jest", "@typescript-eslint"],
|
"plugins": [
|
||||||
"extends": ["plugin:github/recommended"],
|
"jest",
|
||||||
|
"@typescript-eslint",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:github/recommended",
|
||||||
|
"plugin:prettier/recommended"
|
||||||
|
],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": 9,
|
"ecmaVersion": 9,
|
||||||
@@ -8,14 +15,33 @@
|
|||||||
"project": "./tsconfig.eslint.json"
|
"project": "./tsconfig.eslint.json"
|
||||||
},
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"prettier/prettier": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"endOfLine": "auto"
|
||||||
|
}
|
||||||
|
],
|
||||||
"eslint-comments/no-use": "off",
|
"eslint-comments/no-use": "off",
|
||||||
|
"no-constant-condition": ["error", { "checkLoops": false }],
|
||||||
"github/no-then": "off",
|
"github/no-then": "off",
|
||||||
"import/no-namespace": "off",
|
"import/no-namespace": "off",
|
||||||
"no-shadow": "off",
|
"no-shadow": "off",
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
|
"i18n-text/no-en": "off",
|
||||||
|
"filenames/match-regex": "off",
|
||||||
|
"import/no-commonjs": "off",
|
||||||
|
"import/named": "off",
|
||||||
|
"no-sequences": "off",
|
||||||
|
"import/no-unresolved": "off",
|
||||||
"no-undef": "off",
|
"no-undef": "off",
|
||||||
|
"no-only-tests/no-only-tests": "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",
|
||||||
@@ -23,8 +49,16 @@
|
|||||||
"camelcase": "off",
|
"camelcase": "off",
|
||||||
"@typescript-eslint/camelcase": "off",
|
"@typescript-eslint/camelcase": "off",
|
||||||
"@typescript-eslint/consistent-type-assertions": "off",
|
"@typescript-eslint/consistent-type-assertions": "off",
|
||||||
"@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}],
|
"@typescript-eslint/explicit-function-return-type": [
|
||||||
"@typescript-eslint/func-call-spacing": ["error", "never"],
|
"error",
|
||||||
|
{
|
||||||
|
"allowExpressions": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/func-call-spacing": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
"@typescript-eslint/naming-convention": [
|
"@typescript-eslint/naming-convention": [
|
||||||
"error",
|
"error",
|
||||||
{
|
{
|
||||||
@@ -56,12 +90,15 @@
|
|||||||
"@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",
|
||||||
"@typescript-eslint/restrict-plus-operands": "error",
|
|
||||||
"semi": "off",
|
"semi": "off",
|
||||||
"@typescript-eslint/semi": ["error", "never"],
|
"@typescript-eslint/semi": [
|
||||||
|
"error",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
"@typescript-eslint/type-annotation-spacing": "error",
|
"@typescript-eslint/type-annotation-spacing": "error",
|
||||||
"@typescript-eslint/unbound-method": "error"
|
"@typescript-eslint/unbound-method": "error"
|
||||||
},
|
},
|
||||||
|
"ignorePatterns": "packages/glob/__tests__/_temp/**/",
|
||||||
"env": {
|
"env": {
|
||||||
"node": true,
|
"node": true,
|
||||||
"es6": true,
|
"es6": true,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Note that before a PR will be accepted, you must ensure:
|
|||||||
|
|
||||||
### Useful Scripts
|
### Useful Scripts
|
||||||
|
|
||||||
- `npm run bootstrap` This runs `lerna bootstrap` which will install dependencies in this repository's packages and cross-link packages where necessary.
|
- `npm run bootstrap` This runs `lerna exec -- npm install` which will install dependencies in this repository's packages and cross-link packages where necessary.
|
||||||
- `npm run build` This compiles TypeScript code in each package (this is especially important if one package relies on changes in another when you're running tests). This is just an alias for `lerna run tsc`.
|
- `npm run build` This compiles TypeScript code in each package (this is especially important if one package relies on changes in another when you're running tests). This is just an alias for `lerna run tsc`.
|
||||||
- `npm run format` This checks that formatting has been applied with Prettier.
|
- `npm run format` This checks that formatting has been applied with Prettier.
|
||||||
- `npm test` This runs all Jest tests in all packages in this repository.
|
- `npm test` This runs all Jest tests in all packages in this repository.
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
# Temporarily disabled while v2.0.0 of @actions/artifact is under development
|
||||||
|
|
||||||
name: artifact-unit-tests
|
name: artifact-unit-tests
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -22,17 +24,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 20.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 20.x
|
||||||
|
|
||||||
# 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
|
|
||||||
- name: Set env variables
|
|
||||||
uses: ./packages/artifact/__tests__/ci-test-action/
|
|
||||||
|
|
||||||
# Need root node_modules because certain npm packages like jest are configured for the entire repository and it won't be possible
|
# 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 artifacts package
|
# without these to just compile the artifacts package
|
||||||
@@ -48,48 +45,91 @@ jobs:
|
|||||||
- 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 "file1=hello from file 1" >> $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 "file2=hello from file 2" >> $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.file1 }}' > artifact-path/first.txt
|
||||||
echo ${{ env.gzip-artifact-content }} > artifact-path/gzip.txt
|
echo '${{ env.file2 }}' > artifact-path/second.txt
|
||||||
touch artifact-path/empty.txt
|
|
||||||
|
|
||||||
# We're using node -e to call the functions directly available in the @actions/artifact package
|
- name: Upload Artifacts using actions/github-script@v6
|
||||||
- name: Upload artifacts using uploadArtifact()
|
uses: actions/github-script@v6
|
||||||
run: |
|
with:
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-1',['artifact-path/world.txt'], '${{ github.workspace }}'))"
|
script: |
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-2',['artifact-path/gzip.txt'], '${{ github.workspace }}'))"
|
const artifact = require('./packages/artifact/lib/artifact')
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().uploadArtifact('my-artifact-3',['artifact-path/empty.txt'], '${{ github.workspace }}'))"
|
|
||||||
|
|
||||||
- name: Download artifacts using downloadArtifact()
|
const artifactName = 'my-artifact-${{ matrix.runs-on }}'
|
||||||
run: |
|
console.log('artifactName: ' + artifactName)
|
||||||
mkdir 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
|
|
||||||
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()
|
const fileContents = ['artifact-path/first.txt','artifact-path/second.txt']
|
||||||
shell: bash
|
|
||||||
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-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()
|
const uploadResult = await artifact.create().uploadArtifact(artifactName, fileContents, './')
|
||||||
run: |
|
console.log(uploadResult)
|
||||||
mkdir multi-artifact-directory
|
|
||||||
node -e "Promise.resolve(require('./packages/artifact/lib/artifact-client').create().downloadAllArtifacts('multi-artifact-directory'))"
|
|
||||||
|
|
||||||
- name: Verify downloadAllArtifacts()
|
const success = uploadResult.success
|
||||||
shell: bash
|
const size = uploadResult.size
|
||||||
|
const id = uploadResult.id
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
throw new Error('Failed to upload artifact')
|
||||||
|
} else {
|
||||||
|
console.log(`Successfully uploaded artifact ${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set Node.js 20.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 20.x
|
||||||
|
|
||||||
|
# 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 artifacts package
|
||||||
|
- name: Install root npm packages
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Compile artifact package
|
||||||
run: |
|
run: |
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-1/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
|
npm ci
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-2/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
|
npm run tsc
|
||||||
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-3/artifact-path/empty.txt" "${{ env.empty-artifact-content }}"
|
working-directory: packages/artifact
|
||||||
|
|
||||||
|
- name: List artifacts using actions/github-script@v6
|
||||||
|
uses: actions/github-script@v6
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const artifact = require('./packages/artifact/lib/artifact')
|
||||||
|
|
||||||
|
const workflowRunId = process.env.GITHUB_RUN_ID
|
||||||
|
const repository = process.env.GITHUB_REPOSITORY
|
||||||
|
const repositoryOwner = repository.split('/')[0]
|
||||||
|
const repositoryName = repository.split('/')[1]
|
||||||
|
|
||||||
|
const listResult = await artifact.create().listArtifacts(workflowRunId, repositoryOwner, repositoryName, '${{ secrets.GITHUB_TOKEN }}')
|
||||||
|
console.log(listResult)
|
||||||
|
|
||||||
|
const artifacts = listResult.artifacts
|
||||||
|
|
||||||
|
if (artifacts.length !== 3) {
|
||||||
|
throw new Error('Expected 3 artifacts but only found ' + artifacts.length + ' artifacts')
|
||||||
|
}
|
||||||
|
|
||||||
|
const artifactNames = artifacts.map(artifact => artifact.name)
|
||||||
|
if (!artifactNames.includes('my-artifact-ubuntu-latest')){
|
||||||
|
throw new Error("Expected artifact list to contain an artifact named my-artifact-ubuntu-latest but it's missing")
|
||||||
|
}
|
||||||
|
if (!artifactNames.includes('my-artifact-windows-latest')){
|
||||||
|
throw new Error("Expected artifact list to contain an artifact named my-artifact-windows-latest but it's missing")
|
||||||
|
}
|
||||||
|
if (!artifactNames.includes('my-artifact-macos-latest')){
|
||||||
|
throw new Error("Expected artifact list to contain an artifact named my-artifact-macos-latest but it's missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Successfully listed artifacts that were uploaded')
|
||||||
|
|||||||
@@ -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 20.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install
|
run: npm install
|
||||||
@@ -31,9 +31,8 @@ jobs:
|
|||||||
- name: Bootstrap
|
- name: Bootstrap
|
||||||
run: npm run bootstrap
|
run: npm run bootstrap
|
||||||
|
|
||||||
- name: audit tools
|
- name: audit tools (without allow-list)
|
||||||
# `|| npm audit` to pretty-print the output if vulnerabilies are found after filtering.
|
run: npm audit --audit-level=moderate
|
||||||
run: npm audit --audit-level=moderate --json | scripts/audit-allow-list || 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 20.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 20.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 20.x
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 20.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
|
||||||
@@ -20,18 +20,18 @@ 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
|
||||||
uses: github/codeql-action/init@v1
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: javascript
|
languages: javascript
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v1
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v1
|
uses: github/codeql-action/analyze@v2
|
||||||
|
|||||||
@@ -13,15 +13,15 @@ jobs:
|
|||||||
|
|
||||||
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 12.x
|
- name: Set Node.js 20.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 20.x
|
||||||
|
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: npm install
|
run: npm install
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
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
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
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 }}
|
||||||
|
|
||||||
|
|||||||
@@ -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 20.x
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 12.x
|
node-version: 20.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 --forceExit
|
||||||
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: |
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
packages/*/node_modules/
|
packages/*/node_modules/
|
||||||
packages/*/lib/
|
packages/*/lib/
|
||||||
|
packages/glob/__tests__/_temp/**/
|
||||||
|
packages/*/src/generated/*/
|
||||||
|
|||||||
+2
-1
@@ -7,5 +7,6 @@
|
|||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"bracketSpacing": false,
|
"bracketSpacing": false,
|
||||||
"arrowParens": "avoid",
|
"arrowParens": "avoid",
|
||||||
"parser": "typescript"
|
"parser": "typescript",
|
||||||
|
"endOfLine": "auto"
|
||||||
}
|
}
|
||||||
@@ -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: |
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/**/*"
|
||||||
],
|
],
|
||||||
"version": "independent"
|
"version": "independent"
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"tasksRunnerOptions": {
|
||||||
|
"default": {
|
||||||
|
"runner": "nx/tasks-runners/default",
|
||||||
|
"options": {
|
||||||
|
"cacheableOperations": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"affected": {
|
||||||
|
"defaultBase": "master"
|
||||||
|
},
|
||||||
|
"$schema": "./node_modules/nx/schemas/nx-schema.json",
|
||||||
|
"namedInputs": {
|
||||||
|
"default": [
|
||||||
|
"{projectRoot}/**/*",
|
||||||
|
"sharedGlobals"
|
||||||
|
],
|
||||||
|
"sharedGlobals": [],
|
||||||
|
"production": [
|
||||||
|
"default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+6212
-21913
File diff suppressed because it is too large
Load Diff
+16
-12
@@ -3,8 +3,10 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"audit-all": "lerna run audit-moderate",
|
"audit-all": "lerna run audit-moderate",
|
||||||
"bootstrap": "lerna bootstrap",
|
"bootstrap": "lerna exec -- npm install",
|
||||||
"build": "lerna run tsc",
|
"build": "lerna run tsc",
|
||||||
|
"clean": "lerna clean",
|
||||||
|
"repair": "lerna repair",
|
||||||
"check-all": "concurrently \"npm:format-check\" \"npm:lint\" \"npm:test\" \"npm:build -- -- --noEmit\"",
|
"check-all": "concurrently \"npm:format-check\" \"npm:lint\" \"npm:test\" \"npm:build -- -- --noEmit\"",
|
||||||
"format": "prettier --write packages/**/*.ts",
|
"format": "prettier --write packages/**/*.ts",
|
||||||
"format-check": "prettier --check packages/**/*.ts",
|
"format-check": "prettier --check packages/**/*.ts",
|
||||||
@@ -14,19 +16,21 @@
|
|||||||
"test": "jest --testTimeout 10000"
|
"test": "jest --testTimeout 10000"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.0.2",
|
"@types/jest": "^29.5.4",
|
||||||
"@types/node": "^12.20.13",
|
"@types/node": "^20.5.7",
|
||||||
"@types/signale": "^1.4.1",
|
"@types/signale": "^1.4.1",
|
||||||
"@typescript-eslint/parser": "^4.0.0",
|
|
||||||
"concurrently": "^6.1.0",
|
"concurrently": "^6.1.0",
|
||||||
"eslint": "^7.23.0",
|
"eslint": "^8.0.1",
|
||||||
"eslint-plugin-github": "^4.1.3",
|
"eslint-config-prettier": "^8.9.0",
|
||||||
"eslint-plugin-jest": "^22.21.0",
|
"eslint-plugin-github": "^4.9.2",
|
||||||
|
"eslint-plugin-jest": "^27.2.3",
|
||||||
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
"flow-bin": "^0.115.0",
|
"flow-bin": "^0.115.0",
|
||||||
"jest": "^27.2.5",
|
"jest": "^29.6.4",
|
||||||
"lerna": "^5.4.0",
|
"lerna": "^7.1.4",
|
||||||
"prettier": "^1.19.1",
|
"nx": "16.6.0",
|
||||||
"ts-jest": "^27.0.5",
|
"prettier": "^3.0.0",
|
||||||
"typescript": "^3.9.9"
|
"ts-jest": "^29.1.1",
|
||||||
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-205
@@ -2,212 +2,12 @@
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
You can use this package to interact with the actions artifacts.
|
You can use this package to interact with the Actions artifacts.
|
||||||
- [Upload an Artifact](#Upload-an-Artifact)
|
|
||||||
- [Download a Single Artifact](#Download-a-Single-Artifact)
|
|
||||||
- [Download All Artifacts](#Download-all-Artifacts)
|
|
||||||
- [Additional Documentation](#Additional-Documentation)
|
|
||||||
- [Contributions](#Contributions)
|
|
||||||
|
|
||||||
Relative paths and absolute paths are both allowed. Relative paths are rooted against the current working directory.
|
This most recently published version of this package (`1.1.1`) can be found [here](https://github.com/actions/toolkit/tree/@actions/[email protected]/packages/artifact)
|
||||||
|
|
||||||
## Upload an Artifact
|
## 🚧 Under construction 🚧
|
||||||
|
|
||||||
Method Name: `uploadArtifact`
|
This package is currently undergoing a major overhaul in preparation for `v4` versions of `upload-artifact` and `download-artifact` (these Actions will use a new `2.0.0` version of `@actions/artifact` that will soon be released). The upcoming version of `@actions/artifact` will take advantage of a major re-architecture with entirely new APIs.
|
||||||
|
|
||||||
#### Inputs
|
The upcoming `2.0.0` package and `v4` artifact Actions aim to solve some of the major pain-points that have made artifact usage difficult up until now.
|
||||||
- `name`
|
|
||||||
- The name of the artifact that is being uploaded
|
|
||||||
- Required
|
|
||||||
- `files`
|
|
||||||
- A list of file paths that describe what should be uploaded as part of the artifact
|
|
||||||
- If a path is provided that does not exist, an error will be thrown
|
|
||||||
- Can be absolute or relative. Internally everything is normalized and resolved
|
|
||||||
- Required
|
|
||||||
- `rootDirectory`
|
|
||||||
- A file path that denotes the root directory of the files being uploaded. This path is used to strip the paths provided in `files` to control how they are uploaded and structured
|
|
||||||
- If a file specified in `files` is not in the `rootDirectory`, an error will be thrown
|
|
||||||
- Required
|
|
||||||
- `options`
|
|
||||||
- Extra options that allow for the customization of the upload behavior
|
|
||||||
- Optional
|
|
||||||
|
|
||||||
#### Available Options
|
|
||||||
|
|
||||||
- `continueOnError`
|
|
||||||
- Indicates if the artifact upload should continue in the event a file fails to upload. If there is a error during upload, a partial artifact will always be created and available for download at the end. The `size` reported will be the amount of storage that the user or org will be charged for the partial artifact.
|
|
||||||
- If set to `false`, and an error is encountered, all other uploads will stop and any files that were queued will not be attempted to be uploaded. The partial artifact available will only include files up until the failure.
|
|
||||||
- If set to `true` and an error is encountered, the failed file will be skipped and ignored and all other queued files will be attempted to be uploaded. There will be an artifact available for download at the end with everything excluding the file that failed to upload
|
|
||||||
- Optional, defaults to `true` if not specified
|
|
||||||
- `retentionDays`
|
|
||||||
- Duration after which artifact will expire in days
|
|
||||||
- Minimum value: 1
|
|
||||||
- Maximum value: 90 unless changed by repository setting
|
|
||||||
- If this is set to a greater value than the retention settings allowed, the retention on artifacts will be reduced to match the max value allowed on the server, and the upload process will continue. An input of 0 assumes default retention value.
|
|
||||||
|
|
||||||
#### Example using Absolute File Paths
|
|
||||||
|
|
||||||
```js
|
|
||||||
const artifact = require('@actions/artifact');
|
|
||||||
const artifactClient = artifact.create()
|
|
||||||
const artifactName = 'my-artifact';
|
|
||||||
const files = [
|
|
||||||
'/home/user/files/plz-upload/file1.txt',
|
|
||||||
'/home/user/files/plz-upload/file2.txt',
|
|
||||||
'/home/user/files/plz-upload/dir/file3.txt'
|
|
||||||
]
|
|
||||||
const rootDirectory = '/home/user/files/plz-upload'
|
|
||||||
const options = {
|
|
||||||
continueOnError: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadResult = await artifactClient.uploadArtifact(artifactName, files, rootDirectory, options)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Example using Relative File Paths
|
|
||||||
```js
|
|
||||||
// Assuming the current working directory is /home/user/files/plz-upload
|
|
||||||
const artifact = require('@actions/artifact');
|
|
||||||
const artifactClient = artifact.create()
|
|
||||||
const artifactName = 'my-artifact';
|
|
||||||
const files = [
|
|
||||||
'file1.txt',
|
|
||||||
'file2.txt',
|
|
||||||
'dir/file3.txt'
|
|
||||||
]
|
|
||||||
|
|
||||||
const rootDirectory = '.' // Also possible to use __dirname
|
|
||||||
const options = {
|
|
||||||
continueOnError: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadResponse = await artifactClient.uploadArtifact(artifactName, files, rootDirectory, options)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Upload Result
|
|
||||||
|
|
||||||
The returned `UploadResponse` will contain the following information
|
|
||||||
|
|
||||||
- `artifactName`
|
|
||||||
- The name of the artifact that was uploaded
|
|
||||||
- `artifactItems`
|
|
||||||
- A list of all files that describe what is uploaded if there are no errors encountered. Usually this will be equal to the inputted `files` with the exception of empty directories (will not be uploaded)
|
|
||||||
- `size`
|
|
||||||
- Total size of the artifact that was uploaded in bytes
|
|
||||||
- `failedItems`
|
|
||||||
- A list of items that were not uploaded successfully (this will include queued items that were not uploaded if `continueOnError` is set to false). This is a subset of `artifactItems`
|
|
||||||
|
|
||||||
## Download a Single Artifact
|
|
||||||
|
|
||||||
Method Name: `downloadArtifact`
|
|
||||||
|
|
||||||
#### Inputs
|
|
||||||
- `name`
|
|
||||||
- The name of the artifact to download
|
|
||||||
- Required
|
|
||||||
- `path`
|
|
||||||
- Path that denotes where the artifact will be downloaded to
|
|
||||||
- Optional. Defaults to the GitHub workspace directory(`$GITHUB_WORKSPACE`) if not specified
|
|
||||||
- `options`
|
|
||||||
- Extra options that allow for the customization of the download behavior
|
|
||||||
- Optional
|
|
||||||
|
|
||||||
|
|
||||||
#### Available Options
|
|
||||||
|
|
||||||
- `createArtifactFolder`
|
|
||||||
- Specifies if a folder (the artifact name) is created for the artifact that is downloaded (contents downloaded into this folder),
|
|
||||||
- Optional. Defaults to false if not specified
|
|
||||||
|
|
||||||
#### Example
|
|
||||||
|
|
||||||
```js
|
|
||||||
const artifact = require('@actions/artifact');
|
|
||||||
const artifactClient = artifact.create()
|
|
||||||
const artifactName = 'my-artifact';
|
|
||||||
const path = 'some/directory'
|
|
||||||
const options = {
|
|
||||||
createArtifactFolder: false
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadResponse = await artifactClient.downloadArtifact(artifactName, path, options)
|
|
||||||
|
|
||||||
// Post download, the directory structure will look like this
|
|
||||||
/some
|
|
||||||
/directory
|
|
||||||
/file1.txt
|
|
||||||
/file2.txt
|
|
||||||
/dir
|
|
||||||
/file3.txt
|
|
||||||
|
|
||||||
// If createArtifactFolder is set to true, the directory structure will look like this
|
|
||||||
/some
|
|
||||||
/directory
|
|
||||||
/my-artifact
|
|
||||||
/file1.txt
|
|
||||||
/file2.txt
|
|
||||||
/dir
|
|
||||||
/file3.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Download Response
|
|
||||||
|
|
||||||
The returned `DownloadResponse` will contain the following information
|
|
||||||
|
|
||||||
- `artifactName`
|
|
||||||
- The name of the artifact that was downloaded
|
|
||||||
- `downloadPath`
|
|
||||||
- The full Path to where the artifact was downloaded
|
|
||||||
|
|
||||||
|
|
||||||
## Download All Artifacts
|
|
||||||
|
|
||||||
Method Name: `downloadAllArtifacts`
|
|
||||||
|
|
||||||
#### Inputs
|
|
||||||
- `path`
|
|
||||||
- Path that denotes where the artifact will be downloaded to
|
|
||||||
- Optional. Defaults to the GitHub workspace directory(`$GITHUB_WORKSPACE`) if not specified
|
|
||||||
|
|
||||||
```js
|
|
||||||
const artifact = require('@actions/artifact');
|
|
||||||
const artifactClient = artifact.create();
|
|
||||||
const downloadResponse = await artifactClient.downloadAllArtifacts();
|
|
||||||
|
|
||||||
// output result
|
|
||||||
for (response in downloadResponse) {
|
|
||||||
console.log(response.artifactName);
|
|
||||||
console.log(response.downloadPath);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Because there are multiple artifacts, an extra directory (denoted by the name of the artifact) will be created for each artifact in the path. With 2 artifacts(`my-artifact-1` and `my-artifact-2` for example) and the default path, the directory structure will be as follows:
|
|
||||||
```js
|
|
||||||
/GITHUB_WORKSPACE
|
|
||||||
/my-artifact-1
|
|
||||||
/ .. contents of `my-artifact-1`
|
|
||||||
/my-artifact-2
|
|
||||||
/ .. contents of `my-artifact-2`
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Download Result
|
|
||||||
|
|
||||||
An array will be returned that describes the results for downloading all artifacts. The number of items in the array indicates the number of artifacts that were downloaded.
|
|
||||||
|
|
||||||
Each artifact will have the same `DownloadResponse` as if it was individually downloaded
|
|
||||||
- `artifactName`
|
|
||||||
- The name of the artifact that was downloaded
|
|
||||||
- `downloadPath`
|
|
||||||
- The full Path to where the artifact was downloaded
|
|
||||||
|
|
||||||
## Additional Documentation
|
|
||||||
|
|
||||||
Check out [additional-information](docs/additional-information.md) for extra documentation around usage, restrictions and behavior.
|
|
||||||
|
|
||||||
Check out [implementation-details](docs/implementation-details.md) for extra information about the implementation of this package.
|
|
||||||
|
|
||||||
## Contributions
|
|
||||||
|
|
||||||
See [contributor guidelines](https://github.com/actions/toolkit/blob/main/.github/CONTRIBUTING.md) for general guidelines and information about toolkit contributions.
|
|
||||||
|
|
||||||
For contributions related to this package, see [artifact contributions](CONTRIBUTIONS.md) for more information.
|
|
||||||
|
|||||||
@@ -90,3 +90,7 @@
|
|||||||
### 1.1.0
|
### 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)
|
- 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,200 @@
|
|||||||
|
import * as http from 'http'
|
||||||
|
import * as net from 'net'
|
||||||
|
import {HttpClient} from '@actions/http-client'
|
||||||
|
import * as config from '../src/internal/shared/config'
|
||||||
|
import {createArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
|
||||||
|
jest.mock('@actions/http-client')
|
||||||
|
|
||||||
|
describe('artifact-http-client', () => {
|
||||||
|
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(() => {})
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getResultsServiceUrl')
|
||||||
|
.mockReturnValue('http://localhost:8080')
|
||||||
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('token')
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should successfully create a client', () => {
|
||||||
|
const client = createArtifactTwirpClient('upload')
|
||||||
|
expect(client).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should make a request', async () => {
|
||||||
|
const mockPost = jest.fn(() => {
|
||||||
|
const msg = new http.IncomingMessage(new net.Socket())
|
||||||
|
msg.statusCode = 200
|
||||||
|
return {
|
||||||
|
message: msg,
|
||||||
|
readBody: async () => {
|
||||||
|
return Promise.resolve(
|
||||||
|
`{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const mockHttpClient = (
|
||||||
|
HttpClient as unknown as jest.Mock
|
||||||
|
).mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
post: mockPost
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const client = createArtifactTwirpClient('upload')
|
||||||
|
const artifact = await client.CreateArtifact({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678',
|
||||||
|
name: 'artifact',
|
||||||
|
version: 4
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockPost).toHaveBeenCalledTimes(1)
|
||||||
|
expect(artifact).toBeDefined()
|
||||||
|
expect(artifact.ok).toBe(true)
|
||||||
|
expect(artifact.signedUploadUrl).toBe('http://localhost:8080/upload')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should retry if the request fails', async () => {
|
||||||
|
const mockPost = jest
|
||||||
|
.fn(() => {
|
||||||
|
const msgSucceeded = new http.IncomingMessage(new net.Socket())
|
||||||
|
msgSucceeded.statusCode = 200
|
||||||
|
return {
|
||||||
|
message: msgSucceeded,
|
||||||
|
readBody: async () => {
|
||||||
|
return Promise.resolve(
|
||||||
|
`{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
const msgFailed = new http.IncomingMessage(new net.Socket())
|
||||||
|
msgFailed.statusCode = 500
|
||||||
|
msgFailed.statusMessage = 'Internal Server Error'
|
||||||
|
return {
|
||||||
|
message: msgFailed,
|
||||||
|
readBody: async () => {
|
||||||
|
return Promise.resolve(`{"ok": false}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const mockHttpClient = (
|
||||||
|
HttpClient as unknown as jest.Mock
|
||||||
|
).mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
post: mockPost
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const client = createArtifactTwirpClient(
|
||||||
|
'upload',
|
||||||
|
5, // retry 5 times
|
||||||
|
1, // wait 1 ms
|
||||||
|
1.5 // backoff factor
|
||||||
|
)
|
||||||
|
const artifact = await client.CreateArtifact({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678',
|
||||||
|
name: 'artifact',
|
||||||
|
version: 4
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
||||||
|
expect(artifact).toBeDefined()
|
||||||
|
expect(artifact.ok).toBe(true)
|
||||||
|
expect(artifact.signedUploadUrl).toBe('http://localhost:8080/upload')
|
||||||
|
expect(mockPost).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail if the request fails 5 times', async () => {
|
||||||
|
const mockPost = jest.fn(() => {
|
||||||
|
const msgFailed = new http.IncomingMessage(new net.Socket())
|
||||||
|
msgFailed.statusCode = 500
|
||||||
|
msgFailed.statusMessage = 'Internal Server Error'
|
||||||
|
return {
|
||||||
|
message: msgFailed,
|
||||||
|
readBody: async () => {
|
||||||
|
return Promise.resolve(`{"ok": false}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockHttpClient = (
|
||||||
|
HttpClient as unknown as jest.Mock
|
||||||
|
).mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
post: mockPost
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const client = createArtifactTwirpClient(
|
||||||
|
'upload',
|
||||||
|
5, // retry 5 times
|
||||||
|
1, // wait 1 ms
|
||||||
|
1.5 // backoff factor
|
||||||
|
)
|
||||||
|
await expect(async () => {
|
||||||
|
await client.CreateArtifact({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678',
|
||||||
|
name: 'artifact',
|
||||||
|
version: 4
|
||||||
|
})
|
||||||
|
}).rejects.toThrowError(
|
||||||
|
'Failed to make request after 5 attempts: Failed request: (500) Internal Server Error'
|
||||||
|
)
|
||||||
|
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockPost).toHaveBeenCalledTimes(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail immediately if there is a non-retryable error', async () => {
|
||||||
|
const mockPost = jest.fn(() => {
|
||||||
|
const msgFailed = new http.IncomingMessage(new net.Socket())
|
||||||
|
msgFailed.statusCode = 401
|
||||||
|
msgFailed.statusMessage = 'Unauthorized'
|
||||||
|
return {
|
||||||
|
message: msgFailed,
|
||||||
|
readBody: async () => {
|
||||||
|
return Promise.resolve(`{"ok": false}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const mockHttpClient = (
|
||||||
|
HttpClient as unknown as jest.Mock
|
||||||
|
).mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
post: mockPost
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const client = createArtifactTwirpClient(
|
||||||
|
'upload',
|
||||||
|
5, // retry 5 times
|
||||||
|
1, // wait 1 ms
|
||||||
|
1.5 // backoff factor
|
||||||
|
)
|
||||||
|
await expect(async () => {
|
||||||
|
await client.CreateArtifact({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678',
|
||||||
|
name: 'artifact',
|
||||||
|
version: 4
|
||||||
|
})
|
||||||
|
}).rejects.toThrowError(
|
||||||
|
'Received non-retryable error: Failed request: (401) Unauthorized'
|
||||||
|
)
|
||||||
|
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockPost).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
name: 'Set env variables'
|
|
||||||
description: 'Sets certain env variables so that e2e artifact upload and download can be tested in a shell'
|
|
||||||
runs:
|
|
||||||
using: 'node12'
|
|
||||||
main: 'index.js'
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
// Certain env variables are not set by default in a shell context and are only available in a node context from a running action
|
|
||||||
// In order to be able to upload and download artifacts e2e in a shell when running CI tests, we need these env variables set
|
|
||||||
const fs = require('fs');
|
|
||||||
const os = require('os');
|
|
||||||
const filePath = process.env[`GITHUB_ENV`]
|
|
||||||
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_URL=${process.env.ACTIONS_RUNTIME_URL}${os.EOL}`, {
|
|
||||||
encoding: 'utf8'
|
|
||||||
})
|
|
||||||
fs.appendFileSync(filePath, `ACTIONS_RUNTIME_TOKEN=${process.env.ACTIONS_RUNTIME_TOKEN}${os.EOL}`, {
|
|
||||||
encoding: 'utf8'
|
|
||||||
})
|
|
||||||
fs.appendFileSync(filePath, `GITHUB_RUN_ID=${process.env.GITHUB_RUN_ID}${os.EOL}`, {
|
|
||||||
encoding: 'utf8'
|
|
||||||
})
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import * as http from 'http'
|
||||||
|
import * as net from 'net'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as github from '@actions/github'
|
||||||
|
import {HttpClient} from '@actions/http-client'
|
||||||
|
import type {RestEndpointMethods} from '@octokit/plugin-rest-endpoint-methods/dist-types/generated/method-types'
|
||||||
|
import archiver from 'archiver'
|
||||||
|
|
||||||
|
import {downloadArtifact} from '../src/internal/download/download-artifact'
|
||||||
|
import {getUserAgentString} from '../src/internal/shared/user-agent'
|
||||||
|
|
||||||
|
type MockedDownloadArtifact = jest.MockedFunction<
|
||||||
|
RestEndpointMethods['actions']['downloadArtifact']
|
||||||
|
>
|
||||||
|
|
||||||
|
const testDir = path.join(__dirname, '_temp', 'download-artifact')
|
||||||
|
const fixtures = {
|
||||||
|
workspaceDir: path.join(testDir, 'workspace'),
|
||||||
|
exampleArtifact: {
|
||||||
|
path: path.join(testDir, 'artifact.zip'),
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: 'hello.txt',
|
||||||
|
content: 'Hello World!'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'goodbye.txt',
|
||||||
|
content: 'Goodbye World!'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
artifactID: 1234,
|
||||||
|
repositoryOwner: 'actions',
|
||||||
|
repositoryName: 'toolkit',
|
||||||
|
token: 'ghp_1234567890',
|
||||||
|
blobStorageUrl: 'https://blob-storage.local?signed=true'
|
||||||
|
}
|
||||||
|
|
||||||
|
jest.mock('@actions/github', () => ({
|
||||||
|
getOctokit: jest.fn().mockReturnValue({
|
||||||
|
rest: {
|
||||||
|
actions: {
|
||||||
|
downloadArtifact: jest.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@actions/http-client')
|
||||||
|
|
||||||
|
// Create a zip archive with the contents of the example artifact
|
||||||
|
const createTestArchive = async (): Promise<void> => {
|
||||||
|
const archive = archiver('zip', {
|
||||||
|
zlib: {level: 9}
|
||||||
|
})
|
||||||
|
for (const file of fixtures.exampleArtifact.files) {
|
||||||
|
archive.append(file.content, {name: file.path})
|
||||||
|
}
|
||||||
|
archive.finalize()
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
archive.pipe(fs.createWriteStream(fixtures.exampleArtifact.path))
|
||||||
|
archive.on('error', reject)
|
||||||
|
archive.on('finish', resolve)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectExtractedArchive = async (dir: string): Promise<void> => {
|
||||||
|
for (const file of fixtures.exampleArtifact.files) {
|
||||||
|
const filePath = path.join(dir, file.path)
|
||||||
|
expect(fs.readFileSync(filePath, 'utf8')).toEqual(file.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('download-artifact', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||||
|
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||||
|
|
||||||
|
await fs.promises.mkdir(testDir, {recursive: true})
|
||||||
|
await createTestArchive()
|
||||||
|
|
||||||
|
process.env['GITHUB_WORKSPACE'] = fixtures.workspaceDir
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
await fs.promises.rm(testDir, {recursive: true})
|
||||||
|
delete process.env['GITHUB_WORKSPACE']
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should successfully download an artifact to $GITHUB_WORKSPACE', async () => {
|
||||||
|
const downloadArtifactMock = github.getOctokit(fixtures.token).rest.actions
|
||||||
|
.downloadArtifact as MockedDownloadArtifact
|
||||||
|
downloadArtifactMock.mockResolvedValueOnce({
|
||||||
|
headers: {
|
||||||
|
location: fixtures.blobStorageUrl
|
||||||
|
},
|
||||||
|
status: 302,
|
||||||
|
url: '',
|
||||||
|
data: Buffer.from('')
|
||||||
|
})
|
||||||
|
|
||||||
|
const getMock = jest.fn(() => {
|
||||||
|
const message = new http.IncomingMessage(new net.Socket())
|
||||||
|
message.statusCode = 200
|
||||||
|
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
|
||||||
|
return {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const httpClientMock = (HttpClient as jest.Mock).mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
get: getMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await downloadArtifact(
|
||||||
|
fixtures.artifactID,
|
||||||
|
fixtures.repositoryOwner,
|
||||||
|
fixtures.repositoryName,
|
||||||
|
fixtures.token
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
||||||
|
owner: fixtures.repositoryOwner,
|
||||||
|
repo: fixtures.repositoryName,
|
||||||
|
artifact_id: fixtures.artifactID,
|
||||||
|
archive_format: 'zip',
|
||||||
|
request: {
|
||||||
|
redirect: 'manual'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(httpClientMock).toHaveBeenCalledWith(getUserAgentString())
|
||||||
|
expect(getMock).toHaveBeenCalledWith(fixtures.blobStorageUrl)
|
||||||
|
|
||||||
|
expectExtractedArchive(fixtures.workspaceDir)
|
||||||
|
|
||||||
|
expect(response.success).toBe(true)
|
||||||
|
expect(response.downloadPath).toBe(fixtures.workspaceDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should successfully download an artifact to user defined path', async () => {
|
||||||
|
const customPath = path.join(testDir, 'custom')
|
||||||
|
|
||||||
|
const downloadArtifactMock = github.getOctokit(fixtures.token).rest.actions
|
||||||
|
.downloadArtifact as MockedDownloadArtifact
|
||||||
|
downloadArtifactMock.mockResolvedValueOnce({
|
||||||
|
headers: {
|
||||||
|
location: fixtures.blobStorageUrl
|
||||||
|
},
|
||||||
|
status: 302,
|
||||||
|
url: '',
|
||||||
|
data: Buffer.from('')
|
||||||
|
})
|
||||||
|
|
||||||
|
const getMock = jest.fn(() => {
|
||||||
|
const message = new http.IncomingMessage(new net.Socket())
|
||||||
|
message.statusCode = 200
|
||||||
|
message.push(fs.readFileSync(fixtures.exampleArtifact.path))
|
||||||
|
return {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const httpClientMock = (HttpClient as jest.Mock).mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
get: getMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await downloadArtifact(
|
||||||
|
fixtures.artifactID,
|
||||||
|
fixtures.repositoryOwner,
|
||||||
|
fixtures.repositoryName,
|
||||||
|
fixtures.token,
|
||||||
|
{
|
||||||
|
path: customPath
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
||||||
|
owner: fixtures.repositoryOwner,
|
||||||
|
repo: fixtures.repositoryName,
|
||||||
|
artifact_id: fixtures.artifactID,
|
||||||
|
archive_format: 'zip',
|
||||||
|
request: {
|
||||||
|
redirect: 'manual'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(httpClientMock).toHaveBeenCalledWith(getUserAgentString())
|
||||||
|
expect(getMock).toHaveBeenCalledWith(fixtures.blobStorageUrl)
|
||||||
|
|
||||||
|
expectExtractedArchive(customPath)
|
||||||
|
|
||||||
|
expect(response.success).toBe(true)
|
||||||
|
expect(response.downloadPath).toBe(customPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail if download artifact API does not respond with location', async () => {
|
||||||
|
const downloadArtifactMock = github.getOctokit(fixtures.token).rest.actions
|
||||||
|
.downloadArtifact as MockedDownloadArtifact
|
||||||
|
downloadArtifactMock.mockResolvedValueOnce({
|
||||||
|
headers: {},
|
||||||
|
status: 302,
|
||||||
|
url: '',
|
||||||
|
data: Buffer.from('')
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadArtifact(
|
||||||
|
fixtures.artifactID,
|
||||||
|
fixtures.repositoryOwner,
|
||||||
|
fixtures.repositoryName,
|
||||||
|
fixtures.token
|
||||||
|
)
|
||||||
|
).rejects.toBeInstanceOf(Error)
|
||||||
|
|
||||||
|
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
||||||
|
owner: fixtures.repositoryOwner,
|
||||||
|
repo: fixtures.repositoryName,
|
||||||
|
artifact_id: fixtures.artifactID,
|
||||||
|
archive_format: 'zip',
|
||||||
|
request: {
|
||||||
|
redirect: 'manual'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail if blob storage response is non-200', async () => {
|
||||||
|
const downloadArtifactMock = github.getOctokit(fixtures.token).rest.actions
|
||||||
|
.downloadArtifact as MockedDownloadArtifact
|
||||||
|
downloadArtifactMock.mockResolvedValueOnce({
|
||||||
|
headers: {
|
||||||
|
location: fixtures.blobStorageUrl
|
||||||
|
},
|
||||||
|
status: 302,
|
||||||
|
url: '',
|
||||||
|
data: Buffer.from('')
|
||||||
|
})
|
||||||
|
|
||||||
|
const getMock = jest.fn(() => {
|
||||||
|
const message = new http.IncomingMessage(new net.Socket())
|
||||||
|
message.statusCode = 500
|
||||||
|
message.push('Internal Server Error')
|
||||||
|
return {
|
||||||
|
message
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const httpClientMock = (HttpClient as jest.Mock).mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
get: getMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
downloadArtifact(
|
||||||
|
fixtures.artifactID,
|
||||||
|
fixtures.repositoryOwner,
|
||||||
|
fixtures.repositoryName,
|
||||||
|
fixtures.token
|
||||||
|
)
|
||||||
|
).rejects.toBeInstanceOf(Error)
|
||||||
|
|
||||||
|
expect(downloadArtifactMock).toHaveBeenCalledWith({
|
||||||
|
owner: fixtures.repositoryOwner,
|
||||||
|
repo: fixtures.repositoryName,
|
||||||
|
artifact_id: fixtures.artifactID,
|
||||||
|
archive_format: 'zip',
|
||||||
|
request: {
|
||||||
|
redirect: 'manual'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(httpClientMock).toHaveBeenCalledWith(getUserAgentString())
|
||||||
|
expect(getMock).toHaveBeenCalledWith(fixtures.blobStorageUrl)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,552 +0,0 @@
|
|||||||
import * as path from 'path'
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import {URL} from 'url'
|
|
||||||
import {getDownloadSpecification} from '../src/internal/download-specification'
|
|
||||||
import {ContainerEntry} from '../src/internal/contracts'
|
|
||||||
|
|
||||||
const artifact1Name = 'my-artifact'
|
|
||||||
const artifact2Name = 'my-artifact-extra'
|
|
||||||
|
|
||||||
// Populating with only the information that is necessary
|
|
||||||
function getPartialContainerEntry(): ContainerEntry {
|
|
||||||
return {
|
|
||||||
containerId: 10,
|
|
||||||
scopeIdentifier: '00000000-0000-0000-0000-000000000000',
|
|
||||||
path: 'ADD_INFORMATION',
|
|
||||||
itemType: 'ADD_INFORMATION',
|
|
||||||
status: 'created',
|
|
||||||
dateCreated: '2020-02-06T22:13:35.373Z',
|
|
||||||
dateLastModified: '2020-02-06T22:13:35.453Z',
|
|
||||||
createdBy: '82f0bf89-6e55-4e5a-b8b6-f75eb992578c',
|
|
||||||
lastModifiedBy: '82f0bf89-6e55-4e5a-b8b6-f75eb992578c',
|
|
||||||
itemLocation: 'ADD_INFORMATION',
|
|
||||||
contentLocation: 'ADD_INFORMATION',
|
|
||||||
contentId: '',
|
|
||||||
fileLength: 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createFileEntry(entryPath: string): ContainerEntry {
|
|
||||||
const newFileEntry = getPartialContainerEntry()
|
|
||||||
newFileEntry.path = entryPath
|
|
||||||
newFileEntry.itemType = 'file'
|
|
||||||
newFileEntry.itemLocation = createItemLocation(entryPath)
|
|
||||||
newFileEntry.contentLocation = createContentLocation(entryPath)
|
|
||||||
return newFileEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
function createDirectoryEntry(directoryPath: string): ContainerEntry {
|
|
||||||
const newDirectoryEntry = getPartialContainerEntry()
|
|
||||||
newDirectoryEntry.path = directoryPath
|
|
||||||
newDirectoryEntry.itemType = 'folder'
|
|
||||||
newDirectoryEntry.itemLocation = createItemLocation(directoryPath)
|
|
||||||
newDirectoryEntry.contentLocation = createContentLocation(directoryPath)
|
|
||||||
return newDirectoryEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
function createItemLocation(relativePath: string): string {
|
|
||||||
const itemLocation = new URL(
|
|
||||||
'https://testing/_apis/resources/Containers/10000'
|
|
||||||
)
|
|
||||||
itemLocation.searchParams.append('itemPath', relativePath)
|
|
||||||
itemLocation.searchParams.append('metadata', 'true')
|
|
||||||
return itemLocation.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
function createContentLocation(relativePath: string): string {
|
|
||||||
const itemLocation = new URL(
|
|
||||||
'https://testing/_apis/resources/Containers/10000'
|
|
||||||
)
|
|
||||||
itemLocation.searchParams.append('itemPath', relativePath)
|
|
||||||
return itemLocation.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Represents a set of container entries for two artifacts with the following directory structure
|
|
||||||
|
|
||||||
/my-artifact
|
|
||||||
/file1.txt
|
|
||||||
/file2.txt
|
|
||||||
/dir1
|
|
||||||
/file3.txt
|
|
||||||
/dir2
|
|
||||||
/dir3
|
|
||||||
/dir4
|
|
||||||
file4.txt
|
|
||||||
file5.txt (no length property)
|
|
||||||
file6.txt (empty file)
|
|
||||||
/my-artifact-extra
|
|
||||||
/file1.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
// main artifact
|
|
||||||
const file1Path = path.join(artifact1Name, 'file1.txt')
|
|
||||||
const file2Path = path.join(artifact1Name, 'file2.txt')
|
|
||||||
const dir1Path = path.join(artifact1Name, 'dir1')
|
|
||||||
const file3Path = path.join(dir1Path, 'file3.txt')
|
|
||||||
const dir2Path = path.join(dir1Path, 'dir2')
|
|
||||||
const dir3Path = path.join(dir2Path, 'dir3')
|
|
||||||
const dir4Path = path.join(dir3Path, 'dir4')
|
|
||||||
const file4Path = path.join(dir4Path, 'file4.txt')
|
|
||||||
const file5Path = path.join(dir4Path, 'file5.txt')
|
|
||||||
const file6Path = path.join(dir4Path, 'file6.txt')
|
|
||||||
|
|
||||||
const rootDirectoryEntry = createDirectoryEntry(artifact1Name)
|
|
||||||
const directoryEntry1 = createDirectoryEntry(dir1Path)
|
|
||||||
const directoryEntry2 = createDirectoryEntry(dir2Path)
|
|
||||||
const directoryEntry3 = createDirectoryEntry(dir3Path)
|
|
||||||
const directoryEntry4 = createDirectoryEntry(dir4Path)
|
|
||||||
const fileEntry1 = createFileEntry(file1Path)
|
|
||||||
const fileEntry2 = createFileEntry(file2Path)
|
|
||||||
const fileEntry3 = createFileEntry(file3Path)
|
|
||||||
const fileEntry4 = createFileEntry(file4Path)
|
|
||||||
|
|
||||||
const missingLengthFileEntry = createFileEntry(file5Path)
|
|
||||||
missingLengthFileEntry.fileLength = undefined // one file does not have a fileLength
|
|
||||||
const emptyLengthFileEntry = createFileEntry(file6Path)
|
|
||||||
emptyLengthFileEntry.fileLength = 0 // empty file path
|
|
||||||
|
|
||||||
// extra artifact
|
|
||||||
const artifact2File1Path = path.join(artifact2Name, 'file1.txt')
|
|
||||||
const rootDirectoryEntry2 = createDirectoryEntry(artifact2Name)
|
|
||||||
const extraFileEntry = createFileEntry(artifact2File1Path)
|
|
||||||
|
|
||||||
const artifactContainerEntries: ContainerEntry[] = [
|
|
||||||
rootDirectoryEntry,
|
|
||||||
fileEntry1,
|
|
||||||
fileEntry2,
|
|
||||||
directoryEntry1,
|
|
||||||
fileEntry3,
|
|
||||||
directoryEntry2,
|
|
||||||
directoryEntry3,
|
|
||||||
directoryEntry4,
|
|
||||||
fileEntry4,
|
|
||||||
missingLengthFileEntry,
|
|
||||||
emptyLengthFileEntry,
|
|
||||||
rootDirectoryEntry2,
|
|
||||||
extraFileEntry
|
|
||||||
]
|
|
||||||
|
|
||||||
describe('Search', () => {
|
|
||||||
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(() => {})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Download Specification - Absolute Path with no root directory', () => {
|
|
||||||
const testDownloadPath = path.join(
|
|
||||||
__dirname,
|
|
||||||
'some',
|
|
||||||
'destination',
|
|
||||||
'folder'
|
|
||||||
)
|
|
||||||
|
|
||||||
const specification = getDownloadSpecification(
|
|
||||||
artifact1Name,
|
|
||||||
artifactContainerEntries,
|
|
||||||
testDownloadPath,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(specification.rootDownloadLocation).toEqual(testDownloadPath)
|
|
||||||
expect(specification.filesToDownload.length).toEqual(5)
|
|
||||||
|
|
||||||
const item1ExpectedTargetPath = path.join(testDownloadPath, 'file1.txt')
|
|
||||||
const item2ExpectedTargetPath = path.join(testDownloadPath, 'file2.txt')
|
|
||||||
const item3ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
'dir1',
|
|
||||||
'file3.txt'
|
|
||||||
)
|
|
||||||
const item4ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file4.txt'
|
|
||||||
)
|
|
||||||
const item5ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file5.txt'
|
|
||||||
)
|
|
||||||
const item6ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file6.txt'
|
|
||||||
)
|
|
||||||
|
|
||||||
const targetLocations = specification.filesToDownload.map(
|
|
||||||
item => item.targetPath
|
|
||||||
)
|
|
||||||
expect(targetLocations).toContain(item1ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item2ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item3ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item4ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item5ExpectedTargetPath)
|
|
||||||
|
|
||||||
for (const downloadItem of specification.filesToDownload) {
|
|
||||||
if (downloadItem.targetPath === item1ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file1Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item2ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file2Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item3ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file3Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item4ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file4Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item5ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file5Path)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw new Error('this should never be reached')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(specification.directoryStructure.length).toEqual(3)
|
|
||||||
expect(specification.directoryStructure).toContain(testDownloadPath)
|
|
||||||
expect(specification.directoryStructure).toContain(
|
|
||||||
path.join(testDownloadPath, 'dir1')
|
|
||||||
)
|
|
||||||
expect(specification.directoryStructure).toContain(
|
|
||||||
path.join(testDownloadPath, 'dir1', 'dir2', 'dir3', 'dir4')
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(specification.emptyFilesToCreate.length).toEqual(1)
|
|
||||||
expect(specification.emptyFilesToCreate).toContain(item6ExpectedTargetPath)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Download Specification - Relative Path with no root directory', () => {
|
|
||||||
const testDownloadPath = path.join('some', 'destination', 'folder')
|
|
||||||
|
|
||||||
const specification = getDownloadSpecification(
|
|
||||||
artifact1Name,
|
|
||||||
artifactContainerEntries,
|
|
||||||
testDownloadPath,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(specification.rootDownloadLocation).toEqual(testDownloadPath)
|
|
||||||
expect(specification.filesToDownload.length).toEqual(5)
|
|
||||||
|
|
||||||
const item1ExpectedTargetPath = path.join(testDownloadPath, 'file1.txt')
|
|
||||||
const item2ExpectedTargetPath = path.join(testDownloadPath, 'file2.txt')
|
|
||||||
const item3ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
'dir1',
|
|
||||||
'file3.txt'
|
|
||||||
)
|
|
||||||
const item4ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file4.txt'
|
|
||||||
)
|
|
||||||
const item5ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file5.txt'
|
|
||||||
)
|
|
||||||
const item6ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file6.txt'
|
|
||||||
)
|
|
||||||
|
|
||||||
const targetLocations = specification.filesToDownload.map(
|
|
||||||
item => item.targetPath
|
|
||||||
)
|
|
||||||
expect(targetLocations).toContain(item1ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item2ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item3ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item4ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item5ExpectedTargetPath)
|
|
||||||
|
|
||||||
for (const downloadItem of specification.filesToDownload) {
|
|
||||||
if (downloadItem.targetPath === item1ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file1Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item2ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file2Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item3ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file3Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item4ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file4Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item5ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file5Path)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw new Error('this should never be reached')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(specification.directoryStructure.length).toEqual(3)
|
|
||||||
expect(specification.directoryStructure).toContain(testDownloadPath)
|
|
||||||
expect(specification.directoryStructure).toContain(
|
|
||||||
path.join(testDownloadPath, 'dir1')
|
|
||||||
)
|
|
||||||
expect(specification.directoryStructure).toContain(
|
|
||||||
path.join(testDownloadPath, 'dir1', 'dir2', 'dir3', 'dir4')
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(specification.emptyFilesToCreate.length).toEqual(1)
|
|
||||||
expect(specification.emptyFilesToCreate).toContain(item6ExpectedTargetPath)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Download Specification - Absolute Path with root directory', () => {
|
|
||||||
const testDownloadPath = path.join(
|
|
||||||
__dirname,
|
|
||||||
'some',
|
|
||||||
'destination',
|
|
||||||
'folder'
|
|
||||||
)
|
|
||||||
|
|
||||||
const specification = getDownloadSpecification(
|
|
||||||
artifact1Name,
|
|
||||||
artifactContainerEntries,
|
|
||||||
testDownloadPath,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(specification.rootDownloadLocation).toEqual(
|
|
||||||
path.join(testDownloadPath, artifact1Name)
|
|
||||||
)
|
|
||||||
expect(specification.filesToDownload.length).toEqual(5)
|
|
||||||
|
|
||||||
const item1ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'file1.txt'
|
|
||||||
)
|
|
||||||
const item2ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'file2.txt'
|
|
||||||
)
|
|
||||||
const item3ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'dir1',
|
|
||||||
'file3.txt'
|
|
||||||
)
|
|
||||||
const item4ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file4.txt'
|
|
||||||
)
|
|
||||||
const item5ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file5.txt'
|
|
||||||
)
|
|
||||||
const item6ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file6.txt'
|
|
||||||
)
|
|
||||||
|
|
||||||
const targetLocations = specification.filesToDownload.map(
|
|
||||||
item => item.targetPath
|
|
||||||
)
|
|
||||||
expect(targetLocations).toContain(item1ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item2ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item3ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item4ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item5ExpectedTargetPath)
|
|
||||||
|
|
||||||
for (const downloadItem of specification.filesToDownload) {
|
|
||||||
if (downloadItem.targetPath === item1ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file1Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item2ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file2Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item3ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file3Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item4ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file4Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item5ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file5Path)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw new Error('this should never be reached')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(specification.directoryStructure.length).toEqual(3)
|
|
||||||
expect(specification.directoryStructure).toContain(
|
|
||||||
path.join(testDownloadPath, artifact1Name)
|
|
||||||
)
|
|
||||||
expect(specification.directoryStructure).toContain(
|
|
||||||
path.join(testDownloadPath, dir1Path)
|
|
||||||
)
|
|
||||||
expect(specification.directoryStructure).toContain(
|
|
||||||
path.join(testDownloadPath, dir4Path)
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(specification.emptyFilesToCreate.length).toEqual(1)
|
|
||||||
expect(specification.emptyFilesToCreate).toContain(item6ExpectedTargetPath)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Download Specification - Relative Path with root directory', () => {
|
|
||||||
const testDownloadPath = path.join('some', 'destination', 'folder')
|
|
||||||
|
|
||||||
const specification = getDownloadSpecification(
|
|
||||||
artifact1Name,
|
|
||||||
artifactContainerEntries,
|
|
||||||
testDownloadPath,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(specification.rootDownloadLocation).toEqual(
|
|
||||||
path.join(testDownloadPath, artifact1Name)
|
|
||||||
)
|
|
||||||
expect(specification.filesToDownload.length).toEqual(5)
|
|
||||||
|
|
||||||
const item1ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'file1.txt'
|
|
||||||
)
|
|
||||||
const item2ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'file2.txt'
|
|
||||||
)
|
|
||||||
const item3ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'dir1',
|
|
||||||
'file3.txt'
|
|
||||||
)
|
|
||||||
const item4ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file4.txt'
|
|
||||||
)
|
|
||||||
const item5ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file5.txt'
|
|
||||||
)
|
|
||||||
const item6ExpectedTargetPath = path.join(
|
|
||||||
testDownloadPath,
|
|
||||||
artifact1Name,
|
|
||||||
'dir1',
|
|
||||||
'dir2',
|
|
||||||
'dir3',
|
|
||||||
'dir4',
|
|
||||||
'file6.txt'
|
|
||||||
)
|
|
||||||
|
|
||||||
const targetLocations = specification.filesToDownload.map(
|
|
||||||
item => item.targetPath
|
|
||||||
)
|
|
||||||
expect(targetLocations).toContain(item1ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item2ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item3ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item4ExpectedTargetPath)
|
|
||||||
expect(targetLocations).toContain(item5ExpectedTargetPath)
|
|
||||||
|
|
||||||
for (const downloadItem of specification.filesToDownload) {
|
|
||||||
if (downloadItem.targetPath === item1ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file1Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item2ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file2Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item3ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file3Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item4ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file4Path)
|
|
||||||
)
|
|
||||||
} else if (downloadItem.targetPath === item5ExpectedTargetPath) {
|
|
||||||
expect(downloadItem.sourceLocation).toEqual(
|
|
||||||
createContentLocation(file5Path)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw new Error('this should never be reached')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(specification.directoryStructure.length).toEqual(3)
|
|
||||||
expect(specification.directoryStructure).toContain(
|
|
||||||
path.join(testDownloadPath, artifact1Name)
|
|
||||||
)
|
|
||||||
expect(specification.directoryStructure).toContain(
|
|
||||||
path.join(testDownloadPath, dir1Path)
|
|
||||||
)
|
|
||||||
expect(specification.directoryStructure).toContain(
|
|
||||||
path.join(testDownloadPath, dir4Path)
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(specification.emptyFilesToCreate.length).toEqual(1)
|
|
||||||
expect(specification.emptyFilesToCreate).toContain(item6ExpectedTargetPath)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,490 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import * as http from 'http'
|
|
||||||
import * as io from '../../io/src/io'
|
|
||||||
import * as net from 'net'
|
|
||||||
import * as path from 'path'
|
|
||||||
import * as configVariables from '../src/internal/config-variables'
|
|
||||||
import {promises as fs} from 'fs'
|
|
||||||
import {DownloadItem} from '../src/internal/download-specification'
|
|
||||||
import {HttpClient, HttpClientResponse} from '@actions/http-client'
|
|
||||||
import {DownloadHttpClient} from '../src/internal/download-http-client'
|
|
||||||
import {
|
|
||||||
ListArtifactsResponse,
|
|
||||||
QueryArtifactResponse
|
|
||||||
} from '../src/internal/contracts'
|
|
||||||
import * as stream from 'stream'
|
|
||||||
import {gzip} from 'zlib'
|
|
||||||
import {promisify} from 'util'
|
|
||||||
|
|
||||||
const root = path.join(__dirname, '_temp', 'artifact-download-tests')
|
|
||||||
const defaultEncoding = 'utf8'
|
|
||||||
|
|
||||||
jest.mock('../src/internal/config-variables')
|
|
||||||
jest.mock('@actions/http-client')
|
|
||||||
|
|
||||||
describe('Download Tests', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
await io.rmRF(root)
|
|
||||||
await fs.mkdir(path.join(root), {
|
|
||||||
recursive: true
|
|
||||||
})
|
|
||||||
|
|
||||||
// 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(() => {})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Listing Artifacts
|
|
||||||
*/
|
|
||||||
it('List Artifacts - Success', async () => {
|
|
||||||
setupSuccessfulListArtifactsResponse()
|
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
|
||||||
const artifacts = await downloadHttpClient.listArtifacts()
|
|
||||||
expect(artifacts.count).toEqual(2)
|
|
||||||
|
|
||||||
const artifactNames = artifacts.value.map(item => item.name)
|
|
||||||
expect(artifactNames).toContain('artifact1-name')
|
|
||||||
expect(artifactNames).toContain('artifact2-name')
|
|
||||||
|
|
||||||
for (const artifact of artifacts.value) {
|
|
||||||
if (artifact.name === 'artifact1-name') {
|
|
||||||
expect(artifact.url).toEqual(
|
|
||||||
`${configVariables.getRuntimeUrl()}_apis/pipelines/1/runs/1/artifacts?artifactName=artifact1-name`
|
|
||||||
)
|
|
||||||
} else if (artifact.name === 'artifact2-name') {
|
|
||||||
expect(artifact.url).toEqual(
|
|
||||||
`${configVariables.getRuntimeUrl()}_apis/pipelines/1/runs/1/artifacts?artifactName=artifact2-name`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
'Invalid artifact combination. This should never be reached'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('List Artifacts - Failure', async () => {
|
|
||||||
setupFailedResponse()
|
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
|
||||||
expect(downloadHttpClient.listArtifacts()).rejects.toThrow(
|
|
||||||
'List Artifacts failed: Artifact service responded with 400'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Test Container Items
|
|
||||||
*/
|
|
||||||
it('Container Items - Success', async () => {
|
|
||||||
setupSuccessfulContainerItemsResponse()
|
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
|
||||||
const response = await downloadHttpClient.getContainerItems(
|
|
||||||
'artifact-name',
|
|
||||||
configVariables.getRuntimeUrl()
|
|
||||||
)
|
|
||||||
expect(response.count).toEqual(2)
|
|
||||||
|
|
||||||
const itemPaths = response.value.map(item => item.path)
|
|
||||||
expect(itemPaths).toContain('artifact-name')
|
|
||||||
expect(itemPaths).toContain('artifact-name/file1.txt')
|
|
||||||
|
|
||||||
for (const containerEntry of response.value) {
|
|
||||||
if (containerEntry.path === 'artifact-name') {
|
|
||||||
expect(containerEntry.itemType).toEqual('folder')
|
|
||||||
} else if (containerEntry.path === 'artifact-name/file1.txt') {
|
|
||||||
expect(containerEntry.itemType).toEqual('file')
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
'Invalid container combination. This should never be reached'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Container Items - Failure', async () => {
|
|
||||||
setupFailedResponse()
|
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
|
||||||
expect(
|
|
||||||
downloadHttpClient.getContainerItems(
|
|
||||||
'artifact-name',
|
|
||||||
configVariables.getRuntimeUrl()
|
|
||||||
)
|
|
||||||
).rejects.toThrow(
|
|
||||||
`Get Container Items failed: Artifact service responded with 400`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test downloading an individual artifact with gzip', async () => {
|
|
||||||
const fileContents = Buffer.from(
|
|
||||||
'gzip worked on the first try\n',
|
|
||||||
defaultEncoding
|
|
||||||
)
|
|
||||||
const targetPath = path.join(root, 'FileA.txt')
|
|
||||||
|
|
||||||
setupDownloadItemResponse(fileContents, true, 200, false, false)
|
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
|
||||||
|
|
||||||
const items: DownloadItem[] = []
|
|
||||||
items.push({
|
|
||||||
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileA.txt`,
|
|
||||||
targetPath
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
downloadHttpClient.downloadSingleArtifact(items)
|
|
||||||
).resolves.not.toThrow()
|
|
||||||
|
|
||||||
await checkDestinationFile(targetPath, fileContents)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test downloading an individual artifact without gzip', async () => {
|
|
||||||
const fileContents = Buffer.from(
|
|
||||||
'plaintext worked on the first try\n',
|
|
||||||
defaultEncoding
|
|
||||||
)
|
|
||||||
const targetPath = path.join(root, 'FileB.txt')
|
|
||||||
|
|
||||||
setupDownloadItemResponse(fileContents, false, 200, false, false)
|
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
|
||||||
|
|
||||||
const items: DownloadItem[] = []
|
|
||||||
items.push({
|
|
||||||
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileB.txt`,
|
|
||||||
targetPath
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
downloadHttpClient.downloadSingleArtifact(items)
|
|
||||||
).resolves.not.toThrow()
|
|
||||||
|
|
||||||
await checkDestinationFile(targetPath, fileContents)
|
|
||||||
})
|
|
||||||
|
|
||||||
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 download should successfully finish
|
|
||||||
const retryableStatusCodes = [429, 500, 502, 503, 504]
|
|
||||||
for (const statusCode of retryableStatusCodes) {
|
|
||||||
const fileContents = Buffer.from('try, try again\n', defaultEncoding)
|
|
||||||
const targetPath = path.join(root, `FileC-${statusCode}.txt`)
|
|
||||||
|
|
||||||
setupDownloadItemResponse(fileContents, false, statusCode, false, true)
|
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
|
||||||
|
|
||||||
const items: DownloadItem[] = []
|
|
||||||
items.push({
|
|
||||||
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileC.txt`,
|
|
||||||
targetPath
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
downloadHttpClient.downloadSingleArtifact(items)
|
|
||||||
).resolves.not.toThrow()
|
|
||||||
|
|
||||||
await checkDestinationFile(targetPath, fileContents)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test retry on truncated response with gzip', async () => {
|
|
||||||
const fileContents = Buffer.from(
|
|
||||||
'Sometimes gunzip fails on the first try\n',
|
|
||||||
defaultEncoding
|
|
||||||
)
|
|
||||||
const targetPath = path.join(root, 'FileD.txt')
|
|
||||||
|
|
||||||
setupDownloadItemResponse(fileContents, true, 200, true, true)
|
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
|
||||||
|
|
||||||
const items: DownloadItem[] = []
|
|
||||||
items.push({
|
|
||||||
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileD.txt`,
|
|
||||||
targetPath
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
downloadHttpClient.downloadSingleArtifact(items)
|
|
||||||
).resolves.not.toThrow()
|
|
||||||
|
|
||||||
await checkDestinationFile(targetPath, fileContents)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test retry on truncated response without gzip', async () => {
|
|
||||||
const fileContents = Buffer.from(
|
|
||||||
'You have to inspect the content-length header to know if you got everything\n',
|
|
||||||
defaultEncoding
|
|
||||||
)
|
|
||||||
const targetPath = path.join(root, 'FileE.txt')
|
|
||||||
|
|
||||||
setupDownloadItemResponse(fileContents, false, 200, true, true)
|
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
|
||||||
|
|
||||||
const items: DownloadItem[] = []
|
|
||||||
items.push({
|
|
||||||
sourceLocation: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13?itemPath=my-artifact%2FFileD.txt`,
|
|
||||||
targetPath
|
|
||||||
})
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
downloadHttpClient.downloadSingleArtifact(items)
|
|
||||||
).resolves.not.toThrow()
|
|
||||||
|
|
||||||
await checkDestinationFile(targetPath, fileContents)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper used to setup mocking for the HttpClient
|
|
||||||
*/
|
|
||||||
async function emptyMockReadBody(): Promise<string> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setups up HTTP GET response for a successful listArtifacts() call
|
|
||||||
*/
|
|
||||||
function setupSuccessfulListArtifactsResponse(): void {
|
|
||||||
jest.spyOn(HttpClient.prototype, 'get').mockImplementationOnce(async () => {
|
|
||||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
|
||||||
let mockReadBody = emptyMockReadBody
|
|
||||||
|
|
||||||
mockMessage.statusCode = 201
|
|
||||||
const response: ListArtifactsResponse = {
|
|
||||||
count: 2,
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
containerId: '13',
|
|
||||||
size: -1,
|
|
||||||
signedContent: 'false',
|
|
||||||
fileContainerResourceUrl: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13`,
|
|
||||||
type: 'actions_storage',
|
|
||||||
name: 'artifact1-name',
|
|
||||||
url: `${configVariables.getRuntimeUrl()}_apis/pipelines/1/runs/1/artifacts?artifactName=artifact1-name`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
containerId: '13',
|
|
||||||
size: -1,
|
|
||||||
signedContent: 'false',
|
|
||||||
fileContainerResourceUrl: `${configVariables.getRuntimeUrl()}_apis/resources/Containers/13`,
|
|
||||||
type: 'actions_storage',
|
|
||||||
name: 'artifact2-name',
|
|
||||||
url: `${configVariables.getRuntimeUrl()}_apis/pipelines/1/runs/1/artifacts?artifactName=artifact2-name`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
const returnData: string = JSON.stringify(response, null, 2)
|
|
||||||
mockReadBody = async function(): Promise<string> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
resolve(returnData)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<HttpClientResponse>(resolve => {
|
|
||||||
resolve({
|
|
||||||
message: mockMessage,
|
|
||||||
readBody: mockReadBody
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setups up HTTP GET response for downloading items
|
|
||||||
* @param isGzip is the downloaded item gzip encoded
|
|
||||||
* @param firstHttpResponseCode the http response code that should be returned
|
|
||||||
*/
|
|
||||||
function setupDownloadItemResponse(
|
|
||||||
fileContents: Buffer,
|
|
||||||
isGzip: boolean,
|
|
||||||
firstHttpResponseCode: number,
|
|
||||||
truncateFirstResponse: boolean,
|
|
||||||
retryExpected: boolean
|
|
||||||
): void {
|
|
||||||
const spyInstance = jest
|
|
||||||
.spyOn(HttpClient.prototype, 'get')
|
|
||||||
.mockImplementationOnce(async () => {
|
|
||||||
if (firstHttpResponseCode === 200) {
|
|
||||||
const fullResponse = await constructResponse(isGzip, fileContents)
|
|
||||||
const actualResponse = truncateFirstResponse
|
|
||||||
? fullResponse.subarray(0, 3)
|
|
||||||
: fullResponse
|
|
||||||
|
|
||||||
return {
|
|
||||||
message: getDownloadResponseMessage(
|
|
||||||
firstHttpResponseCode,
|
|
||||||
isGzip,
|
|
||||||
fullResponse.length,
|
|
||||||
actualResponse
|
|
||||||
),
|
|
||||||
readBody: emptyMockReadBody
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
message: getDownloadResponseMessage(
|
|
||||||
firstHttpResponseCode,
|
|
||||||
false,
|
|
||||||
0,
|
|
||||||
null
|
|
||||||
),
|
|
||||||
readBody: emptyMockReadBody
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// set up a second mock only if we expect a retry. Otherwise this mock will affect other tests.
|
|
||||||
if (retryExpected) {
|
|
||||||
spyInstance.mockImplementationOnce(async () => {
|
|
||||||
// chained response, if the HTTP GET function gets called again, return a successful response
|
|
||||||
const fullResponse = await constructResponse(isGzip, fileContents)
|
|
||||||
return {
|
|
||||||
message: getDownloadResponseMessage(
|
|
||||||
200,
|
|
||||||
isGzip,
|
|
||||||
fullResponse.length,
|
|
||||||
fullResponse
|
|
||||||
),
|
|
||||||
readBody: emptyMockReadBody
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function constructResponse(
|
|
||||||
isGzip: boolean,
|
|
||||||
plaintext: Buffer | string
|
|
||||||
): Promise<Buffer> {
|
|
||||||
if (isGzip) {
|
|
||||||
return await promisify(gzip)(plaintext)
|
|
||||||
} else if (typeof plaintext === 'string') {
|
|
||||||
return Buffer.from(plaintext, defaultEncoding)
|
|
||||||
} else {
|
|
||||||
return plaintext
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDownloadResponseMessage(
|
|
||||||
httpResponseCode: number,
|
|
||||||
isGzip: boolean,
|
|
||||||
contentLength: number,
|
|
||||||
response: Buffer | null
|
|
||||||
): http.IncomingMessage {
|
|
||||||
let readCallCount = 0
|
|
||||||
const mockMessage = <http.IncomingMessage>new stream.Readable({
|
|
||||||
read(size) {
|
|
||||||
switch (readCallCount++) {
|
|
||||||
case 0:
|
|
||||||
if (!!response && response.byteLength > size) {
|
|
||||||
throw new Error(
|
|
||||||
`test response larger than requested size (${size})`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
this.push(response)
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
// end the stream
|
|
||||||
this.push(null)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
mockMessage.statusCode = httpResponseCode
|
|
||||||
mockMessage.headers = {
|
|
||||||
'content-length': contentLength.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGzip) {
|
|
||||||
mockMessage.headers['content-encoding'] = 'gzip'
|
|
||||||
}
|
|
||||||
|
|
||||||
return mockMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setups up HTTP GET response when querying for container items
|
|
||||||
*/
|
|
||||||
function setupSuccessfulContainerItemsResponse(): void {
|
|
||||||
jest.spyOn(HttpClient.prototype, 'get').mockImplementationOnce(async () => {
|
|
||||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
|
||||||
let mockReadBody = emptyMockReadBody
|
|
||||||
|
|
||||||
mockMessage.statusCode = 201
|
|
||||||
const response: QueryArtifactResponse = {
|
|
||||||
count: 2,
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
containerId: 10000,
|
|
||||||
scopeIdentifier: '00000000-0000-0000-0000-000000000000',
|
|
||||||
path: 'artifact-name',
|
|
||||||
itemType: 'folder',
|
|
||||||
status: 'created',
|
|
||||||
dateCreated: '2020-02-06T22:13:35.373Z',
|
|
||||||
dateLastModified: '2020-02-06T22:13:35.453Z',
|
|
||||||
createdBy: '82f0bf89-6e55-4e5a-b8b6-f75eb992578c',
|
|
||||||
lastModifiedBy: '82f0bf89-6e55-4e5a-b8b6-f75eb992578c',
|
|
||||||
itemLocation: `${configVariables.getRuntimeUrl()}/_apis/resources/Containers/10000?itemPath=artifact-name&metadata=True`,
|
|
||||||
contentLocation: `${configVariables.getRuntimeUrl()}/_apis/resources/Containers/10000?itemPath=artifact-name`,
|
|
||||||
contentId: ''
|
|
||||||
},
|
|
||||||
{
|
|
||||||
containerId: 10000,
|
|
||||||
scopeIdentifier: '00000000-0000-0000-0000-000000000000',
|
|
||||||
path: 'artifact-name/file1.txt',
|
|
||||||
itemType: 'file',
|
|
||||||
status: 'created',
|
|
||||||
dateCreated: '2020-02-06T22:13:35.373Z',
|
|
||||||
dateLastModified: '2020-02-06T22:13:35.453Z',
|
|
||||||
createdBy: '82f0bf89-6e55-4e5a-b8b6-f75eb992578c',
|
|
||||||
lastModifiedBy: '82f0bf89-6e55-4e5a-b8b6-f75eb992578c',
|
|
||||||
itemLocation: `${configVariables.getRuntimeUrl()}/_apis/resources/Containers/10000?itemPath=artifact-name%2Ffile1.txt&metadata=True`,
|
|
||||||
contentLocation: `${configVariables.getRuntimeUrl()}/_apis/resources/Containers/10000?itemPath=artifact-name%2Ffile1.txt`,
|
|
||||||
contentId: ''
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
const returnData: string = JSON.stringify(response, null, 2)
|
|
||||||
mockReadBody = async function(): Promise<string> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
resolve(returnData)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<HttpClientResponse>(resolve => {
|
|
||||||
resolve({
|
|
||||||
message: mockMessage,
|
|
||||||
readBody: mockReadBody
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Setups up HTTP GET response for a generic failed request
|
|
||||||
*/
|
|
||||||
function setupFailedResponse(): void {
|
|
||||||
jest.spyOn(HttpClient.prototype, 'get').mockImplementationOnce(async () => {
|
|
||||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
|
||||||
mockMessage.statusCode = 400
|
|
||||||
return new Promise<HttpClientResponse>(resolve => {
|
|
||||||
resolve({
|
|
||||||
message: mockMessage,
|
|
||||||
readBody: emptyMockReadBody
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkDestinationFile(
|
|
||||||
targetPath: string,
|
|
||||||
expectedContents: Buffer
|
|
||||||
): Promise<void> {
|
|
||||||
const fileContents = await fs.readFile(targetPath)
|
|
||||||
|
|
||||||
expect(fileContents.byteLength).toEqual(expectedContents.byteLength)
|
|
||||||
expect(fileContents.equals(expectedContents)).toBeTruthy()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
checkArtifactName,
|
validateArtifactName,
|
||||||
checkArtifactFilePath
|
validateFilePath
|
||||||
} from '../src/internal/path-and-artifact-name-validation'
|
} from '../src/internal/upload/path-and-artifact-name-validation'
|
||||||
|
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
|
|
||||||
describe('Path and artifact name validation', () => {
|
describe('Path and artifact name validation', () => {
|
||||||
@@ -28,7 +29,7 @@ describe('Path and artifact name validation', () => {
|
|||||||
]
|
]
|
||||||
for (const invalidName of invalidNames) {
|
for (const invalidName of invalidNames) {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
checkArtifactName(invalidName)
|
validateArtifactName(invalidName)
|
||||||
}).toThrow()
|
}).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ describe('Path and artifact name validation', () => {
|
|||||||
]
|
]
|
||||||
for (const validName of validNames) {
|
for (const validName of validNames) {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
checkArtifactName(validName)
|
validateArtifactName(validName)
|
||||||
}).not.toThrow()
|
}).not.toThrow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -60,7 +61,7 @@ describe('Path and artifact name validation', () => {
|
|||||||
]
|
]
|
||||||
for (const invalidName of invalidNames) {
|
for (const invalidName of invalidNames) {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
checkArtifactFilePath(invalidName)
|
validateFilePath(invalidName)
|
||||||
}).toThrow()
|
}).toThrow()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ describe('Path and artifact name validation', () => {
|
|||||||
]
|
]
|
||||||
for (const validName of validNames) {
|
for (const validName of validNames) {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
checkArtifactFilePath(validName)
|
validateFilePath(validName)
|
||||||
}).not.toThrow()
|
}).not.toThrow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {Timestamp} from '../src/generated'
|
||||||
|
import * as retention from '../src/internal/upload/retention'
|
||||||
|
|
||||||
|
describe('retention', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
delete process.env['GITHUB_RETENTION_DAYS']
|
||||||
|
})
|
||||||
|
it('should return the inputted retention days if it is less than the max retention days', () => {
|
||||||
|
// setup
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest.useFakeTimers().setSystemTime(mockDate)
|
||||||
|
process.env['GITHUB_RETENTION_DAYS'] = '90'
|
||||||
|
|
||||||
|
const exp = retention.getExpiration(30)
|
||||||
|
|
||||||
|
expect(exp).toBeDefined()
|
||||||
|
if (exp) {
|
||||||
|
const expDate = Timestamp.toDate(exp)
|
||||||
|
const expected = new Date()
|
||||||
|
expected.setDate(expected.getDate() + 30)
|
||||||
|
|
||||||
|
expect(expDate).toEqual(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the max retention days if the inputted retention days is greater than the max retention days', () => {
|
||||||
|
// setup
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest.useFakeTimers().setSystemTime(mockDate)
|
||||||
|
process.env['GITHUB_RETENTION_DAYS'] = '90'
|
||||||
|
|
||||||
|
const exp = retention.getExpiration(120)
|
||||||
|
|
||||||
|
expect(exp).toBeDefined()
|
||||||
|
if (exp) {
|
||||||
|
const expDate = Timestamp.toDate(exp) // we check whether exp is defined above
|
||||||
|
const expected = new Date()
|
||||||
|
expected.setDate(expected.getDate() + 90)
|
||||||
|
|
||||||
|
expect(expDate).toEqual(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return undefined if the inputted retention days is undefined', () => {
|
||||||
|
const exp = retention.getExpiration()
|
||||||
|
expect(exp).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return the inputted retention days if there is no max retention days', () => {
|
||||||
|
// setup
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest.useFakeTimers().setSystemTime(mockDate)
|
||||||
|
|
||||||
|
const exp = retention.getExpiration(30)
|
||||||
|
|
||||||
|
expect(exp).toBeDefined()
|
||||||
|
if (exp) {
|
||||||
|
const expDate = Timestamp.toDate(exp) // we check whether exp is defined above
|
||||||
|
const expected = new Date()
|
||||||
|
expected.setDate(expected.getDate() + 30)
|
||||||
|
|
||||||
|
expect(expDate).toEqual(expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import * as http from 'http'
|
|
||||||
import * as net from 'net'
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import * as configVariables from '../src/internal/config-variables'
|
|
||||||
import {retry} from '../src/internal/requestUtils'
|
|
||||||
import {HttpClientResponse} from '@actions/http-client'
|
|
||||||
|
|
||||||
jest.mock('../src/internal/config-variables')
|
|
||||||
|
|
||||||
interface ITestResult {
|
|
||||||
responseCode: number
|
|
||||||
errorMessage: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testRetry(
|
|
||||||
responseCodes: number[],
|
|
||||||
expectedResult: ITestResult
|
|
||||||
): Promise<void> {
|
|
||||||
const reverse = responseCodes.reverse() // Reverse responses since we pop from end
|
|
||||||
if (expectedResult.errorMessage) {
|
|
||||||
// we expect some exception to be thrown
|
|
||||||
expect(
|
|
||||||
retry(
|
|
||||||
'test',
|
|
||||||
async () => handleResponse(reverse.pop()),
|
|
||||||
new Map(), // extra error message for any particular http codes
|
|
||||||
configVariables.getRetryLimit()
|
|
||||||
)
|
|
||||||
).rejects.toThrow(expectedResult.errorMessage)
|
|
||||||
} else {
|
|
||||||
// we expect a correct status code to be returned
|
|
||||||
const actualResult = await retry(
|
|
||||||
'test',
|
|
||||||
async () => handleResponse(reverse.pop()),
|
|
||||||
new Map(), // extra error message for any particular http codes
|
|
||||||
configVariables.getRetryLimit()
|
|
||||||
)
|
|
||||||
expect(actualResult.message.statusCode).toEqual(expectedResult.responseCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleResponse(
|
|
||||||
testResponseCode: number | undefined
|
|
||||||
): Promise<HttpClientResponse> {
|
|
||||||
if (!testResponseCode) {
|
|
||||||
throw new Error(
|
|
||||||
'Test incorrectly set up. reverse.pop() was called too many times so not enough test response codes were supplied'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return setupSingleMockResponse(testResponseCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(() => {})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helpers used to setup mocking for the HttpClient
|
|
||||||
*/
|
|
||||||
async function emptyMockReadBody(): Promise<string> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setupSingleMockResponse(
|
|
||||||
statusCode: number
|
|
||||||
): Promise<HttpClientResponse> {
|
|
||||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
|
||||||
const mockReadBody = emptyMockReadBody
|
|
||||||
mockMessage.statusCode = statusCode
|
|
||||||
return new Promise<HttpClientResponse>(resolve => {
|
|
||||||
resolve({
|
|
||||||
message: mockMessage,
|
|
||||||
readBody: mockReadBody
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
test('retry works on successful response', async () => {
|
|
||||||
await testRetry([200], {
|
|
||||||
responseCode: 200,
|
|
||||||
errorMessage: null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('retry works after retryable status code', async () => {
|
|
||||||
await testRetry([503, 200], {
|
|
||||||
responseCode: 200,
|
|
||||||
errorMessage: null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('retry fails after exhausting retries', async () => {
|
|
||||||
// __mocks__/config-variables caps the max retry count in tests to 2
|
|
||||||
await testRetry([503, 503, 200], {
|
|
||||||
responseCode: 200,
|
|
||||||
errorMessage: 'test failed: Artifact service responded with 503'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
test('retry fails after non-retryable status code', async () => {
|
|
||||||
await testRetry([400, 200], {
|
|
||||||
responseCode: 400,
|
|
||||||
errorMessage: 'test failed: Artifact service responded with 400'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
path="$1"
|
|
||||||
expectedContent="$2"
|
|
||||||
|
|
||||||
if [ "$path" == "" ]; then
|
|
||||||
echo "File path not provided"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$expectedContent" == "" ]; then
|
|
||||||
echo "Expected file contents not provided"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ ! -f "$path" ]; then
|
|
||||||
echo "Expected file $path does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
actualContent=$(cat "$path")
|
|
||||||
if [ "$expectedContent" == "_EMPTY_" ] && [ ! -s "$path" ]; then
|
|
||||||
exit 0
|
|
||||||
elif [ "$actualContent" != "$expectedContent" ]; then
|
|
||||||
echo "File contents are not correct, expected $expectedContent, received $actualContent"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as uploadZipSpecification from '../src/internal/upload/upload-zip-specification'
|
||||||
|
import * as zip from '../src/internal/upload/zip'
|
||||||
|
import * as util from '../src/internal/shared/util'
|
||||||
|
import * as retention from '../src/internal/upload/retention'
|
||||||
|
import * as config from '../src/internal/shared/config'
|
||||||
|
import {Timestamp, ArtifactServiceClientJSON} from '../src/generated'
|
||||||
|
import * as blobUpload from '../src/internal/upload/blob-upload'
|
||||||
|
import {uploadArtifact} from '../src/internal/upload/upload-artifact'
|
||||||
|
|
||||||
|
describe('upload-artifact', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// 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(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should successfully upload an artifact', () => {
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file1.txt',
|
||||||
|
destinationPath: 'file1.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file2.txt',
|
||||||
|
destinationPath: 'file2.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
|
||||||
|
destinationPath: 'dir/file3.txt'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(zip, 'createZipUploadStream')
|
||||||
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||||
|
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678'
|
||||||
|
})
|
||||||
|
jest
|
||||||
|
.spyOn(retention, 'getExpiration')
|
||||||
|
.mockReturnValue(Timestamp.fromDate(mockDate))
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||||
|
.mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
signedUploadUrl: 'https://signed-upload-url.com'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
jest.spyOn(blobUpload, 'uploadZipToBlobStorage').mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
isSuccess: true,
|
||||||
|
uploadSize: 1234,
|
||||||
|
md5Hash: 'test-md5-hash'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
|
||||||
|
.mockReturnValue(Promise.resolve({ok: true, artifactId: '1'}))
|
||||||
|
|
||||||
|
// ArtifactHttpClient mocks
|
||||||
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getResultsServiceUrl')
|
||||||
|
.mockReturnValue('https://test-url.com')
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).resolves.toEqual({success: true, size: 1234, id: 1})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should throw an error if the root directory is invalid', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockImplementation(() => {
|
||||||
|
throw new Error('Invalid root directory')
|
||||||
|
})
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).rejects.toThrow('Invalid root directory')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if there are no files to upload', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([])
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
expect(uploadResp).resolves.toEqual({success: false})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if no backend IDs are found', () => {
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file1.txt',
|
||||||
|
destinationPath: 'file1.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file2.txt',
|
||||||
|
destinationPath: 'file2.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
|
||||||
|
destinationPath: 'dir/file3.txt'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(zip, 'createZipUploadStream')
|
||||||
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||||
|
jest
|
||||||
|
.spyOn(util, 'getBackendIdsFromToken')
|
||||||
|
.mockReturnValue({workflowRunBackendId: '', workflowJobRunBackendId: ''})
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).resolves.toEqual({success: false})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if the creation request fails', () => {
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file1.txt',
|
||||||
|
destinationPath: 'file1.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file2.txt',
|
||||||
|
destinationPath: 'file2.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
|
||||||
|
destinationPath: 'dir/file3.txt'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(zip, 'createZipUploadStream')
|
||||||
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||||
|
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678'
|
||||||
|
})
|
||||||
|
jest
|
||||||
|
.spyOn(retention, 'getExpiration')
|
||||||
|
.mockReturnValue(Timestamp.fromDate(mockDate))
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||||
|
.mockReturnValue(Promise.resolve({ok: false, signedUploadUrl: ''}))
|
||||||
|
|
||||||
|
// ArtifactHttpClient mocks
|
||||||
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getResultsServiceUrl')
|
||||||
|
.mockReturnValue('https://test-url.com')
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).resolves.toEqual({success: false})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if blob storage upload is unsuccessful', () => {
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file1.txt',
|
||||||
|
destinationPath: 'file1.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file2.txt',
|
||||||
|
destinationPath: 'file2.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
|
||||||
|
destinationPath: 'dir/file3.txt'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(zip, 'createZipUploadStream')
|
||||||
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||||
|
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678'
|
||||||
|
})
|
||||||
|
jest
|
||||||
|
.spyOn(retention, 'getExpiration')
|
||||||
|
.mockReturnValue(Timestamp.fromDate(mockDate))
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||||
|
.mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
signedUploadUrl: 'https://signed-upload-url.com'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
jest
|
||||||
|
.spyOn(blobUpload, 'uploadZipToBlobStorage')
|
||||||
|
.mockReturnValue(Promise.resolve({isSuccess: false}))
|
||||||
|
|
||||||
|
// ArtifactHttpClient mocks
|
||||||
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getResultsServiceUrl')
|
||||||
|
.mockReturnValue('https://test-url.com')
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).resolves.toEqual({success: false})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if finalize artifact fails', () => {
|
||||||
|
const mockDate = new Date('2020-01-01')
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'validateRootDirectory')
|
||||||
|
.mockReturnValue()
|
||||||
|
jest
|
||||||
|
.spyOn(uploadZipSpecification, 'getUploadZipSpecification')
|
||||||
|
.mockReturnValue([
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file1.txt',
|
||||||
|
destinationPath: 'file1.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/file2.txt',
|
||||||
|
destinationPath: 'file2.txt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sourcePath: '/home/user/files/plz-upload/dir/file3.txt',
|
||||||
|
destinationPath: 'dir/file3.txt'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
jest
|
||||||
|
.spyOn(zip, 'createZipUploadStream')
|
||||||
|
.mockReturnValue(Promise.resolve(new zip.ZipUploadStream(1)))
|
||||||
|
jest.spyOn(util, 'getBackendIdsFromToken').mockReturnValue({
|
||||||
|
workflowRunBackendId: '1234',
|
||||||
|
workflowJobRunBackendId: '5678'
|
||||||
|
})
|
||||||
|
jest
|
||||||
|
.spyOn(retention, 'getExpiration')
|
||||||
|
.mockReturnValue(Timestamp.fromDate(mockDate))
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'CreateArtifact')
|
||||||
|
.mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
signedUploadUrl: 'https://signed-upload-url.com'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
jest.spyOn(blobUpload, 'uploadZipToBlobStorage').mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
isSuccess: true,
|
||||||
|
uploadSize: 1234,
|
||||||
|
md5Hash: 'test-md5-hash'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
jest
|
||||||
|
.spyOn(ArtifactServiceClientJSON.prototype, 'FinalizeArtifact')
|
||||||
|
.mockReturnValue(Promise.resolve({ok: false, artifactId: ''}))
|
||||||
|
|
||||||
|
// ArtifactHttpClient mocks
|
||||||
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('test-token')
|
||||||
|
jest
|
||||||
|
.spyOn(config, 'getResultsServiceUrl')
|
||||||
|
.mockReturnValue('https://test-url.com')
|
||||||
|
|
||||||
|
const uploadResp = uploadArtifact(
|
||||||
|
'test-artifact',
|
||||||
|
[
|
||||||
|
'/home/user/files/plz-upload/file1.txt',
|
||||||
|
'/home/user/files/plz-upload/file2.txt',
|
||||||
|
'/home/user/files/plz-upload/dir/file3.txt'
|
||||||
|
],
|
||||||
|
'/home/user/files/plz-upload'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(uploadResp).resolves.toEqual({success: false})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
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 tempGzipFilePath = path.join(root, 'file1.gzip')
|
|
||||||
const tempZipFilePath = path.join(root, 'file2.zip')
|
|
||||||
const tempTarlzFilePath = path.join(root, 'file3.tar.lz')
|
|
||||||
const tempGzFilePath = path.join(root, 'file4.tar.gz')
|
|
||||||
const tempBz2FilePath = path.join(root, 'file5.tar.bz2')
|
|
||||||
const temp7zFilePath = path.join(root, 'file6.7z')
|
|
||||||
const tempNormalFilePath = path.join(root, 'file6.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(tempGzipFilePath, 'a file with a .gzip file extension')
|
|
||||||
await fs.writeFile(tempZipFilePath, 'a file with a .zip file extension')
|
|
||||||
await fs.writeFile(tempTarlzFilePath, 'a file with a tar.lz file extension')
|
|
||||||
await fs.writeFile(tempGzFilePath, 'a file with a gz file file extension')
|
|
||||||
await fs.writeFile(tempBz2FilePath, 'a file with a .bz2 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(tempGzipFilePath, tempFile.path)).toEqual(
|
|
||||||
Number.MAX_SAFE_INTEGER
|
|
||||||
)
|
|
||||||
expect(await createGZipFileOnDisk(tempZipFilePath, tempFile.path)).toEqual(
|
|
||||||
Number.MAX_SAFE_INTEGER
|
|
||||||
)
|
|
||||||
expect(await createGZipFileOnDisk(tempTarlzFilePath, tempFile.path)).toEqual(
|
|
||||||
Number.MAX_SAFE_INTEGER
|
|
||||||
)
|
|
||||||
expect(await createGZipFileOnDisk(tempGzFilePath, tempFile.path)).toEqual(
|
|
||||||
Number.MAX_SAFE_INTEGER
|
|
||||||
)
|
|
||||||
expect(await createGZipFileOnDisk(tempBz2FilePath, 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)
|
|
||||||
})
|
|
||||||
+84
-125
@@ -2,9 +2,11 @@ import * as io from '../../io/src/io'
|
|||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import {promises as fs} from 'fs'
|
import {promises as fs} from 'fs'
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import {getUploadSpecification} from '../src/internal/upload-specification'
|
import {
|
||||||
|
getUploadZipSpecification,
|
||||||
|
validateRootDirectory
|
||||||
|
} from '../src/internal/upload/upload-zip-specification'
|
||||||
|
|
||||||
const artifactName = 'my-artifact'
|
|
||||||
const root = path.join(__dirname, '_temp', 'upload-specification')
|
const root = path.join(__dirname, '_temp', 'upload-specification')
|
||||||
const goodItem1Path = path.join(
|
const goodItem1Path = path.join(
|
||||||
root,
|
root,
|
||||||
@@ -125,31 +127,28 @@ describe('Search', () => {
|
|||||||
'upload-specification-invalid'
|
'upload-specification-invalid'
|
||||||
)
|
)
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getUploadSpecification(
|
validateRootDirectory(invalidRootDirectory)
|
||||||
artifactName,
|
}).toThrow(
|
||||||
invalidRootDirectory,
|
`The provided rootDirectory ${invalidRootDirectory} does not exist`
|
||||||
artifactFilesToUpload
|
)
|
||||||
)
|
|
||||||
}).toThrow(`Provided rootDirectory ${invalidRootDirectory} does not exist`)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Upload Specification - Fail invalid rootDirectory', async () => {
|
it('Upload Specification - Fail invalid rootDirectory', async () => {
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getUploadSpecification(artifactName, goodItem1Path, artifactFilesToUpload)
|
validateRootDirectory(goodItem1Path)
|
||||||
}).toThrow(
|
}).toThrow(
|
||||||
`Provided rootDirectory ${goodItem1Path} is not a valid directory`
|
`The provided rootDirectory ${goodItem1Path} is not a valid directory`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Upload Specification - File does not exist', async () => {
|
it('Upload Specification - File does not exist', async () => {
|
||||||
const fakeFilePath = path.join(
|
const fakeFilePath = path.join(
|
||||||
artifactName,
|
|
||||||
'folder-a',
|
'folder-a',
|
||||||
'folder-b',
|
'folder-b',
|
||||||
'non-existent-file.txt'
|
'non-existent-file.txt'
|
||||||
)
|
)
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getUploadSpecification(artifactName, root, [fakeFilePath])
|
getUploadZipSpecification([fakeFilePath], root)
|
||||||
}).toThrow(`File ${fakeFilePath} does not exist`)
|
}).toThrow(`File ${fakeFilePath} does not exist`)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -162,21 +161,20 @@ describe('Search', () => {
|
|||||||
goodItem5Path
|
goodItem5Path
|
||||||
]
|
]
|
||||||
expect(() => {
|
expect(() => {
|
||||||
getUploadSpecification(artifactName, folderADirectory, artifactFiles)
|
getUploadZipSpecification(artifactFiles, folderADirectory)
|
||||||
}).toThrow(
|
}).toThrow(
|
||||||
`The rootDirectory: ${folderADirectory} is not a parent directory of the file: ${goodItem5Path}`
|
`The rootDirectory: ${folderADirectory} is not a parent directory of the file: ${goodItem5Path}`
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Upload Specification - Success', async () => {
|
it('Upload Specification - Success', async () => {
|
||||||
const specifications = getUploadSpecification(
|
const specifications = getUploadZipSpecification(
|
||||||
artifactName,
|
artifactFilesToUpload,
|
||||||
root,
|
root
|
||||||
artifactFilesToUpload
|
|
||||||
)
|
)
|
||||||
expect(specifications.length).toEqual(7)
|
expect(specifications.length).toEqual(7)
|
||||||
|
|
||||||
const absolutePaths = specifications.map(item => item.absoluteFilePath)
|
const absolutePaths = specifications.map(item => item.sourcePath)
|
||||||
expect(absolutePaths).toContain(goodItem1Path)
|
expect(absolutePaths).toContain(goodItem1Path)
|
||||||
expect(absolutePaths).toContain(goodItem2Path)
|
expect(absolutePaths).toContain(goodItem2Path)
|
||||||
expect(absolutePaths).toContain(goodItem3Path)
|
expect(absolutePaths).toContain(goodItem3Path)
|
||||||
@@ -186,45 +184,38 @@ describe('Search', () => {
|
|||||||
expect(absolutePaths).toContain(amazingFileInFolderHPath)
|
expect(absolutePaths).toContain(amazingFileInFolderHPath)
|
||||||
|
|
||||||
for (const specification of specifications) {
|
for (const specification of specifications) {
|
||||||
if (specification.absoluteFilePath === goodItem1Path) {
|
if (specification.sourcePath === goodItem1Path) {
|
||||||
expect(specification.uploadFilePath).toEqual(
|
expect(specification.destinationPath).toEqual(
|
||||||
|
path.join('/folder-a', 'folder-b', 'folder-c', 'good-item1.txt')
|
||||||
|
)
|
||||||
|
} else if (specification.sourcePath === goodItem2Path) {
|
||||||
|
expect(specification.destinationPath).toEqual(
|
||||||
|
path.join('/folder-d', 'good-item2.txt')
|
||||||
|
)
|
||||||
|
} else if (specification.sourcePath === goodItem3Path) {
|
||||||
|
expect(specification.destinationPath).toEqual(
|
||||||
|
path.join('/folder-d', 'good-item3.txt')
|
||||||
|
)
|
||||||
|
} else if (specification.sourcePath === goodItem4Path) {
|
||||||
|
expect(specification.destinationPath).toEqual(
|
||||||
|
path.join('/folder-d', 'good-item4.txt')
|
||||||
|
)
|
||||||
|
} else if (specification.sourcePath === goodItem5Path) {
|
||||||
|
expect(specification.destinationPath).toEqual(
|
||||||
|
path.join('/good-item5.txt')
|
||||||
|
)
|
||||||
|
} else if (specification.sourcePath === extraFileInFolderCPath) {
|
||||||
|
expect(specification.destinationPath).toEqual(
|
||||||
path.join(
|
path.join(
|
||||||
artifactName,
|
'/folder-a',
|
||||||
'folder-a',
|
|
||||||
'folder-b',
|
|
||||||
'folder-c',
|
|
||||||
'good-item1.txt'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === goodItem2Path) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(artifactName, 'folder-d', 'good-item2.txt')
|
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === goodItem3Path) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(artifactName, 'folder-d', 'good-item3.txt')
|
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === goodItem4Path) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(artifactName, 'folder-d', 'good-item4.txt')
|
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === goodItem5Path) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(artifactName, 'good-item5.txt')
|
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === extraFileInFolderCPath) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(
|
|
||||||
artifactName,
|
|
||||||
'folder-a',
|
|
||||||
'folder-b',
|
'folder-b',
|
||||||
'folder-c',
|
'folder-c',
|
||||||
'extra-file-in-folder-c.txt'
|
'extra-file-in-folder-c.txt'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else if (specification.absoluteFilePath === amazingFileInFolderHPath) {
|
} else if (specification.sourcePath === amazingFileInFolderHPath) {
|
||||||
expect(specification.uploadFilePath).toEqual(
|
expect(specification.destinationPath).toEqual(
|
||||||
path.join(artifactName, 'folder-h', 'amazing-item.txt')
|
path.join('/folder-h', 'amazing-item.txt')
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -236,14 +227,13 @@ describe('Search', () => {
|
|||||||
|
|
||||||
it('Upload Specification - Success with extra slash', async () => {
|
it('Upload Specification - Success with extra slash', async () => {
|
||||||
const rootWithSlash = `${root}/`
|
const rootWithSlash = `${root}/`
|
||||||
const specifications = getUploadSpecification(
|
const specifications = getUploadZipSpecification(
|
||||||
artifactName,
|
artifactFilesToUpload,
|
||||||
rootWithSlash,
|
rootWithSlash
|
||||||
artifactFilesToUpload
|
|
||||||
)
|
)
|
||||||
expect(specifications.length).toEqual(7)
|
expect(specifications.length).toEqual(7)
|
||||||
|
|
||||||
const absolutePaths = specifications.map(item => item.absoluteFilePath)
|
const absolutePaths = specifications.map(item => item.sourcePath)
|
||||||
expect(absolutePaths).toContain(goodItem1Path)
|
expect(absolutePaths).toContain(goodItem1Path)
|
||||||
expect(absolutePaths).toContain(goodItem2Path)
|
expect(absolutePaths).toContain(goodItem2Path)
|
||||||
expect(absolutePaths).toContain(goodItem3Path)
|
expect(absolutePaths).toContain(goodItem3Path)
|
||||||
@@ -253,45 +243,38 @@ describe('Search', () => {
|
|||||||
expect(absolutePaths).toContain(amazingFileInFolderHPath)
|
expect(absolutePaths).toContain(amazingFileInFolderHPath)
|
||||||
|
|
||||||
for (const specification of specifications) {
|
for (const specification of specifications) {
|
||||||
if (specification.absoluteFilePath === goodItem1Path) {
|
if (specification.sourcePath === goodItem1Path) {
|
||||||
expect(specification.uploadFilePath).toEqual(
|
expect(specification.destinationPath).toEqual(
|
||||||
|
path.join('/folder-a', 'folder-b', 'folder-c', 'good-item1.txt')
|
||||||
|
)
|
||||||
|
} else if (specification.sourcePath === goodItem2Path) {
|
||||||
|
expect(specification.destinationPath).toEqual(
|
||||||
|
path.join('/folder-d', 'good-item2.txt')
|
||||||
|
)
|
||||||
|
} else if (specification.sourcePath === goodItem3Path) {
|
||||||
|
expect(specification.destinationPath).toEqual(
|
||||||
|
path.join('/folder-d', 'good-item3.txt')
|
||||||
|
)
|
||||||
|
} else if (specification.sourcePath === goodItem4Path) {
|
||||||
|
expect(specification.destinationPath).toEqual(
|
||||||
|
path.join('/folder-d', 'good-item4.txt')
|
||||||
|
)
|
||||||
|
} else if (specification.sourcePath === goodItem5Path) {
|
||||||
|
expect(specification.destinationPath).toEqual(
|
||||||
|
path.join('/good-item5.txt')
|
||||||
|
)
|
||||||
|
} else if (specification.sourcePath === extraFileInFolderCPath) {
|
||||||
|
expect(specification.destinationPath).toEqual(
|
||||||
path.join(
|
path.join(
|
||||||
artifactName,
|
'/folder-a',
|
||||||
'folder-a',
|
|
||||||
'folder-b',
|
|
||||||
'folder-c',
|
|
||||||
'good-item1.txt'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === goodItem2Path) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(artifactName, 'folder-d', 'good-item2.txt')
|
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === goodItem3Path) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(artifactName, 'folder-d', 'good-item3.txt')
|
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === goodItem4Path) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(artifactName, 'folder-d', 'good-item4.txt')
|
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === goodItem5Path) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(artifactName, 'good-item5.txt')
|
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === extraFileInFolderCPath) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(
|
|
||||||
artifactName,
|
|
||||||
'folder-a',
|
|
||||||
'folder-b',
|
'folder-b',
|
||||||
'folder-c',
|
'folder-c',
|
||||||
'extra-file-in-folder-c.txt'
|
'extra-file-in-folder-c.txt'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else if (specification.absoluteFilePath === amazingFileInFolderHPath) {
|
} else if (specification.sourcePath === amazingFileInFolderHPath) {
|
||||||
expect(specification.uploadFilePath).toEqual(
|
expect(specification.destinationPath).toEqual(
|
||||||
path.join(artifactName, 'folder-h', 'amazing-item.txt')
|
path.join('/folder-h', 'amazing-item.txt')
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -301,47 +284,23 @@ describe('Search', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Upload Specification - Directories should not be included', async () => {
|
it('Upload Specification - Empty Directories are included', async () => {
|
||||||
const folderEPath = path.join(root, 'folder-a', 'folder-b', 'folder-e')
|
const folderEPath = path.join(root, 'folder-a', 'folder-b', 'folder-e')
|
||||||
const filesWithDirectory = [
|
const filesWithDirectory = [goodItem1Path, folderEPath]
|
||||||
goodItem1Path,
|
const specifications = getUploadZipSpecification(filesWithDirectory, root)
|
||||||
goodItem4Path,
|
expect(specifications.length).toEqual(2)
|
||||||
folderEPath,
|
const absolutePaths = specifications.map(item => item.sourcePath)
|
||||||
badItem3Path
|
|
||||||
]
|
|
||||||
const specifications = getUploadSpecification(
|
|
||||||
artifactName,
|
|
||||||
root,
|
|
||||||
filesWithDirectory
|
|
||||||
)
|
|
||||||
expect(specifications.length).toEqual(3)
|
|
||||||
const absolutePaths = specifications.map(item => item.absoluteFilePath)
|
|
||||||
expect(absolutePaths).toContain(goodItem1Path)
|
expect(absolutePaths).toContain(goodItem1Path)
|
||||||
expect(absolutePaths).toContain(goodItem4Path)
|
expect(absolutePaths).toContain(null)
|
||||||
expect(absolutePaths).toContain(badItem3Path)
|
|
||||||
|
|
||||||
for (const specification of specifications) {
|
for (const specification of specifications) {
|
||||||
if (specification.absoluteFilePath === goodItem1Path) {
|
if (specification.sourcePath === goodItem1Path) {
|
||||||
expect(specification.uploadFilePath).toEqual(
|
expect(specification.destinationPath).toEqual(
|
||||||
path.join(
|
path.join('/folder-a', 'folder-b', 'folder-c', 'good-item1.txt')
|
||||||
artifactName,
|
|
||||||
'folder-a',
|
|
||||||
'folder-b',
|
|
||||||
'folder-c',
|
|
||||||
'good-item1.txt'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
} else if (specification.absoluteFilePath === goodItem2Path) {
|
} else if (specification.sourcePath === null) {
|
||||||
expect(specification.uploadFilePath).toEqual(
|
expect(specification.destinationPath).toEqual(
|
||||||
path.join(artifactName, 'folder-d', 'good-item2.txt')
|
path.join('/folder-a', 'folder-b', 'folder-e')
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === goodItem4Path) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(artifactName, 'folder-d', 'good-item4.txt')
|
|
||||||
)
|
|
||||||
} else if (specification.absoluteFilePath === badItem3Path) {
|
|
||||||
expect(specification.uploadFilePath).toEqual(
|
|
||||||
path.join(artifactName, 'folder-f', 'bad-item3.txt')
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -1,551 +0,0 @@
|
|||||||
import * as http from 'http'
|
|
||||||
import * as io from '../../io/src/io'
|
|
||||||
import * as net from 'net'
|
|
||||||
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 * as core from '@actions/core'
|
|
||||||
import {promises as fs} from 'fs'
|
|
||||||
import {getRuntimeUrl} from '../src/internal/config-variables'
|
|
||||||
import {HttpClient, HttpClientResponse} from '@actions/http-client'
|
|
||||||
import {
|
|
||||||
ArtifactResponse,
|
|
||||||
PatchArtifactSizeSuccessResponse
|
|
||||||
} from '../src/internal/contracts'
|
|
||||||
import {UploadSpecification} from '../src/internal/upload-specification'
|
|
||||||
import {getArtifactUrl} from '../src/internal/utils'
|
|
||||||
import {UploadOptions} from '../src/internal/upload-options'
|
|
||||||
|
|
||||||
const root = path.join(__dirname, '_temp', 'artifact-upload')
|
|
||||||
const file1Path = path.join(root, 'file1.txt')
|
|
||||||
const file2Path = path.join(root, 'file2.txt')
|
|
||||||
const file3Path = path.join(root, 'folder1', 'file3.txt')
|
|
||||||
const file4Path = path.join(root, 'folder1', 'file4.txt')
|
|
||||||
const file5Path = path.join(root, 'folder1', 'folder2', 'folder3', 'file5.txt')
|
|
||||||
|
|
||||||
let file1Size = 0
|
|
||||||
let file2Size = 0
|
|
||||||
let file3Size = 0
|
|
||||||
let file4Size = 0
|
|
||||||
let file5Size = 0
|
|
||||||
|
|
||||||
jest.mock('../src/internal/config-variables')
|
|
||||||
jest.mock('@actions/http-client')
|
|
||||||
|
|
||||||
describe('Upload Tests', () => {
|
|
||||||
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(() => {})
|
|
||||||
|
|
||||||
// setup mocking for calls that got through the HttpClient
|
|
||||||
setupHttpClientMock()
|
|
||||||
|
|
||||||
// clear temp directory and create files that will be "uploaded"
|
|
||||||
await io.rmRF(root)
|
|
||||||
await fs.mkdir(path.join(root, 'folder1', 'folder2', 'folder3'), {
|
|
||||||
recursive: true
|
|
||||||
})
|
|
||||||
await fs.writeFile(file1Path, 'this is file 1')
|
|
||||||
await fs.writeFile(file2Path, 'this is file 2')
|
|
||||||
await fs.writeFile(file3Path, 'this is file 3')
|
|
||||||
await fs.writeFile(file4Path, 'this is file 4')
|
|
||||||
await fs.writeFile(file5Path, 'this is file 5')
|
|
||||||
/*
|
|
||||||
Directory structure for files that get created:
|
|
||||||
root/
|
|
||||||
file1.txt
|
|
||||||
file2.txt
|
|
||||||
folder1/
|
|
||||||
file3.txt
|
|
||||||
file4.txt
|
|
||||||
folder2/
|
|
||||||
folder3/
|
|
||||||
file5.txt
|
|
||||||
*/
|
|
||||||
|
|
||||||
file1Size = (await fs.stat(file1Path)).size
|
|
||||||
file2Size = (await fs.stat(file2Path)).size
|
|
||||||
file3Size = (await fs.stat(file3Path)).size
|
|
||||||
file4Size = (await fs.stat(file4Path)).size
|
|
||||||
file5Size = (await fs.stat(file5Path)).size
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Artifact Creation Tests
|
|
||||||
*/
|
|
||||||
it('Create Artifact - Success', async () => {
|
|
||||||
const artifactName = 'valid-artifact-name'
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
const response = await uploadHttpClient.createArtifactInFileContainer(
|
|
||||||
artifactName
|
|
||||||
)
|
|
||||||
expect(response.containerId).toEqual('13')
|
|
||||||
expect(response.size).toEqual(-1)
|
|
||||||
expect(response.signedContent).toEqual('false')
|
|
||||||
expect(response.fileContainerResourceUrl).toEqual(
|
|
||||||
`${getRuntimeUrl()}_apis/resources/Containers/13`
|
|
||||||
)
|
|
||||||
expect(response.type).toEqual('actions_storage')
|
|
||||||
expect(response.name).toEqual(artifactName)
|
|
||||||
expect(response.url).toEqual(
|
|
||||||
`${getRuntimeUrl()}_apis/pipelines/1/runs/1/artifacts?artifactName=${artifactName}`
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Create Artifact - Failure', async () => {
|
|
||||||
const artifactName = 'invalid-artifact-name'
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
expect(
|
|
||||||
uploadHttpClient.createArtifactInFileContainer(artifactName)
|
|
||||||
).rejects.toEqual(
|
|
||||||
new Error(
|
|
||||||
`Create Artifact Container failed: The artifact name invalid-artifact-name is not valid. Request URL ${getArtifactUrl()}`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Create Artifact - Retention Less Than Min Value Error', async () => {
|
|
||||||
const artifactName = 'valid-artifact-name'
|
|
||||||
const options: UploadOptions = {
|
|
||||||
retentionDays: -1
|
|
||||||
}
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
expect(
|
|
||||||
uploadHttpClient.createArtifactInFileContainer(artifactName, options)
|
|
||||||
).rejects.toEqual(new Error('Invalid retention, minimum value is 1.'))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Create Artifact - Storage Quota Error', async () => {
|
|
||||||
const artifactName = 'storage-quota-hit'
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
expect(
|
|
||||||
uploadHttpClient.createArtifactInFileContainer(artifactName)
|
|
||||||
).rejects.toEqual(
|
|
||||||
new Error(
|
|
||||||
'Create Artifact Container failed: Artifact storage quota has been hit. Unable to upload any new artifacts'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Artifact Upload Tests
|
|
||||||
*/
|
|
||||||
it('Upload Artifact - Success', async () => {
|
|
||||||
/**
|
|
||||||
* Normally search.findFilesToUpload() would be used for providing information about what to upload. These tests however
|
|
||||||
* focuses solely on the upload APIs so searchResult[] will be hard-coded
|
|
||||||
*/
|
|
||||||
const artifactName = 'successful-artifact'
|
|
||||||
const uploadSpecification: UploadSpecification[] = [
|
|
||||||
{
|
|
||||||
absoluteFilePath: file1Path,
|
|
||||||
uploadFilePath: `${artifactName}/file1.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file2Path,
|
|
||||||
uploadFilePath: `${artifactName}/file2.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file3Path,
|
|
||||||
uploadFilePath: `${artifactName}/folder1/file3.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file4Path,
|
|
||||||
uploadFilePath: `${artifactName}/folder1/file4.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file5Path,
|
|
||||||
uploadFilePath: `${artifactName}/folder1/folder2/folder3/file5.txt`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const expectedTotalSize =
|
|
||||||
file1Size + file2Size + file3Size + file4Size + file5Size
|
|
||||||
const uploadUrl = `${getRuntimeUrl()}_apis/resources/Containers/13`
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
|
||||||
uploadUrl,
|
|
||||||
uploadSpecification
|
|
||||||
)
|
|
||||||
expect(uploadResult.failedItems.length).toEqual(0)
|
|
||||||
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 () => {
|
|
||||||
const uploadSpecification: UploadSpecification[] = [
|
|
||||||
{
|
|
||||||
absoluteFilePath: file1Path,
|
|
||||||
uploadFilePath: `this-file-upload-will-fail`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const uploadUrl = `${getRuntimeUrl()}_apis/resources/Containers/13`
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
|
||||||
uploadUrl,
|
|
||||||
uploadSpecification
|
|
||||||
)
|
|
||||||
expect(uploadResult.failedItems.length).toEqual(1)
|
|
||||||
expect(uploadResult.uploadSize).toEqual(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Upload Artifact - Partial Upload Continue On Error', async () => {
|
|
||||||
const artifactName = 'partial-artifact'
|
|
||||||
const uploadSpecification: UploadSpecification[] = [
|
|
||||||
{
|
|
||||||
absoluteFilePath: file1Path,
|
|
||||||
uploadFilePath: `${artifactName}/file1.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file2Path,
|
|
||||||
uploadFilePath: `${artifactName}/file2.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file3Path,
|
|
||||||
uploadFilePath: `${artifactName}/folder1/file3.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file4Path,
|
|
||||||
uploadFilePath: `this-file-upload-will-fail`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file5Path,
|
|
||||||
uploadFilePath: `${artifactName}/folder1/folder2/folder3/file5.txt`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const expectedPartialSize = file1Size + file2Size + file4Size + file5Size
|
|
||||||
const uploadUrl = `${getRuntimeUrl()}_apis/resources/Containers/13`
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
|
||||||
uploadUrl,
|
|
||||||
uploadSpecification,
|
|
||||||
{continueOnError: true}
|
|
||||||
)
|
|
||||||
expect(uploadResult.failedItems.length).toEqual(1)
|
|
||||||
expect(uploadResult.uploadSize).toEqual(expectedPartialSize)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Upload Artifact - Partial Upload Fail Fast', async () => {
|
|
||||||
const artifactName = 'partial-artifact'
|
|
||||||
const uploadSpecification: UploadSpecification[] = [
|
|
||||||
{
|
|
||||||
absoluteFilePath: file1Path,
|
|
||||||
uploadFilePath: `${artifactName}/file1.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file2Path,
|
|
||||||
uploadFilePath: `${artifactName}/file2.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file3Path,
|
|
||||||
uploadFilePath: `${artifactName}/folder1/file3.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file4Path,
|
|
||||||
uploadFilePath: `this-file-upload-will-fail`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file5Path,
|
|
||||||
uploadFilePath: `${artifactName}/folder1/folder2/folder3/file5.txt`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const expectedPartialSize = file1Size + file2Size + file3Size
|
|
||||||
const uploadUrl = `${getRuntimeUrl()}_apis/resources/Containers/13`
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
|
||||||
uploadUrl,
|
|
||||||
uploadSpecification,
|
|
||||||
{continueOnError: false}
|
|
||||||
)
|
|
||||||
expect(uploadResult.failedItems.length).toEqual(2)
|
|
||||||
expect(uploadResult.uploadSize).toEqual(expectedPartialSize)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Upload Artifact - Failed upload with no options', async () => {
|
|
||||||
const artifactName = 'partial-artifact'
|
|
||||||
const uploadSpecification: UploadSpecification[] = [
|
|
||||||
{
|
|
||||||
absoluteFilePath: file1Path,
|
|
||||||
uploadFilePath: `${artifactName}/file1.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file2Path,
|
|
||||||
uploadFilePath: `${artifactName}/file2.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file3Path,
|
|
||||||
uploadFilePath: `${artifactName}/folder1/file3.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file4Path,
|
|
||||||
uploadFilePath: `this-file-upload-will-fail`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file5Path,
|
|
||||||
uploadFilePath: `${artifactName}/folder1/folder2/folder3/file5.txt`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const expectedPartialSize = file1Size + file2Size + file3Size + file5Size
|
|
||||||
const uploadUrl = `${getRuntimeUrl()}_apis/resources/Containers/13`
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
|
||||||
uploadUrl,
|
|
||||||
uploadSpecification
|
|
||||||
)
|
|
||||||
expect(uploadResult.failedItems.length).toEqual(1)
|
|
||||||
expect(uploadResult.uploadSize).toEqual(expectedPartialSize)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Upload Artifact - Failed upload with empty options', async () => {
|
|
||||||
const artifactName = 'partial-artifact'
|
|
||||||
const uploadSpecification: UploadSpecification[] = [
|
|
||||||
{
|
|
||||||
absoluteFilePath: file1Path,
|
|
||||||
uploadFilePath: `${artifactName}/file1.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file2Path,
|
|
||||||
uploadFilePath: `${artifactName}/file2.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file3Path,
|
|
||||||
uploadFilePath: `${artifactName}/folder1/file3.txt`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file4Path,
|
|
||||||
uploadFilePath: `this-file-upload-will-fail`
|
|
||||||
},
|
|
||||||
{
|
|
||||||
absoluteFilePath: file5Path,
|
|
||||||
uploadFilePath: `${artifactName}/folder1/folder2/folder3/file5.txt`
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const expectedPartialSize = file1Size + file2Size + file3Size + file5Size
|
|
||||||
const uploadUrl = `${getRuntimeUrl()}_apis/resources/Containers/13`
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
|
||||||
uploadUrl,
|
|
||||||
uploadSpecification,
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
expect(uploadResult.failedItems.length).toEqual(1)
|
|
||||||
expect(uploadResult.uploadSize).toEqual(expectedPartialSize)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Artifact Association Tests
|
|
||||||
*/
|
|
||||||
it('Associate Artifact - Success', async () => {
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
expect(async () => {
|
|
||||||
uploadHttpClient.patchArtifactSize(130, 'my-artifact')
|
|
||||||
}).not.toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Associate Artifact - Not Found', async () => {
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
expect(
|
|
||||||
uploadHttpClient.patchArtifactSize(100, 'non-existent-artifact')
|
|
||||||
).rejects.toThrow(
|
|
||||||
'An Artifact with the name non-existent-artifact was not found'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Associate Artifact - Error', async () => {
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
expect(
|
|
||||||
uploadHttpClient.patchArtifactSize(-2, 'my-artifact')
|
|
||||||
).rejects.toThrow(
|
|
||||||
'Finalize artifact upload failed: Artifact service responded with 400'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helpers used to setup mocking for the HttpClient
|
|
||||||
*/
|
|
||||||
async function emptyMockReadBody(): Promise<string> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupHttpClientMock(): void {
|
|
||||||
/**
|
|
||||||
* Mocks Post calls that are used during Artifact Creation tests
|
|
||||||
*
|
|
||||||
* Simulates success and non-success status codes depending on the artifact name along with an appropriate
|
|
||||||
* payload that represents an expected response
|
|
||||||
*/
|
|
||||||
jest
|
|
||||||
.spyOn(HttpClient.prototype, 'post')
|
|
||||||
.mockImplementation(async (requestdata, data) => {
|
|
||||||
// parse the input data and use the provided artifact name as part of the response
|
|
||||||
const inputData = JSON.parse(data)
|
|
||||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
|
||||||
let mockReadBody = emptyMockReadBody
|
|
||||||
|
|
||||||
if (inputData.Name === 'invalid-artifact-name') {
|
|
||||||
mockMessage.statusCode = 400
|
|
||||||
} else if (inputData.Name === 'storage-quota-hit') {
|
|
||||||
mockMessage.statusCode = 403
|
|
||||||
} else {
|
|
||||||
mockMessage.statusCode = 201
|
|
||||||
const response: ArtifactResponse = {
|
|
||||||
containerId: '13',
|
|
||||||
size: -1,
|
|
||||||
signedContent: 'false',
|
|
||||||
fileContainerResourceUrl: `${getRuntimeUrl()}_apis/resources/Containers/13`,
|
|
||||||
type: 'actions_storage',
|
|
||||||
name: inputData.Name,
|
|
||||||
url: `${getRuntimeUrl()}_apis/pipelines/1/runs/1/artifacts?artifactName=${
|
|
||||||
inputData.Name
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
const returnData: string = JSON.stringify(response, null, 2)
|
|
||||||
mockReadBody = async function(): Promise<string> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
resolve(returnData)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Promise<HttpClientResponse>(resolve => {
|
|
||||||
resolve({
|
|
||||||
message: mockMessage,
|
|
||||||
readBody: mockReadBody
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mocks SendStream calls that are made during Artifact Upload tests
|
|
||||||
*
|
|
||||||
* A 500 response is used to simulate a failed upload stream. The uploadUrl can be set to
|
|
||||||
* include 'fail' to specify that the upload should fail
|
|
||||||
*/
|
|
||||||
jest
|
|
||||||
.spyOn(HttpClient.prototype, 'sendStream')
|
|
||||||
.mockImplementation(async (verb, requestUrl) => {
|
|
||||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
|
||||||
mockMessage.statusCode = 200
|
|
||||||
if (requestUrl.includes('fail')) {
|
|
||||||
mockMessage.statusCode = 500
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise<HttpClientResponse>(resolve => {
|
|
||||||
resolve({
|
|
||||||
message: mockMessage,
|
|
||||||
readBody: emptyMockReadBody
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mocks Patch calls that are made during Artifact Association tests
|
|
||||||
*
|
|
||||||
* Simulates success and non-success status codes depending on the input size along with an appropriate
|
|
||||||
* payload that represents an expected response
|
|
||||||
*/
|
|
||||||
jest
|
|
||||||
.spyOn(HttpClient.prototype, 'patch')
|
|
||||||
.mockImplementation(async (requestdata, data) => {
|
|
||||||
const inputData = JSON.parse(data)
|
|
||||||
const mockMessage = new http.IncomingMessage(new net.Socket())
|
|
||||||
|
|
||||||
// Get the name from the end of requestdata. Will be something like https://www.example.com/_apis/pipelines/workflows/15/artifacts?api-version=6.0-preview&artifactName=my-artifact
|
|
||||||
const artifactName = requestdata.split('=')[2]
|
|
||||||
let mockReadBody = emptyMockReadBody
|
|
||||||
if (inputData.Size < 1) {
|
|
||||||
mockMessage.statusCode = 400
|
|
||||||
} else if (artifactName === 'non-existent-artifact') {
|
|
||||||
mockMessage.statusCode = 404
|
|
||||||
} else {
|
|
||||||
mockMessage.statusCode = 200
|
|
||||||
const response: PatchArtifactSizeSuccessResponse = {
|
|
||||||
containerId: 13,
|
|
||||||
size: inputData.Size,
|
|
||||||
signedContent: 'false',
|
|
||||||
type: 'actions_storage',
|
|
||||||
name: artifactName,
|
|
||||||
url: `${getRuntimeUrl()}_apis/pipelines/1/runs/1/artifacts?artifactName=${artifactName}`,
|
|
||||||
uploadUrl: `${getRuntimeUrl()}_apis/resources/Containers/13`
|
|
||||||
}
|
|
||||||
const returnData: string = JSON.stringify(response, null, 2)
|
|
||||||
mockReadBody = async function(): Promise<string> {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
resolve(returnData)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new Promise<HttpClientResponse>(resolve => {
|
|
||||||
resolve({
|
|
||||||
message: mockMessage,
|
|
||||||
readBody: mockReadBody
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -1,239 +1,62 @@
|
|||||||
import * as fs from 'fs'
|
import * as config from '../src/internal/shared/config'
|
||||||
import * as io from '../../io/src/io'
|
import * as util from '../src/internal/shared/util'
|
||||||
import * as path from 'path'
|
|
||||||
import * as utils from '../src/internal/utils'
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import {HttpCodes} from '@actions/http-client'
|
|
||||||
import {
|
|
||||||
getRuntimeUrl,
|
|
||||||
getWorkFlowRunId,
|
|
||||||
getInitialRetryIntervalInMilliseconds,
|
|
||||||
getRetryMultiplier
|
|
||||||
} from '../src/internal/config-variables'
|
|
||||||
import {Readable} from 'stream'
|
|
||||||
|
|
||||||
jest.mock('../src/internal/config-variables')
|
describe('get-backend-ids-from-token', () => {
|
||||||
|
it('should return backend ids when the token is valid', () => {
|
||||||
describe('Utils', () => {
|
jest
|
||||||
beforeAll(() => {
|
.spyOn(config, 'getRuntimeToken')
|
||||||
// mock all output so that there is less noise when running tests
|
.mockReturnValue(
|
||||||
jest.spyOn(console, 'log').mockImplementation(() => {})
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2NwIjoiQWN0aW9ucy5FeGFtcGxlIEFjdGlvbnMuQW5vdGhlckV4YW1wbGU6dGVzdCBBY3Rpb25zLlJlc3VsdHM6Y2U3ZjU0YzctNjFjNy00YWFlLTg4N2YtMzBkYTQ3NWY1ZjFhOmNhMzk1MDg1LTA0MGEtNTI2Yi0yY2U4LWJkYzg1ZjY5Mjc3NCIsImlhdCI6MTUxNjIzOTAyMn0.XYnI_wHPBlUi1mqYveJnnkJhp4dlFjqxzRmISPsqfw8'
|
||||||
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
|
||||||
jest.spyOn(core, 'info').mockImplementation(() => {})
|
|
||||||
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Check exponential retry range', () => {
|
|
||||||
// No retries should return the initial retry interval
|
|
||||||
const retryWaitTime0 = utils.getExponentialRetryTimeInMilliseconds(0)
|
|
||||||
expect(retryWaitTime0).toEqual(getInitialRetryIntervalInMilliseconds())
|
|
||||||
|
|
||||||
const testMinMaxRange = (retryCount: number): void => {
|
|
||||||
const retryWaitTime = utils.getExponentialRetryTimeInMilliseconds(
|
|
||||||
retryCount
|
|
||||||
)
|
)
|
||||||
const minRange =
|
|
||||||
getInitialRetryIntervalInMilliseconds() *
|
|
||||||
getRetryMultiplier() *
|
|
||||||
retryCount
|
|
||||||
const maxRange = minRange * getRetryMultiplier()
|
|
||||||
|
|
||||||
expect(retryWaitTime).toBeGreaterThanOrEqual(minRange)
|
const backendIds = util.getBackendIdsFromToken()
|
||||||
expect(retryWaitTime).toBeLessThan(maxRange)
|
expect(backendIds.workflowRunBackendId).toBe(
|
||||||
}
|
'ce7f54c7-61c7-4aae-887f-30da475f5f1a'
|
||||||
|
)
|
||||||
for (let i = 1; i < 10; i++) {
|
expect(backendIds.workflowJobRunBackendId).toBe(
|
||||||
testMinMaxRange(i)
|
'ca395085-040a-526b-2ce8-bdc85f692774'
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test negative artifact retention throws', () => {
|
|
||||||
expect(() => {
|
|
||||||
utils.getProperRetention(-1, undefined)
|
|
||||||
}).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test no setting specified takes artifact retention input', () => {
|
|
||||||
expect(utils.getProperRetention(180, undefined)).toEqual(180)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test artifact retention must conform to max allowed', () => {
|
|
||||||
expect(utils.getProperRetention(180, '45')).toEqual(45)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test constructing artifact URL', () => {
|
|
||||||
const runtimeUrl = getRuntimeUrl()
|
|
||||||
const runId = getWorkFlowRunId()
|
|
||||||
const artifactUrl = utils.getArtifactUrl()
|
|
||||||
expect(artifactUrl).toEqual(
|
|
||||||
`${runtimeUrl}_apis/pipelines/workflows/${runId}/artifacts?api-version=${utils.getApiVersion()}`
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Test constructing upload headers with all optional parameters', () => {
|
it("should throw an error when the token doesn't have the right scope", () => {
|
||||||
const contentType = 'application/octet-stream'
|
jest
|
||||||
const size = 24
|
.spyOn(config, 'getRuntimeToken')
|
||||||
const uncompressedLength = 100
|
.mockReturnValue(
|
||||||
const range = 'bytes 0-199/200'
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2NwIjoiQWN0aW9ucy5FeGFtcGxlIEFjdGlvbnMuQW5vdGhlckV4YW1wbGU6dGVzdCIsImlhdCI6MTUxNjIzOTAyMn0.K0IEoULZteGevF38G94xiaA8zcZ5UlKWfGfqE6q3dhw'
|
||||||
const digest = {
|
)
|
||||||
crc64: 'bSzITYnW/P8=',
|
|
||||||
md5: 'Xiv1fT9AxLbfadrxk2y3ZvgyN0tPwCWafL/wbi9w8mk='
|
|
||||||
}
|
|
||||||
const headers = utils.getUploadHeaders(
|
|
||||||
contentType,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
uncompressedLength,
|
|
||||||
size,
|
|
||||||
range,
|
|
||||||
digest
|
|
||||||
)
|
|
||||||
expect(Object.keys(headers).length).toEqual(10)
|
|
||||||
expect(headers['Accept']).toEqual(
|
|
||||||
`application/json;api-version=${utils.getApiVersion()}`
|
|
||||||
)
|
|
||||||
expect(headers['Content-Type']).toEqual(contentType)
|
|
||||||
expect(headers['Connection']).toEqual('Keep-Alive')
|
|
||||||
expect(headers['Keep-Alive']).toEqual('10')
|
|
||||||
expect(headers['Content-Encoding']).toEqual('gzip')
|
|
||||||
expect(headers['x-tfs-filelength']).toEqual(uncompressedLength)
|
|
||||||
expect(headers['Content-Length']).toEqual(size)
|
|
||||||
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', () => {
|
expect(util.getBackendIdsFromToken).toThrowError(
|
||||||
const headers = utils.getUploadHeaders('application/octet-stream')
|
'Failed to get backend IDs: The provided JWT token is invalid'
|
||||||
expect(Object.keys(headers).length).toEqual(2)
|
|
||||||
expect(headers['Accept']).toEqual(
|
|
||||||
`application/json;api-version=${utils.getApiVersion()}`
|
|
||||||
)
|
|
||||||
expect(headers['Content-Type']).toEqual('application/octet-stream')
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test constructing download headers with all optional parameters', () => {
|
|
||||||
const contentType = 'application/json'
|
|
||||||
const headers = utils.getDownloadHeaders(contentType, true, true)
|
|
||||||
expect(Object.keys(headers).length).toEqual(5)
|
|
||||||
expect(headers['Content-Type']).toEqual(contentType)
|
|
||||||
expect(headers['Connection']).toEqual('Keep-Alive')
|
|
||||||
expect(headers['Keep-Alive']).toEqual('10')
|
|
||||||
expect(headers['Accept-Encoding']).toEqual('gzip')
|
|
||||||
expect(headers['Accept']).toEqual(
|
|
||||||
`application/octet-stream;api-version=${utils.getApiVersion()}`
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Test constructing download headers with only required parameter', () => {
|
it('should throw an error when the token has a malformed scope', () => {
|
||||||
const headers = utils.getDownloadHeaders('application/octet-stream')
|
jest
|
||||||
expect(Object.keys(headers).length).toEqual(2)
|
.spyOn(config, 'getRuntimeToken')
|
||||||
expect(headers['Content-Type']).toEqual('application/octet-stream')
|
.mockReturnValue(
|
||||||
// check for default accept type
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwic2NwIjoiQWN0aW9ucy5FeGFtcGxlIEFjdGlvbnMuQW5vdGhlckV4YW1wbGU6dGVzdCBBY3Rpb25zLlJlc3VsdHM6Y2U3ZjU0YzctNjFjNy00YWFlLTg4N2YtMzBkYTQ3NWY1ZjFhIiwiaWF0IjoxNTE2MjM5MDIyfQ.7D0_LRfRFRZFImHQ7GxH2S6ZyFjjZ5U0ujjGCfle1XE'
|
||||||
expect(headers['Accept']).toEqual(
|
)
|
||||||
`application/json;api-version=${utils.getApiVersion()}`
|
|
||||||
|
expect(util.getBackendIdsFromToken).toThrowError(
|
||||||
|
'Failed to get backend IDs: The provided JWT token is invalid'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Test Success Status Code', () => {
|
it('should throw an error when the token is in an invalid format', () => {
|
||||||
expect(utils.isSuccessStatusCode(HttpCodes.OK)).toEqual(true)
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('token')
|
||||||
expect(utils.isSuccessStatusCode(201)).toEqual(true)
|
|
||||||
expect(utils.isSuccessStatusCode(299)).toEqual(true)
|
expect(util.getBackendIdsFromToken).toThrowError('Invalid token specified')
|
||||||
expect(utils.isSuccessStatusCode(HttpCodes.NotFound)).toEqual(false)
|
|
||||||
expect(utils.isSuccessStatusCode(HttpCodes.BadGateway)).toEqual(false)
|
|
||||||
expect(utils.isSuccessStatusCode(HttpCodes.Forbidden)).toEqual(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Test Retry Status Code', () => {
|
it("should throw an error when the token doesn't have the right field", () => {
|
||||||
expect(utils.isRetryableStatusCode(HttpCodes.BadGateway)).toEqual(true)
|
jest
|
||||||
expect(utils.isRetryableStatusCode(HttpCodes.ServiceUnavailable)).toEqual(
|
.spyOn(config, 'getRuntimeToken')
|
||||||
true
|
.mockReturnValue(
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(util.getBackendIdsFromToken).toThrowError(
|
||||||
|
'Failed to get backend IDs: The provided JWT token is invalid'
|
||||||
)
|
)
|
||||||
expect(utils.isRetryableStatusCode(HttpCodes.GatewayTimeout)).toEqual(true)
|
|
||||||
expect(utils.isRetryableStatusCode(HttpCodes.TooManyRequests)).toEqual(true)
|
|
||||||
expect(utils.isRetryableStatusCode(HttpCodes.OK)).toEqual(false)
|
|
||||||
expect(utils.isRetryableStatusCode(HttpCodes.NotFound)).toEqual(false)
|
|
||||||
expect(utils.isRetryableStatusCode(HttpCodes.Forbidden)).toEqual(false)
|
|
||||||
expect(utils.isRetryableStatusCode(413)).toEqual(true) // Payload Too Large
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test Throttled Status Code', () => {
|
|
||||||
expect(utils.isThrottledStatusCode(HttpCodes.TooManyRequests)).toEqual(true)
|
|
||||||
expect(utils.isThrottledStatusCode(HttpCodes.InternalServerError)).toEqual(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
expect(utils.isThrottledStatusCode(HttpCodes.BadGateway)).toEqual(false)
|
|
||||||
expect(utils.isThrottledStatusCode(HttpCodes.ServiceUnavailable)).toEqual(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test Forbidden Status Code', () => {
|
|
||||||
expect(utils.isForbiddenStatusCode(HttpCodes.Forbidden)).toEqual(true)
|
|
||||||
expect(utils.isForbiddenStatusCode(HttpCodes.InternalServerError)).toEqual(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
expect(utils.isForbiddenStatusCode(HttpCodes.TooManyRequests)).toEqual(
|
|
||||||
false
|
|
||||||
)
|
|
||||||
expect(utils.isForbiddenStatusCode(HttpCodes.OK)).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test Creating Artifact Directories', async () => {
|
|
||||||
const root = path.join(__dirname, '_temp', 'artifact-download')
|
|
||||||
// remove directory before starting
|
|
||||||
await io.rmRF(root)
|
|
||||||
|
|
||||||
const directory1 = path.join(root, 'folder2', 'folder3')
|
|
||||||
const directory2 = path.join(directory1, 'folder1')
|
|
||||||
|
|
||||||
// Initially should not exist
|
|
||||||
await expect(fs.promises.access(directory1)).rejects.not.toBeUndefined()
|
|
||||||
await expect(fs.promises.access(directory2)).rejects.not.toBeUndefined()
|
|
||||||
const directoryStructure = [directory1, directory2]
|
|
||||||
await utils.createDirectoriesForArtifact(directoryStructure)
|
|
||||||
// directories should now be created
|
|
||||||
await expect(fs.promises.access(directory1)).resolves.toEqual(undefined)
|
|
||||||
await expect(fs.promises.access(directory2)).resolves.toEqual(undefined)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Test Creating Empty Files', async () => {
|
|
||||||
const root = path.join(__dirname, '_temp', 'empty-files')
|
|
||||||
await io.rmRF(root)
|
|
||||||
|
|
||||||
const emptyFile1 = path.join(root, 'emptyFile1')
|
|
||||||
const directoryToCreate = path.join(root, 'folder1')
|
|
||||||
const emptyFile2 = path.join(directoryToCreate, 'emptyFile2')
|
|
||||||
|
|
||||||
// empty files should only be created after the directory structure is fully setup
|
|
||||||
// ensure they are first created by using the createDirectoriesForArtifact method
|
|
||||||
const directoryStructure = [root, directoryToCreate]
|
|
||||||
await utils.createDirectoriesForArtifact(directoryStructure)
|
|
||||||
await expect(fs.promises.access(root)).resolves.toEqual(undefined)
|
|
||||||
await expect(fs.promises.access(directoryToCreate)).resolves.toEqual(
|
|
||||||
undefined
|
|
||||||
)
|
|
||||||
|
|
||||||
await expect(fs.promises.access(emptyFile1)).rejects.not.toBeUndefined()
|
|
||||||
await expect(fs.promises.access(emptyFile2)).rejects.not.toBeUndefined()
|
|
||||||
|
|
||||||
const emptyFilesToCreate = [emptyFile1, emptyFile2]
|
|
||||||
await utils.createEmptyFilesForArtifact(emptyFilesToCreate)
|
|
||||||
|
|
||||||
await expect(fs.promises.access(emptyFile1)).resolves.toEqual(undefined)
|
|
||||||
const size1 = (await fs.promises.stat(emptyFile1)).size
|
|
||||||
expect(size1).toEqual(0)
|
|
||||||
await expect(fs.promises.access(emptyFile2)).resolves.toEqual(undefined)
|
|
||||||
const size2 = (await fs.promises.stat(emptyFile2)).size
|
|
||||||
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==')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
# Additional Information
|
|
||||||
|
|
||||||
Extra information
|
|
||||||
- [Non-Supported Characters](#Non-Supported-Characters)
|
|
||||||
- [Permission loss](#Permission-Loss)
|
|
||||||
- [Considerations](#Considerations)
|
|
||||||
- [Compression](#Is-my-artifact-compressed)
|
|
||||||
|
|
||||||
## Non-Supported Characters
|
|
||||||
|
|
||||||
When uploading an artifact, the inputted `name` parameter along with the files specified in `files` cannot contain any of the following characters. They will be rejected by the server if attempted to be sent over and the upload will fail. These characters are not allowed due to limitations and restrictions 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.
|
|
||||||
|
|
||||||
- "
|
|
||||||
- :
|
|
||||||
- <
|
|
||||||
- \>
|
|
||||||
- |
|
|
||||||
- \*
|
|
||||||
- ?
|
|
||||||
|
|
||||||
In addition to the aforementioned characters, the inputted `name` also cannot include the following
|
|
||||||
- \
|
|
||||||
- /
|
|
||||||
|
|
||||||
|
|
||||||
## Permission Loss
|
|
||||||
|
|
||||||
File permissions are not maintained between uploaded and downloaded artifacts. If file permissions are something that need to be maintained (such as an executable), consider archiving all of the files using something like `tar` and then uploading the single archive. After downloading the artifact, you can `un-tar` the individual file and permissions will be preserved.
|
|
||||||
|
|
||||||
```js
|
|
||||||
const artifact = require('@actions/artifact');
|
|
||||||
const artifactClient = artifact.create()
|
|
||||||
const artifactName = 'my-artifact';
|
|
||||||
const files = [
|
|
||||||
'/home/user/files/plz-upload/my-archive.tgz',
|
|
||||||
]
|
|
||||||
const rootDirectory = '/home/user/files/plz-upload'
|
|
||||||
const uploadResult = await artifactClient.uploadArtifact(artifactName, files, rootDirectory)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Considerations
|
|
||||||
|
|
||||||
During upload, each file is uploaded concurrently in 4MB chunks using a separate HTTPS connection per file. Chunked uploads are used so that in the event of a failure (which is entirely possible because the internet is not perfect), the upload can be retried. If there is an error, a retry will be attempted after a certain period of time.
|
|
||||||
|
|
||||||
Uploading will be generally be faster if there are fewer files that are larger in size vs if there are lots of smaller files. Depending on the types and quantities of files being uploaded, it might be beneficial to separately compress and archive everything into a single archive (using something like `tar` or `zip`) before starting and artifact upload to speed things up.
|
|
||||||
|
|
||||||
## Is my artifact compressed?
|
|
||||||
|
|
||||||
GZip is used internally to compress individual files before starting an upload. Compression helps reduce the total amount of data that must be uploaded and stored while helping to speed up uploads (this performance benefit is significant especially on self hosted runners). If GZip does not reduce the size of the file that is being uploaded, the original file is uploaded as-is.
|
|
||||||
|
|
||||||
Compression using GZip also helps speed up artifact download as part of a workflow. Header information is used to determine if an individual file was uploaded using GZip and if necessary, decompression is used.
|
|
||||||
|
|
||||||
When downloading an artifact from the GitHub UI (this differs from downloading an artifact during a workflow), a single Zip file is dynamically created that contains all of the files uploaded as part of an artifact. Any files that were uploaded using GZip will be decompressed on the server before being added to the Zip file with the remaining files.
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Docs will be added here once development of version `2.0.0` has finished
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
# Implementation Details
|
|
||||||
|
|
||||||
Warning: Implementation details may change at any time without notice. This is meant to serve as a reference to help users understand the package.
|
|
||||||
|
|
||||||
## Upload/Compression flow
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
During artifact upload, gzip is used to compress individual files that then get uploaded. This is used to minimize the amount of data that gets uploaded which reduces the total amount of HTTP calls (upload happens in 4MB chunks). This results in considerably faster uploads with huge performance implications especially on self-hosted runners.
|
|
||||||
|
|
||||||
If a file is less than 64KB in size, a passthrough stream (readable and writable) is used to convert an in-memory buffer into a readable stream without any extra streams or pipping.
|
|
||||||
|
|
||||||
## Retry Logic when downloading an individual file
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## Proxy support
|
|
||||||
|
|
||||||
This package uses the `@actions/http-client` NPM package internally which supports proxied requests out of the box.
|
|
||||||
|
|
||||||
## HttpManager
|
|
||||||
|
|
||||||
### `keep-alive` header
|
|
||||||
|
|
||||||
When an HTTP call is made to upload or download an individual file, the server will close the HTTP connection after the upload/download is complete and respond with a header indicating `Connection: close`.
|
|
||||||
|
|
||||||
[HTTP closed connection header information](https://tools.ietf.org/html/rfc2616#section-14.10)
|
|
||||||
|
|
||||||
TCP connections are sometimes not immediately closed by the node client (Windows might hold on to the port for an extra period of time before actually releasing it for example) and a large amount of closed connections can cause port exhaustion before ports get released and are available again.
|
|
||||||
|
|
||||||
VMs hosted by GitHub Actions have 1024 available ports so uploading 1000+ files very quickly can cause port exhaustion if connections get closed immediately. This can start to cause strange undefined behavior and timeouts.
|
|
||||||
|
|
||||||
In order for connections to not close immediately, the `keep-alive` header is used to indicate to the server that the connection should stay open. If a `keep-alive` header is used, the connection needs to be disposed of by calling `dispose()` in the `HttpClient`.
|
|
||||||
|
|
||||||
[`keep-alive` header information](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive)
|
|
||||||
[@actions/http-client client disposal](https://github.com/actions/http-client/blob/04e5ad73cd3fd1f5610a32116b0759eddf6570d2/index.ts#L292)
|
|
||||||
|
|
||||||
|
|
||||||
### Multiple HTTP clients
|
|
||||||
|
|
||||||
During an artifact upload or download, files are concurrently uploaded or downloaded using `async/await`. When an error or retry is encountered, the `HttpClient` that made a call is disposed of and a new one is created. If a single `HttpClient` was used for all HTTP calls and it had to be disposed, it could inadvertently effect any other calls that could be concurrently happening.
|
|
||||||
|
|
||||||
Any other concurrent uploads or downloads should be left untouched. Because of this, each concurrent upload or download gets its own `HttpClient`. The `http-manager` is used to manage all available clients and each concurrent upload or download maintains a `httpClientIndex` that keep track of which client should be used (and potentially disposed and recycled if necessary)
|
|
||||||
|
|
||||||
### Potential resource leaks
|
|
||||||
|
|
||||||
When an HTTP response is received, it consists of two parts
|
|
||||||
- `message`
|
|
||||||
- `body`
|
|
||||||
|
|
||||||
The `message` contains information such as the response code and header information and it is available immediately. The body however is not available immediately and it can be read by calling `await response.readBody()`.
|
|
||||||
|
|
||||||
TCP connections consist of an input and output buffer to manage what is sent and received across a connection. If the body is not read (even if its contents are not needed) the buffers can stay in use even after `dispose()` gets called on the `HttpClient`. The buffers get released automatically after a certain period of time, but in order for them to be explicitly cleared, `readBody()` is always called.
|
|
||||||
|
|
||||||
### Non Concurrent calls
|
|
||||||
|
|
||||||
Both `upload-http-client` and `download-http-client` do not instantiate or create any HTTP clients (the `HttpManager` has that responsibility). If an HTTP call has to be made that does not require the `keep-alive` header (such as when calling `listArtifacts` or `patchArtifactSize`), the first `HttpClient` in the `HttpManager` is used. The number of available clients is equal to the upload or download concurrency and there will always be at least one available.
|
|
||||||
Generated
+1332
-187
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@actions/artifact",
|
"name": "@actions/artifact",
|
||||||
"version": "1.1.0",
|
"version": "2.0.0",
|
||||||
"preview": true,
|
"preview": true,
|
||||||
"description": "Actions artifact lib",
|
"description": "Actions artifact lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@@ -10,8 +10,8 @@
|
|||||||
],
|
],
|
||||||
"homepage": "https://github.com/actions/toolkit/tree/main/packages/artifact",
|
"homepage": "https://github.com/actions/toolkit/tree/main/packages/artifact",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "lib/artifact-client.js",
|
"main": "lib/artifact.js",
|
||||||
"types": "lib/artifact-client.d.ts",
|
"types": "lib/artifact.d.ts",
|
||||||
"directories": {
|
"directories": {
|
||||||
"lib": "lib",
|
"lib": "lib",
|
||||||
"test": "__tests__"
|
"test": "__tests__"
|
||||||
@@ -31,19 +31,32 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"audit-moderate": "npm install && npm audit --json --audit-level=moderate > audit.json",
|
"audit-moderate": "npm install && npm audit --json --audit-level=moderate > audit.json",
|
||||||
"test": "echo \"Error: run tests from root\" && exit 1",
|
"test": "echo \"Error: run tests from root\" && exit 1",
|
||||||
"tsc": "tsc"
|
"bootstrap": "cd ../../ && npm run bootstrap",
|
||||||
|
"tsc-run": "tsc",
|
||||||
|
"tsc": "npm run bootstrap && npm run tsc-run"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/actions/toolkit/issues"
|
"url": "https://github.com/actions/toolkit/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.9.1",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/github": "^5.1.1",
|
||||||
"tmp": "^0.2.1",
|
"@actions/http-client": "^2.1.0",
|
||||||
"tmp-promise": "^3.0.2"
|
"@azure/storage-blob": "^12.15.0",
|
||||||
|
"@octokit/core": "^3.5.1",
|
||||||
|
"@octokit/plugin-request-log": "^1.0.4",
|
||||||
|
"@octokit/plugin-retry": "^3.0.9",
|
||||||
|
"@octokit/request-error": "^5.0.0",
|
||||||
|
"@protobuf-ts/plugin": "^2.2.3-alpha.1",
|
||||||
|
"@types/unzipper": "^0.10.6",
|
||||||
|
"archiver": "^5.3.1",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
|
"jwt-decode": "^3.1.2",
|
||||||
|
"twirp-ts": "^2.5.0",
|
||||||
|
"unzipper": "^0.10.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/tmp": "^0.2.1",
|
"@types/archiver": "^5.3.2",
|
||||||
"typescript": "^3.8.3"
|
"typescript": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
import {UploadOptions} from './internal/upload-options'
|
|
||||||
import {UploadResponse} from './internal/upload-response'
|
|
||||||
import {DownloadOptions} from './internal/download-options'
|
|
||||||
import {DownloadResponse} from './internal/download-response'
|
|
||||||
import {ArtifactClient, DefaultArtifactClient} from './internal/artifact-client'
|
|
||||||
|
|
||||||
export {
|
|
||||||
ArtifactClient,
|
|
||||||
UploadResponse,
|
|
||||||
UploadOptions,
|
|
||||||
DownloadResponse,
|
|
||||||
DownloadOptions
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs an ArtifactClient
|
|
||||||
*/
|
|
||||||
export function create(): ArtifactClient {
|
|
||||||
return DefaultArtifactClient.create()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import {ArtifactClient, Client} from './internal/client'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exported functionality that we want to expose for any users of @actions/artifact
|
||||||
|
*/
|
||||||
|
export * from './internal/shared/interfaces'
|
||||||
|
export {ArtifactClient}
|
||||||
|
|
||||||
|
export function create(): ArtifactClient {
|
||||||
|
return Client.create()
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
// @generated by protobuf-ts 2.9.1 with parameter long_type_string,client_none,generate_dependencies
|
||||||
|
// @generated from protobuf file "google/protobuf/timestamp.proto" (package "google.protobuf", syntax proto3)
|
||||||
|
// tslint:disable
|
||||||
|
//
|
||||||
|
// Protocol Buffers - Google's data interchange format
|
||||||
|
// Copyright 2008 Google Inc. All rights reserved.
|
||||||
|
// https://developers.google.com/protocol-buffers/
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following disclaimer
|
||||||
|
// in the documentation and/or other materials provided with the
|
||||||
|
// distribution.
|
||||||
|
// * Neither the name of Google Inc. nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
//
|
||||||
|
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { IBinaryWriter } from "@protobuf-ts/runtime";
|
||||||
|
import { WireType } from "@protobuf-ts/runtime";
|
||||||
|
import type { BinaryReadOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { IBinaryReader } from "@protobuf-ts/runtime";
|
||||||
|
import { UnknownFieldHandler } from "@protobuf-ts/runtime";
|
||||||
|
import type { PartialMessage } from "@protobuf-ts/runtime";
|
||||||
|
import { reflectionMergePartial } from "@protobuf-ts/runtime";
|
||||||
|
import { MESSAGE_TYPE } from "@protobuf-ts/runtime";
|
||||||
|
import { typeofJsonValue } from "@protobuf-ts/runtime";
|
||||||
|
import type { JsonValue } from "@protobuf-ts/runtime";
|
||||||
|
import type { JsonReadOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { JsonWriteOptions } from "@protobuf-ts/runtime";
|
||||||
|
import { PbLong } from "@protobuf-ts/runtime";
|
||||||
|
import { MessageType } from "@protobuf-ts/runtime";
|
||||||
|
/**
|
||||||
|
* A Timestamp represents a point in time independent of any time zone
|
||||||
|
* or calendar, represented as seconds and fractions of seconds at
|
||||||
|
* nanosecond resolution in UTC Epoch time. It is encoded using the
|
||||||
|
* Proleptic Gregorian Calendar which extends the Gregorian calendar
|
||||||
|
* backwards to year one. It is encoded assuming all minutes are 60
|
||||||
|
* seconds long, i.e. leap seconds are "smeared" so that no leap second
|
||||||
|
* table is needed for interpretation. Range is from
|
||||||
|
* 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z.
|
||||||
|
* By restricting to that range, we ensure that we can convert to
|
||||||
|
* and from RFC 3339 date strings.
|
||||||
|
* See [https://www.ietf.org/rfc/rfc3339.txt](https://www.ietf.org/rfc/rfc3339.txt).
|
||||||
|
*
|
||||||
|
* # Examples
|
||||||
|
*
|
||||||
|
* Example 1: Compute Timestamp from POSIX `time()`.
|
||||||
|
*
|
||||||
|
* Timestamp timestamp;
|
||||||
|
* timestamp.set_seconds(time(NULL));
|
||||||
|
* timestamp.set_nanos(0);
|
||||||
|
*
|
||||||
|
* Example 2: Compute Timestamp from POSIX `gettimeofday()`.
|
||||||
|
*
|
||||||
|
* struct timeval tv;
|
||||||
|
* gettimeofday(&tv, NULL);
|
||||||
|
*
|
||||||
|
* Timestamp timestamp;
|
||||||
|
* timestamp.set_seconds(tv.tv_sec);
|
||||||
|
* timestamp.set_nanos(tv.tv_usec * 1000);
|
||||||
|
*
|
||||||
|
* Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
|
||||||
|
*
|
||||||
|
* FILETIME ft;
|
||||||
|
* GetSystemTimeAsFileTime(&ft);
|
||||||
|
* UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
|
||||||
|
*
|
||||||
|
* // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z
|
||||||
|
* // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.
|
||||||
|
* Timestamp timestamp;
|
||||||
|
* timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
|
||||||
|
* timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
|
||||||
|
*
|
||||||
|
* Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
|
||||||
|
*
|
||||||
|
* long millis = System.currentTimeMillis();
|
||||||
|
*
|
||||||
|
* Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
|
||||||
|
* .setNanos((int) ((millis % 1000) * 1000000)).build();
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Example 5: Compute Timestamp from current time in Python.
|
||||||
|
*
|
||||||
|
* timestamp = Timestamp()
|
||||||
|
* timestamp.GetCurrentTime()
|
||||||
|
*
|
||||||
|
* # JSON Mapping
|
||||||
|
*
|
||||||
|
* In JSON format, the Timestamp type is encoded as a string in the
|
||||||
|
* [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
|
||||||
|
* format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
|
||||||
|
* where {year} is always expressed using four digits while {month}, {day},
|
||||||
|
* {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
|
||||||
|
* seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
|
||||||
|
* are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
|
||||||
|
* is required. A proto3 JSON serializer should always use UTC (as indicated by
|
||||||
|
* "Z") when printing the Timestamp type and a proto3 JSON parser should be
|
||||||
|
* able to accept both UTC and other timezones (as indicated by an offset).
|
||||||
|
*
|
||||||
|
* For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
|
||||||
|
* 01:30 UTC on January 15, 2017.
|
||||||
|
*
|
||||||
|
* In JavaScript, one can convert a Date object to this format using the
|
||||||
|
* standard [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString]
|
||||||
|
* method. In Python, a standard `datetime.datetime` object can be converted
|
||||||
|
* to this format using [`strftime`](https://docs.python.org/2/library/time.html#time.strftime)
|
||||||
|
* with the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one
|
||||||
|
* can use the Joda Time's [`ISODateTimeFormat.dateTime()`](
|
||||||
|
* http://www.joda.org/joda-time/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime--
|
||||||
|
* ) to obtain a formatter capable of generating timestamps in this format.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @generated from protobuf message google.protobuf.Timestamp
|
||||||
|
*/
|
||||||
|
export interface Timestamp {
|
||||||
|
/**
|
||||||
|
* Represents seconds of UTC time since Unix epoch
|
||||||
|
* 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
|
||||||
|
* 9999-12-31T23:59:59Z inclusive.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: int64 seconds = 1;
|
||||||
|
*/
|
||||||
|
seconds: string;
|
||||||
|
/**
|
||||||
|
* Non-negative fractions of a second at nanosecond resolution. Negative
|
||||||
|
* second values with fractions must still have non-negative nanos values
|
||||||
|
* that count forward in time. Must be from 0 to 999,999,999
|
||||||
|
* inclusive.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: int32 nanos = 2;
|
||||||
|
*/
|
||||||
|
nanos: number;
|
||||||
|
}
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class Timestamp$Type extends MessageType<Timestamp> {
|
||||||
|
constructor() {
|
||||||
|
super("google.protobuf.Timestamp", [
|
||||||
|
{ no: 1, name: "seconds", kind: "scalar", T: 3 /*ScalarType.INT64*/ },
|
||||||
|
{ no: 2, name: "nanos", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Creates a new `Timestamp` for the current time.
|
||||||
|
*/
|
||||||
|
now(): Timestamp {
|
||||||
|
const msg = this.create();
|
||||||
|
const ms = Date.now();
|
||||||
|
msg.seconds = PbLong.from(Math.floor(ms / 1000)).toString();
|
||||||
|
msg.nanos = (ms % 1000) * 1000000;
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Converts a `Timestamp` to a JavaScript Date.
|
||||||
|
*/
|
||||||
|
toDate(message: Timestamp): Date {
|
||||||
|
return new Date(PbLong.from(message.seconds).toNumber() * 1000 + Math.ceil(message.nanos / 1000000));
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Converts a JavaScript Date to a `Timestamp`.
|
||||||
|
*/
|
||||||
|
fromDate(date: Date): Timestamp {
|
||||||
|
const msg = this.create();
|
||||||
|
const ms = date.getTime();
|
||||||
|
msg.seconds = PbLong.from(Math.floor(ms / 1000)).toString();
|
||||||
|
msg.nanos = (ms % 1000) * 1000000;
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* In JSON format, the `Timestamp` type is encoded as a string
|
||||||
|
* in the RFC 3339 format.
|
||||||
|
*/
|
||||||
|
internalJsonWrite(message: Timestamp, options: JsonWriteOptions): JsonValue {
|
||||||
|
let ms = PbLong.from(message.seconds).toNumber() * 1000;
|
||||||
|
if (ms < Date.parse("0001-01-01T00:00:00Z") || ms > Date.parse("9999-12-31T23:59:59Z"))
|
||||||
|
throw new Error("Unable to encode Timestamp to JSON. Must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive.");
|
||||||
|
if (message.nanos < 0)
|
||||||
|
throw new Error("Unable to encode invalid Timestamp to JSON. Nanos must not be negative.");
|
||||||
|
let z = "Z";
|
||||||
|
if (message.nanos > 0) {
|
||||||
|
let nanosStr = (message.nanos + 1000000000).toString().substring(1);
|
||||||
|
if (nanosStr.substring(3) === "000000")
|
||||||
|
z = "." + nanosStr.substring(0, 3) + "Z";
|
||||||
|
else if (nanosStr.substring(6) === "000")
|
||||||
|
z = "." + nanosStr.substring(0, 6) + "Z";
|
||||||
|
else
|
||||||
|
z = "." + nanosStr + "Z";
|
||||||
|
}
|
||||||
|
return new Date(ms).toISOString().replace(".000Z", z);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* In JSON format, the `Timestamp` type is encoded as a string
|
||||||
|
* in the RFC 3339 format.
|
||||||
|
*/
|
||||||
|
internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: Timestamp): Timestamp {
|
||||||
|
if (typeof json !== "string")
|
||||||
|
throw new Error("Unable to parse Timestamp from JSON " + typeofJsonValue(json) + ".");
|
||||||
|
let matches = json.match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:Z|\.([0-9]{3,9})Z|([+-][0-9][0-9]:[0-9][0-9]))$/);
|
||||||
|
if (!matches)
|
||||||
|
throw new Error("Unable to parse Timestamp from JSON. Invalid format.");
|
||||||
|
let ms = Date.parse(matches[1] + "-" + matches[2] + "-" + matches[3] + "T" + matches[4] + ":" + matches[5] + ":" + matches[6] + (matches[8] ? matches[8] : "Z"));
|
||||||
|
if (Number.isNaN(ms))
|
||||||
|
throw new Error("Unable to parse Timestamp from JSON. Invalid value.");
|
||||||
|
if (ms < Date.parse("0001-01-01T00:00:00Z") || ms > Date.parse("9999-12-31T23:59:59Z"))
|
||||||
|
throw new globalThis.Error("Unable to parse Timestamp from JSON. Must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive.");
|
||||||
|
if (!target)
|
||||||
|
target = this.create();
|
||||||
|
target.seconds = PbLong.from(ms / 1000).toString();
|
||||||
|
target.nanos = 0;
|
||||||
|
if (matches[7])
|
||||||
|
target.nanos = (parseInt("1" + matches[7] + "0".repeat(9 - matches[7].length)) - 1000000000);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<Timestamp>): Timestamp {
|
||||||
|
const message = { seconds: "0", nanos: 0 };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<Timestamp>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Timestamp): Timestamp {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* int64 seconds */ 1:
|
||||||
|
message.seconds = reader.int64().toString();
|
||||||
|
break;
|
||||||
|
case /* int32 nanos */ 2:
|
||||||
|
message.nanos = reader.int32();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: Timestamp, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* int64 seconds = 1; */
|
||||||
|
if (message.seconds !== "0")
|
||||||
|
writer.tag(1, WireType.Varint).int64(message.seconds);
|
||||||
|
/* int32 nanos = 2; */
|
||||||
|
if (message.nanos !== 0)
|
||||||
|
writer.tag(2, WireType.Varint).int32(message.nanos);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message google.protobuf.Timestamp
|
||||||
|
*/
|
||||||
|
export const Timestamp = new Timestamp$Type();
|
||||||
@@ -0,0 +1,748 @@
|
|||||||
|
// @generated by protobuf-ts 2.9.1 with parameter long_type_string,client_none,generate_dependencies
|
||||||
|
// @generated from protobuf file "google/protobuf/wrappers.proto" (package "google.protobuf", syntax proto3)
|
||||||
|
// tslint:disable
|
||||||
|
//
|
||||||
|
// Protocol Buffers - Google's data interchange format
|
||||||
|
// Copyright 2008 Google Inc. All rights reserved.
|
||||||
|
// https://developers.google.com/protocol-buffers/
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following disclaimer
|
||||||
|
// in the documentation and/or other materials provided with the
|
||||||
|
// distribution.
|
||||||
|
// * Neither the name of Google Inc. nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Wrappers for primitive (non-message) types. These types are useful
|
||||||
|
// for embedding primitives in the `google.protobuf.Any` type and for places
|
||||||
|
// where we need to distinguish between the absence of a primitive
|
||||||
|
// typed field and its default value.
|
||||||
|
//
|
||||||
|
import { ScalarType } from "@protobuf-ts/runtime";
|
||||||
|
import { LongType } from "@protobuf-ts/runtime";
|
||||||
|
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { IBinaryWriter } from "@protobuf-ts/runtime";
|
||||||
|
import { WireType } from "@protobuf-ts/runtime";
|
||||||
|
import type { BinaryReadOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { IBinaryReader } from "@protobuf-ts/runtime";
|
||||||
|
import { UnknownFieldHandler } from "@protobuf-ts/runtime";
|
||||||
|
import type { PartialMessage } from "@protobuf-ts/runtime";
|
||||||
|
import { reflectionMergePartial } from "@protobuf-ts/runtime";
|
||||||
|
import { MESSAGE_TYPE } from "@protobuf-ts/runtime";
|
||||||
|
import type { JsonValue } from "@protobuf-ts/runtime";
|
||||||
|
import type { JsonReadOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { JsonWriteOptions } from "@protobuf-ts/runtime";
|
||||||
|
import { MessageType } from "@protobuf-ts/runtime";
|
||||||
|
/**
|
||||||
|
* Wrapper message for `double`.
|
||||||
|
*
|
||||||
|
* The JSON representation for `DoubleValue` is JSON number.
|
||||||
|
*
|
||||||
|
* @generated from protobuf message google.protobuf.DoubleValue
|
||||||
|
*/
|
||||||
|
export interface DoubleValue {
|
||||||
|
/**
|
||||||
|
* The double value.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: double value = 1;
|
||||||
|
*/
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wrapper message for `float`.
|
||||||
|
*
|
||||||
|
* The JSON representation for `FloatValue` is JSON number.
|
||||||
|
*
|
||||||
|
* @generated from protobuf message google.protobuf.FloatValue
|
||||||
|
*/
|
||||||
|
export interface FloatValue {
|
||||||
|
/**
|
||||||
|
* The float value.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: float value = 1;
|
||||||
|
*/
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wrapper message for `int64`.
|
||||||
|
*
|
||||||
|
* The JSON representation for `Int64Value` is JSON string.
|
||||||
|
*
|
||||||
|
* @generated from protobuf message google.protobuf.Int64Value
|
||||||
|
*/
|
||||||
|
export interface Int64Value {
|
||||||
|
/**
|
||||||
|
* The int64 value.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: int64 value = 1;
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wrapper message for `uint64`.
|
||||||
|
*
|
||||||
|
* The JSON representation for `UInt64Value` is JSON string.
|
||||||
|
*
|
||||||
|
* @generated from protobuf message google.protobuf.UInt64Value
|
||||||
|
*/
|
||||||
|
export interface UInt64Value {
|
||||||
|
/**
|
||||||
|
* The uint64 value.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: uint64 value = 1;
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wrapper message for `int32`.
|
||||||
|
*
|
||||||
|
* The JSON representation for `Int32Value` is JSON number.
|
||||||
|
*
|
||||||
|
* @generated from protobuf message google.protobuf.Int32Value
|
||||||
|
*/
|
||||||
|
export interface Int32Value {
|
||||||
|
/**
|
||||||
|
* The int32 value.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: int32 value = 1;
|
||||||
|
*/
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wrapper message for `uint32`.
|
||||||
|
*
|
||||||
|
* The JSON representation for `UInt32Value` is JSON number.
|
||||||
|
*
|
||||||
|
* @generated from protobuf message google.protobuf.UInt32Value
|
||||||
|
*/
|
||||||
|
export interface UInt32Value {
|
||||||
|
/**
|
||||||
|
* The uint32 value.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: uint32 value = 1;
|
||||||
|
*/
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wrapper message for `bool`.
|
||||||
|
*
|
||||||
|
* The JSON representation for `BoolValue` is JSON `true` and `false`.
|
||||||
|
*
|
||||||
|
* @generated from protobuf message google.protobuf.BoolValue
|
||||||
|
*/
|
||||||
|
export interface BoolValue {
|
||||||
|
/**
|
||||||
|
* The bool value.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: bool value = 1;
|
||||||
|
*/
|
||||||
|
value: boolean;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wrapper message for `string`.
|
||||||
|
*
|
||||||
|
* The JSON representation for `StringValue` is JSON string.
|
||||||
|
*
|
||||||
|
* @generated from protobuf message google.protobuf.StringValue
|
||||||
|
*/
|
||||||
|
export interface StringValue {
|
||||||
|
/**
|
||||||
|
* The string value.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: string value = 1;
|
||||||
|
*/
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Wrapper message for `bytes`.
|
||||||
|
*
|
||||||
|
* The JSON representation for `BytesValue` is JSON string.
|
||||||
|
*
|
||||||
|
* @generated from protobuf message google.protobuf.BytesValue
|
||||||
|
*/
|
||||||
|
export interface BytesValue {
|
||||||
|
/**
|
||||||
|
* The bytes value.
|
||||||
|
*
|
||||||
|
* @generated from protobuf field: bytes value = 1;
|
||||||
|
*/
|
||||||
|
value: Uint8Array;
|
||||||
|
}
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class DoubleValue$Type extends MessageType<DoubleValue> {
|
||||||
|
constructor() {
|
||||||
|
super("google.protobuf.DoubleValue", [
|
||||||
|
{ no: 1, name: "value", kind: "scalar", T: 1 /*ScalarType.DOUBLE*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encode `DoubleValue` to JSON number.
|
||||||
|
*/
|
||||||
|
internalJsonWrite(message: DoubleValue, options: JsonWriteOptions): JsonValue {
|
||||||
|
return this.refJsonWriter.scalar(2, message.value, "value", false, true);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode `DoubleValue` from JSON number.
|
||||||
|
*/
|
||||||
|
internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: DoubleValue): DoubleValue {
|
||||||
|
if (!target)
|
||||||
|
target = this.create();
|
||||||
|
target.value = this.refJsonReader.scalar(json, 1, undefined, "value") as number;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<DoubleValue>): DoubleValue {
|
||||||
|
const message = { value: 0 };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<DoubleValue>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: DoubleValue): DoubleValue {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* double value */ 1:
|
||||||
|
message.value = reader.double();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: DoubleValue, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* double value = 1; */
|
||||||
|
if (message.value !== 0)
|
||||||
|
writer.tag(1, WireType.Bit64).double(message.value);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message google.protobuf.DoubleValue
|
||||||
|
*/
|
||||||
|
export const DoubleValue = new DoubleValue$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class FloatValue$Type extends MessageType<FloatValue> {
|
||||||
|
constructor() {
|
||||||
|
super("google.protobuf.FloatValue", [
|
||||||
|
{ no: 1, name: "value", kind: "scalar", T: 2 /*ScalarType.FLOAT*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encode `FloatValue` to JSON number.
|
||||||
|
*/
|
||||||
|
internalJsonWrite(message: FloatValue, options: JsonWriteOptions): JsonValue {
|
||||||
|
return this.refJsonWriter.scalar(1, message.value, "value", false, true);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode `FloatValue` from JSON number.
|
||||||
|
*/
|
||||||
|
internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: FloatValue): FloatValue {
|
||||||
|
if (!target)
|
||||||
|
target = this.create();
|
||||||
|
target.value = this.refJsonReader.scalar(json, 1, undefined, "value") as number;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<FloatValue>): FloatValue {
|
||||||
|
const message = { value: 0 };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<FloatValue>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FloatValue): FloatValue {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* float value */ 1:
|
||||||
|
message.value = reader.float();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: FloatValue, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* float value = 1; */
|
||||||
|
if (message.value !== 0)
|
||||||
|
writer.tag(1, WireType.Bit32).float(message.value);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message google.protobuf.FloatValue
|
||||||
|
*/
|
||||||
|
export const FloatValue = new FloatValue$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class Int64Value$Type extends MessageType<Int64Value> {
|
||||||
|
constructor() {
|
||||||
|
super("google.protobuf.Int64Value", [
|
||||||
|
{ no: 1, name: "value", kind: "scalar", T: 3 /*ScalarType.INT64*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encode `Int64Value` to JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonWrite(message: Int64Value, options: JsonWriteOptions): JsonValue {
|
||||||
|
return this.refJsonWriter.scalar(ScalarType.INT64, message.value, "value", false, true);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode `Int64Value` from JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: Int64Value): Int64Value {
|
||||||
|
if (!target)
|
||||||
|
target = this.create();
|
||||||
|
target.value = this.refJsonReader.scalar(json, ScalarType.INT64, LongType.STRING, "value") as any;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<Int64Value>): Int64Value {
|
||||||
|
const message = { value: "0" };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<Int64Value>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Int64Value): Int64Value {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* int64 value */ 1:
|
||||||
|
message.value = reader.int64().toString();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: Int64Value, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* int64 value = 1; */
|
||||||
|
if (message.value !== "0")
|
||||||
|
writer.tag(1, WireType.Varint).int64(message.value);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message google.protobuf.Int64Value
|
||||||
|
*/
|
||||||
|
export const Int64Value = new Int64Value$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class UInt64Value$Type extends MessageType<UInt64Value> {
|
||||||
|
constructor() {
|
||||||
|
super("google.protobuf.UInt64Value", [
|
||||||
|
{ no: 1, name: "value", kind: "scalar", T: 4 /*ScalarType.UINT64*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encode `UInt64Value` to JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonWrite(message: UInt64Value, options: JsonWriteOptions): JsonValue {
|
||||||
|
return this.refJsonWriter.scalar(ScalarType.UINT64, message.value, "value", false, true);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode `UInt64Value` from JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: UInt64Value): UInt64Value {
|
||||||
|
if (!target)
|
||||||
|
target = this.create();
|
||||||
|
target.value = this.refJsonReader.scalar(json, ScalarType.UINT64, LongType.STRING, "value") as any;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<UInt64Value>): UInt64Value {
|
||||||
|
const message = { value: "0" };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<UInt64Value>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UInt64Value): UInt64Value {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* uint64 value */ 1:
|
||||||
|
message.value = reader.uint64().toString();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: UInt64Value, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* uint64 value = 1; */
|
||||||
|
if (message.value !== "0")
|
||||||
|
writer.tag(1, WireType.Varint).uint64(message.value);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message google.protobuf.UInt64Value
|
||||||
|
*/
|
||||||
|
export const UInt64Value = new UInt64Value$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class Int32Value$Type extends MessageType<Int32Value> {
|
||||||
|
constructor() {
|
||||||
|
super("google.protobuf.Int32Value", [
|
||||||
|
{ no: 1, name: "value", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encode `Int32Value` to JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonWrite(message: Int32Value, options: JsonWriteOptions): JsonValue {
|
||||||
|
return this.refJsonWriter.scalar(5, message.value, "value", false, true);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode `Int32Value` from JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: Int32Value): Int32Value {
|
||||||
|
if (!target)
|
||||||
|
target = this.create();
|
||||||
|
target.value = this.refJsonReader.scalar(json, 5, undefined, "value") as number;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<Int32Value>): Int32Value {
|
||||||
|
const message = { value: 0 };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<Int32Value>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: Int32Value): Int32Value {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* int32 value */ 1:
|
||||||
|
message.value = reader.int32();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: Int32Value, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* int32 value = 1; */
|
||||||
|
if (message.value !== 0)
|
||||||
|
writer.tag(1, WireType.Varint).int32(message.value);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message google.protobuf.Int32Value
|
||||||
|
*/
|
||||||
|
export const Int32Value = new Int32Value$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class UInt32Value$Type extends MessageType<UInt32Value> {
|
||||||
|
constructor() {
|
||||||
|
super("google.protobuf.UInt32Value", [
|
||||||
|
{ no: 1, name: "value", kind: "scalar", T: 13 /*ScalarType.UINT32*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encode `UInt32Value` to JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonWrite(message: UInt32Value, options: JsonWriteOptions): JsonValue {
|
||||||
|
return this.refJsonWriter.scalar(13, message.value, "value", false, true);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode `UInt32Value` from JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: UInt32Value): UInt32Value {
|
||||||
|
if (!target)
|
||||||
|
target = this.create();
|
||||||
|
target.value = this.refJsonReader.scalar(json, 13, undefined, "value") as number;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<UInt32Value>): UInt32Value {
|
||||||
|
const message = { value: 0 };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<UInt32Value>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: UInt32Value): UInt32Value {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* uint32 value */ 1:
|
||||||
|
message.value = reader.uint32();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: UInt32Value, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* uint32 value = 1; */
|
||||||
|
if (message.value !== 0)
|
||||||
|
writer.tag(1, WireType.Varint).uint32(message.value);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message google.protobuf.UInt32Value
|
||||||
|
*/
|
||||||
|
export const UInt32Value = new UInt32Value$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class BoolValue$Type extends MessageType<BoolValue> {
|
||||||
|
constructor() {
|
||||||
|
super("google.protobuf.BoolValue", [
|
||||||
|
{ no: 1, name: "value", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encode `BoolValue` to JSON bool.
|
||||||
|
*/
|
||||||
|
internalJsonWrite(message: BoolValue, options: JsonWriteOptions): JsonValue {
|
||||||
|
return message.value;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode `BoolValue` from JSON bool.
|
||||||
|
*/
|
||||||
|
internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: BoolValue): BoolValue {
|
||||||
|
if (!target)
|
||||||
|
target = this.create();
|
||||||
|
target.value = this.refJsonReader.scalar(json, 8, undefined, "value") as boolean;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<BoolValue>): BoolValue {
|
||||||
|
const message = { value: false };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<BoolValue>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: BoolValue): BoolValue {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* bool value */ 1:
|
||||||
|
message.value = reader.bool();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: BoolValue, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* bool value = 1; */
|
||||||
|
if (message.value !== false)
|
||||||
|
writer.tag(1, WireType.Varint).bool(message.value);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message google.protobuf.BoolValue
|
||||||
|
*/
|
||||||
|
export const BoolValue = new BoolValue$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class StringValue$Type extends MessageType<StringValue> {
|
||||||
|
constructor() {
|
||||||
|
super("google.protobuf.StringValue", [
|
||||||
|
{ no: 1, name: "value", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encode `StringValue` to JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonWrite(message: StringValue, options: JsonWriteOptions): JsonValue {
|
||||||
|
return message.value;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode `StringValue` from JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: StringValue): StringValue {
|
||||||
|
if (!target)
|
||||||
|
target = this.create();
|
||||||
|
target.value = this.refJsonReader.scalar(json, 9, undefined, "value") as string;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<StringValue>): StringValue {
|
||||||
|
const message = { value: "" };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<StringValue>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: StringValue): StringValue {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* string value */ 1:
|
||||||
|
message.value = reader.string();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: StringValue, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* string value = 1; */
|
||||||
|
if (message.value !== "")
|
||||||
|
writer.tag(1, WireType.LengthDelimited).string(message.value);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message google.protobuf.StringValue
|
||||||
|
*/
|
||||||
|
export const StringValue = new StringValue$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class BytesValue$Type extends MessageType<BytesValue> {
|
||||||
|
constructor() {
|
||||||
|
super("google.protobuf.BytesValue", [
|
||||||
|
{ no: 1, name: "value", kind: "scalar", T: 12 /*ScalarType.BYTES*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Encode `BytesValue` to JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonWrite(message: BytesValue, options: JsonWriteOptions): JsonValue {
|
||||||
|
return this.refJsonWriter.scalar(12, message.value, "value", false, true);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Decode `BytesValue` from JSON string.
|
||||||
|
*/
|
||||||
|
internalJsonRead(json: JsonValue, options: JsonReadOptions, target?: BytesValue): BytesValue {
|
||||||
|
if (!target)
|
||||||
|
target = this.create();
|
||||||
|
target.value = this.refJsonReader.scalar(json, 12, undefined, "value") as Uint8Array;
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<BytesValue>): BytesValue {
|
||||||
|
const message = { value: new Uint8Array(0) };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<BytesValue>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: BytesValue): BytesValue {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* bytes value */ 1:
|
||||||
|
message.value = reader.bytes();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: BytesValue, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* bytes value = 1; */
|
||||||
|
if (message.value.length)
|
||||||
|
writer.tag(1, WireType.LengthDelimited).bytes(message.value);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message google.protobuf.BytesValue
|
||||||
|
*/
|
||||||
|
export const BytesValue = new BytesValue$Type();
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './google/protobuf/timestamp'
|
||||||
|
export * from './google/protobuf/wrappers'
|
||||||
|
export * from './results/api/v1/artifact'
|
||||||
|
export * from './results/api/v1/artifact.twirp'
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
// @generated by protobuf-ts 2.9.1 with parameter long_type_string,client_none,generate_dependencies
|
||||||
|
// @generated from protobuf file "results/api/v1/artifact.proto" (package "github.actions.results.api.v1", syntax proto3)
|
||||||
|
// tslint:disable
|
||||||
|
import { ServiceType } from "@protobuf-ts/runtime-rpc";
|
||||||
|
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { IBinaryWriter } from "@protobuf-ts/runtime";
|
||||||
|
import { WireType } from "@protobuf-ts/runtime";
|
||||||
|
import type { BinaryReadOptions } from "@protobuf-ts/runtime";
|
||||||
|
import type { IBinaryReader } from "@protobuf-ts/runtime";
|
||||||
|
import { UnknownFieldHandler } from "@protobuf-ts/runtime";
|
||||||
|
import type { PartialMessage } from "@protobuf-ts/runtime";
|
||||||
|
import { reflectionMergePartial } from "@protobuf-ts/runtime";
|
||||||
|
import { MESSAGE_TYPE } from "@protobuf-ts/runtime";
|
||||||
|
import { MessageType } from "@protobuf-ts/runtime";
|
||||||
|
import { StringValue } from "../../../google/protobuf/wrappers";
|
||||||
|
import { Timestamp } from "../../../google/protobuf/timestamp";
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message github.actions.results.api.v1.CreateArtifactRequest
|
||||||
|
*/
|
||||||
|
export interface CreateArtifactRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string workflow_run_backend_id = 1;
|
||||||
|
*/
|
||||||
|
workflowRunBackendId: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string workflow_job_run_backend_id = 2;
|
||||||
|
*/
|
||||||
|
workflowJobRunBackendId: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string name = 3;
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: google.protobuf.Timestamp expires_at = 4;
|
||||||
|
*/
|
||||||
|
expiresAt?: Timestamp;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: int32 version = 5;
|
||||||
|
*/
|
||||||
|
version: number;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message github.actions.results.api.v1.CreateArtifactResponse
|
||||||
|
*/
|
||||||
|
export interface CreateArtifactResponse {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: bool ok = 1;
|
||||||
|
*/
|
||||||
|
ok: boolean;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string signed_upload_url = 2;
|
||||||
|
*/
|
||||||
|
signedUploadUrl: string;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message github.actions.results.api.v1.FinalizeArtifactRequest
|
||||||
|
*/
|
||||||
|
export interface FinalizeArtifactRequest {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string workflow_run_backend_id = 1;
|
||||||
|
*/
|
||||||
|
workflowRunBackendId: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string workflow_job_run_backend_id = 2;
|
||||||
|
*/
|
||||||
|
workflowJobRunBackendId: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: string name = 3;
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: int64 size = 4;
|
||||||
|
*/
|
||||||
|
size: string;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: google.protobuf.StringValue hash = 5;
|
||||||
|
*/
|
||||||
|
hash?: StringValue; // optional
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated from protobuf message github.actions.results.api.v1.FinalizeArtifactResponse
|
||||||
|
*/
|
||||||
|
export interface FinalizeArtifactResponse {
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: bool ok = 1;
|
||||||
|
*/
|
||||||
|
ok: boolean;
|
||||||
|
/**
|
||||||
|
* @generated from protobuf field: int64 artifact_id = 2;
|
||||||
|
*/
|
||||||
|
artifactId: string;
|
||||||
|
}
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class CreateArtifactRequest$Type extends MessageType<CreateArtifactRequest> {
|
||||||
|
constructor() {
|
||||||
|
super("github.actions.results.api.v1.CreateArtifactRequest", [
|
||||||
|
{ no: 1, name: "workflow_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 2, name: "workflow_job_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 3, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 4, name: "expires_at", kind: "message", T: () => Timestamp },
|
||||||
|
{ no: 5, name: "version", kind: "scalar", T: 5 /*ScalarType.INT32*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<CreateArtifactRequest>): CreateArtifactRequest {
|
||||||
|
const message = { workflowRunBackendId: "", workflowJobRunBackendId: "", name: "", version: 0 };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<CreateArtifactRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: CreateArtifactRequest): CreateArtifactRequest {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* string workflow_run_backend_id */ 1:
|
||||||
|
message.workflowRunBackendId = reader.string();
|
||||||
|
break;
|
||||||
|
case /* string workflow_job_run_backend_id */ 2:
|
||||||
|
message.workflowJobRunBackendId = reader.string();
|
||||||
|
break;
|
||||||
|
case /* string name */ 3:
|
||||||
|
message.name = reader.string();
|
||||||
|
break;
|
||||||
|
case /* google.protobuf.Timestamp expires_at */ 4:
|
||||||
|
message.expiresAt = Timestamp.internalBinaryRead(reader, reader.uint32(), options, message.expiresAt);
|
||||||
|
break;
|
||||||
|
case /* int32 version */ 5:
|
||||||
|
message.version = reader.int32();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: CreateArtifactRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* string workflow_run_backend_id = 1; */
|
||||||
|
if (message.workflowRunBackendId !== "")
|
||||||
|
writer.tag(1, WireType.LengthDelimited).string(message.workflowRunBackendId);
|
||||||
|
/* string workflow_job_run_backend_id = 2; */
|
||||||
|
if (message.workflowJobRunBackendId !== "")
|
||||||
|
writer.tag(2, WireType.LengthDelimited).string(message.workflowJobRunBackendId);
|
||||||
|
/* string name = 3; */
|
||||||
|
if (message.name !== "")
|
||||||
|
writer.tag(3, WireType.LengthDelimited).string(message.name);
|
||||||
|
/* google.protobuf.Timestamp expires_at = 4; */
|
||||||
|
if (message.expiresAt)
|
||||||
|
Timestamp.internalBinaryWrite(message.expiresAt, writer.tag(4, WireType.LengthDelimited).fork(), options).join();
|
||||||
|
/* int32 version = 5; */
|
||||||
|
if (message.version !== 0)
|
||||||
|
writer.tag(5, WireType.Varint).int32(message.version);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message github.actions.results.api.v1.CreateArtifactRequest
|
||||||
|
*/
|
||||||
|
export const CreateArtifactRequest = new CreateArtifactRequest$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class CreateArtifactResponse$Type extends MessageType<CreateArtifactResponse> {
|
||||||
|
constructor() {
|
||||||
|
super("github.actions.results.api.v1.CreateArtifactResponse", [
|
||||||
|
{ no: 1, name: "ok", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
|
||||||
|
{ no: 2, name: "signed_upload_url", kind: "scalar", T: 9 /*ScalarType.STRING*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<CreateArtifactResponse>): CreateArtifactResponse {
|
||||||
|
const message = { ok: false, signedUploadUrl: "" };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<CreateArtifactResponse>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: CreateArtifactResponse): CreateArtifactResponse {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* bool ok */ 1:
|
||||||
|
message.ok = reader.bool();
|
||||||
|
break;
|
||||||
|
case /* string signed_upload_url */ 2:
|
||||||
|
message.signedUploadUrl = reader.string();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: CreateArtifactResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* bool ok = 1; */
|
||||||
|
if (message.ok !== false)
|
||||||
|
writer.tag(1, WireType.Varint).bool(message.ok);
|
||||||
|
/* string signed_upload_url = 2; */
|
||||||
|
if (message.signedUploadUrl !== "")
|
||||||
|
writer.tag(2, WireType.LengthDelimited).string(message.signedUploadUrl);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message github.actions.results.api.v1.CreateArtifactResponse
|
||||||
|
*/
|
||||||
|
export const CreateArtifactResponse = new CreateArtifactResponse$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class FinalizeArtifactRequest$Type extends MessageType<FinalizeArtifactRequest> {
|
||||||
|
constructor() {
|
||||||
|
super("github.actions.results.api.v1.FinalizeArtifactRequest", [
|
||||||
|
{ no: 1, name: "workflow_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 2, name: "workflow_job_run_backend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 3, name: "name", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
|
||||||
|
{ no: 4, name: "size", kind: "scalar", T: 3 /*ScalarType.INT64*/ },
|
||||||
|
{ no: 5, name: "hash", kind: "message", T: () => StringValue }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<FinalizeArtifactRequest>): FinalizeArtifactRequest {
|
||||||
|
const message = { workflowRunBackendId: "", workflowJobRunBackendId: "", name: "", size: "0" };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<FinalizeArtifactRequest>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FinalizeArtifactRequest): FinalizeArtifactRequest {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* string workflow_run_backend_id */ 1:
|
||||||
|
message.workflowRunBackendId = reader.string();
|
||||||
|
break;
|
||||||
|
case /* string workflow_job_run_backend_id */ 2:
|
||||||
|
message.workflowJobRunBackendId = reader.string();
|
||||||
|
break;
|
||||||
|
case /* string name */ 3:
|
||||||
|
message.name = reader.string();
|
||||||
|
break;
|
||||||
|
case /* int64 size */ 4:
|
||||||
|
message.size = reader.int64().toString();
|
||||||
|
break;
|
||||||
|
case /* google.protobuf.StringValue hash */ 5:
|
||||||
|
message.hash = StringValue.internalBinaryRead(reader, reader.uint32(), options, message.hash);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: FinalizeArtifactRequest, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* string workflow_run_backend_id = 1; */
|
||||||
|
if (message.workflowRunBackendId !== "")
|
||||||
|
writer.tag(1, WireType.LengthDelimited).string(message.workflowRunBackendId);
|
||||||
|
/* string workflow_job_run_backend_id = 2; */
|
||||||
|
if (message.workflowJobRunBackendId !== "")
|
||||||
|
writer.tag(2, WireType.LengthDelimited).string(message.workflowJobRunBackendId);
|
||||||
|
/* string name = 3; */
|
||||||
|
if (message.name !== "")
|
||||||
|
writer.tag(3, WireType.LengthDelimited).string(message.name);
|
||||||
|
/* int64 size = 4; */
|
||||||
|
if (message.size !== "0")
|
||||||
|
writer.tag(4, WireType.Varint).int64(message.size);
|
||||||
|
/* google.protobuf.StringValue hash = 5; */
|
||||||
|
if (message.hash)
|
||||||
|
StringValue.internalBinaryWrite(message.hash, writer.tag(5, WireType.LengthDelimited).fork(), options).join();
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message github.actions.results.api.v1.FinalizeArtifactRequest
|
||||||
|
*/
|
||||||
|
export const FinalizeArtifactRequest = new FinalizeArtifactRequest$Type();
|
||||||
|
// @generated message type with reflection information, may provide speed optimized methods
|
||||||
|
class FinalizeArtifactResponse$Type extends MessageType<FinalizeArtifactResponse> {
|
||||||
|
constructor() {
|
||||||
|
super("github.actions.results.api.v1.FinalizeArtifactResponse", [
|
||||||
|
{ no: 1, name: "ok", kind: "scalar", T: 8 /*ScalarType.BOOL*/ },
|
||||||
|
{ no: 2, name: "artifact_id", kind: "scalar", T: 3 /*ScalarType.INT64*/ }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
create(value?: PartialMessage<FinalizeArtifactResponse>): FinalizeArtifactResponse {
|
||||||
|
const message = { ok: false, artifactId: "0" };
|
||||||
|
globalThis.Object.defineProperty(message, MESSAGE_TYPE, { enumerable: false, value: this });
|
||||||
|
if (value !== undefined)
|
||||||
|
reflectionMergePartial<FinalizeArtifactResponse>(this, message, value);
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryRead(reader: IBinaryReader, length: number, options: BinaryReadOptions, target?: FinalizeArtifactResponse): FinalizeArtifactResponse {
|
||||||
|
let message = target ?? this.create(), end = reader.pos + length;
|
||||||
|
while (reader.pos < end) {
|
||||||
|
let [fieldNo, wireType] = reader.tag();
|
||||||
|
switch (fieldNo) {
|
||||||
|
case /* bool ok */ 1:
|
||||||
|
message.ok = reader.bool();
|
||||||
|
break;
|
||||||
|
case /* int64 artifact_id */ 2:
|
||||||
|
message.artifactId = reader.int64().toString();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
let u = options.readUnknownField;
|
||||||
|
if (u === "throw")
|
||||||
|
throw new globalThis.Error(`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`);
|
||||||
|
let d = reader.skip(wireType);
|
||||||
|
if (u !== false)
|
||||||
|
(u === true ? UnknownFieldHandler.onRead : u)(this.typeName, message, fieldNo, wireType, d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
internalBinaryWrite(message: FinalizeArtifactResponse, writer: IBinaryWriter, options: BinaryWriteOptions): IBinaryWriter {
|
||||||
|
/* bool ok = 1; */
|
||||||
|
if (message.ok !== false)
|
||||||
|
writer.tag(1, WireType.Varint).bool(message.ok);
|
||||||
|
/* int64 artifact_id = 2; */
|
||||||
|
if (message.artifactId !== "0")
|
||||||
|
writer.tag(2, WireType.Varint).int64(message.artifactId);
|
||||||
|
let u = options.writeUnknownFields;
|
||||||
|
if (u !== false)
|
||||||
|
(u == true ? UnknownFieldHandler.onWrite : u)(this.typeName, message, writer);
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @generated MessageType for protobuf message github.actions.results.api.v1.FinalizeArtifactResponse
|
||||||
|
*/
|
||||||
|
export const FinalizeArtifactResponse = new FinalizeArtifactResponse$Type();
|
||||||
|
/**
|
||||||
|
* @generated ServiceType for protobuf service github.actions.results.api.v1.ArtifactService
|
||||||
|
*/
|
||||||
|
export const ArtifactService = new ServiceType("github.actions.results.api.v1.ArtifactService", [
|
||||||
|
{ name: "CreateArtifact", options: {}, I: CreateArtifactRequest, O: CreateArtifactResponse },
|
||||||
|
{ name: "FinalizeArtifact", options: {}, I: FinalizeArtifactRequest, O: FinalizeArtifactResponse }
|
||||||
|
]);
|
||||||
@@ -0,0 +1,437 @@
|
|||||||
|
import {
|
||||||
|
TwirpContext,
|
||||||
|
TwirpServer,
|
||||||
|
RouterEvents,
|
||||||
|
TwirpError,
|
||||||
|
TwirpErrorCode,
|
||||||
|
Interceptor,
|
||||||
|
TwirpContentType,
|
||||||
|
chainInterceptors
|
||||||
|
} from 'twirp-ts'
|
||||||
|
import {
|
||||||
|
CreateArtifactRequest,
|
||||||
|
CreateArtifactResponse,
|
||||||
|
FinalizeArtifactRequest,
|
||||||
|
FinalizeArtifactResponse
|
||||||
|
} from './artifact'
|
||||||
|
|
||||||
|
//==================================//
|
||||||
|
// Client Code //
|
||||||
|
//==================================//
|
||||||
|
|
||||||
|
interface Rpc {
|
||||||
|
request(
|
||||||
|
service: string,
|
||||||
|
method: string,
|
||||||
|
contentType: 'application/json' | 'application/protobuf',
|
||||||
|
data: object | Uint8Array
|
||||||
|
): Promise<object | Uint8Array>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArtifactServiceClient {
|
||||||
|
CreateArtifact(
|
||||||
|
request: CreateArtifactRequest
|
||||||
|
): Promise<CreateArtifactResponse>
|
||||||
|
FinalizeArtifact(
|
||||||
|
request: FinalizeArtifactRequest
|
||||||
|
): Promise<FinalizeArtifactResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArtifactServiceClientJSON implements ArtifactServiceClient {
|
||||||
|
private readonly rpc: Rpc
|
||||||
|
constructor(rpc: Rpc) {
|
||||||
|
this.rpc = rpc
|
||||||
|
this.CreateArtifact.bind(this)
|
||||||
|
this.FinalizeArtifact.bind(this)
|
||||||
|
}
|
||||||
|
CreateArtifact(
|
||||||
|
request: CreateArtifactRequest
|
||||||
|
): Promise<CreateArtifactResponse> {
|
||||||
|
const data = CreateArtifactRequest.toJson(request, {
|
||||||
|
useProtoFieldName: true,
|
||||||
|
emitDefaultValues: false
|
||||||
|
})
|
||||||
|
const promise = this.rpc.request(
|
||||||
|
'github.actions.results.api.v1.ArtifactService',
|
||||||
|
'CreateArtifact',
|
||||||
|
'application/json',
|
||||||
|
data as object
|
||||||
|
)
|
||||||
|
return promise.then(data =>
|
||||||
|
CreateArtifactResponse.fromJson(data as any, {ignoreUnknownFields: true})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FinalizeArtifact(
|
||||||
|
request: FinalizeArtifactRequest
|
||||||
|
): Promise<FinalizeArtifactResponse> {
|
||||||
|
const data = FinalizeArtifactRequest.toJson(request, {
|
||||||
|
useProtoFieldName: true,
|
||||||
|
emitDefaultValues: false
|
||||||
|
})
|
||||||
|
const promise = this.rpc.request(
|
||||||
|
'github.actions.results.api.v1.ArtifactService',
|
||||||
|
'FinalizeArtifact',
|
||||||
|
'application/json',
|
||||||
|
data as object
|
||||||
|
)
|
||||||
|
return promise.then(data =>
|
||||||
|
FinalizeArtifactResponse.fromJson(data as any, {
|
||||||
|
ignoreUnknownFields: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ArtifactServiceClientProtobuf implements ArtifactServiceClient {
|
||||||
|
private readonly rpc: Rpc
|
||||||
|
constructor(rpc: Rpc) {
|
||||||
|
this.rpc = rpc
|
||||||
|
this.CreateArtifact.bind(this)
|
||||||
|
this.FinalizeArtifact.bind(this)
|
||||||
|
}
|
||||||
|
CreateArtifact(
|
||||||
|
request: CreateArtifactRequest
|
||||||
|
): Promise<CreateArtifactResponse> {
|
||||||
|
const data = CreateArtifactRequest.toBinary(request)
|
||||||
|
const promise = this.rpc.request(
|
||||||
|
'github.actions.results.api.v1.ArtifactService',
|
||||||
|
'CreateArtifact',
|
||||||
|
'application/protobuf',
|
||||||
|
data
|
||||||
|
)
|
||||||
|
return promise.then(data =>
|
||||||
|
CreateArtifactResponse.fromBinary(data as Uint8Array)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FinalizeArtifact(
|
||||||
|
request: FinalizeArtifactRequest
|
||||||
|
): Promise<FinalizeArtifactResponse> {
|
||||||
|
const data = FinalizeArtifactRequest.toBinary(request)
|
||||||
|
const promise = this.rpc.request(
|
||||||
|
'github.actions.results.api.v1.ArtifactService',
|
||||||
|
'FinalizeArtifact',
|
||||||
|
'application/protobuf',
|
||||||
|
data
|
||||||
|
)
|
||||||
|
return promise.then(data =>
|
||||||
|
FinalizeArtifactResponse.fromBinary(data as Uint8Array)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//==================================//
|
||||||
|
// Server Code //
|
||||||
|
//==================================//
|
||||||
|
|
||||||
|
export interface ArtifactServiceTwirp<T extends TwirpContext = TwirpContext> {
|
||||||
|
CreateArtifact(
|
||||||
|
ctx: T,
|
||||||
|
request: CreateArtifactRequest
|
||||||
|
): Promise<CreateArtifactResponse>
|
||||||
|
FinalizeArtifact(
|
||||||
|
ctx: T,
|
||||||
|
request: FinalizeArtifactRequest
|
||||||
|
): Promise<FinalizeArtifactResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ArtifactServiceMethod {
|
||||||
|
CreateArtifact = 'CreateArtifact',
|
||||||
|
FinalizeArtifact = 'FinalizeArtifact'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArtifactServiceMethodList = [
|
||||||
|
ArtifactServiceMethod.CreateArtifact,
|
||||||
|
ArtifactServiceMethod.FinalizeArtifact
|
||||||
|
]
|
||||||
|
|
||||||
|
export function createArtifactServiceServer<
|
||||||
|
T extends TwirpContext = TwirpContext
|
||||||
|
>(service: ArtifactServiceTwirp<T>) {
|
||||||
|
return new TwirpServer<ArtifactServiceTwirp, T>({
|
||||||
|
service,
|
||||||
|
packageName: 'github.actions.results.api.v1',
|
||||||
|
serviceName: 'ArtifactService',
|
||||||
|
methodList: ArtifactServiceMethodList,
|
||||||
|
matchRoute: matchArtifactServiceRoute
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchArtifactServiceRoute<T extends TwirpContext = TwirpContext>(
|
||||||
|
method: string,
|
||||||
|
events: RouterEvents<T>
|
||||||
|
) {
|
||||||
|
switch (method) {
|
||||||
|
case 'CreateArtifact':
|
||||||
|
return async (
|
||||||
|
ctx: T,
|
||||||
|
service: ArtifactServiceTwirp,
|
||||||
|
data: Buffer,
|
||||||
|
interceptors?: Interceptor<
|
||||||
|
T,
|
||||||
|
CreateArtifactRequest,
|
||||||
|
CreateArtifactResponse
|
||||||
|
>[]
|
||||||
|
) => {
|
||||||
|
ctx = {...ctx, methodName: 'CreateArtifact'}
|
||||||
|
await events.onMatch(ctx)
|
||||||
|
return handleArtifactServiceCreateArtifactRequest(
|
||||||
|
ctx,
|
||||||
|
service,
|
||||||
|
data,
|
||||||
|
interceptors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case 'FinalizeArtifact':
|
||||||
|
return async (
|
||||||
|
ctx: T,
|
||||||
|
service: ArtifactServiceTwirp,
|
||||||
|
data: Buffer,
|
||||||
|
interceptors?: Interceptor<
|
||||||
|
T,
|
||||||
|
FinalizeArtifactRequest,
|
||||||
|
FinalizeArtifactResponse
|
||||||
|
>[]
|
||||||
|
) => {
|
||||||
|
ctx = {...ctx, methodName: 'FinalizeArtifact'}
|
||||||
|
await events.onMatch(ctx)
|
||||||
|
return handleArtifactServiceFinalizeArtifactRequest(
|
||||||
|
ctx,
|
||||||
|
service,
|
||||||
|
data,
|
||||||
|
interceptors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
events.onNotFound()
|
||||||
|
const msg = `no handler found`
|
||||||
|
throw new TwirpError(TwirpErrorCode.BadRoute, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArtifactServiceCreateArtifactRequest<
|
||||||
|
T extends TwirpContext = TwirpContext
|
||||||
|
>(
|
||||||
|
ctx: T,
|
||||||
|
service: ArtifactServiceTwirp,
|
||||||
|
data: Buffer,
|
||||||
|
interceptors?: Interceptor<T, CreateArtifactRequest, CreateArtifactResponse>[]
|
||||||
|
): Promise<string | Uint8Array> {
|
||||||
|
switch (ctx.contentType) {
|
||||||
|
case TwirpContentType.JSON:
|
||||||
|
return handleArtifactServiceCreateArtifactJSON<T>(
|
||||||
|
ctx,
|
||||||
|
service,
|
||||||
|
data,
|
||||||
|
interceptors
|
||||||
|
)
|
||||||
|
case TwirpContentType.Protobuf:
|
||||||
|
return handleArtifactServiceCreateArtifactProtobuf<T>(
|
||||||
|
ctx,
|
||||||
|
service,
|
||||||
|
data,
|
||||||
|
interceptors
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
const msg = 'unexpected Content-Type'
|
||||||
|
throw new TwirpError(TwirpErrorCode.BadRoute, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleArtifactServiceFinalizeArtifactRequest<
|
||||||
|
T extends TwirpContext = TwirpContext
|
||||||
|
>(
|
||||||
|
ctx: T,
|
||||||
|
service: ArtifactServiceTwirp,
|
||||||
|
data: Buffer,
|
||||||
|
interceptors?: Interceptor<
|
||||||
|
T,
|
||||||
|
FinalizeArtifactRequest,
|
||||||
|
FinalizeArtifactResponse
|
||||||
|
>[]
|
||||||
|
): Promise<string | Uint8Array> {
|
||||||
|
switch (ctx.contentType) {
|
||||||
|
case TwirpContentType.JSON:
|
||||||
|
return handleArtifactServiceFinalizeArtifactJSON<T>(
|
||||||
|
ctx,
|
||||||
|
service,
|
||||||
|
data,
|
||||||
|
interceptors
|
||||||
|
)
|
||||||
|
case TwirpContentType.Protobuf:
|
||||||
|
return handleArtifactServiceFinalizeArtifactProtobuf<T>(
|
||||||
|
ctx,
|
||||||
|
service,
|
||||||
|
data,
|
||||||
|
interceptors
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
const msg = 'unexpected Content-Type'
|
||||||
|
throw new TwirpError(TwirpErrorCode.BadRoute, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function handleArtifactServiceCreateArtifactJSON<
|
||||||
|
T extends TwirpContext = TwirpContext
|
||||||
|
>(
|
||||||
|
ctx: T,
|
||||||
|
service: ArtifactServiceTwirp,
|
||||||
|
data: Buffer,
|
||||||
|
interceptors?: Interceptor<T, CreateArtifactRequest, CreateArtifactResponse>[]
|
||||||
|
) {
|
||||||
|
let request: CreateArtifactRequest
|
||||||
|
let response: CreateArtifactResponse
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(data.toString() || '{}')
|
||||||
|
request = CreateArtifactRequest.fromJson(body, {ignoreUnknownFields: true})
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
const msg = 'the json request could not be decoded'
|
||||||
|
throw new TwirpError(TwirpErrorCode.Malformed, msg).withCause(e, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interceptors && interceptors.length > 0) {
|
||||||
|
const interceptor = chainInterceptors(...interceptors) as Interceptor<
|
||||||
|
T,
|
||||||
|
CreateArtifactRequest,
|
||||||
|
CreateArtifactResponse
|
||||||
|
>
|
||||||
|
response = await interceptor(ctx, request!, (ctx, inputReq) => {
|
||||||
|
return service.CreateArtifact(ctx, inputReq)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
response = await service.CreateArtifact(ctx, request!)
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(
|
||||||
|
CreateArtifactResponse.toJson(response, {
|
||||||
|
useProtoFieldName: true,
|
||||||
|
emitDefaultValues: false
|
||||||
|
}) as string
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleArtifactServiceFinalizeArtifactJSON<
|
||||||
|
T extends TwirpContext = TwirpContext
|
||||||
|
>(
|
||||||
|
ctx: T,
|
||||||
|
service: ArtifactServiceTwirp,
|
||||||
|
data: Buffer,
|
||||||
|
interceptors?: Interceptor<
|
||||||
|
T,
|
||||||
|
FinalizeArtifactRequest,
|
||||||
|
FinalizeArtifactResponse
|
||||||
|
>[]
|
||||||
|
) {
|
||||||
|
let request: FinalizeArtifactRequest
|
||||||
|
let response: FinalizeArtifactResponse
|
||||||
|
|
||||||
|
try {
|
||||||
|
const body = JSON.parse(data.toString() || '{}')
|
||||||
|
request = FinalizeArtifactRequest.fromJson(body, {
|
||||||
|
ignoreUnknownFields: true
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
const msg = 'the json request could not be decoded'
|
||||||
|
throw new TwirpError(TwirpErrorCode.Malformed, msg).withCause(e, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interceptors && interceptors.length > 0) {
|
||||||
|
const interceptor = chainInterceptors(...interceptors) as Interceptor<
|
||||||
|
T,
|
||||||
|
FinalizeArtifactRequest,
|
||||||
|
FinalizeArtifactResponse
|
||||||
|
>
|
||||||
|
response = await interceptor(ctx, request!, (ctx, inputReq) => {
|
||||||
|
return service.FinalizeArtifact(ctx, inputReq)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
response = await service.FinalizeArtifact(ctx, request!)
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(
|
||||||
|
FinalizeArtifactResponse.toJson(response, {
|
||||||
|
useProtoFieldName: true,
|
||||||
|
emitDefaultValues: false
|
||||||
|
}) as string
|
||||||
|
)
|
||||||
|
}
|
||||||
|
async function handleArtifactServiceCreateArtifactProtobuf<
|
||||||
|
T extends TwirpContext = TwirpContext
|
||||||
|
>(
|
||||||
|
ctx: T,
|
||||||
|
service: ArtifactServiceTwirp,
|
||||||
|
data: Buffer,
|
||||||
|
interceptors?: Interceptor<T, CreateArtifactRequest, CreateArtifactResponse>[]
|
||||||
|
) {
|
||||||
|
let request: CreateArtifactRequest
|
||||||
|
let response: CreateArtifactResponse
|
||||||
|
|
||||||
|
try {
|
||||||
|
request = CreateArtifactRequest.fromBinary(data)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
const msg = 'the protobuf request could not be decoded'
|
||||||
|
throw new TwirpError(TwirpErrorCode.Malformed, msg).withCause(e, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interceptors && interceptors.length > 0) {
|
||||||
|
const interceptor = chainInterceptors(...interceptors) as Interceptor<
|
||||||
|
T,
|
||||||
|
CreateArtifactRequest,
|
||||||
|
CreateArtifactResponse
|
||||||
|
>
|
||||||
|
response = await interceptor(ctx, request!, (ctx, inputReq) => {
|
||||||
|
return service.CreateArtifact(ctx, inputReq)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
response = await service.CreateArtifact(ctx, request!)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from(CreateArtifactResponse.toBinary(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleArtifactServiceFinalizeArtifactProtobuf<
|
||||||
|
T extends TwirpContext = TwirpContext
|
||||||
|
>(
|
||||||
|
ctx: T,
|
||||||
|
service: ArtifactServiceTwirp,
|
||||||
|
data: Buffer,
|
||||||
|
interceptors?: Interceptor<
|
||||||
|
T,
|
||||||
|
FinalizeArtifactRequest,
|
||||||
|
FinalizeArtifactResponse
|
||||||
|
>[]
|
||||||
|
) {
|
||||||
|
let request: FinalizeArtifactRequest
|
||||||
|
let response: FinalizeArtifactResponse
|
||||||
|
|
||||||
|
try {
|
||||||
|
request = FinalizeArtifactRequest.fromBinary(data)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error) {
|
||||||
|
const msg = 'the protobuf request could not be decoded'
|
||||||
|
throw new TwirpError(TwirpErrorCode.Malformed, msg).withCause(e, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interceptors && interceptors.length > 0) {
|
||||||
|
const interceptor = chainInterceptors(...interceptors) as Interceptor<
|
||||||
|
T,
|
||||||
|
FinalizeArtifactRequest,
|
||||||
|
FinalizeArtifactResponse
|
||||||
|
>
|
||||||
|
response = await interceptor(ctx, request!, (ctx, inputReq) => {
|
||||||
|
return service.FinalizeArtifact(ctx, inputReq)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
response = await service.FinalizeArtifact(ctx, request!)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from(FinalizeArtifactResponse.toBinary(response))
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
/**
|
|
||||||
* Mocks default limits for easier testing
|
|
||||||
*/
|
|
||||||
export function getUploadFileConcurrency(): number {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUploadChunkConcurrency(): number {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUploadChunkSize(): number {
|
|
||||||
return 4 * 1024 * 1024 // 4 MB Chunks
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRetryLimit(): number {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRetryMultiplier(): number {
|
|
||||||
return 1.5
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getInitialRetryIntervalInMilliseconds(): number {
|
|
||||||
return 10
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDownloadFileConcurrency(): number {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mocks the 'ACTIONS_RUNTIME_TOKEN', 'ACTIONS_RUNTIME_URL' and 'GITHUB_RUN_ID' env variables
|
|
||||||
* that are only available from a node context on the runner. This allows for tests to run
|
|
||||||
* locally without the env variables actually being set
|
|
||||||
*/
|
|
||||||
export function getRuntimeToken(): string {
|
|
||||||
return 'totally-valid-token'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRuntimeUrl(): string {
|
|
||||||
return 'https://www.example.com/'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWorkFlowRunId(): string {
|
|
||||||
return '15'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRetentionDays(): string | undefined {
|
|
||||||
return '45'
|
|
||||||
}
|
|
||||||
@@ -1,282 +0,0 @@
|
|||||||
import * as core from '@actions/core'
|
|
||||||
import {
|
|
||||||
UploadSpecification,
|
|
||||||
getUploadSpecification
|
|
||||||
} from './upload-specification'
|
|
||||||
import {UploadHttpClient} from './upload-http-client'
|
|
||||||
import {UploadResponse} from './upload-response'
|
|
||||||
import {UploadOptions} from './upload-options'
|
|
||||||
import {DownloadOptions} from './download-options'
|
|
||||||
import {DownloadResponse} from './download-response'
|
|
||||||
import {
|
|
||||||
createDirectoriesForArtifact,
|
|
||||||
createEmptyFilesForArtifact
|
|
||||||
} from './utils'
|
|
||||||
import {checkArtifactName} from './path-and-artifact-name-validation'
|
|
||||||
import {DownloadHttpClient} from './download-http-client'
|
|
||||||
import {getDownloadSpecification} from './download-specification'
|
|
||||||
import {getWorkSpaceDirectory} from './config-variables'
|
|
||||||
import {normalize, resolve} from 'path'
|
|
||||||
|
|
||||||
export interface ArtifactClient {
|
|
||||||
/**
|
|
||||||
* Uploads an artifact
|
|
||||||
*
|
|
||||||
* @param name the name of the artifact, required
|
|
||||||
* @param files a list of absolute or relative paths that denote what files should be uploaded
|
|
||||||
* @param rootDirectory an absolute or relative file path that denotes the root parent directory of the files being uploaded
|
|
||||||
* @param options extra options for customizing the upload behavior
|
|
||||||
* @returns single UploadInfo object
|
|
||||||
*/
|
|
||||||
uploadArtifact(
|
|
||||||
name: string,
|
|
||||||
files: string[],
|
|
||||||
rootDirectory: string,
|
|
||||||
options?: UploadOptions
|
|
||||||
): Promise<UploadResponse>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads a single artifact associated with a run
|
|
||||||
*
|
|
||||||
* @param name the name of the artifact being downloaded
|
|
||||||
* @param path optional path that denotes where the artifact will be downloaded to
|
|
||||||
* @param options extra options that allow for the customization of the download behavior
|
|
||||||
*/
|
|
||||||
downloadArtifact(
|
|
||||||
name: string,
|
|
||||||
path?: string,
|
|
||||||
options?: DownloadOptions
|
|
||||||
): Promise<DownloadResponse>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads all artifacts associated with a run. Because there are multiple artifacts being downloaded, a folder will be created for each one in the specified or default directory
|
|
||||||
* @param path optional path that denotes where the artifacts will be downloaded to
|
|
||||||
*/
|
|
||||||
downloadAllArtifacts(path?: string): Promise<DownloadResponse[]>
|
|
||||||
}
|
|
||||||
|
|
||||||
export class DefaultArtifactClient implements ArtifactClient {
|
|
||||||
/**
|
|
||||||
* Constructs a DefaultArtifactClient
|
|
||||||
*/
|
|
||||||
static create(): DefaultArtifactClient {
|
|
||||||
return new DefaultArtifactClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uploads an artifact
|
|
||||||
*/
|
|
||||||
async uploadArtifact(
|
|
||||||
name: string,
|
|
||||||
files: string[],
|
|
||||||
rootDirectory: string,
|
|
||||||
options?: UploadOptions | undefined
|
|
||||||
): 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)
|
|
||||||
|
|
||||||
// Get specification for the files being uploaded
|
|
||||||
const uploadSpecification: UploadSpecification[] = getUploadSpecification(
|
|
||||||
name,
|
|
||||||
rootDirectory,
|
|
||||||
files
|
|
||||||
)
|
|
||||||
const uploadResponse: UploadResponse = {
|
|
||||||
artifactName: name,
|
|
||||||
artifactItems: [],
|
|
||||||
size: 0,
|
|
||||||
failedItems: []
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadHttpClient = new UploadHttpClient()
|
|
||||||
|
|
||||||
if (uploadSpecification.length === 0) {
|
|
||||||
core.warning(`No files found that can be uploaded`)
|
|
||||||
} else {
|
|
||||||
// Create an entry for the artifact in the file container
|
|
||||||
const response = await uploadHttpClient.createArtifactInFileContainer(
|
|
||||||
name,
|
|
||||||
options
|
|
||||||
)
|
|
||||||
if (!response.fileContainerResourceUrl) {
|
|
||||||
core.debug(response.toString())
|
|
||||||
throw new Error(
|
|
||||||
'No URL provided by the Artifact Service to upload an artifact to'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
const uploadResult = await uploadHttpClient.uploadArtifactToFileContainer(
|
|
||||||
response.fileContainerResourceUrl,
|
|
||||||
uploadSpecification,
|
|
||||||
options
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
core.info(
|
|
||||||
`File upload process has finished. Finalizing the artifact upload`
|
|
||||||
)
|
|
||||||
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(
|
|
||||||
`
|
|
||||||
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(
|
|
||||||
item => item.absoluteFilePath
|
|
||||||
)
|
|
||||||
uploadResponse.size = uploadResult.uploadSize
|
|
||||||
uploadResponse.failedItems = uploadResult.failedItems
|
|
||||||
}
|
|
||||||
return uploadResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadArtifact(
|
|
||||||
name: string,
|
|
||||||
path?: string | undefined,
|
|
||||||
options?: DownloadOptions | undefined
|
|
||||||
): Promise<DownloadResponse> {
|
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
|
||||||
|
|
||||||
const artifacts = await downloadHttpClient.listArtifacts()
|
|
||||||
if (artifacts.count === 0) {
|
|
||||||
throw new Error(
|
|
||||||
`Unable to find any artifacts for the associated workflow`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const artifactToDownload = artifacts.value.find(artifact => {
|
|
||||||
return artifact.name === name
|
|
||||||
})
|
|
||||||
if (!artifactToDownload) {
|
|
||||||
throw new Error(`Unable to find an artifact with the name: ${name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = await downloadHttpClient.getContainerItems(
|
|
||||||
artifactToDownload.name,
|
|
||||||
artifactToDownload.fileContainerResourceUrl
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!path) {
|
|
||||||
path = getWorkSpaceDirectory()
|
|
||||||
}
|
|
||||||
path = normalize(path)
|
|
||||||
path = resolve(path)
|
|
||||||
|
|
||||||
// During upload, empty directories are rejected by the remote server so there should be no artifacts that consist of only empty directories
|
|
||||||
const downloadSpecification = getDownloadSpecification(
|
|
||||||
name,
|
|
||||||
items.value,
|
|
||||||
path,
|
|
||||||
options?.createArtifactFolder || false
|
|
||||||
)
|
|
||||||
|
|
||||||
if (downloadSpecification.filesToDownload.length === 0) {
|
|
||||||
core.info(
|
|
||||||
`No downloadable files were found for the artifact: ${artifactToDownload.name}`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Create all necessary directories recursively before starting any download
|
|
||||||
await createDirectoriesForArtifact(
|
|
||||||
downloadSpecification.directoryStructure
|
|
||||||
)
|
|
||||||
core.info('Directory structure has been setup for the artifact')
|
|
||||||
await createEmptyFilesForArtifact(
|
|
||||||
downloadSpecification.emptyFilesToCreate
|
|
||||||
)
|
|
||||||
await downloadHttpClient.downloadSingleArtifact(
|
|
||||||
downloadSpecification.filesToDownload
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
artifactName: name,
|
|
||||||
downloadPath: downloadSpecification.rootDownloadLocation
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async downloadAllArtifacts(
|
|
||||||
path?: string | undefined
|
|
||||||
): Promise<DownloadResponse[]> {
|
|
||||||
const downloadHttpClient = new DownloadHttpClient()
|
|
||||||
|
|
||||||
const response: DownloadResponse[] = []
|
|
||||||
const artifacts = await downloadHttpClient.listArtifacts()
|
|
||||||
if (artifacts.count === 0) {
|
|
||||||
core.info('Unable to find any artifacts for the associated workflow')
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!path) {
|
|
||||||
path = getWorkSpaceDirectory()
|
|
||||||
}
|
|
||||||
path = normalize(path)
|
|
||||||
path = resolve(path)
|
|
||||||
|
|
||||||
let downloadedArtifacts = 0
|
|
||||||
while (downloadedArtifacts < artifacts.count) {
|
|
||||||
const currentArtifactToDownload = artifacts.value[downloadedArtifacts]
|
|
||||||
downloadedArtifacts += 1
|
|
||||||
core.info(
|
|
||||||
`starting download of artifact ${currentArtifactToDownload.name} : ${downloadedArtifacts}/${artifacts.count}`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Get container entries for the specific artifact
|
|
||||||
const items = await downloadHttpClient.getContainerItems(
|
|
||||||
currentArtifactToDownload.name,
|
|
||||||
currentArtifactToDownload.fileContainerResourceUrl
|
|
||||||
)
|
|
||||||
|
|
||||||
const downloadSpecification = getDownloadSpecification(
|
|
||||||
currentArtifactToDownload.name,
|
|
||||||
items.value,
|
|
||||||
path,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
if (downloadSpecification.filesToDownload.length === 0) {
|
|
||||||
core.info(
|
|
||||||
`No downloadable files were found for any artifact ${currentArtifactToDownload.name}`
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
await createDirectoriesForArtifact(
|
|
||||||
downloadSpecification.directoryStructure
|
|
||||||
)
|
|
||||||
await createEmptyFilesForArtifact(
|
|
||||||
downloadSpecification.emptyFilesToCreate
|
|
||||||
)
|
|
||||||
await downloadHttpClient.downloadSingleArtifact(
|
|
||||||
downloadSpecification.filesToDownload
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
response.push({
|
|
||||||
artifactName: currentArtifactToDownload.name,
|
|
||||||
downloadPath: downloadSpecification.rootDownloadLocation
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import {warning} from '@actions/core'
|
||||||
|
import {isGhes} from './shared/config'
|
||||||
|
import {
|
||||||
|
UploadOptions,
|
||||||
|
UploadResponse,
|
||||||
|
DownloadArtifactOptions,
|
||||||
|
GetArtifactResponse,
|
||||||
|
ListArtifactsResponse,
|
||||||
|
DownloadArtifactResponse
|
||||||
|
} from './shared/interfaces'
|
||||||
|
import {uploadArtifact} from './upload/upload-artifact'
|
||||||
|
import {downloadArtifact} from './download/download-artifact'
|
||||||
|
import {getArtifact} from './find/get-artifact'
|
||||||
|
import {listArtifacts} from './find/list-artifacts'
|
||||||
|
|
||||||
|
export interface ArtifactClient {
|
||||||
|
/**
|
||||||
|
* Uploads an artifact
|
||||||
|
*
|
||||||
|
* @param name The name of the artifact, required
|
||||||
|
* @param files A list of absolute or relative paths that denote what files should be uploaded
|
||||||
|
* @param rootDirectory An absolute or relative file path that denotes the root parent directory of the files being uploaded
|
||||||
|
* @param options Extra options for customizing the upload behavior
|
||||||
|
* @returns single UploadResponse object
|
||||||
|
*/
|
||||||
|
uploadArtifact(
|
||||||
|
name: string,
|
||||||
|
files: string[],
|
||||||
|
rootDirectory: string,
|
||||||
|
options?: UploadOptions
|
||||||
|
): Promise<UploadResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all artifacts that are part of a workflow run.
|
||||||
|
*
|
||||||
|
* This calls the public List-Artifacts API https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28#list-workflow-run-artifacts
|
||||||
|
* Due to paginated responses from the public API. This function will return at most 1000 artifacts per workflow run (100 per page * maximum 10 calls)
|
||||||
|
*
|
||||||
|
* @param workflowRunId The workflow run id that the artifact belongs to
|
||||||
|
* @param repositoryOwner The owner of the repository that the artifact belongs to
|
||||||
|
* @param repositoryName The name of the repository that the artifact belongs to
|
||||||
|
* @param token A token with the appropriate permission to the repository to list artifacts
|
||||||
|
* @returns ListArtifactResponse object
|
||||||
|
*/
|
||||||
|
listArtifacts(
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<ListArtifactsResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds an artifact by name given a repository and workflow run id.
|
||||||
|
*
|
||||||
|
* This calls the public List-Artifacts API with a name filter https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28#list-workflow-run-artifacts
|
||||||
|
* @actions/artifact > 2.0.0 does not allow for creating multiple artifacts with the same name in the same workflow run.
|
||||||
|
* It is possible to have multiple artifacts with the same name in the same workflow run by using old versions of upload-artifact (v1,v2 and v3) or @actions/artifact < v2.0.0
|
||||||
|
* If there are multiple artifacts with the same name in the same workflow run this function will return the first artifact that matches the name.
|
||||||
|
*
|
||||||
|
* @param artifactName The name of the artifact to find
|
||||||
|
* @param workflowRunId The workflow run id that the artifact belongs to
|
||||||
|
* @param repositoryOwner The owner of the repository that the artifact belongs to
|
||||||
|
* @param repositoryName The name of the repository that the artifact belongs to
|
||||||
|
* @param token A token with the appropriate permission to the repository to find the artifact
|
||||||
|
*/
|
||||||
|
getArtifact(
|
||||||
|
artifactName: string,
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<GetArtifactResponse>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads an artifact and unzips the content
|
||||||
|
*
|
||||||
|
* @param artifactId The name of the artifact to download
|
||||||
|
* @param repositoryOwner The owner of the repository that the artifact belongs to
|
||||||
|
* @param repositoryName The name of the repository that the artifact belongs to
|
||||||
|
* @param token A token with the appropriate permission to the repository to download the artifact
|
||||||
|
* @param options Extra options that allow for the customization of the download behavior
|
||||||
|
* @returns single DownloadArtifactResponse object
|
||||||
|
*/
|
||||||
|
downloadArtifact(
|
||||||
|
artifactId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string,
|
||||||
|
options?: DownloadArtifactOptions
|
||||||
|
): Promise<DownloadArtifactResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Client implements ArtifactClient {
|
||||||
|
/**
|
||||||
|
* Constructs a Client
|
||||||
|
*/
|
||||||
|
static create(): Client {
|
||||||
|
return new Client()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload Artifact
|
||||||
|
*/
|
||||||
|
async uploadArtifact(
|
||||||
|
name: string,
|
||||||
|
files: string[],
|
||||||
|
rootDirectory: string,
|
||||||
|
options?: UploadOptions | undefined
|
||||||
|
): Promise<UploadResponse> {
|
||||||
|
if (isGhes()) {
|
||||||
|
warning(
|
||||||
|
`@actions/artifact v2.0.0+ and upload-artifact@v4+ are not currently supported on GHES.`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return uploadArtifact(name, files, rootDirectory, options)
|
||||||
|
} catch (error) {
|
||||||
|
warning(
|
||||||
|
`Artifact upload failed with error: ${error}.
|
||||||
|
|
||||||
|
Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information.
|
||||||
|
|
||||||
|
If the error persists, please check whether Actions is operating normally at [https://githubstatus.com](https://www.githubstatus.com).`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download Artifact
|
||||||
|
*/
|
||||||
|
async downloadArtifact(
|
||||||
|
artifactId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string,
|
||||||
|
options?: DownloadArtifactOptions
|
||||||
|
): Promise<DownloadArtifactResponse> {
|
||||||
|
if (isGhes()) {
|
||||||
|
warning(
|
||||||
|
`@actions/artifact v2.0.0+ and download-artifact@v4+ are not currently supported on GHES.`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return downloadArtifact(
|
||||||
|
artifactId,
|
||||||
|
repositoryOwner,
|
||||||
|
repositoryName,
|
||||||
|
token,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
warning(
|
||||||
|
`Artifact download failed with error: ${error}.
|
||||||
|
|
||||||
|
Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information.
|
||||||
|
|
||||||
|
If the error persists, please check whether Actions and API requests are operating normally at [https://githubstatus.com](https://www.githubstatus.com).`
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List Artifacts
|
||||||
|
*/
|
||||||
|
async listArtifacts(
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<ListArtifactsResponse> {
|
||||||
|
if (isGhes()) {
|
||||||
|
warning(
|
||||||
|
`@actions/artifact v2.0.0+ and download-artifact@v4+ are not currently supported on GHES.`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
artifacts: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return listArtifacts(
|
||||||
|
workflowRunId,
|
||||||
|
repositoryOwner,
|
||||||
|
repositoryName,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
warning(
|
||||||
|
`Listing Artifacts failed with error: ${error}.
|
||||||
|
|
||||||
|
Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information.
|
||||||
|
|
||||||
|
If the error persists, please check whether Actions and API requests are operating normally at [https://githubstatus.com](https://www.githubstatus.com).`
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
artifacts: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Artifact
|
||||||
|
*/
|
||||||
|
async getArtifact(
|
||||||
|
artifactName: string,
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<GetArtifactResponse> {
|
||||||
|
if (isGhes()) {
|
||||||
|
warning(
|
||||||
|
`@actions/artifact v2.0.0+ and download-artifact@v4+ are not currently supported on GHES.`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return getArtifact(
|
||||||
|
artifactName,
|
||||||
|
workflowRunId,
|
||||||
|
repositoryOwner,
|
||||||
|
repositoryName,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
} catch (error: unknown) {
|
||||||
|
warning(
|
||||||
|
`Fetching Artifact failed with error: ${error}.
|
||||||
|
|
||||||
|
Errors can be temporary, so please try again and optionally run the action with debug mode enabled for more information.
|
||||||
|
|
||||||
|
If the error persists, please check whether Actions and API requests are operating normally at [https://githubstatus.com](https://www.githubstatus.com).`
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
// The number of concurrent uploads that happens at the same time
|
|
||||||
export function getUploadFileConcurrency(): number {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
// When uploading large files that can't be uploaded with a single http call, this controls
|
|
||||||
// the chunk size that is used during upload
|
|
||||||
export function getUploadChunkSize(): number {
|
|
||||||
return 8 * 1024 * 1024 // 8 MB Chunks
|
|
||||||
}
|
|
||||||
|
|
||||||
// The maximum number of retries that can be attempted before an upload or download fails
|
|
||||||
export function getRetryLimit(): number {
|
|
||||||
return 5
|
|
||||||
}
|
|
||||||
|
|
||||||
// With exponential backoff, the larger the retry count, the larger the wait time before another attempt
|
|
||||||
// The retry multiplier controls by how much the backOff time increases depending on the number of retries
|
|
||||||
export function getRetryMultiplier(): number {
|
|
||||||
return 1.5
|
|
||||||
}
|
|
||||||
|
|
||||||
// The initial wait time if an upload or download fails and a retry is being attempted for the first time
|
|
||||||
export function getInitialRetryIntervalInMilliseconds(): number {
|
|
||||||
return 3000
|
|
||||||
}
|
|
||||||
|
|
||||||
// The number of concurrent downloads that happens at the same time
|
|
||||||
export function getDownloadFileConcurrency(): number {
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRuntimeToken(): string {
|
|
||||||
const token = process.env['ACTIONS_RUNTIME_TOKEN']
|
|
||||||
if (!token) {
|
|
||||||
throw new Error('Unable to get ACTIONS_RUNTIME_TOKEN env variable')
|
|
||||||
}
|
|
||||||
return token
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRuntimeUrl(): string {
|
|
||||||
const runtimeUrl = process.env['ACTIONS_RUNTIME_URL']
|
|
||||||
if (!runtimeUrl) {
|
|
||||||
throw new Error('Unable to get ACTIONS_RUNTIME_URL env variable')
|
|
||||||
}
|
|
||||||
return runtimeUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWorkFlowRunId(): string {
|
|
||||||
const workFlowRunId = process.env['GITHUB_RUN_ID']
|
|
||||||
if (!workFlowRunId) {
|
|
||||||
throw new Error('Unable to get GITHUB_RUN_ID env variable')
|
|
||||||
}
|
|
||||||
return workFlowRunId
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getWorkSpaceDirectory(): string {
|
|
||||||
const workspaceDirectory = process.env['GITHUB_WORKSPACE']
|
|
||||||
if (!workspaceDirectory) {
|
|
||||||
throw new Error('Unable to get GITHUB_WORKSPACE env variable')
|
|
||||||
}
|
|
||||||
return workspaceDirectory
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRetentionDays(): string | undefined {
|
|
||||||
return process.env['GITHUB_RETENTION_DAYS']
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
export interface ArtifactResponse {
|
|
||||||
containerId: string
|
|
||||||
size: number
|
|
||||||
signedContent: string
|
|
||||||
fileContainerResourceUrl: string
|
|
||||||
type: string
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateArtifactParameters {
|
|
||||||
Type: string
|
|
||||||
Name: string
|
|
||||||
RetentionDays?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PatchArtifactSize {
|
|
||||||
Size: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PatchArtifactSizeSuccessResponse {
|
|
||||||
containerId: number
|
|
||||||
size: number
|
|
||||||
signedContent: string
|
|
||||||
type: string
|
|
||||||
name: string
|
|
||||||
url: string
|
|
||||||
uploadUrl: string
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The raw size of the files that were specified for upload
|
|
||||||
*/
|
|
||||||
totalSize: number
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An array of files that failed to upload
|
|
||||||
*/
|
|
||||||
failedItems: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ListArtifactsResponse {
|
|
||||||
count: number
|
|
||||||
value: ArtifactResponse[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueryArtifactResponse {
|
|
||||||
count: number
|
|
||||||
value: ContainerEntry[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ContainerEntry {
|
|
||||||
containerId: number
|
|
||||||
scopeIdentifier: string
|
|
||||||
path: string
|
|
||||||
itemType: string
|
|
||||||
status: string
|
|
||||||
fileLength?: number
|
|
||||||
fileEncoding?: number
|
|
||||||
fileType?: number
|
|
||||||
dateCreated: string
|
|
||||||
dateLastModified: string
|
|
||||||
createdBy: string
|
|
||||||
lastModifiedBy: string
|
|
||||||
itemLocation: string
|
|
||||||
contentLocation: string
|
|
||||||
fileId?: number
|
|
||||||
contentId: string
|
|
||||||
}
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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
|
|
||||||
@@ -1,355 +0,0 @@
|
|||||||
import * as fs from 'fs'
|
|
||||||
import * as core from '@actions/core'
|
|
||||||
import * as zlib from 'zlib'
|
|
||||||
import {
|
|
||||||
getArtifactUrl,
|
|
||||||
getDownloadHeaders,
|
|
||||||
isSuccessStatusCode,
|
|
||||||
isRetryableStatusCode,
|
|
||||||
isThrottledStatusCode,
|
|
||||||
getExponentialRetryTimeInMilliseconds,
|
|
||||||
tryGetRetryAfterValueTimeInMilliseconds,
|
|
||||||
displayHttpDiagnostics,
|
|
||||||
getFileSize,
|
|
||||||
rmFile,
|
|
||||||
sleep
|
|
||||||
} from './utils'
|
|
||||||
import {URL} from 'url'
|
|
||||||
import {StatusReporter} from './status-reporter'
|
|
||||||
import {performance} from 'perf_hooks'
|
|
||||||
import {ListArtifactsResponse, QueryArtifactResponse} from './contracts'
|
|
||||||
import {HttpClientResponse} from '@actions/http-client'
|
|
||||||
import {HttpManager} from './http-manager'
|
|
||||||
import {DownloadItem} from './download-specification'
|
|
||||||
import {getDownloadFileConcurrency, getRetryLimit} from './config-variables'
|
|
||||||
import {IncomingHttpHeaders} from 'http'
|
|
||||||
import {retryHttpClientRequest} from './requestUtils'
|
|
||||||
|
|
||||||
export class DownloadHttpClient {
|
|
||||||
// http manager is used for concurrent connections when downloading multiple files at once
|
|
||||||
private downloadHttpManager: HttpManager
|
|
||||||
private statusReporter: StatusReporter
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.downloadHttpManager = new HttpManager(
|
|
||||||
getDownloadFileConcurrency(),
|
|
||||||
'@actions/artifact-download'
|
|
||||||
)
|
|
||||||
// downloads are usually significantly faster than uploads so display status information every second
|
|
||||||
this.statusReporter = new StatusReporter(1000)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a list of all artifacts that are in a specific container
|
|
||||||
*/
|
|
||||||
async listArtifacts(): Promise<ListArtifactsResponse> {
|
|
||||||
const artifactUrl = getArtifactUrl()
|
|
||||||
|
|
||||||
// use the first client from the httpManager, `keep-alive` is not used so the connection will close immediately
|
|
||||||
const client = this.downloadHttpManager.getClient(0)
|
|
||||||
const headers = getDownloadHeaders('application/json')
|
|
||||||
const response = await retryHttpClientRequest('List Artifacts', async () =>
|
|
||||||
client.get(artifactUrl, headers)
|
|
||||||
)
|
|
||||||
const body: string = await response.readBody()
|
|
||||||
return JSON.parse(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a set of container items that describe the contents of an artifact
|
|
||||||
* @param artifactName the name of the artifact
|
|
||||||
* @param containerUrl the artifact container URL for the run
|
|
||||||
*/
|
|
||||||
async getContainerItems(
|
|
||||||
artifactName: string,
|
|
||||||
containerUrl: string
|
|
||||||
): Promise<QueryArtifactResponse> {
|
|
||||||
// the itemPath search parameter controls which containers will be returned
|
|
||||||
const resourceUrl = new URL(containerUrl)
|
|
||||||
resourceUrl.searchParams.append('itemPath', artifactName)
|
|
||||||
|
|
||||||
// use the first client from the httpManager, `keep-alive` is not used so the connection will close immediately
|
|
||||||
const client = this.downloadHttpManager.getClient(0)
|
|
||||||
const headers = getDownloadHeaders('application/json')
|
|
||||||
const response = await retryHttpClientRequest(
|
|
||||||
'Get Container Items',
|
|
||||||
async () => client.get(resourceUrl.toString(), headers)
|
|
||||||
)
|
|
||||||
const body: string = await response.readBody()
|
|
||||||
return JSON.parse(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Concurrently downloads all the files that are part of an artifact
|
|
||||||
* @param downloadItems information about what items to download and where to save them
|
|
||||||
*/
|
|
||||||
async downloadSingleArtifact(downloadItems: DownloadItem[]): Promise<void> {
|
|
||||||
const DOWNLOAD_CONCURRENCY = getDownloadFileConcurrency()
|
|
||||||
// limit the number of files downloaded at a single time
|
|
||||||
core.debug(`Download file concurrency is set to ${DOWNLOAD_CONCURRENCY}`)
|
|
||||||
const parallelDownloads = [...new Array(DOWNLOAD_CONCURRENCY).keys()]
|
|
||||||
let currentFile = 0
|
|
||||||
let downloadedFiles = 0
|
|
||||||
|
|
||||||
core.info(
|
|
||||||
`Total number of files that will be downloaded: ${downloadItems.length}`
|
|
||||||
)
|
|
||||||
|
|
||||||
this.statusReporter.setTotalNumberOfFilesToProcess(downloadItems.length)
|
|
||||||
this.statusReporter.start()
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
parallelDownloads.map(async index => {
|
|
||||||
while (currentFile < downloadItems.length) {
|
|
||||||
const currentFileToDownload = downloadItems[currentFile]
|
|
||||||
currentFile += 1
|
|
||||||
|
|
||||||
const startTime = performance.now()
|
|
||||||
await this.downloadIndividualFile(
|
|
||||||
index,
|
|
||||||
currentFileToDownload.sourceLocation,
|
|
||||||
currentFileToDownload.targetPath
|
|
||||||
)
|
|
||||||
|
|
||||||
if (core.isDebug()) {
|
|
||||||
core.debug(
|
|
||||||
`File: ${++downloadedFiles}/${downloadItems.length}. ${
|
|
||||||
currentFileToDownload.targetPath
|
|
||||||
} took ${(performance.now() - startTime).toFixed(
|
|
||||||
3
|
|
||||||
)} milliseconds to finish downloading`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.statusReporter.incrementProcessedCount()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.catch(error => {
|
|
||||||
throw new Error(`Unable to download the artifact: ${error}`)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.statusReporter.stop()
|
|
||||||
// safety dispose all connections
|
|
||||||
this.downloadHttpManager.disposeAndReplaceAllClients()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads an individual file
|
|
||||||
* @param httpClientIndex the index of the http client that is used to make all of the calls
|
|
||||||
* @param artifactLocation origin location where a file will be downloaded from
|
|
||||||
* @param downloadPath destination location for the file being downloaded
|
|
||||||
*/
|
|
||||||
private async downloadIndividualFile(
|
|
||||||
httpClientIndex: number,
|
|
||||||
artifactLocation: string,
|
|
||||||
downloadPath: string
|
|
||||||
): Promise<void> {
|
|
||||||
let retryCount = 0
|
|
||||||
const retryLimit = getRetryLimit()
|
|
||||||
let destinationStream = fs.createWriteStream(downloadPath)
|
|
||||||
const headers = getDownloadHeaders('application/json', true, true)
|
|
||||||
|
|
||||||
// a single GET request is used to download a file
|
|
||||||
const makeDownloadRequest = async (): Promise<HttpClientResponse> => {
|
|
||||||
const client = this.downloadHttpManager.getClient(httpClientIndex)
|
|
||||||
return await client.get(artifactLocation, headers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check the response headers to determine if the file was compressed using gzip
|
|
||||||
const isGzip = (incomingHeaders: IncomingHttpHeaders): boolean => {
|
|
||||||
return (
|
|
||||||
'content-encoding' in incomingHeaders &&
|
|
||||||
incomingHeaders['content-encoding'] === 'gzip'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 is a retryAfterValue value provided,
|
|
||||||
// it will be used
|
|
||||||
const backOff = async (retryAfterValue?: number): Promise<void> => {
|
|
||||||
retryCount++
|
|
||||||
if (retryCount > retryLimit) {
|
|
||||||
return Promise.reject(
|
|
||||||
new Error(
|
|
||||||
`Retry limit has been reached. Unable to download ${artifactLocation}`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this.downloadHttpManager.disposeAndReplaceClient(httpClientIndex)
|
|
||||||
if (retryAfterValue) {
|
|
||||||
// Back off by waiting the specified time denoted by the retry-after header
|
|
||||||
core.info(
|
|
||||||
`Backoff due to too many requests, retry #${retryCount}. Waiting for ${retryAfterValue} milliseconds before continuing the download`
|
|
||||||
)
|
|
||||||
await sleep(retryAfterValue)
|
|
||||||
} else {
|
|
||||||
// Back off using an exponential value that depends on the retry count
|
|
||||||
const backoffTime = getExponentialRetryTimeInMilliseconds(retryCount)
|
|
||||||
core.info(
|
|
||||||
`Exponential backoff for retry #${retryCount}. Waiting for ${backoffTime} milliseconds before continuing the download`
|
|
||||||
)
|
|
||||||
await sleep(backoffTime)
|
|
||||||
}
|
|
||||||
core.info(
|
|
||||||
`Finished backoff for retry #${retryCount}, continuing with download`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAllBytesReceived = (
|
|
||||||
expected?: string,
|
|
||||||
received?: number
|
|
||||||
): boolean => {
|
|
||||||
// be lenient, if any input is missing, assume success, i.e. not truncated
|
|
||||||
if (
|
|
||||||
!expected ||
|
|
||||||
!received ||
|
|
||||||
process.env['ACTIONS_ARTIFACT_SKIP_DOWNLOAD_VALIDATION']
|
|
||||||
) {
|
|
||||||
core.info('Skipping download validation.')
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return parseInt(expected) === received
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetDestinationStream = async (
|
|
||||||
fileDownloadPath: string
|
|
||||||
): Promise<void> => {
|
|
||||||
destinationStream.close()
|
|
||||||
await rmFile(fileDownloadPath)
|
|
||||||
destinationStream = fs.createWriteStream(fileDownloadPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep trying to download a file until a retry limit has been reached
|
|
||||||
while (retryCount <= retryLimit) {
|
|
||||||
let response: HttpClientResponse
|
|
||||||
try {
|
|
||||||
response = await makeDownloadRequest()
|
|
||||||
} catch (error) {
|
|
||||||
// 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')
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log(error)
|
|
||||||
|
|
||||||
// increment the retryCount and use exponential backoff to wait before making the next request
|
|
||||||
await backOff()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
let forceRetry = false
|
|
||||||
if (isSuccessStatusCode(response.message.statusCode)) {
|
|
||||||
// The body contains the contents of the file however calling response.readBody() causes all the content to be converted to a string
|
|
||||||
// which can cause some gzip encoded data to be lost
|
|
||||||
// Instead of using response.readBody(), response.message is a readableStream that can be directly used to get the raw body contents
|
|
||||||
try {
|
|
||||||
const isGzipped = isGzip(response.message.headers)
|
|
||||||
await this.pipeResponseToFile(response, destinationStream, isGzipped)
|
|
||||||
|
|
||||||
if (
|
|
||||||
isGzipped ||
|
|
||||||
isAllBytesReceived(
|
|
||||||
response.message.headers['content-length'],
|
|
||||||
await getFileSize(downloadPath)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
forceRetry = true
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// retry on error, most likely streams were corrupted
|
|
||||||
forceRetry = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (forceRetry || isRetryableStatusCode(response.message.statusCode)) {
|
|
||||||
core.info(
|
|
||||||
`A ${response.message.statusCode} response code has been received while attempting to download an artifact`
|
|
||||||
)
|
|
||||||
resetDestinationStream(downloadPath)
|
|
||||||
// if a throttled status code is received, try to get the retryAfter header value, else differ to standard exponential backoff
|
|
||||||
isThrottledStatusCode(response.message.statusCode)
|
|
||||||
? await backOff(
|
|
||||||
tryGetRetryAfterValueTimeInMilliseconds(response.message.headers)
|
|
||||||
)
|
|
||||||
: await backOff()
|
|
||||||
} else {
|
|
||||||
// Some unexpected response code, fail immediately and stop the download
|
|
||||||
displayHttpDiagnostics(response)
|
|
||||||
return Promise.reject(
|
|
||||||
new Error(
|
|
||||||
`Unexpected http ${response.message.statusCode} during download for ${artifactLocation}`
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pipes the response from downloading an individual file to the appropriate destination stream while decoding gzip content if necessary
|
|
||||||
* @param response the http response received when downloading a file
|
|
||||||
* @param destinationStream the stream where the file should be written to
|
|
||||||
* @param isGzip a boolean denoting if the content is compressed using gzip and if we need to decode it
|
|
||||||
*/
|
|
||||||
async pipeResponseToFile(
|
|
||||||
response: HttpClientResponse,
|
|
||||||
destinationStream: fs.WriteStream,
|
|
||||||
isGzip: boolean
|
|
||||||
): Promise<void> {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
if (isGzip) {
|
|
||||||
const gunzip = zlib.createGunzip()
|
|
||||||
response.message
|
|
||||||
.on('error', error => {
|
|
||||||
core.error(
|
|
||||||
`An error occurred while attempting to read the response stream`
|
|
||||||
)
|
|
||||||
gunzip.close()
|
|
||||||
destinationStream.close()
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
.pipe(gunzip)
|
|
||||||
.on('error', error => {
|
|
||||||
core.error(
|
|
||||||
`An error occurred while attempting to decompress the response stream`
|
|
||||||
)
|
|
||||||
destinationStream.close()
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
.pipe(destinationStream)
|
|
||||||
.on('close', () => {
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.on('error', error => {
|
|
||||||
core.error(
|
|
||||||
`An error occurred while writing a downloaded file to ${destinationStream.path}`
|
|
||||||
)
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
response.message
|
|
||||||
.on('error', error => {
|
|
||||||
core.error(
|
|
||||||
`An error occurred while attempting to read the response stream`
|
|
||||||
)
|
|
||||||
destinationStream.close()
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
.pipe(destinationStream)
|
|
||||||
.on('close', () => {
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
.on('error', error => {
|
|
||||||
core.error(
|
|
||||||
`An error occurred while writing a downloaded file to ${destinationStream.path}`
|
|
||||||
)
|
|
||||||
reject(error)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export interface DownloadOptions {
|
|
||||||
/**
|
|
||||||
* Specifies if a folder is created for the artifact that is downloaded (contents downloaded into this folder),
|
|
||||||
* defaults to false if not specified
|
|
||||||
* */
|
|
||||||
createArtifactFolder?: boolean
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
export interface DownloadResponse {
|
|
||||||
/**
|
|
||||||
* The name of the artifact that was downloaded
|
|
||||||
*/
|
|
||||||
artifactName: string
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The full Path to where the artifact was downloaded
|
|
||||||
*/
|
|
||||||
downloadPath: string
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
import * as path from 'path'
|
|
||||||
import {ContainerEntry} from './contracts'
|
|
||||||
|
|
||||||
export interface DownloadSpecification {
|
|
||||||
// root download location for the artifact
|
|
||||||
rootDownloadLocation: string
|
|
||||||
|
|
||||||
// directories that need to be created for all the items in the artifact
|
|
||||||
directoryStructure: string[]
|
|
||||||
|
|
||||||
// empty files that are part of the artifact that don't require any downloading
|
|
||||||
emptyFilesToCreate: string[]
|
|
||||||
|
|
||||||
// individual files that need to be downloaded as part of the artifact
|
|
||||||
filesToDownload: DownloadItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DownloadItem {
|
|
||||||
// Url that denotes where to download the item from
|
|
||||||
sourceLocation: string
|
|
||||||
|
|
||||||
// Information about where the file should be downloaded to
|
|
||||||
targetPath: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a specification for a set of files that will be downloaded
|
|
||||||
* @param artifactName the name of the artifact
|
|
||||||
* @param artifactEntries a set of container entries that describe that files that make up an artifact
|
|
||||||
* @param downloadPath the path where the artifact will be downloaded to
|
|
||||||
* @param includeRootDirectory specifies if there should be an extra directory (denoted by the artifact name) where the artifact files should be downloaded to
|
|
||||||
*/
|
|
||||||
export function getDownloadSpecification(
|
|
||||||
artifactName: string,
|
|
||||||
artifactEntries: ContainerEntry[],
|
|
||||||
downloadPath: string,
|
|
||||||
includeRootDirectory: boolean
|
|
||||||
): DownloadSpecification {
|
|
||||||
// use a set for the directory paths so that there are no duplicates
|
|
||||||
const directories = new Set<string>()
|
|
||||||
|
|
||||||
const specifications: DownloadSpecification = {
|
|
||||||
rootDownloadLocation: includeRootDirectory
|
|
||||||
? path.join(downloadPath, artifactName)
|
|
||||||
: downloadPath,
|
|
||||||
directoryStructure: [],
|
|
||||||
emptyFilesToCreate: [],
|
|
||||||
filesToDownload: []
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const entry of artifactEntries) {
|
|
||||||
// Ignore artifacts in the container that don't begin with the same name
|
|
||||||
if (
|
|
||||||
entry.path.startsWith(`${artifactName}/`) ||
|
|
||||||
entry.path.startsWith(`${artifactName}\\`)
|
|
||||||
) {
|
|
||||||
// normalize all separators to the local OS
|
|
||||||
const normalizedPathEntry = path.normalize(entry.path)
|
|
||||||
// entry.path always starts with the artifact name, if includeRootDirectory is false, remove the name from the beginning of the path
|
|
||||||
const filePath = path.join(
|
|
||||||
downloadPath,
|
|
||||||
includeRootDirectory
|
|
||||||
? normalizedPathEntry
|
|
||||||
: normalizedPathEntry.replace(artifactName, '')
|
|
||||||
)
|
|
||||||
|
|
||||||
// Case insensitive folder structure maintained in the backend, not every folder is created so the 'folder'
|
|
||||||
// itemType cannot be relied upon. The file must be used to determine the directory structure
|
|
||||||
if (entry.itemType === 'file') {
|
|
||||||
// Get the directories that we need to create from the filePath for each individual file
|
|
||||||
directories.add(path.dirname(filePath))
|
|
||||||
if (entry.fileLength === 0) {
|
|
||||||
// An empty file was uploaded, create the empty files locally so that no extra http calls are made
|
|
||||||
specifications.emptyFilesToCreate.push(filePath)
|
|
||||||
} else {
|
|
||||||
specifications.filesToDownload.push({
|
|
||||||
sourceLocation: entry.contentLocation,
|
|
||||||
targetPath: filePath
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
specifications.directoryStructure = Array.from(directories)
|
|
||||||
return specifications
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
import fs from 'fs/promises'
|
||||||
|
import * as github from '@actions/github'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as httpClient from '@actions/http-client'
|
||||||
|
import unzipper from 'unzipper'
|
||||||
|
import {
|
||||||
|
DownloadArtifactOptions,
|
||||||
|
DownloadArtifactResponse
|
||||||
|
} from '../shared/interfaces'
|
||||||
|
import {getUserAgentString} from '../shared/user-agent'
|
||||||
|
import {getGitHubWorkspaceDir} from '../shared/config'
|
||||||
|
|
||||||
|
const scrubQueryParameters = (url: string): string => {
|
||||||
|
const parsed = new URL(url)
|
||||||
|
parsed.search = ''
|
||||||
|
return parsed.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exists(path: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(path)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'ENOENT') {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function streamExtract(url: string, directory: string): Promise<void> {
|
||||||
|
const client = new httpClient.HttpClient(getUserAgentString())
|
||||||
|
const response = await client.get(url)
|
||||||
|
|
||||||
|
if (response.message.statusCode !== 200) {
|
||||||
|
throw new Error(
|
||||||
|
`Unexpected HTTP response from blob storage: ${response.message.statusCode} ${response.message.statusMessage}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.message.pipe(unzipper.Extract({path: directory})).promise()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadArtifact(
|
||||||
|
artifactId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string,
|
||||||
|
options?: DownloadArtifactOptions
|
||||||
|
): Promise<DownloadArtifactResponse> {
|
||||||
|
const downloadPath = options?.path || getGitHubWorkspaceDir()
|
||||||
|
|
||||||
|
if (!(await exists(downloadPath))) {
|
||||||
|
core.debug(
|
||||||
|
`Artifact destination folder does not exist, creating: ${downloadPath}`
|
||||||
|
)
|
||||||
|
await fs.mkdir(downloadPath, {recursive: true})
|
||||||
|
} else {
|
||||||
|
core.debug(`Artifact destination folder already exists: ${downloadPath}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = github.getOctokit(token)
|
||||||
|
|
||||||
|
core.info(
|
||||||
|
`Downloading artifact '${artifactId}' from '${repositoryOwner}/${repositoryName}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
const {headers, status} = await api.rest.actions.downloadArtifact({
|
||||||
|
owner: repositoryOwner,
|
||||||
|
repo: repositoryName,
|
||||||
|
artifact_id: artifactId,
|
||||||
|
archive_format: 'zip',
|
||||||
|
request: {
|
||||||
|
redirect: 'manual'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (status !== 302) {
|
||||||
|
throw new Error(`Unable to download artifact. Unexpected status: ${status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {location} = headers
|
||||||
|
if (!location) {
|
||||||
|
throw new Error(`Unable to redirect to artifact download url`)
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(
|
||||||
|
`Redirecting to blob download url: ${scrubQueryParameters(location)}`
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
core.info(`Starting download of artifact to: ${downloadPath}`)
|
||||||
|
await streamExtract(location, downloadPath)
|
||||||
|
core.info(`Artifact download completed successfully.`)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Unable to download and extract artifact: ${error.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {success: true, downloadPath}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import {GetArtifactResponse} from '../shared/interfaces'
|
||||||
|
import {getOctokit} from '@actions/github'
|
||||||
|
import {getUserAgentString} from '../shared/user-agent'
|
||||||
|
import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils'
|
||||||
|
import {getRetryOptions} from './retry-options'
|
||||||
|
import {requestLog} from '@octokit/plugin-request-log'
|
||||||
|
import {retry} from '@octokit/plugin-retry'
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import {OctokitOptions} from '@octokit/core/dist-types/types'
|
||||||
|
|
||||||
|
export async function getArtifact(
|
||||||
|
artifactName: string,
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<GetArtifactResponse> {
|
||||||
|
const [retryOpts, requestOpts] = getRetryOptions(defaultGitHubOptions)
|
||||||
|
|
||||||
|
const opts: OctokitOptions = {
|
||||||
|
log: undefined,
|
||||||
|
userAgent: getUserAgentString(),
|
||||||
|
previews: undefined,
|
||||||
|
retry: retryOpts,
|
||||||
|
request: requestOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
const github = getOctokit(token, opts, retry, requestLog)
|
||||||
|
|
||||||
|
const getArtifactResp = await github.request(
|
||||||
|
'GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts{?name}',
|
||||||
|
{
|
||||||
|
owner: repositoryOwner,
|
||||||
|
repo: repositoryName,
|
||||||
|
run_id: workflowRunId,
|
||||||
|
name: artifactName
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (getArtifactResp.status !== 200) {
|
||||||
|
core.warning(`non-200 response from GitHub API: ${getArtifactResp.status}`)
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getArtifactResp.data.artifacts.length === 0) {
|
||||||
|
core.warning('no artifacts found')
|
||||||
|
return {
|
||||||
|
success: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getArtifactResp.data.artifacts.length > 1) {
|
||||||
|
core.warning(
|
||||||
|
'more than one artifact found for a single name, returning first'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
artifact: {
|
||||||
|
name: getArtifactResp.data.artifacts[0].name,
|
||||||
|
id: getArtifactResp.data.artifacts[0].id,
|
||||||
|
url: getArtifactResp.data.artifacts[0].url,
|
||||||
|
size: getArtifactResp.data.artifacts[0].size_in_bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import {info, warning, debug} from '@actions/core'
|
||||||
|
import {getOctokit} from '@actions/github'
|
||||||
|
import {ListArtifactsResponse, Artifact} from '../shared/interfaces'
|
||||||
|
import {getUserAgentString} from '../shared/user-agent'
|
||||||
|
import {getRetryOptions} from './retry-options'
|
||||||
|
import {defaults as defaultGitHubOptions} from '@actions/github/lib/utils'
|
||||||
|
import {requestLog} from '@octokit/plugin-request-log'
|
||||||
|
import {retry} from '@octokit/plugin-retry'
|
||||||
|
import {OctokitOptions} from '@octokit/core/dist-types/types'
|
||||||
|
|
||||||
|
// Limiting to 1000 for perf reasons
|
||||||
|
const maximumArtifactCount = 1000
|
||||||
|
const paginationCount = 100
|
||||||
|
const maxNumberOfPages = maximumArtifactCount / paginationCount
|
||||||
|
|
||||||
|
export async function listArtifacts(
|
||||||
|
workflowRunId: number,
|
||||||
|
repositoryOwner: string,
|
||||||
|
repositoryName: string,
|
||||||
|
token: string
|
||||||
|
): Promise<ListArtifactsResponse> {
|
||||||
|
info(
|
||||||
|
`Fetching artifact list for workflow run ${workflowRunId} in repository ${repositoryOwner}/${repositoryName}`
|
||||||
|
)
|
||||||
|
|
||||||
|
const artifacts: Artifact[] = []
|
||||||
|
const [retryOpts, requestOpts] = getRetryOptions(defaultGitHubOptions)
|
||||||
|
|
||||||
|
const opts: OctokitOptions = {
|
||||||
|
log: undefined,
|
||||||
|
userAgent: getUserAgentString(),
|
||||||
|
previews: undefined,
|
||||||
|
retry: retryOpts,
|
||||||
|
request: requestOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
const github = getOctokit(token, opts, retry, requestLog)
|
||||||
|
|
||||||
|
let currentPageNumber = 1
|
||||||
|
const {data: listArtifactResponse} =
|
||||||
|
await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: repositoryOwner,
|
||||||
|
repo: repositoryName,
|
||||||
|
run_id: workflowRunId,
|
||||||
|
per_page: paginationCount,
|
||||||
|
page: currentPageNumber
|
||||||
|
})
|
||||||
|
|
||||||
|
let numberOfPages = Math.ceil(
|
||||||
|
listArtifactResponse.total_count / paginationCount
|
||||||
|
)
|
||||||
|
const totalArtifactCount = listArtifactResponse.total_count
|
||||||
|
if (totalArtifactCount > maximumArtifactCount) {
|
||||||
|
warning(
|
||||||
|
`Workflow run ${workflowRunId} has more than 1000 artifacts. Results will be incomplete as only the first ${maximumArtifactCount} artifacts will be returned`
|
||||||
|
)
|
||||||
|
numberOfPages = maxNumberOfPages
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over the first page
|
||||||
|
for (const artifact of listArtifactResponse.artifacts) {
|
||||||
|
artifacts.push({
|
||||||
|
name: artifact.name,
|
||||||
|
id: artifact.id,
|
||||||
|
url: artifact.url,
|
||||||
|
size: artifact.size_in_bytes
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over any remaining pages
|
||||||
|
for (
|
||||||
|
currentPageNumber;
|
||||||
|
currentPageNumber < numberOfPages;
|
||||||
|
currentPageNumber++
|
||||||
|
) {
|
||||||
|
currentPageNumber++
|
||||||
|
debug(`Fetching page ${currentPageNumber} of artifact list`)
|
||||||
|
|
||||||
|
const {data: | ||||||