Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0e253f8e0 |
@@ -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:
|
||||||
@@ -10,8 +12,8 @@ on:
|
|||||||
- '**.md'
|
- '**.md'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
upload:
|
build:
|
||||||
name: Upload
|
name: Build
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -40,13 +42,19 @@ jobs:
|
|||||||
npm run tsc
|
npm run tsc
|
||||||
working-directory: packages/artifact
|
working-directory: packages/artifact
|
||||||
|
|
||||||
|
- name: Set artifact file contents
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "file1=hello from file 1" >> $GITHUB_ENV
|
||||||
|
echo "file2=hello from file 2" >> $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 -n 'hello from file 1' > artifact-path/first.txt
|
echo '${{ env.file1 }}' > artifact-path/first.txt
|
||||||
echo -n 'hello from file 2' > artifact-path/second.txt
|
echo '${{ env.file2 }}' > artifact-path/second.txt
|
||||||
|
|
||||||
- name: Upload Artifacts
|
- name: Upload Artifacts using actions/github-script@v7
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
@@ -65,16 +73,9 @@ jobs:
|
|||||||
|
|
||||||
console.log(`Successfully uploaded artifact ${id}`)
|
console.log(`Successfully uploaded artifact ${id}`)
|
||||||
|
|
||||||
try {
|
|
||||||
await artifact.uploadArtifact(artifactName, fileContents, './')
|
|
||||||
throw new Error('should have failed second upload')
|
|
||||||
} catch (err) {
|
|
||||||
console.log('Successfully blocked second artifact upload')
|
|
||||||
}
|
|
||||||
verify:
|
verify:
|
||||||
name: Verify
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [upload]
|
needs: [build]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -95,72 +96,35 @@ jobs:
|
|||||||
npm run tsc
|
npm run tsc
|
||||||
working-directory: packages/artifact
|
working-directory: packages/artifact
|
||||||
|
|
||||||
- name: List and Download Artifacts
|
- name: List artifacts using actions/github-script@v7
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const {default: artifactClient} = require('./packages/artifact/lib/artifact')
|
const {default: artifact} = require('./packages/artifact/lib/artifact')
|
||||||
|
|
||||||
const {readFile} = require('fs/promises')
|
const workflowRunId = process.env.GITHUB_RUN_ID
|
||||||
const path = require('path')
|
const repository = process.env.GITHUB_REPOSITORY
|
||||||
|
const repositoryOwner = repository.split('/')[0]
|
||||||
|
const repositoryName = repository.split('/')[1]
|
||||||
|
|
||||||
const findBy = {
|
const listResult = await artifact.listArtifacts(workflowRunId, repositoryOwner, repositoryName, '${{ secrets.GITHUB_TOKEN }}')
|
||||||
repositoryOwner: process.env.GITHUB_REPOSITORY.split('/')[0],
|
|
||||||
repositoryName: process.env.GITHUB_REPOSITORY.split('/')[1],
|
|
||||||
token: '${{ secrets.GITHUB_TOKEN }}',
|
|
||||||
workflowRunId: process.env.GITHUB_RUN_ID
|
|
||||||
}
|
|
||||||
|
|
||||||
const listResult = await artifactClient.listArtifacts({latest: true, findBy})
|
|
||||||
console.log(listResult)
|
console.log(listResult)
|
||||||
|
|
||||||
const artifacts = listResult.artifacts
|
const artifacts = listResult.artifacts
|
||||||
const expected = [
|
|
||||||
'my-artifact-ubuntu-latest',
|
|
||||||
'my-artifact-windows-latest',
|
|
||||||
'my-artifact-macos-latest'
|
|
||||||
]
|
|
||||||
|
|
||||||
const foundArtifacts = artifacts.filter(artifact =>
|
if (artifacts.length !== 3) {
|
||||||
expected.includes(artifact.name)
|
throw new Error('Expected 3 artifacts but only found ' + artifacts.length + ' artifacts')
|
||||||
)
|
}
|
||||||
|
|
||||||
if (foundArtifacts.length !== 3) {
|
const artifactNames = artifacts.map(artifact => artifact.name)
|
||||||
console.log('Unexpected length of found artifacts', foundArtifacts)
|
if (!artifactNames.includes('my-artifact-ubuntu-latest')){
|
||||||
throw new Error(
|
throw new Error("Expected artifact list to contain an artifact named my-artifact-ubuntu-latest but it's missing")
|
||||||
`Expected 3 artifacts but found ${foundArtifacts.length} artifacts.`
|
}
|
||||||
)
|
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')
|
console.log('Successfully listed artifacts that were uploaded')
|
||||||
|
|
||||||
const files = [
|
|
||||||
{name: 'artifact-path/first.txt', content: 'hello from file 1'},
|
|
||||||
{name: 'artifact-path/second.txt', content: 'hello from file 2'}
|
|
||||||
]
|
|
||||||
|
|
||||||
for (const artifact of foundArtifacts) {
|
|
||||||
const {downloadPath} = await artifactClient.downloadArtifact(artifact.id, {
|
|
||||||
path: artifact.name,
|
|
||||||
findBy
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('Downloaded artifact to:', downloadPath)
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const filepath = path.join(
|
|
||||||
process.env.GITHUB_WORKSPACE,
|
|
||||||
downloadPath,
|
|
||||||
file.name
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('Checking file:', filepath)
|
|
||||||
|
|
||||||
const content = await readFile(filepath, 'utf8')
|
|
||||||
if (content.trim() !== file.content.trim()) {
|
|
||||||
throw new Error(
|
|
||||||
`Expected file '${file.name}' to contain '${file.content}' but found '${content}'`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -63,16 +63,10 @@ Import the module:
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
// ES6 module
|
// ES6 module
|
||||||
import {DefaultArtifactClient} from '@actions/artifact'
|
import artifact from '@actions/artifact'
|
||||||
|
|
||||||
// CommonJS
|
// CommonJS
|
||||||
const {DefaultArtifactClient} = require('@actions/artifact')
|
const {default: artifact} = require('@actions/artifact')
|
||||||
```
|
|
||||||
|
|
||||||
Then instantiate:
|
|
||||||
|
|
||||||
```js
|
|
||||||
const artifact = new DefaultArtifactClient()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
ℹ️ For a comprehensive list of classes, interfaces, functions and more, see the [generated documentation](./docs/generated/README.md).
|
ℹ️ For a comprehensive list of classes, interfaces, functions and more, see the [generated documentation](./docs/generated/README.md).
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import {HttpClient} from '@actions/http-client'
|
|||||||
import * as config from '../src/internal/shared/config'
|
import * as config from '../src/internal/shared/config'
|
||||||
import {internalArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client'
|
import {internalArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client'
|
||||||
import {noopLogs} from './common'
|
import {noopLogs} from './common'
|
||||||
import {NetworkError, UsageError} from '../src/internal/shared/errors'
|
|
||||||
|
|
||||||
jest.mock('@actions/http-client')
|
jest.mock('@actions/http-client')
|
||||||
|
|
||||||
@@ -258,42 +257,9 @@ describe('artifact-http-client', () => {
|
|||||||
name: 'artifact',
|
name: 'artifact',
|
||||||
version: 4
|
version: 4
|
||||||
})
|
})
|
||||||
}).rejects.toThrowError(new NetworkError('ENOTFOUND').message)
|
}).rejects.toThrowError(
|
||||||
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
'Failed to CreateArtifact: Unable to make request: ENOTFOUND\nIf you are using self-hosted runners, please make sure your runner has access to all GitHub endpoints: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#communication-between-self-hosted-runners-and-github'
|
||||||
expect(mockPost).toHaveBeenCalledTimes(1)
|
)
|
||||||
})
|
|
||||||
|
|
||||||
it('should properly describe a usage error', async () => {
|
|
||||||
const mockPost = jest.fn(() => {
|
|
||||||
const msgFailed = new http.IncomingMessage(new net.Socket())
|
|
||||||
msgFailed.statusCode = 403
|
|
||||||
msgFailed.statusMessage = 'Forbidden'
|
|
||||||
return {
|
|
||||||
message: msgFailed,
|
|
||||||
readBody: async () => {
|
|
||||||
return Promise.resolve(
|
|
||||||
`{"msg": "insufficient usage to create artifact"}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const mockHttpClient = (
|
|
||||||
HttpClient as unknown as jest.Mock
|
|
||||||
).mockImplementation(() => {
|
|
||||||
return {
|
|
||||||
post: mockPost
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const client = internalArtifactTwirpClient()
|
|
||||||
await expect(async () => {
|
|
||||||
await client.CreateArtifact({
|
|
||||||
workflowRunBackendId: '1234',
|
|
||||||
workflowJobRunBackendId: '5678',
|
|
||||||
name: 'artifact',
|
|
||||||
version: 4
|
|
||||||
})
|
|
||||||
}).rejects.toThrowError(new UsageError().message)
|
|
||||||
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
||||||
expect(mockPost).toHaveBeenCalledTimes(1)
|
expect(mockPost).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -352,3 +352,46 @@ describe('upload-artifact', () => {
|
|||||||
expect(uploadResp).rejects.toThrow()
|
expect(uploadResp).rejects.toThrow()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('getBlobClientOptions', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
delete process.env['HTTPS_PROXY']
|
||||||
|
delete process.env['HTTP_PROXY']
|
||||||
|
delete process.env['NO_PROXY']
|
||||||
|
jest.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not use proxy settings if not specified', () => {
|
||||||
|
const opts = blobUpload.getBlobClientOptions('https://blob-storage.local')
|
||||||
|
expect(opts.proxyOptions).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use https proxy settings from environment', () => {
|
||||||
|
process.env['HTTPS_PROXY'] = 'https://foo:[email protected]'
|
||||||
|
const opts = blobUpload.getBlobClientOptions('https://blob-storage.local')
|
||||||
|
expect(opts.proxyOptions).toEqual({
|
||||||
|
host: 'my-proxy.local',
|
||||||
|
port: 443,
|
||||||
|
username: 'foo',
|
||||||
|
password: 'bar'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use http proxy settings from environment', () => {
|
||||||
|
process.env['HTTP_PROXY'] = 'http://foo:[email protected]:1234'
|
||||||
|
const opts = blobUpload.getBlobClientOptions('http://blob-storage.local')
|
||||||
|
expect(opts.proxyOptions).toEqual({
|
||||||
|
host: 'my-proxy.local',
|
||||||
|
port: 1234,
|
||||||
|
username: 'foo',
|
||||||
|
password: 'bar'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respect NO_PROXY', () => {
|
||||||
|
process.env['HTTPS_PROXY'] = 'https://foo:[email protected]'
|
||||||
|
process.env['NO_PROXY'] = 'no-proxy-me.local'
|
||||||
|
const opts = blobUpload.getBlobClientOptions('https://no-proxy-me.local')
|
||||||
|
expect(opts.proxyOptions).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Generated
+14
-81
@@ -21,7 +21,6 @@
|
|||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"nock": "^13.4.0",
|
|
||||||
"twirp-ts": "^2.5.0",
|
"twirp-ts": "^2.5.0",
|
||||||
"unzip-stream": "^0.3.1"
|
"unzip-stream": "^0.3.1"
|
||||||
},
|
},
|
||||||
@@ -109,25 +108,6 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/core-http/node_modules/node-fetch": {
|
|
||||||
"version": "2.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
|
||||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
|
||||||
"dependencies": {
|
|
||||||
"whatwg-url": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "4.x || >=6.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"encoding": "^0.1.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"encoding": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@azure/core-lro": {
|
"node_modules/@azure/core-lro": {
|
||||||
"version": "2.5.4",
|
"version": "2.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/core-lro/-/core-lro-2.5.4.tgz",
|
||||||
@@ -352,25 +332,6 @@
|
|||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@octokit/request/node_modules/node-fetch": {
|
|
||||||
"version": "2.7.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
|
||||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
|
||||||
"dependencies": {
|
|
||||||
"whatwg-url": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "4.x || >=6.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"encoding": "^0.1.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"encoding": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@octokit/types": {
|
"node_modules/@octokit/types": {
|
||||||
"version": "6.41.0",
|
"version": "6.41.0",
|
||||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz",
|
||||||
@@ -795,22 +756,6 @@
|
|||||||
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
|
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
|
||||||
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
|
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in."
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
|
||||||
"version": "4.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
|
||||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "2.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
@@ -966,11 +911,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
|
||||||
},
|
},
|
||||||
"node_modules/json-stringify-safe": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
|
|
||||||
},
|
|
||||||
"node_modules/jsonc-parser": {
|
"node_modules/jsonc-parser": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz",
|
||||||
@@ -1125,11 +1065,6 @@
|
|||||||
"mkdirp": "bin/cmd.js"
|
"mkdirp": "bin/cmd.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
|
||||||
},
|
|
||||||
"node_modules/neo-async": {
|
"node_modules/neo-async": {
|
||||||
"version": "2.6.2",
|
"version": "2.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||||
@@ -1145,17 +1080,23 @@
|
|||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nock": {
|
"node_modules/node-fetch": {
|
||||||
"version": "13.4.0",
|
"version": "2.6.12",
|
||||||
"resolved": "https://registry.npmjs.org/nock/-/nock-13.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||||
"integrity": "sha512-W8NVHjO/LCTNA64yxAPHV/K47LpGYcVzgKd3Q0n6owhwvD0Dgoterc25R4rnZbckJEb6Loxz1f5QMuJpJnbSyQ==",
|
"integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.1.0",
|
"whatwg-url": "^5.0.0"
|
||||||
"json-stringify-safe": "^5.0.1",
|
|
||||||
"propagate": "^2.0.0"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.13"
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
@@ -1223,14 +1164,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||||
},
|
},
|
||||||
"node_modules/propagate": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
|
|||||||
@@ -52,7 +52,6 @@
|
|||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"nock": "^13.4.0",
|
|
||||||
"twirp-ts": "^2.5.0",
|
"twirp-ts": "^2.5.0",
|
||||||
"unzip-stream": "^0.3.1"
|
"unzip-stream": "^0.3.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {info, debug} from '@actions/core'
|
|||||||
import {ArtifactServiceClientJSON} from '../../generated'
|
import {ArtifactServiceClientJSON} from '../../generated'
|
||||||
import {getResultsServiceUrl, getRuntimeToken} from './config'
|
import {getResultsServiceUrl, getRuntimeToken} from './config'
|
||||||
import {getUserAgentString} from './user-agent'
|
import {getUserAgentString} from './user-agent'
|
||||||
import {NetworkError, UsageError} from './errors'
|
import {NetworkError} from './errors'
|
||||||
|
|
||||||
// The twirp http client must implement this interface
|
// The twirp http client must implement this interface
|
||||||
interface Rpc {
|
interface Rpc {
|
||||||
@@ -64,7 +64,7 @@ class ArtifactHttpClient implements Rpc {
|
|||||||
this.httpClient.post(url, JSON.stringify(data), headers)
|
this.httpClient.post(url, JSON.stringify(data), headers)
|
||||||
)
|
)
|
||||||
|
|
||||||
return body
|
return JSON.parse(body)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to ${method}: ${error.message}`)
|
throw new Error(`Failed to ${method}: ${error.message}`)
|
||||||
}
|
}
|
||||||
@@ -72,49 +72,34 @@ class ArtifactHttpClient implements Rpc {
|
|||||||
|
|
||||||
async retryableRequest(
|
async retryableRequest(
|
||||||
operation: () => Promise<HttpClientResponse>
|
operation: () => Promise<HttpClientResponse>
|
||||||
): Promise<{response: HttpClientResponse; body: object}> {
|
): Promise<{response: HttpClientResponse; body: string}> {
|
||||||
let attempt = 0
|
let attempt = 0
|
||||||
let errorMessage = ''
|
let errorMessage = ''
|
||||||
let rawBody = ''
|
|
||||||
while (attempt < this.maxAttempts) {
|
while (attempt < this.maxAttempts) {
|
||||||
let isRetryable = false
|
let isRetryable = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await operation()
|
const response = await operation()
|
||||||
const statusCode = response.message.statusCode
|
const statusCode = response.message.statusCode
|
||||||
rawBody = await response.readBody()
|
const body = await response.readBody()
|
||||||
debug(`[Response] - ${response.message.statusCode}`)
|
debug(`[Response] - ${response.message.statusCode}`)
|
||||||
debug(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`)
|
debug(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`)
|
||||||
const body = JSON.parse(rawBody)
|
debug(`Body: ${body}`)
|
||||||
debug(`Body: ${JSON.stringify(body, null, 2)}`)
|
|
||||||
if (this.isSuccessStatusCode(statusCode)) {
|
if (this.isSuccessStatusCode(statusCode)) {
|
||||||
return {response, body}
|
return {response, body}
|
||||||
}
|
}
|
||||||
isRetryable = this.isRetryableHttpStatusCode(statusCode)
|
isRetryable = this.isRetryableHttpStatusCode(statusCode)
|
||||||
errorMessage = `Failed request: (${statusCode}) ${response.message.statusMessage}`
|
errorMessage = `Failed request: (${statusCode}) ${response.message.statusMessage}`
|
||||||
if (body.msg) {
|
const responseMessage = JSON.parse(body).msg
|
||||||
if (UsageError.isUsageErrorMessage(body.msg)) {
|
if (responseMessage) {
|
||||||
throw new UsageError()
|
errorMessage = `${errorMessage}: ${responseMessage}`
|
||||||
}
|
|
||||||
|
|
||||||
errorMessage = `${errorMessage}: ${body.msg}`
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof SyntaxError) {
|
isRetryable = true
|
||||||
debug(`Raw Body: ${rawBody}`)
|
errorMessage = error.message
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof UsageError) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
|
|
||||||
if (NetworkError.isNetworkErrorCode(error?.code)) {
|
if (NetworkError.isNetworkErrorCode(error?.code)) {
|
||||||
throw new NetworkError(error?.code)
|
throw new NetworkError(error?.code)
|
||||||
}
|
}
|
||||||
|
|
||||||
isRetryable = true
|
|
||||||
errorMessage = error.message
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isRetryable) {
|
if (!isRetryable) {
|
||||||
|
|||||||
@@ -57,16 +57,3 @@ export class NetworkError extends Error {
|
|||||||
].includes(code)
|
].includes(code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UsageError extends Error {
|
|
||||||
constructor() {
|
|
||||||
const message = `Artifact storage quota has been hit. Unable to upload any new artifacts. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`
|
|
||||||
super(message)
|
|
||||||
this.name = 'UsageError'
|
|
||||||
}
|
|
||||||
|
|
||||||
static isUsageErrorMessage = (msg?: string): boolean => {
|
|
||||||
if (!msg) return false
|
|
||||||
return msg.includes('insufficient usage')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -45,21 +45,6 @@ export interface UploadArtifactOptions {
|
|||||||
* For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads.
|
* For large files that are not easily compressed, a value of 0 is recommended for significantly faster uploads.
|
||||||
*/
|
*/
|
||||||
compressionLevel?: number
|
compressionLevel?: number
|
||||||
/**
|
|
||||||
* The simulated network error we'll temporarily use to test the azure blob
|
|
||||||
* client behavior.
|
|
||||||
* The value can range from 0 to 6
|
|
||||||
* 0 - fetchError
|
|
||||||
* 1 - abortError
|
|
||||||
* 2 - networkError
|
|
||||||
* 3 - securityError
|
|
||||||
* 4 - notAllowedError
|
|
||||||
* 5 - quotaExceededError
|
|
||||||
* 6 - random
|
|
||||||
* 7 - none
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
simulateError?: number
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import {BlobClient, BlockBlobUploadStreamOptions} from '@azure/storage-blob'
|
import {
|
||||||
|
AnonymousCredential,
|
||||||
|
BlobClient,
|
||||||
|
BlockBlobUploadStreamOptions,
|
||||||
|
StoragePipelineOptions
|
||||||
|
} from '@azure/storage-blob'
|
||||||
import {TransferProgressEvent} from '@azure/core-http'
|
import {TransferProgressEvent} from '@azure/core-http'
|
||||||
import {ZipUploadStream} from './zip'
|
import {ZipUploadStream} from './zip'
|
||||||
import {getUploadChunkSize, getConcurrency} from '../shared/config'
|
import {getUploadChunkSize, getConcurrency} from '../shared/config'
|
||||||
|
import {getProxyUrl} from '@actions/http-client'
|
||||||
import * as core from '@actions/core'
|
import * as core from '@actions/core'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import * as stream from 'stream'
|
import * as stream from 'stream'
|
||||||
import nock from 'nock'
|
|
||||||
import {NetworkError} from '../shared/errors'
|
import {NetworkError} from '../shared/errors'
|
||||||
|
import {getUserAgentString} from '../shared/user-agent'
|
||||||
|
|
||||||
export const DEFAULT_ERROR_NUMBER = 7
|
|
||||||
export const ERROR_TYPES = [
|
|
||||||
'fetchError',
|
|
||||||
'abortError',
|
|
||||||
'securityError',
|
|
||||||
'notAllowedError',
|
|
||||||
'quotaExceededError'
|
|
||||||
]
|
|
||||||
export interface BlobUploadResponse {
|
export interface BlobUploadResponse {
|
||||||
/**
|
/**
|
||||||
* The total reported upload size in bytes. Empty if the upload failed
|
* The total reported upload size in bytes. Empty if the upload failed
|
||||||
@@ -27,64 +25,21 @@ export interface BlobUploadResponse {
|
|||||||
*/
|
*/
|
||||||
sha256Hash?: string
|
sha256Hash?: string
|
||||||
}
|
}
|
||||||
export async function sendSimulatedError(
|
|
||||||
simulatedError: number,
|
|
||||||
authenticatedUploadURL: string
|
|
||||||
): Promise<void> {
|
|
||||||
switch (simulatedError) {
|
|
||||||
case 0: {
|
|
||||||
nock(authenticatedUploadURL).get('/').replyWithError({
|
|
||||||
code: 'ECONNRESET',
|
|
||||||
message: 'socket hang up'
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 1: {
|
|
||||||
const controller = new AbortController()
|
|
||||||
controller.abort()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
nock(authenticatedUploadURL).get('/').replyWithError({
|
|
||||||
code: 'ETIMEDOUT'
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 3: {
|
|
||||||
nock(authenticatedUploadURL).get('/').reply(403)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 4: {
|
|
||||||
nock(authenticatedUploadURL).get('/').reply(405)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 5: {
|
|
||||||
nock(authenticatedUploadURL).get('/').reply(429)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 6: {
|
|
||||||
const rand = Math.floor(Math.random() * ERROR_TYPES.length)
|
|
||||||
sendSimulatedError(rand, authenticatedUploadURL)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 7: {
|
|
||||||
core.info('no error selected')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
core.error('something went wrong')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export async function uploadZipToBlobStorage(
|
export async function uploadZipToBlobStorage(
|
||||||
authenticatedUploadURL: string,
|
authenticatedUploadURL: string,
|
||||||
zipUploadStream: ZipUploadStream,
|
zipUploadStream: ZipUploadStream
|
||||||
simulatedError: number = DEFAULT_ERROR_NUMBER
|
|
||||||
): Promise<BlobUploadResponse> {
|
): Promise<BlobUploadResponse> {
|
||||||
let uploadByteCount = 0
|
let uploadByteCount = 0
|
||||||
|
|
||||||
const maxConcurrency = getConcurrency()
|
const maxConcurrency = getConcurrency()
|
||||||
const bufferSize = getUploadChunkSize()
|
const bufferSize = getUploadChunkSize()
|
||||||
const blobClient = new BlobClient(authenticatedUploadURL)
|
|
||||||
|
const blobClient = new BlobClient(
|
||||||
|
authenticatedUploadURL,
|
||||||
|
new AnonymousCredential(),
|
||||||
|
getBlobClientOptions(authenticatedUploadURL)
|
||||||
|
)
|
||||||
const blockBlobClient = blobClient.getBlockBlobClient()
|
const blockBlobClient = blobClient.getBlockBlobClient()
|
||||||
|
|
||||||
core.debug(
|
core.debug(
|
||||||
@@ -93,9 +48,6 @@ export async function uploadZipToBlobStorage(
|
|||||||
|
|
||||||
const uploadCallback = (progress: TransferProgressEvent): void => {
|
const uploadCallback = (progress: TransferProgressEvent): void => {
|
||||||
core.info(`Uploaded bytes ${progress.loadedBytes}`)
|
core.info(`Uploaded bytes ${progress.loadedBytes}`)
|
||||||
if (progress.loadedBytes > 1) {
|
|
||||||
sendSimulatedError(simulatedError, authenticatedUploadURL)
|
|
||||||
}
|
|
||||||
uploadByteCount = progress.loadedBytes
|
uploadByteCount = progress.loadedBytes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,3 +97,37 @@ export async function uploadZipToBlobStorage(
|
|||||||
sha256Hash
|
sha256Hash
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBlobClientOptions(sasURL: string): StoragePipelineOptions {
|
||||||
|
const options: StoragePipelineOptions = {
|
||||||
|
userAgentOptions: {
|
||||||
|
userAgentPrefix: getUserAgentString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyUrl = getProxyUrl(sasURL)
|
||||||
|
if (proxyUrl !== '') {
|
||||||
|
const {
|
||||||
|
port: portString,
|
||||||
|
hostname: host,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
protocol
|
||||||
|
} = new URL(proxyUrl)
|
||||||
|
core.debug(`Using proxy server for blob storage upload, host: ${host}`)
|
||||||
|
|
||||||
|
let port = protocol === 'https:' ? 443 : 80
|
||||||
|
if (portString !== '') {
|
||||||
|
port = parseInt(portString)
|
||||||
|
}
|
||||||
|
|
||||||
|
options.proxyOptions = {
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,8 +76,7 @@ export async function uploadArtifact(
|
|||||||
// Upload zip to blob storage
|
// Upload zip to blob storage
|
||||||
const uploadResult = await uploadZipToBlobStorage(
|
const uploadResult = await uploadZipToBlobStorage(
|
||||||
createArtifactResp.signedUploadUrl,
|
createArtifactResp.signedUploadUrl,
|
||||||
zipUploadStream,
|
zipUploadStream
|
||||||
options?.simulateError
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// finalize the artifact
|
// finalize the artifact
|
||||||
|
|||||||
Reference in New Issue
Block a user