diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index b5f21974..cbabd034 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -5,8 +5,8 @@ on: inputs: package: required: true - description: 'core, artifact, cache, exec, github, glob, io, tool-cache' - + description: 'core, artifact, cache, exec, github, glob, http-client, io, tool-cache' + jobs: test: runs-on: macos-latest @@ -17,40 +17,40 @@ jobs: - name: verify package exists run: ls packages/${{ github.event.inputs.package }} - + - name: Set Node.js 12.x uses: actions/setup-node@v1 with: node-version: 12.x - + - name: npm install run: npm install - + - name: bootstrap run: npm run bootstrap - + - name: build run: npm run build - + - name: test run: npm run test - name: pack run: npm pack working-directory: packages/${{ github.event.inputs.package }} - + - name: upload artifact uses: actions/upload-artifact@v2 with: name: ${{ github.event.inputs.package }} path: packages/${{ github.event.inputs.package }}/*.tgz - + publish: runs-on: macos-latest needs: test environment: npm-publish steps: - + - name: download artifact uses: actions/download-artifact@v2 with: @@ -58,7 +58,7 @@ jobs: - name: setup authentication run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc - env: + env: NPM_TOKEN: ${{ secrets.TOKEN }} - name: publish @@ -68,13 +68,13 @@ jobs: if: failure() run: | curl -X POST -H 'Content-type: application/json' --data '{"text":":pb__failed: Failed to publish a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK - env: + env: SLACK_WEBHOOK: ${{ secrets.SLACK }} - + - name: notify slack on success if: success() run: | curl -X POST -H 'Content-type: application/json' --data '{"text":":dance: Successfully published a new version of ${{ github.event.inputs.package }}"}' $SLACK_WEBHOOK - env: + env: SLACK_WEBHOOK: ${{ secrets.SLACK }} - + diff --git a/README.md b/README.md index 7571d1eb..43ee8acd 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,15 @@ $ npm install @actions/glob ```
+:phone: [@actions/http-client](packages/http-client) + +A lightweight HTTP client optimized for building actions. Read more [here](packages/http-client) + +```bash +$ npm install @actions/http-client +``` +
+ :pencil2: [@actions/io](packages/io) Provides disk i/o functions like cp, mv, rmRF, which etc. Read more [here](packages/io) diff --git a/packages/artifact/RELEASES.md b/packages/artifact/RELEASES.md index 8f3fd713..d9e1935c 100644 --- a/packages/artifact/RELEASES.md +++ b/packages/artifact/RELEASES.md @@ -77,4 +77,12 @@ ### 1.0.0 -- Update `lockfileVersion` to `v2` in `package-lock.json [#1009](https://github.com/actions/toolkit/pull/1009) \ No newline at end of file +- Update `lockfileVersion` to `v2` in `package-lock.json` [#1009](https://github.com/actions/toolkit/pull/1009) + +### 1.0.1 + +- Update to v2.0.0 of `@actions/http-client` + +### 1.0.2 + +- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087) \ No newline at end of file diff --git a/packages/artifact/__tests__/retry.test.ts b/packages/artifact/__tests__/retry.test.ts index 12c49784..d1e9256e 100644 --- a/packages/artifact/__tests__/retry.test.ts +++ b/packages/artifact/__tests__/retry.test.ts @@ -3,7 +3,6 @@ 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 {IHttpClientResponse} from '@actions/http-client/interfaces' import {HttpClientResponse} from '@actions/http-client' jest.mock('../src/internal/config-variables') @@ -42,7 +41,7 @@ async function testRetry( async function handleResponse( testResponseCode: number | undefined -): Promise { +): Promise { if (!testResponseCode) { throw new Error( 'Test incorrectly set up. reverse.pop() was called too many times so not enough test response codes were supplied' @@ -72,7 +71,7 @@ async function emptyMockReadBody(): Promise { async function setupSingleMockResponse( statusCode: number -): Promise { +): Promise { const mockMessage = new http.IncomingMessage(new net.Socket()) const mockReadBody = emptyMockReadBody mockMessage.statusCode = statusCode diff --git a/packages/artifact/package-lock.json b/packages/artifact/package-lock.json index 7f5ec7d2..f46f3efd 100644 --- a/packages/artifact/package-lock.json +++ b/packages/artifact/package-lock.json @@ -1,16 +1,16 @@ { "name": "@actions/artifact", - "version": "0.6.1", + "version": "1.0.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@actions/artifact", - "version": "0.6.1", + "version": "1.0.1", "license": "MIT", "dependencies": { "@actions/core": "^1.2.6", - "@actions/http-client": "^1.0.11", + "@actions/http-client": "^2.0.1", "tmp": "^0.2.1", "tmp-promise": "^3.0.2" }, @@ -20,14 +20,14 @@ } }, "node_modules/@actions/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz", - "integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.0.tgz", + "integrity": "sha512-XirM+Zo/PFlA+1h+i4bkfvagujta+LIM2AOSzPbt8JqXbbuxb1HTB+FqIyaKmue9yiCx/JIJY6pXsOl3+T8JGw==", "dependencies": { "@actions/http-client": "^1.0.11" } }, - "node_modules/@actions/http-client": { + "node_modules/@actions/core/node_modules/@actions/http-client": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", @@ -35,6 +35,14 @@ "tunnel": "0.0.6" } }, + "node_modules/@actions/http-client": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", + "dependencies": { + "tunnel": "^0.0.6" + } + }, "node_modules/@types/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.3.tgz", @@ -187,19 +195,29 @@ }, "dependencies": { "@actions/core": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.6.0.tgz", - "integrity": "sha512-NB1UAZomZlCV/LmJqkLhNTqtKfFXJZAUPcfl/zqG7EfsQdeUJtaWO98SGbuQ3pydJ3fHl2CvI/51OKYlCYYcaw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.0.tgz", + "integrity": "sha512-XirM+Zo/PFlA+1h+i4bkfvagujta+LIM2AOSzPbt8JqXbbuxb1HTB+FqIyaKmue9yiCx/JIJY6pXsOl3+T8JGw==", "requires": { "@actions/http-client": "^1.0.11" + }, + "dependencies": { + "@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "requires": { + "tunnel": "0.0.6" + } + } } }, "@actions/http-client": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", - "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "requires": { - "tunnel": "0.0.6" + "tunnel": "^0.0.6" } }, "@types/tmp": { diff --git a/packages/artifact/package.json b/packages/artifact/package.json index 6dd84aa2..1a8e1b7a 100644 --- a/packages/artifact/package.json +++ b/packages/artifact/package.json @@ -1,6 +1,6 @@ { "name": "@actions/artifact", - "version": "1.0.0", + "version": "1.0.2", "preview": true, "description": "Actions artifact lib", "keywords": [ @@ -38,7 +38,7 @@ }, "dependencies": { "@actions/core": "^1.2.6", - "@actions/http-client": "^1.0.11", + "@actions/http-client": "^2.0.1", "tmp": "^0.2.1", "tmp-promise": "^3.0.2" }, diff --git a/packages/artifact/src/internal/download-http-client.ts b/packages/artifact/src/internal/download-http-client.ts index c81ac917..2df4675f 100644 --- a/packages/artifact/src/internal/download-http-client.ts +++ b/packages/artifact/src/internal/download-http-client.ts @@ -18,7 +18,7 @@ import {URL} from 'url' import {StatusReporter} from './status-reporter' import {performance} from 'perf_hooks' import {ListArtifactsResponse, QueryArtifactResponse} from './contracts' -import {IHttpClientResponse} from '@actions/http-client/interfaces' +import {HttpClientResponse} from '@actions/http-client' import {HttpManager} from './http-manager' import {DownloadItem} from './download-specification' import {getDownloadFileConcurrency, getRetryLimit} from './config-variables' @@ -152,7 +152,7 @@ export class DownloadHttpClient { const headers = getDownloadHeaders('application/json', true, true) // a single GET request is used to download a file - const makeDownloadRequest = async (): Promise => { + const makeDownloadRequest = async (): Promise => { const client = this.downloadHttpManager.getClient(httpClientIndex) return await client.get(artifactLocation, headers) } @@ -225,7 +225,7 @@ export class DownloadHttpClient { // keep trying to download a file until a retry limit has been reached while (retryCount <= retryLimit) { - let response: IHttpClientResponse + let response: HttpClientResponse try { response = await makeDownloadRequest() } catch (error) { @@ -295,7 +295,7 @@ export class DownloadHttpClient { * @param isGzip a boolean denoting if the content is compressed using gzip and if we need to decode it */ async pipeResponseToFile( - response: IHttpClientResponse, + response: HttpClientResponse, destinationStream: fs.WriteStream, isGzip: boolean ): Promise { diff --git a/packages/artifact/src/internal/http-manager.ts b/packages/artifact/src/internal/http-manager.ts index 9c19a620..9b44fb85 100644 --- a/packages/artifact/src/internal/http-manager.ts +++ b/packages/artifact/src/internal/http-manager.ts @@ -1,4 +1,4 @@ -import {HttpClient} from '@actions/http-client/index' +import {HttpClient} from '@actions/http-client' import {createHttpClient} from './utils' /** diff --git a/packages/artifact/src/internal/requestUtils.ts b/packages/artifact/src/internal/requestUtils.ts index 65f40e7b..4abc5b03 100644 --- a/packages/artifact/src/internal/requestUtils.ts +++ b/packages/artifact/src/internal/requestUtils.ts @@ -1,4 +1,4 @@ -import {IHttpClientResponse} from '@actions/http-client/interfaces' +import {HttpClientResponse} from '@actions/http-client' import { isRetryableStatusCode, isSuccessStatusCode, @@ -11,11 +11,11 @@ import {getRetryLimit} from './config-variables' export async function retry( name: string, - operation: () => Promise, + operation: () => Promise, customErrorMessages: Map, maxAttempts: number -): Promise { - let response: IHttpClientResponse | undefined = undefined +): Promise { + let response: HttpClientResponse | undefined = undefined let statusCode: number | undefined = undefined let isRetryable = false let errorMessage = '' @@ -71,9 +71,9 @@ export async function retry( export async function retryHttpClientRequest( name: string, - method: () => Promise, + method: () => Promise, customErrorMessages: Map = new Map(), maxAttempts = getRetryLimit() -): Promise { +): Promise { return await retry(name, method, customErrorMessages, maxAttempts) } diff --git a/packages/artifact/src/internal/upload-http-client.ts b/packages/artifact/src/internal/upload-http-client.ts index a9a77f32..67b3bf6d 100644 --- a/packages/artifact/src/internal/upload-http-client.ts +++ b/packages/artifact/src/internal/upload-http-client.ts @@ -32,8 +32,7 @@ import {promisify} from 'util' import {URL} from 'url' import {performance} from 'perf_hooks' import {StatusReporter} from './status-reporter' -import {HttpCodes} from '@actions/http-client' -import {IHttpClientResponse} from '@actions/http-client/interfaces' +import {HttpCodes, HttpClientResponse} from '@actions/http-client' import {HttpManager} from './http-manager' import {UploadSpecification} from './upload-specification' import {UploadOptions} from './upload-options' @@ -421,7 +420,7 @@ export class UploadHttpClient { digest ) - const uploadChunkRequest = async (): Promise => { + const uploadChunkRequest = async (): Promise => { const client = this.uploadHttpManager.getClient(httpClientIndex) return await client.sendStream('PUT', resourceUrl, openStream(), headers) } @@ -432,7 +431,7 @@ export class UploadHttpClient { // 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 const incrementAndCheckRetryLimit = ( - response?: IHttpClientResponse + response?: HttpClientResponse ): boolean => { retryCount++ if (retryCount > retryLimit) { @@ -469,7 +468,7 @@ export class UploadHttpClient { // allow for failed chunks to be retried multiple times while (retryCount <= retryLimit) { - let response: IHttpClientResponse + let response: HttpClientResponse try { response = await uploadChunkRequest() diff --git a/packages/artifact/src/internal/utils.ts b/packages/artifact/src/internal/utils.ts index 063b5dbf..cb5a9224 100644 --- a/packages/artifact/src/internal/utils.ts +++ b/packages/artifact/src/internal/utils.ts @@ -1,10 +1,9 @@ import crypto from 'crypto' import {promises as fs} from 'fs' -import {IncomingHttpHeaders} from 'http' +import {IncomingHttpHeaders, OutgoingHttpHeaders} from 'http' import {debug, info, warning} from '@actions/core' -import {HttpCodes, HttpClient} from '@actions/http-client' -import {BearerCredentialHandler} from '@actions/http-client/auth' -import {IHeaders, IHttpClientResponse} from '@actions/http-client/interfaces' +import {HttpCodes, HttpClient, HttpClientResponse} from '@actions/http-client' +import {BearerCredentialHandler} from '@actions/http-client/lib/auth' import { getRuntimeToken, getRuntimeUrl, @@ -141,8 +140,8 @@ export function getDownloadHeaders( contentType: string, isKeepAlive?: boolean, acceptGzip?: boolean -): IHeaders { - const requestOptions: IHeaders = {} +): OutgoingHttpHeaders { + const requestOptions: OutgoingHttpHeaders = {} if (contentType) { requestOptions['Content-Type'] = contentType @@ -184,8 +183,8 @@ export function getUploadHeaders( contentLength?: number, contentRange?: string, digest?: StreamDigest -): IHeaders { - const requestOptions: IHeaders = {} +): OutgoingHttpHeaders { + const requestOptions: OutgoingHttpHeaders = {} requestOptions['Accept'] = `application/json;api-version=${getApiVersion()}` if (contentType) { requestOptions['Content-Type'] = contentType @@ -234,7 +233,7 @@ export function getArtifactUrl(): string { * Certain information such as the TLSSocket and the Readable state are not really useful for diagnostic purposes so they can be avoided. * Other information such as the headers, the response code and message might be useful, so this is displayed. */ -export function displayHttpDiagnostics(response: IHttpClientResponse): void { +export function displayHttpDiagnostics(response: HttpClientResponse): void { info( `##### Begin Diagnostic HTTP information ##### Status Code: ${response.message.statusCode} diff --git a/packages/cache/RELEASES.md b/packages/cache/RELEASES.md index ec77c2bc..8adb7e3c 100644 --- a/packages/cache/RELEASES.md +++ b/packages/cache/RELEASES.md @@ -56,3 +56,9 @@ ### 2.0.0 - Added support to check if Actions cache service feature is available or not [#1028](https://github.com/actions/toolkit/pull/1028) + +### 2.0.3 +- Update to v2.0.0 of `@actions/http-client` + +### 2.0.4 +- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087) \ No newline at end of file diff --git a/packages/cache/__tests__/saveCache.test.ts b/packages/cache/__tests__/saveCache.test.ts index 6949759b..4627f2c7 100644 --- a/packages/cache/__tests__/saveCache.test.ts +++ b/packages/cache/__tests__/saveCache.test.ts @@ -5,7 +5,7 @@ import * as cacheHttpClient from '../src/internal/cacheHttpClient' import * as cacheUtils from '../src/internal/cacheUtils' import {CacheFilename, CompressionMethod} from '../src/internal/constants' import * as tar from '../src/internal/tar' -import {ITypedResponse} from '@actions/http-client/interfaces' +import {TypedResponse} from '@actions/http-client/lib/interfaces' import { ReserveCacheResponse, ITypedResponseWithError @@ -172,7 +172,7 @@ test('save with reserve cache failure should fail', async () => { const reserveCacheMock = jest .spyOn(cacheHttpClient, 'reserveCache') .mockImplementation(async () => { - const response: ITypedResponse = { + const response: TypedResponse = { statusCode: 500, result: null, headers: {} @@ -208,7 +208,7 @@ test('save with server error should fail', async () => { const reserveCacheMock = jest .spyOn(cacheHttpClient, 'reserveCache') .mockImplementation(async () => { - const response: ITypedResponse = { + const response: TypedResponse = { statusCode: 500, result: {cacheId}, headers: {} @@ -257,7 +257,7 @@ test('save with valid inputs uploads a cache', async () => { const reserveCacheMock = jest .spyOn(cacheHttpClient, 'reserveCache') .mockImplementation(async () => { - const response: ITypedResponse = { + const response: TypedResponse = { statusCode: 500, result: {cacheId}, headers: {} diff --git a/packages/cache/package-lock.json b/packages/cache/package-lock.json index 2ec7b7d6..8ff62189 100644 --- a/packages/cache/package-lock.json +++ b/packages/cache/package-lock.json @@ -1,18 +1,18 @@ { "name": "@actions/cache", - "version": "2.0.2", + "version": "2.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@actions/cache", - "version": "2.0.0", + "version": "2.0.3", "license": "MIT", "dependencies": { "@actions/core": "^1.2.6", "@actions/exec": "^1.0.1", "@actions/glob": "^0.1.0", - "@actions/http-client": "^1.0.9", + "@actions/http-client": "^2.0.1", "@actions/io": "^1.0.1", "@azure/ms-rest-js": "^2.6.0", "@azure/storage-blob": "^12.8.0", @@ -31,9 +31,9 @@ "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" }, "node_modules/@actions/exec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.4.tgz", - "integrity": "sha512-4DPChWow9yc9W3WqEbUj8Nr86xkpyE29ZzWjXucHItclLbEW6jr80Zx4nqv18QL6KK65+cifiQZXvnqgTV6oHw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "dependencies": { "@actions/io": "^1.0.1" } @@ -48,25 +48,17 @@ } }, "node_modules/@actions/http-client": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.9.tgz", - "integrity": "sha512-0O4SsJ7q+MK0ycvXPl2e6bMXV7dxAXOGjrXS1eTF9s2S401Tp6c/P3c3Joz04QefC1J6Gt942Wl2jbm3f4mLcg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "dependencies": { - "tunnel": "0.0.6" - } - }, - "node_modules/@actions/http-client/node_modules/tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", - "engines": { - "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + "tunnel": "^0.0.6" } }, "node_modules/@actions/io": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz", - "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.2.tgz", + "integrity": "sha512-d+RwPlMp+2qmBfeLYPLXuSRykDIFEwdTA0MMxzS9kh4kvP1ftrc/9fzy6pX6qAjthdXruHQ6/6kjT/DNo5ALuw==" }, "node_modules/@azure/abort-controller": { "version": "1.0.4", @@ -618,9 +610,9 @@ "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" }, "@actions/exec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.4.tgz", - "integrity": "sha512-4DPChWow9yc9W3WqEbUj8Nr86xkpyE29ZzWjXucHItclLbEW6jr80Zx4nqv18QL6KK65+cifiQZXvnqgTV6oHw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "requires": { "@actions/io": "^1.0.1" } @@ -635,24 +627,17 @@ } }, "@actions/http-client": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.9.tgz", - "integrity": "sha512-0O4SsJ7q+MK0ycvXPl2e6bMXV7dxAXOGjrXS1eTF9s2S401Tp6c/P3c3Joz04QefC1J6Gt942Wl2jbm3f4mLcg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "requires": { - "tunnel": "0.0.6" - }, - "dependencies": { - "tunnel": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", - "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" - } + "tunnel": "^0.0.6" } }, "@actions/io": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.2.tgz", - "integrity": "sha512-J8KuFqVPr3p6U8W93DOXlXW6zFvrQAJANdS+vw0YhusLIq+bszW8zmK2Fh1C2kDPX8FMvwIl1OUcFgvJoXLbAg==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.2.tgz", + "integrity": "sha512-d+RwPlMp+2qmBfeLYPLXuSRykDIFEwdTA0MMxzS9kh4kvP1ftrc/9fzy6pX6qAjthdXruHQ6/6kjT/DNo5ALuw==" }, "@azure/abort-controller": { "version": "1.0.4", diff --git a/packages/cache/package.json b/packages/cache/package.json index 97f774b1..0b7bff06 100644 --- a/packages/cache/package.json +++ b/packages/cache/package.json @@ -1,6 +1,6 @@ { "name": "@actions/cache", - "version": "2.0.2", + "version": "2.0.4", "preview": true, "description": "Actions cache lib", "keywords": [ @@ -40,7 +40,7 @@ "@actions/core": "^1.2.6", "@actions/exec": "^1.0.1", "@actions/glob": "^0.1.0", - "@actions/http-client": "^1.0.9", + "@actions/http-client": "^2.0.1", "@actions/io": "^1.0.1", "@azure/ms-rest-js": "^2.6.0", "@azure/storage-blob": "^12.8.0", @@ -48,8 +48,8 @@ "uuid": "^3.3.3" }, "devDependencies": { - "typescript": "^3.8.3", "@types/semver": "^6.0.0", - "@types/uuid": "^3.4.5" + "@types/uuid": "^3.4.5", + "typescript": "^3.8.3" } } diff --git a/packages/cache/src/internal/cacheHttpClient.ts b/packages/cache/src/internal/cacheHttpClient.ts index 21f69917..c66d1a73 100644 --- a/packages/cache/src/internal/cacheHttpClient.ts +++ b/packages/cache/src/internal/cacheHttpClient.ts @@ -1,7 +1,10 @@ import * as core from '@actions/core' import {HttpClient} from '@actions/http-client' -import {BearerCredentialHandler} from '@actions/http-client/auth' -import {IRequestOptions, ITypedResponse} from '@actions/http-client/interfaces' +import {BearerCredentialHandler} from '@actions/http-client/lib/auth' +import { + RequestOptions, + TypedResponse +} from '@actions/http-client/lib/interfaces' import * as crypto from 'crypto' import * as fs from 'fs' import {URL} from 'url' @@ -46,8 +49,8 @@ function createAcceptHeader(type: string, apiVersion: string): string { return `${type};api-version=${apiVersion}` } -function getRequestOptions(): IRequestOptions { - const requestOptions: IRequestOptions = { +function getRequestOptions(): RequestOptions { + const requestOptions: RequestOptions = { headers: { Accept: createAcceptHeader('application/json', '6.0-preview.1') } @@ -275,7 +278,7 @@ async function commitCache( httpClient: HttpClient, cacheId: number, filesize: number -): Promise> { +): Promise> { const commitCacheRequest: CommitCacheRequest = {size: filesize} return await retryTypedResponse('commitCache', async () => httpClient.postJson( diff --git a/packages/cache/src/internal/contracts.d.ts b/packages/cache/src/internal/contracts.d.ts index eb79fdae..1b2a13a1 100644 --- a/packages/cache/src/internal/contracts.d.ts +++ b/packages/cache/src/internal/contracts.d.ts @@ -1,8 +1,8 @@ import {CompressionMethod} from './constants' -import {ITypedResponse} from '@actions/http-client/interfaces' +import {TypedResponse} from '@actions/http-client/lib/interfaces' import {HttpClientError} from '@actions/http-client' -export interface ITypedResponseWithError extends ITypedResponse { +export interface ITypedResponseWithError extends TypedResponse { error?: HttpClientError } diff --git a/packages/cache/src/internal/downloadUtils.ts b/packages/cache/src/internal/downloadUtils.ts index bedaa375..08b2dee8 100644 --- a/packages/cache/src/internal/downloadUtils.ts +++ b/packages/cache/src/internal/downloadUtils.ts @@ -1,6 +1,5 @@ import * as core from '@actions/core' -import {HttpClient} from '@actions/http-client' -import {IHttpClientResponse} from '@actions/http-client/interfaces' +import {HttpClient, HttpClientResponse} from '@actions/http-client' import {BlockBlobClient} from '@azure/storage-blob' import {TransferProgressEvent} from '@azure/ms-rest-js' import * as buffer from 'buffer' @@ -20,7 +19,7 @@ import {retryHttpClientResponse} from './requestUtils' * @param output the writable stream */ async function pipeResponseToStream( - response: IHttpClientResponse, + response: HttpClientResponse, output: NodeJS.WritableStream ): Promise { const pipeline = util.promisify(stream.pipeline) diff --git a/packages/cache/src/internal/requestUtils.ts b/packages/cache/src/internal/requestUtils.ts index be254b93..043c8a7c 100644 --- a/packages/cache/src/internal/requestUtils.ts +++ b/packages/cache/src/internal/requestUtils.ts @@ -1,6 +1,9 @@ import * as core from '@actions/core' -import {HttpCodes, HttpClientError} from '@actions/http-client' -import {IHttpClientResponse} from '@actions/http-client/interfaces' +import { + HttpCodes, + HttpClientError, + HttpClientResponse +} from '@actions/http-client' import {DefaultRetryDelay, DefaultRetryAttempts} from './constants' import {ITypedResponseWithError} from './contracts' @@ -103,7 +106,7 @@ export async function retryTypedResponse( maxAttempts, delay, // If the error object contains the statusCode property, extract it and return - // an ITypedResponse so it can be processed by the retry logic. + // an TypedResponse so it can be processed by the retry logic. (error: Error) => { if (error instanceof HttpClientError) { return { @@ -121,14 +124,14 @@ export async function retryTypedResponse( export async function retryHttpClientResponse( name: string, - method: () => Promise, + method: () => Promise, maxAttempts = DefaultRetryAttempts, delay = DefaultRetryDelay -): Promise { +): Promise { return await retry( name, method, - (response: IHttpClientResponse) => response.message.statusCode, + (response: HttpClientResponse) => response.message.statusCode, maxAttempts, delay ) diff --git a/packages/core/RELEASES.md b/packages/core/RELEASES.md index fa0c93ae..f751b4ef 100644 --- a/packages/core/RELEASES.md +++ b/packages/core/RELEASES.md @@ -1,5 +1,16 @@ # @actions/core Releases +### 1.8.2 +- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087) + +### 1.8.1 +- Update to v2.0.0 of `@actions/http-client` + +### 1.8.0 +- Deprecate `markdownSummary` extension export in favor of `summary` + - https://github.com/actions/toolkit/pull/1072 + - https://github.com/actions/toolkit/pull/1073 + ### 1.7.0 - [Added `markdownSummary` extension](https://github.com/actions/toolkit/pull/1014) diff --git a/packages/core/__tests__/markdown-summary.test.ts b/packages/core/__tests__/summary.test.ts similarity index 76% rename from packages/core/__tests__/markdown-summary.test.ts rename to packages/core/__tests__/summary.test.ts index d9b8ee5c..c13b0179 100644 --- a/packages/core/__tests__/markdown-summary.test.ts +++ b/packages/core/__tests__/summary.test.ts @@ -1,9 +1,10 @@ import * as fs from 'fs' import * as os from 'os' import path from 'path' -import {markdownSummary, SUMMARY_ENV_VAR} from '../src/markdown-summary' +import {summary, SUMMARY_ENV_VAR} from '../src/summary' -const testFilePath = path.join(__dirname, 'test', 'test-summary.md') +const testDirectoryPath = path.join(__dirname, 'test') +const testFilePath = path.join(testDirectoryPath, 'test-summary.md') async function assertSummary(expected: string): Promise { const file = await fs.promises.readFile(testFilePath, {encoding: 'utf8'}) @@ -67,11 +68,12 @@ const fixtures = { } } -describe('@actions/core/src/markdown-summary', () => { +describe('@actions/core/src/summary', () => { beforeEach(async () => { process.env[SUMMARY_ENV_VAR] = testFilePath + await fs.promises.mkdir(testDirectoryPath, {recursive: true}) await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'}) - markdownSummary.emptyBuffer() + summary.emptyBuffer() }) afterAll(async () => { @@ -80,39 +82,39 @@ describe('@actions/core/src/markdown-summary', () => { it('throws if summary env var is undefined', async () => { process.env[SUMMARY_ENV_VAR] = undefined - const write = markdownSummary.addRaw(fixtures.text).write() + const write = summary.addRaw(fixtures.text).write() await expect(write).rejects.toThrow() }) it('throws if summary file does not exist', async () => { await fs.promises.unlink(testFilePath) - const write = markdownSummary.addRaw(fixtures.text).write() + const write = summary.addRaw(fixtures.text).write() await expect(write).rejects.toThrow() }) it('appends text to summary file', async () => { await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'}) - await markdownSummary.addRaw(fixtures.text).write() + await summary.addRaw(fixtures.text).write() await assertSummary(`# ${fixtures.text}`) }) it('overwrites text to summary file', async () => { await fs.promises.writeFile(testFilePath, 'overwrite', {encoding: 'utf8'}) - await markdownSummary.addRaw(fixtures.text).write({overwrite: true}) + await summary.addRaw(fixtures.text).write({overwrite: true}) await assertSummary(fixtures.text) }) it('appends text with EOL to summary file', async () => { await fs.promises.writeFile(testFilePath, '# ', {encoding: 'utf8'}) - await markdownSummary.addRaw(fixtures.text, true).write() + await summary.addRaw(fixtures.text, true).write() await assertSummary(`# ${fixtures.text}${os.EOL}`) }) it('chains appends text to summary file', async () => { await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'}) - await markdownSummary + await summary .addRaw(fixtures.text) .addRaw(fixtures.text) .addRaw(fixtures.text) @@ -122,33 +124,33 @@ describe('@actions/core/src/markdown-summary', () => { it('empties buffer after write', async () => { await fs.promises.writeFile(testFilePath, '', {encoding: 'utf8'}) - await markdownSummary.addRaw(fixtures.text).write() + await summary.addRaw(fixtures.text).write() await assertSummary(fixtures.text) - expect(markdownSummary.isEmptyBuffer()).toBe(true) + expect(summary.isEmptyBuffer()).toBe(true) }) it('returns summary buffer as string', () => { - markdownSummary.addRaw(fixtures.text) - expect(markdownSummary.stringify()).toEqual(fixtures.text) + summary.addRaw(fixtures.text) + expect(summary.stringify()).toEqual(fixtures.text) }) it('return correct values for isEmptyBuffer', () => { - markdownSummary.addRaw(fixtures.text) - expect(markdownSummary.isEmptyBuffer()).toBe(false) + summary.addRaw(fixtures.text) + expect(summary.isEmptyBuffer()).toBe(false) - markdownSummary.emptyBuffer() - expect(markdownSummary.isEmptyBuffer()).toBe(true) + summary.emptyBuffer() + expect(summary.isEmptyBuffer()).toBe(true) }) it('clears a buffer and summary file', async () => { await fs.promises.writeFile(testFilePath, 'content', {encoding: 'utf8'}) - await markdownSummary.clear() + await summary.clear() await assertSummary('') - expect(markdownSummary.isEmptyBuffer()).toBe(true) + expect(summary.isEmptyBuffer()).toBe(true) }) it('adds EOL', async () => { - await markdownSummary + await summary .addRaw(fixtures.text) .addEOL() .write() @@ -156,37 +158,37 @@ describe('@actions/core/src/markdown-summary', () => { }) it('adds a code block without language', async () => { - await markdownSummary.addCodeBlock(fixtures.code).write() + await summary.addCodeBlock(fixtures.code).write() const expected = `
func fork() {\n  for {\n    go fork()\n  }\n}
${os.EOL}` await assertSummary(expected) }) it('adds a code block with a language', async () => { - await markdownSummary.addCodeBlock(fixtures.code, 'go').write() + await summary.addCodeBlock(fixtures.code, 'go').write() const expected = `
func fork() {\n  for {\n    go fork()\n  }\n}
${os.EOL}` await assertSummary(expected) }) it('adds an unordered list', async () => { - await markdownSummary.addList(fixtures.list).write() + await summary.addList(fixtures.list).write() const expected = `
  • foo
  • bar
  • baz
  • 💣
${os.EOL}` await assertSummary(expected) }) it('adds an ordered list', async () => { - await markdownSummary.addList(fixtures.list, true).write() + await summary.addList(fixtures.list, true).write() const expected = `
  1. foo
  2. bar
  3. baz
  4. 💣
${os.EOL}` await assertSummary(expected) }) it('adds a table', async () => { - await markdownSummary.addTable(fixtures.table).write() + await summary.addTable(fixtures.table).write() const expected = `
foobarbaztall
onetwothree
wide
${os.EOL}` await assertSummary(expected) }) it('adds a details element', async () => { - await markdownSummary + await summary .addDetails(fixtures.details.label, fixtures.details.content) .write() const expected = `
open me🎉 surprise
${os.EOL}` @@ -194,13 +196,13 @@ describe('@actions/core/src/markdown-summary', () => { }) it('adds an image with alt text', async () => { - await markdownSummary.addImage(fixtures.img.src, fixtures.img.alt).write() + await summary.addImage(fixtures.img.src, fixtures.img.alt).write() const expected = `actions logo${os.EOL}` await assertSummary(expected) }) it('adds an image with custom dimensions', async () => { - await markdownSummary + await summary .addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options) .write() const expected = `actions logo${os.EOL}` @@ -208,7 +210,7 @@ describe('@actions/core/src/markdown-summary', () => { }) it('adds an image with custom dimensions', async () => { - await markdownSummary + await summary .addImage(fixtures.img.src, fixtures.img.alt, fixtures.img.options) .write() const expected = `actions logo${os.EOL}` @@ -217,21 +219,21 @@ describe('@actions/core/src/markdown-summary', () => { it('adds headings h1...h6', async () => { for (const i of [1, 2, 3, 4, 5, 6]) { - markdownSummary.addHeading('heading', i) + summary.addHeading('heading', i) } - await markdownSummary.write() + await summary.write() const expected = `

heading

${os.EOL}

heading

${os.EOL}

heading

${os.EOL}

heading

${os.EOL}
heading
${os.EOL}
heading
${os.EOL}` await assertSummary(expected) }) it('adds h1 if heading level not specified', async () => { - await markdownSummary.addHeading('heading').write() + await summary.addHeading('heading').write() const expected = `

heading

${os.EOL}` await assertSummary(expected) }) it('uses h1 if heading level is garbage or out of range', async () => { - await markdownSummary + await summary .addHeading('heading', 'foobar') .addHeading('heading', 1337) .addHeading('heading', -1) @@ -242,35 +244,31 @@ describe('@actions/core/src/markdown-summary', () => { }) it('adds a separator', async () => { - await markdownSummary.addSeparator().write() + await summary.addSeparator().write() const expected = `
${os.EOL}` await assertSummary(expected) }) it('adds a break', async () => { - await markdownSummary.addBreak().write() + await summary.addBreak().write() const expected = `
${os.EOL}` await assertSummary(expected) }) it('adds a quote', async () => { - await markdownSummary.addQuote(fixtures.quote.text).write() + await summary.addQuote(fixtures.quote.text).write() const expected = `
Where the world builds software
${os.EOL}` await assertSummary(expected) }) it('adds a quote with citation', async () => { - await markdownSummary - .addQuote(fixtures.quote.text, fixtures.quote.cite) - .write() + await summary.addQuote(fixtures.quote.text, fixtures.quote.cite).write() const expected = `
Where the world builds software
${os.EOL}` await assertSummary(expected) }) it('adds a link with href', async () => { - await markdownSummary - .addLink(fixtures.link.text, fixtures.link.href) - .write() + await summary.addLink(fixtures.link.text, fixtures.link.href).write() const expected = `GitHub${os.EOL}` await assertSummary(expected) }) diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index 9a638c5a..82ddb5f8 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -1,26 +1,26 @@ { "name": "@actions/core", - "version": "1.7.0", + "version": "1.8.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@actions/core", - "version": "1.6.0", + "version": "1.8.1", "license": "MIT", "dependencies": { - "@actions/http-client": "^1.0.11" + "@actions/http-client": "^2.0.1" }, "devDependencies": { "@types/node": "^12.0.2" } }, "node_modules/@actions/http-client": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", - "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "dependencies": { - "tunnel": "0.0.6" + "tunnel": "^0.0.6" } }, "node_modules/@types/node": { @@ -40,11 +40,11 @@ }, "dependencies": { "@actions/http-client": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", - "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "requires": { - "tunnel": "0.0.6" + "tunnel": "^0.0.6" } }, "@types/node": { diff --git a/packages/core/package.json b/packages/core/package.json index 56ee8427..7a0129c0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@actions/core", - "version": "1.7.0", + "version": "1.8.2", "description": "Actions core lib", "keywords": [ "github", @@ -36,7 +36,7 @@ "url": "https://github.com/actions/toolkit/issues" }, "dependencies": { - "@actions/http-client": "^1.0.11" + "@actions/http-client": "^2.0.1" }, "devDependencies": { "@types/node": "^12.0.2" diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 8a9170ca..f5e7941b 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -361,6 +361,11 @@ export async function getIDToken(aud?: string): Promise { } /** - * Markdown summary exports + * Summary exports */ -export {markdownSummary} from './markdown-summary' +export {summary} from './summary' + +/** + * @deprecated use core.summary + */ +export {markdownSummary} from './summary' diff --git a/packages/core/src/oidc-utils.ts b/packages/core/src/oidc-utils.ts index e33da5f1..d490a3ce 100644 --- a/packages/core/src/oidc-utils.ts +++ b/packages/core/src/oidc-utils.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-extraneous-class */ import * as actions_http_client from '@actions/http-client' -import {IRequestOptions} from '@actions/http-client/interfaces' +import {RequestOptions} from '@actions/http-client/lib/interfaces' import {HttpClient} from '@actions/http-client' -import {BearerCredentialHandler} from '@actions/http-client/auth' +import {BearerCredentialHandler} from '@actions/http-client/lib/auth' import {debug, setSecret} from './core' interface TokenResponse { value?: string @@ -13,7 +13,7 @@ export class OidcClient { allowRetry = true, maxRetry = 10 ): actions_http_client.HttpClient { - const requestOptions: IRequestOptions = { + const requestOptions: RequestOptions = { allowRetries: allowRetry, maxRetries: maxRetry } diff --git a/packages/core/src/markdown-summary.ts b/packages/core/src/summary.ts similarity index 80% rename from packages/core/src/markdown-summary.ts rename to packages/core/src/summary.ts index 97d2d3ca..015f2eea 100644 --- a/packages/core/src/markdown-summary.ts +++ b/packages/core/src/summary.ts @@ -4,7 +4,7 @@ const {access, appendFile, writeFile} = promises export const SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY' export const SUMMARY_DOCS_URL = - 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary' + 'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary' export type SummaryTableRow = (SummaryTableCell | string)[] @@ -51,7 +51,7 @@ export interface SummaryWriteOptions { overwrite?: boolean } -class MarkdownSummary { +class Summary { private _buffer: string private _filePath?: string @@ -73,7 +73,7 @@ class MarkdownSummary { const pathFromEnv = process.env[SUMMARY_ENV_VAR] if (!pathFromEnv) { throw new Error( - `Unable to find environment variable for $${SUMMARY_ENV_VAR}. Check if your runtime environment supports markdown summaries.` + `Unable to find environment variable for $${SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.` ) } @@ -119,9 +119,9 @@ class MarkdownSummary { * * @param {SummaryWriteOptions} [options] (optional) options for write operation * - * @returns {Promise} markdown summary instance + * @returns {Promise} summary instance */ - async write(options?: SummaryWriteOptions): Promise { + async write(options?: SummaryWriteOptions): Promise { const overwrite = !!options?.overwrite const filePath = await this.filePath() const writeFunc = overwrite ? writeFile : appendFile @@ -132,9 +132,9 @@ class MarkdownSummary { /** * Clears the summary buffer and wipes the summary file * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - async clear(): Promise { + async clear(): Promise { return this.emptyBuffer().write({overwrite: true}) } @@ -159,9 +159,9 @@ class MarkdownSummary { /** * Resets the summary buffer without writing to summary file * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - emptyBuffer(): MarkdownSummary { + emptyBuffer(): Summary { this._buffer = '' return this } @@ -172,9 +172,9 @@ class MarkdownSummary { * @param {string} text content to add * @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false) * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addRaw(text: string, addEOL = false): MarkdownSummary { + addRaw(text: string, addEOL = false): Summary { this._buffer += text return addEOL ? this.addEOL() : this } @@ -182,9 +182,9 @@ class MarkdownSummary { /** * Adds the operating system-specific end-of-line marker to the buffer * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addEOL(): MarkdownSummary { + addEOL(): Summary { return this.addRaw(EOL) } @@ -194,9 +194,9 @@ class MarkdownSummary { * @param {string} code content to render within fenced code block * @param {string} lang (optional) language to syntax highlight code * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addCodeBlock(code: string, lang?: string): MarkdownSummary { + addCodeBlock(code: string, lang?: string): Summary { const attrs = { ...(lang && {lang}) } @@ -210,9 +210,9 @@ class MarkdownSummary { * @param {string[]} items list of items to render * @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false) * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addList(items: string[], ordered = false): MarkdownSummary { + addList(items: string[], ordered = false): Summary { const tag = ordered ? 'ol' : 'ul' const listItems = items.map(item => this.wrap('li', item)).join('') const element = this.wrap(tag, listItems) @@ -224,9 +224,9 @@ class MarkdownSummary { * * @param {SummaryTableCell[]} rows table rows * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addTable(rows: SummaryTableRow[]): MarkdownSummary { + addTable(rows: SummaryTableRow[]): Summary { const tableBody = rows .map(row => { const cells = row @@ -260,9 +260,9 @@ class MarkdownSummary { * @param {string} label text for the closed state * @param {string} content collapsable content * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addDetails(label: string, content: string): MarkdownSummary { + addDetails(label: string, content: string): Summary { const element = this.wrap('details', this.wrap('summary', label) + content) return this.addRaw(element).addEOL() } @@ -274,13 +274,9 @@ class MarkdownSummary { * @param {string} alt text description of the image * @param {SummaryImageOptions} options (optional) addition image attributes * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addImage( - src: string, - alt: string, - options?: SummaryImageOptions - ): MarkdownSummary { + addImage(src: string, alt: string, options?: SummaryImageOptions): Summary { const {width, height} = options || {} const attrs = { ...(width && {width}), @@ -297,9 +293,9 @@ class MarkdownSummary { * @param {string} text heading text * @param {number | string} [level=1] (optional) the heading level, default: 1 * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addHeading(text: string, level?: number | string): MarkdownSummary { + addHeading(text: string, level?: number | string): Summary { const tag = `h${level}` const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag) ? tag @@ -311,9 +307,9 @@ class MarkdownSummary { /** * Adds an HTML thematic break (
) to the summary buffer * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addSeparator(): MarkdownSummary { + addSeparator(): Summary { const element = this.wrap('hr', null) return this.addRaw(element).addEOL() } @@ -321,9 +317,9 @@ class MarkdownSummary { /** * Adds an HTML line break (
) to the summary buffer * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addBreak(): MarkdownSummary { + addBreak(): Summary { const element = this.wrap('br', null) return this.addRaw(element).addEOL() } @@ -334,9 +330,9 @@ class MarkdownSummary { * @param {string} text quote text * @param {string} cite (optional) citation url * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addQuote(text: string, cite?: string): MarkdownSummary { + addQuote(text: string, cite?: string): Summary { const attrs = { ...(cite && {cite}) } @@ -350,13 +346,18 @@ class MarkdownSummary { * @param {string} text link text/content * @param {string} href hyperlink * - * @returns {MarkdownSummary} markdown summary instance + * @returns {Summary} summary instance */ - addLink(text: string, href: string): MarkdownSummary { + addLink(text: string, href: string): Summary { const element = this.wrap('a', text, {href}) return this.addRaw(element).addEOL() } } -// singleton export -export const markdownSummary = new MarkdownSummary() +const _summary = new Summary() + +/** + * @deprecated use `core.summary` + */ +export const markdownSummary = _summary +export const summary = _summary diff --git a/packages/github/RELEASES.md b/packages/github/RELEASES.md index 225953b5..fb7ac2c7 100644 --- a/packages/github/RELEASES.md +++ b/packages/github/RELEASES.md @@ -1,5 +1,11 @@ # @actions/github Releases +### 5.0.3 +- - Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087) + +### 5.0.2 +- Update to v2.0.0 of `@actions/http-client` + ### 5.0.1 - [Update Octokit Dependencies](https://github.com/actions/toolkit/pull/1037) ### 5.0.0 diff --git a/packages/github/package-lock.json b/packages/github/package-lock.json index 0a497e0d..b66a87f0 100644 --- a/packages/github/package-lock.json +++ b/packages/github/package-lock.json @@ -1,15 +1,15 @@ { "name": "@actions/github", - "version": "5.0.1", + "version": "5.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@actions/github", - "version": "5.0.1", + "version": "5.0.2", "license": "MIT", "dependencies": { - "@actions/http-client": "^1.0.11", + "@actions/http-client": "^2.0.1", "@octokit/core": "^3.6.0", "@octokit/plugin-paginate-rest": "^2.17.0", "@octokit/plugin-rest-endpoint-methods": "^5.13.0" @@ -19,11 +19,11 @@ } }, "node_modules/@actions/http-client": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", - "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "dependencies": { - "tunnel": "0.0.6" + "tunnel": "^0.0.6" } }, "node_modules/@octokit/auth-token": { @@ -361,11 +361,11 @@ }, "dependencies": { "@actions/http-client": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", - "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "requires": { - "tunnel": "0.0.6" + "tunnel": "^0.0.6" } }, "@octokit/auth-token": { diff --git a/packages/github/package.json b/packages/github/package.json index 208b5a79..30da3279 100644 --- a/packages/github/package.json +++ b/packages/github/package.json @@ -1,6 +1,6 @@ { "name": "@actions/github", - "version": "5.0.1", + "version": "5.0.3", "description": "Actions github lib", "keywords": [ "github", @@ -38,7 +38,7 @@ "url": "https://github.com/actions/toolkit/issues" }, "dependencies": { - "@actions/http-client": "^1.0.11", + "@actions/http-client": "^2.0.1", "@octokit/core": "^3.6.0", "@octokit/plugin-paginate-rest": "^2.17.0", "@octokit/plugin-rest-endpoint-methods": "^5.13.0" diff --git a/packages/glob/package-lock.json b/packages/glob/package-lock.json index ee9a1739..0aee7b60 100644 --- a/packages/glob/package-lock.json +++ b/packages/glob/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "@actions/glob", - "version": "0.2.1", + "version": "0.3.0", "license": "MIT", "dependencies": { "@actions/core": "^1.2.6", @@ -14,9 +14,20 @@ } }, "node_modules/@actions/core": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", - "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.0.tgz", + "integrity": "sha512-XirM+Zo/PFlA+1h+i4bkfvagujta+LIM2AOSzPbt8JqXbbuxb1HTB+FqIyaKmue9yiCx/JIJY6pXsOl3+T8JGw==", + "dependencies": { + "@actions/http-client": "^1.0.11" + } + }, + "node_modules/@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "dependencies": { + "tunnel": "0.0.6" + } }, "node_modules/balanced-match": { "version": "1.0.0", @@ -47,13 +58,32 @@ "engines": { "node": "*" } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } } }, "dependencies": { "@actions/core": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", - "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.0.tgz", + "integrity": "sha512-XirM+Zo/PFlA+1h+i4bkfvagujta+LIM2AOSzPbt8JqXbbuxb1HTB+FqIyaKmue9yiCx/JIJY6pXsOl3+T8JGw==", + "requires": { + "@actions/http-client": "^1.0.11" + } + }, + "@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "requires": { + "tunnel": "0.0.6" + } }, "balanced-match": { "version": "1.0.0", @@ -81,6 +111,11 @@ "requires": { "brace-expansion": "^1.1.7" } + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" } } } diff --git a/packages/http-client/.gitignore b/packages/http-client/.gitignore new file mode 100644 index 00000000..d481b576 --- /dev/null +++ b/packages/http-client/.gitignore @@ -0,0 +1,2 @@ +testoutput.txt +npm-debug.log diff --git a/packages/http-client/LICENSE b/packages/http-client/LICENSE new file mode 100644 index 00000000..5823a51c --- /dev/null +++ b/packages/http-client/LICENSE @@ -0,0 +1,21 @@ +Actions Http Client for Node.js + +Copyright (c) GitHub, Inc. + +All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/http-client/README.md b/packages/http-client/README.md new file mode 100644 index 00000000..7e06adeb --- /dev/null +++ b/packages/http-client/README.md @@ -0,0 +1,73 @@ +# `@actions/http-client` + +A lightweight HTTP client optimized for building actions. + +## Features + + - HTTP client with TypeScript generics and async/await/Promises + - Typings included! + - [Proxy support](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/about-self-hosted-runners#using-a-proxy-server-with-self-hosted-runners) just works with actions and the runner + - Targets ES2019 (runner runs actions with node 12+). Only supported on node 12+. + - Basic, Bearer and PAT Support out of the box. Extensible handlers for others. + - Redirects supported + +Features and releases [here](./RELEASES.md) + +## Install + +``` +npm install @actions/http-client --save +``` + +## Samples + +See the [tests](./__tests__) for detailed examples. + +## Errors + +### HTTP + +The HTTP client does not throw unless truly exceptional. + +* A request that successfully executes resulting in a 404, 500 etc... will return a response object with a status code and a body. +* Redirects (3xx) will be followed by default. + +See the [tests](./__tests__) for detailed examples. + +## Debugging + +To enable detailed console logging of all HTTP requests and responses, set the NODE_DEBUG environment varible: + +```shell +export NODE_DEBUG=http +``` + +## Node support + +The http-client is built using the latest LTS version of Node 12. It may work on previous node LTS versions but it's tested and officially supported on Node12+. + +## Support and Versioning + +We follow semver and will hold compatibility between major versions and increment the minor version with new features and capabilities (while holding compat). + +## Contributing + +We welcome PRs. Please create an issue and if applicable, a design before proceeding with code. + +once: + +``` +npm install +``` + +To build: + +``` +npm run build +``` + +To run all tests: + +``` +npm test +``` diff --git a/packages/http-client/RELEASES.md b/packages/http-client/RELEASES.md new file mode 100644 index 00000000..344555f9 --- /dev/null +++ b/packages/http-client/RELEASES.md @@ -0,0 +1,39 @@ +## Releases + +## 2.0.1 +- Fix an issue with missing `tunnel` dependency [#1085](https://github.com/actions/toolkit/pull/1085) + +## 2.0.0 +- The package is now compiled with TypeScript's [`strict` compiler setting](https://www.typescriptlang.org/tsconfig#strict). To comply with stricter rules: + - Some exported types now include `| null` or `| undefined`, matching their actual behavior. + - Types implementing the method `RequestHandler.handleAuthentication()` now throw an `Error` rather than returning `null` if they do not support handling an HTTP 401 response. Callers can still use `canHandleAuthentication()` to determine if this handling is supported or not. + - Types using `any` have been scoped to more specific types. +- Following TypeScript's naming conventions, exported interfaces no longer begin with the prefix `I-`. +- Delete the `IHttpClientResponse` interface in favor of the `HttpClientResponse` class. +- Delete the `IHeaders` interface in favor of `http.OutgoingHttpHeaders`. +- The source code of the package was moved to build with [actions/toolkit](https://github.com/actions/toolkit). + +## 1.0.11 + +Contains a bug fix where proxy is defined without a user and password. see [PR here](https://github.com/actions/http-client/pull/42) + +## 1.0.9 +Throw HttpClientError instead of a generic Error from the \Json() helper methods when the server responds with a non-successful status code. + +## 1.0.8 +Fixed security issue where a redirect (e.g. 302) to another domain would pass headers. The fix was to strip the authorization header if the hostname was different. More [details in PR #27](https://github.com/actions/http-client/pull/27) + +## 1.0.7 +Update NPM dependencies and add 429 to the list of HttpCodes + +## 1.0.6 +Automatically sends Content-Type and Accept application/json headers for \Json() helper methods if not set in the client or parameters. + +## 1.0.5 +Adds \Json() helper methods for json over http scenarios. + +## 1.0.4 +Started to add \Json() helper methods. Do not use this release for that. Use >= 1.0.5 since there was an issue with types. + +## 1.0.1 to 1.0.3 +Adds proxy support. diff --git a/packages/http-client/__tests__/auth.test.ts b/packages/http-client/__tests__/auth.test.ts new file mode 100644 index 00000000..878fafe9 --- /dev/null +++ b/packages/http-client/__tests__/auth.test.ts @@ -0,0 +1,73 @@ +import * as httpm from '../lib' +import * as am from '../lib/auth' + +describe('auth', () => { + beforeEach(() => {}) + + afterEach(() => {}) + + it('does basic http get request with basic auth', async () => { + const bh: am.BasicCredentialHandler = new am.BasicCredentialHandler( + 'johndoe', + 'password' + ) + const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ + bh + ]) + const res: httpm.HttpClientResponse = await http.get( + 'http://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + const auth: string = obj.headers.Authorization + const creds: string = Buffer.from( + auth.substring('Basic '.length), + 'base64' + ).toString() + expect(creds).toBe('johndoe:password') + expect(obj.url).toBe('http://httpbin.org/get') + }) + + it('does basic http get request with pat token auth', async () => { + const token = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs' + const ph: am.PersonalAccessTokenCredentialHandler = new am.PersonalAccessTokenCredentialHandler( + token + ) + + const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ + ph + ]) + const res: httpm.HttpClientResponse = await http.get( + 'http://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + const auth: string = obj.headers.Authorization + const creds: string = Buffer.from( + auth.substring('Basic '.length), + 'base64' + ).toString() + expect(creds).toBe(`PAT:${token}`) + expect(obj.url).toBe('http://httpbin.org/get') + }) + + it('does basic http get request with pat token auth', async () => { + const token = 'scbfb44vxzku5l4xgc3qfazn3lpk4awflfryc76esaiq7aypcbhs' + const ph: am.BearerCredentialHandler = new am.BearerCredentialHandler(token) + + const http: httpm.HttpClient = new httpm.HttpClient('http-client-tests', [ + ph + ]) + const res: httpm.HttpClientResponse = await http.get( + 'http://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + const auth: string = obj.headers.Authorization + expect(auth).toBe(`Bearer ${token}`) + expect(obj.url).toBe('http://httpbin.org/get') + }) +}) diff --git a/packages/http-client/__tests__/basics.test.ts b/packages/http-client/__tests__/basics.test.ts new file mode 100644 index 00000000..7732264a --- /dev/null +++ b/packages/http-client/__tests__/basics.test.ts @@ -0,0 +1,374 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as httpm from '..' +import * as path from 'path' +import * as fs from 'fs' + +const sampleFilePath: string = path.join(__dirname, 'testoutput.txt') + +interface HttpBinData { + url: string + data: any + json: any + headers: any + args?: any +} + +describe('basics', () => { + let _http: httpm.HttpClient + + beforeEach(() => { + _http = new httpm.HttpClient('http-client-tests') + }) + + afterEach(() => {}) + + it('constructs', () => { + const http: httpm.HttpClient = new httpm.HttpClient('thttp-client-tests') + expect(http).toBeDefined() + }) + + // responses from httpbin return something like: + // { + // "args": {}, + // "headers": { + // "Connection": "close", + // "Host": "httpbin.org", + // "User-Agent": "typed-test-client-tests" + // }, + // "origin": "173.95.152.44", + // "url": "https://httpbin.org/get" + // } + + it('does basic http get request', async () => { + const res: httpm.HttpClientResponse = await _http.get( + 'http://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.url).toBe('http://httpbin.org/get') + expect(obj.headers['User-Agent']).toBeTruthy() + }) + + it('does basic http get request with no user agent', async () => { + const http: httpm.HttpClient = new httpm.HttpClient() + const res: httpm.HttpClientResponse = await http.get( + 'http://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.url).toBe('http://httpbin.org/get') + expect(obj.headers['User-Agent']).toBeFalsy() + }) + + it('does basic https get request', async () => { + const res: httpm.HttpClientResponse = await _http.get( + 'https://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.url).toBe('https://httpbin.org/get') + }) + + it('does basic http get request with default headers', async () => { + const http: httpm.HttpClient = new httpm.HttpClient( + 'http-client-tests', + [], + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + } + ) + const res: httpm.HttpClientResponse = await http.get( + 'http://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.headers.Accept).toBe('application/json') + expect(obj.headers['Content-Type']).toBe('application/json') + expect(obj.url).toBe('http://httpbin.org/get') + }) + + it('does basic http get request with merged headers', async () => { + const http: httpm.HttpClient = new httpm.HttpClient( + 'http-client-tests', + [], + { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json' + } + } + ) + const res: httpm.HttpClientResponse = await http.get( + 'http://httpbin.org/get', + { + 'content-type': 'application/x-www-form-urlencoded' + } + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.headers.Accept).toBe('application/json') + expect(obj.headers['Content-Type']).toBe( + 'application/x-www-form-urlencoded' + ) + expect(obj.url).toBe('http://httpbin.org/get') + }) + + it('pipes a get request', async () => { + return new Promise(async resolve => { + const file = fs.createWriteStream(sampleFilePath) + ;(await _http.get('https://httpbin.org/get')).message + .pipe(file) + .on('close', () => { + const body: string = fs.readFileSync(sampleFilePath).toString() + const obj = JSON.parse(body) + expect(obj.url).toBe('https://httpbin.org/get') + resolve() + }) + }) + }) + + it('does basic get request with redirects', async () => { + const res: httpm.HttpClientResponse = await _http.get( + `https://httpbin.org/redirect-to?url=${encodeURIComponent( + 'https://httpbin.org/get' + )}` + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.url).toBe('https://httpbin.org/get') + }) + + it('does basic get request with redirects (303)', async () => { + const res: httpm.HttpClientResponse = await _http.get( + `https://httpbin.org/redirect-to?url=${encodeURIComponent( + 'https://httpbin.org/get' + )}&status_code=303` + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.url).toBe('https://httpbin.org/get') + }) + + it('returns 404 for not found get request on redirect', async () => { + const res: httpm.HttpClientResponse = await _http.get( + `https://httpbin.org/redirect-to?url=${encodeURIComponent( + 'https://httpbin.org/status/404' + )}&status_code=303` + ) + expect(res.message.statusCode).toBe(404) + await res.readBody() + }) + + it('does not follow redirects if disabled', async () => { + const http: httpm.HttpClient = new httpm.HttpClient( + 'typed-test-client-tests', + undefined, + {allowRedirects: false} + ) + const res: httpm.HttpClientResponse = await http.get( + `https://httpbin.org/redirect-to?url=${encodeURIComponent( + 'https://httpbin.org/get' + )}` + ) + expect(res.message.statusCode).toBe(302) + await res.readBody() + }) + + it('does not pass auth with diff hostname redirects', async () => { + const headers = { + accept: 'application/json', + authorization: 'shhh' + } + const res: httpm.HttpClientResponse = await _http.get( + `https://httpbin.org/redirect-to?url=${encodeURIComponent( + 'https://www.httpbin.org/get' + )}`, + headers + ) + + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + // httpbin "fixes" the casing + expect(obj.headers['Accept']).toBe('application/json') + expect(obj.headers['Authorization']).toBeUndefined() + expect(obj.headers['authorization']).toBeUndefined() + expect(obj.url).toBe('https://www.httpbin.org/get') + }) + + it('does not pass Auth with diff hostname redirects', async () => { + const headers = { + Accept: 'application/json', + Authorization: 'shhh' + } + const res: httpm.HttpClientResponse = await _http.get( + `https://httpbin.org/redirect-to?url=${encodeURIComponent( + 'https://www.httpbin.org/get' + )}`, + headers + ) + + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + // httpbin "fixes" the casing + expect(obj.headers['Accept']).toBe('application/json') + expect(obj.headers['Authorization']).toBeUndefined() + expect(obj.headers['authorization']).toBeUndefined() + expect(obj.url).toBe('https://www.httpbin.org/get') + }) + + it('does basic head request', async () => { + const res: httpm.HttpClientResponse = await _http.head( + 'http://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + }) + + it('does basic http delete request', async () => { + const res: httpm.HttpClientResponse = await _http.del( + 'http://httpbin.org/delete' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + JSON.parse(body) + }) + + it('does basic http post request', async () => { + const b = 'Hello World!' + const res: httpm.HttpClientResponse = await _http.post( + 'http://httpbin.org/post', + b + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.data).toBe(b) + expect(obj.url).toBe('http://httpbin.org/post') + }) + + it('does basic http patch request', async () => { + const b = 'Hello World!' + const res: httpm.HttpClientResponse = await _http.patch( + 'http://httpbin.org/patch', + b + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.data).toBe(b) + expect(obj.url).toBe('http://httpbin.org/patch') + }) + + it('does basic http options request', async () => { + const res: httpm.HttpClientResponse = await _http.options( + 'http://httpbin.org' + ) + expect(res.message.statusCode).toBe(200) + await res.readBody() + }) + + it('returns 404 for not found get request', async () => { + const res: httpm.HttpClientResponse = await _http.get( + 'http://httpbin.org/status/404' + ) + expect(res.message.statusCode).toBe(404) + await res.readBody() + }) + + it('gets a json object', async () => { + const jsonObj = await _http.getJson('https://httpbin.org/get') + expect(jsonObj.statusCode).toBe(200) + expect(jsonObj.result).toBeDefined() + expect(jsonObj.result?.url).toBe('https://httpbin.org/get') + expect(jsonObj.result?.headers['Accept']).toBe( + httpm.MediaTypes.ApplicationJson + ) + expect(jsonObj.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + }) + + it('getting a non existent json object returns null', async () => { + const jsonObj = await _http.getJson( + 'https://httpbin.org/status/404' + ) + expect(jsonObj.statusCode).toBe(404) + expect(jsonObj.result).toBeNull() + }) + + it('posts a json object', async () => { + const res = {name: 'foo'} + const restRes = await _http.postJson( + 'https://httpbin.org/post', + res + ) + expect(restRes.statusCode).toBe(200) + expect(restRes.result).toBeDefined() + expect(restRes.result?.url).toBe('https://httpbin.org/post') + expect(restRes.result?.json.name).toBe('foo') + expect(restRes.result?.headers['Accept']).toBe( + httpm.MediaTypes.ApplicationJson + ) + expect(restRes.result?.headers['Content-Type']).toBe( + httpm.MediaTypes.ApplicationJson + ) + expect(restRes.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + }) + + it('puts a json object', async () => { + const res = {name: 'foo'} + const restRes = await _http.putJson( + 'https://httpbin.org/put', + res + ) + expect(restRes.statusCode).toBe(200) + expect(restRes.result).toBeDefined() + expect(restRes.result?.url).toBe('https://httpbin.org/put') + expect(restRes.result?.json.name).toBe('foo') + + expect(restRes.result?.headers['Accept']).toBe( + httpm.MediaTypes.ApplicationJson + ) + expect(restRes.result?.headers['Content-Type']).toBe( + httpm.MediaTypes.ApplicationJson + ) + expect(restRes.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + }) + + it('patch a json object', async () => { + const res = {name: 'foo'} + const restRes = await _http.patchJson( + 'https://httpbin.org/patch', + res + ) + expect(restRes.statusCode).toBe(200) + expect(restRes.result).toBeDefined() + expect(restRes.result?.url).toBe('https://httpbin.org/patch') + expect(restRes.result?.json.name).toBe('foo') + expect(restRes.result?.headers['Accept']).toBe( + httpm.MediaTypes.ApplicationJson + ) + expect(restRes.result?.headers['Content-Type']).toBe( + httpm.MediaTypes.ApplicationJson + ) + expect(restRes.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + }) +}) diff --git a/packages/http-client/__tests__/headers.test.ts b/packages/http-client/__tests__/headers.test.ts new file mode 100644 index 00000000..0af9563c --- /dev/null +++ b/packages/http-client/__tests__/headers.test.ts @@ -0,0 +1,116 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as httpm from '..' + +describe('headers', () => { + let _http: httpm.HttpClient + + beforeEach(() => { + _http = new httpm.HttpClient('http-client-tests') + }) + + it('preserves existing headers on getJson', async () => { + const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} + let jsonObj = await _http.getJson( + 'https://httpbin.org/get', + additionalHeaders + ) + expect(jsonObj.result.headers['Accept']).toBe('foo') + expect(jsonObj.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + + const httpWithHeaders = new httpm.HttpClient() + httpWithHeaders.requestOptions = { + headers: { + [httpm.Headers.Accept]: 'baz' + } + } + jsonObj = await httpWithHeaders.getJson('https://httpbin.org/get') + expect(jsonObj.result.headers['Accept']).toBe('baz') + expect(jsonObj.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + }) + + it('preserves existing headers on postJson', async () => { + const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} + let jsonObj = await _http.postJson( + 'https://httpbin.org/post', + {}, + additionalHeaders + ) + expect(jsonObj.result.headers['Accept']).toBe('foo') + expect(jsonObj.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + + const httpWithHeaders = new httpm.HttpClient() + httpWithHeaders.requestOptions = { + headers: { + [httpm.Headers.Accept]: 'baz' + } + } + jsonObj = await httpWithHeaders.postJson( + 'https://httpbin.org/post', + {} + ) + expect(jsonObj.result.headers['Accept']).toBe('baz') + expect(jsonObj.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + }) + + it('preserves existing headers on putJson', async () => { + const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} + let jsonObj = await _http.putJson( + 'https://httpbin.org/put', + {}, + additionalHeaders + ) + expect(jsonObj.result.headers['Accept']).toBe('foo') + expect(jsonObj.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + + const httpWithHeaders = new httpm.HttpClient() + httpWithHeaders.requestOptions = { + headers: { + [httpm.Headers.Accept]: 'baz' + } + } + jsonObj = await httpWithHeaders.putJson('https://httpbin.org/put', {}) + expect(jsonObj.result.headers['Accept']).toBe('baz') + expect(jsonObj.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + }) + + it('preserves existing headers on patchJson', async () => { + const additionalHeaders = {[httpm.Headers.Accept]: 'foo'} + let jsonObj = await _http.patchJson( + 'https://httpbin.org/patch', + {}, + additionalHeaders + ) + expect(jsonObj.result.headers['Accept']).toBe('foo') + expect(jsonObj.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + + const httpWithHeaders = new httpm.HttpClient() + httpWithHeaders.requestOptions = { + headers: { + [httpm.Headers.Accept]: 'baz' + } + } + jsonObj = await httpWithHeaders.patchJson( + 'https://httpbin.org/patch', + {} + ) + expect(jsonObj.result.headers['Accept']).toBe('baz') + expect(jsonObj.headers[httpm.Headers.ContentType]).toBe( + httpm.MediaTypes.ApplicationJson + ) + }) +}) diff --git a/packages/http-client/__tests__/keepalive.test.ts b/packages/http-client/__tests__/keepalive.test.ts new file mode 100644 index 00000000..ed55be20 --- /dev/null +++ b/packages/http-client/__tests__/keepalive.test.ts @@ -0,0 +1,73 @@ +import * as httpm from '../lib' + +describe('basics', () => { + let _http: httpm.HttpClient + + beforeEach(() => { + _http = new httpm.HttpClient('http-client-tests', [], {keepAlive: true}) + }) + + afterEach(() => { + _http.dispose() + }) + + it('does basic http get request with keepAlive true', async () => { + const res: httpm.HttpClientResponse = await _http.get( + 'http://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.url).toBe('http://httpbin.org/get') + }) + + it('does basic head request with keepAlive true', async () => { + const res: httpm.HttpClientResponse = await _http.head( + 'http://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + }) + + it('does basic http delete request with keepAlive true', async () => { + const res: httpm.HttpClientResponse = await _http.del( + 'http://httpbin.org/delete' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + JSON.parse(body) + }) + + it('does basic http post request with keepAlive true', async () => { + const b = 'Hello World!' + const res: httpm.HttpClientResponse = await _http.post( + 'http://httpbin.org/post', + b + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.data).toBe(b) + expect(obj.url).toBe('http://httpbin.org/post') + }) + + it('does basic http patch request with keepAlive true', async () => { + const b = 'Hello World!' + const res: httpm.HttpClientResponse = await _http.patch( + 'http://httpbin.org/patch', + b + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.data).toBe(b) + expect(obj.url).toBe('http://httpbin.org/patch') + }) + + it('does basic http options request with keepAlive true', async () => { + const res: httpm.HttpClientResponse = await _http.options( + 'http://httpbin.org' + ) + expect(res.message.statusCode).toBe(200) + await res.readBody() + }) +}) diff --git a/packages/http-client/__tests__/proxy.test.ts b/packages/http-client/__tests__/proxy.test.ts new file mode 100644 index 00000000..62e8e962 --- /dev/null +++ b/packages/http-client/__tests__/proxy.test.ts @@ -0,0 +1,232 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as http from 'http' +import * as httpm from '../lib/' +import * as pm from '../lib/proxy' +// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports +const proxy = require('proxy') + +let _proxyConnects: string[] +let _proxyServer: http.Server +const _proxyUrl = 'http://127.0.0.1:8080' + +describe('proxy', () => { + beforeAll(async () => { + // Start proxy server + _proxyServer = proxy() + await new Promise(resolve => { + const port = Number(_proxyUrl.split(':')[2]) + _proxyServer.listen(port, () => resolve()) + }) + _proxyServer.on('connect', req => { + _proxyConnects.push(req.url) + }) + }) + + beforeEach(() => { + _proxyConnects = [] + _clearVars() + }) + + afterEach(() => {}) + + afterAll(async () => { + _clearVars() + + // Stop proxy server + await new Promise(resolve => { + _proxyServer.once('close', () => resolve()) + _proxyServer.close() + }) + }) + + it('getProxyUrl does not return proxyUrl if variables not set', () => { + const proxyUrl = pm.getProxyUrl(new URL('https://github.com')) + expect(proxyUrl).toBeUndefined() + }) + + it('getProxyUrl returns proxyUrl if https_proxy set for https url', () => { + process.env['https_proxy'] = 'https://myproxysvr' + const proxyUrl = pm.getProxyUrl(new URL('https://github.com')) + expect(proxyUrl).toBeDefined() + }) + + it('getProxyUrl does not return proxyUrl if http_proxy set for https url', () => { + process.env['http_proxy'] = 'https://myproxysvr' + const proxyUrl = pm.getProxyUrl(new URL('https://github.com')) + expect(proxyUrl).toBeUndefined() + }) + + it('getProxyUrl returns proxyUrl if http_proxy set for http url', () => { + process.env['http_proxy'] = 'http://myproxysvr' + const proxyUrl = pm.getProxyUrl(new URL('http://github.com')) + expect(proxyUrl).toBeDefined() + }) + + it('getProxyUrl does not return proxyUrl if https_proxy set and in no_proxy list', () => { + process.env['https_proxy'] = 'https://myproxysvr' + process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' + const proxyUrl = pm.getProxyUrl(new URL('https://myserver')) + expect(proxyUrl).toBeUndefined() + }) + + it('getProxyUrl returns proxyUrl if https_proxy set and not in no_proxy list', () => { + process.env['https_proxy'] = 'https://myproxysvr' + process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' + const proxyUrl = pm.getProxyUrl(new URL('https://github.com')) + expect(proxyUrl).toBeDefined() + }) + + it('getProxyUrl does not return proxyUrl if http_proxy set and in no_proxy list', () => { + process.env['http_proxy'] = 'http://myproxysvr' + process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' + const proxyUrl = pm.getProxyUrl(new URL('http://myserver')) + expect(proxyUrl).toBeUndefined() + }) + + it('getProxyUrl returns proxyUrl if http_proxy set and not in no_proxy list', () => { + process.env['http_proxy'] = 'http://myproxysvr' + process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' + const proxyUrl = pm.getProxyUrl(new URL('http://github.com')) + expect(proxyUrl).toBeDefined() + }) + + it('checkBypass returns true if host as no_proxy list', () => { + process.env['no_proxy'] = 'myserver' + const bypass = pm.checkBypass(new URL('https://myserver')) + expect(bypass).toBeTruthy() + }) + + it('checkBypass returns true if host in no_proxy list', () => { + process.env['no_proxy'] = 'otherserver,myserver,anotherserver:8080' + const bypass = pm.checkBypass(new URL('https://myserver')) + expect(bypass).toBeTruthy() + }) + + it('checkBypass returns true if host in no_proxy list with spaces', () => { + process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080' + const bypass = pm.checkBypass(new URL('https://myserver')) + expect(bypass).toBeTruthy() + }) + + it('checkBypass returns true if host in no_proxy list with port', () => { + process.env['no_proxy'] = 'otherserver, myserver:8080 ,anotherserver' + const bypass = pm.checkBypass(new URL('https://myserver:8080')) + expect(bypass).toBeTruthy() + }) + + it('checkBypass returns true if host with port in no_proxy list without port', () => { + process.env['no_proxy'] = 'otherserver, myserver ,anotherserver' + const bypass = pm.checkBypass(new URL('https://myserver:8080')) + expect(bypass).toBeTruthy() + }) + + it('checkBypass returns true if host in no_proxy list with default https port', () => { + process.env['no_proxy'] = 'otherserver, myserver:443 ,anotherserver' + const bypass = pm.checkBypass(new URL('https://myserver')) + expect(bypass).toBeTruthy() + }) + + it('checkBypass returns true if host in no_proxy list with default http port', () => { + process.env['no_proxy'] = 'otherserver, myserver:80 ,anotherserver' + const bypass = pm.checkBypass(new URL('http://myserver')) + expect(bypass).toBeTruthy() + }) + + it('checkBypass returns false if host not in no_proxy list', () => { + process.env['no_proxy'] = 'otherserver, myserver ,anotherserver:8080' + const bypass = pm.checkBypass(new URL('https://github.com')) + expect(bypass).toBeFalsy() + }) + + it('checkBypass returns false if empty no_proxy', () => { + process.env['no_proxy'] = '' + const bypass = pm.checkBypass(new URL('https://github.com')) + expect(bypass).toBeFalsy() + }) + + it('HttpClient does basic http get request through proxy', async () => { + process.env['http_proxy'] = _proxyUrl + const httpClient = new httpm.HttpClient() + const res: httpm.HttpClientResponse = await httpClient.get( + 'http://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.url).toBe('http://httpbin.org/get') + expect(_proxyConnects).toEqual(['httpbin.org:80']) + }) + + it('HttoClient does basic http get request when bypass proxy', async () => { + process.env['http_proxy'] = _proxyUrl + process.env['no_proxy'] = 'httpbin.org' + const httpClient = new httpm.HttpClient() + const res: httpm.HttpClientResponse = await httpClient.get( + 'http://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.url).toBe('http://httpbin.org/get') + expect(_proxyConnects).toHaveLength(0) + }) + + it('HttpClient does basic https get request through proxy', async () => { + process.env['https_proxy'] = _proxyUrl + const httpClient = new httpm.HttpClient() + const res: httpm.HttpClientResponse = await httpClient.get( + 'https://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.url).toBe('https://httpbin.org/get') + expect(_proxyConnects).toEqual(['httpbin.org:443']) + }) + + it('HttpClient does basic https get request when bypass proxy', async () => { + process.env['https_proxy'] = _proxyUrl + process.env['no_proxy'] = 'httpbin.org' + const httpClient = new httpm.HttpClient() + const res: httpm.HttpClientResponse = await httpClient.get( + 'https://httpbin.org/get' + ) + expect(res.message.statusCode).toBe(200) + const body: string = await res.readBody() + const obj = JSON.parse(body) + expect(obj.url).toBe('https://httpbin.org/get') + expect(_proxyConnects).toHaveLength(0) + }) + + it('proxyAuth not set in tunnel agent when authentication is not provided', async () => { + process.env['https_proxy'] = 'http://127.0.0.1:8080' + const httpClient = new httpm.HttpClient() + const agent: any = httpClient.getAgent('https://some-url') + // eslint-disable-next-line no-console + console.log(agent) + expect(agent.proxyOptions.host).toBe('127.0.0.1') + expect(agent.proxyOptions.port).toBe('8080') + expect(agent.proxyOptions.proxyAuth).toBe(undefined) + }) + + it('proxyAuth is set in tunnel agent when authentication is provided', async () => { + process.env['https_proxy'] = 'http://user:password@127.0.0.1:8080' + const httpClient = new httpm.HttpClient() + const agent: any = httpClient.getAgent('https://some-url') + // eslint-disable-next-line no-console + console.log(agent) + expect(agent.proxyOptions.host).toBe('127.0.0.1') + expect(agent.proxyOptions.port).toBe('8080') + expect(agent.proxyOptions.proxyAuth).toBe('user:password') + }) +}) + +function _clearVars(): void { + delete process.env.http_proxy + delete process.env.HTTP_PROXY + delete process.env.https_proxy + delete process.env.HTTPS_PROXY + delete process.env.no_proxy + delete process.env.NO_PROXY +} diff --git a/packages/http-client/package-lock.json b/packages/http-client/package-lock.json new file mode 100644 index 00000000..3c2c4e54 --- /dev/null +++ b/packages/http-client/package-lock.json @@ -0,0 +1,332 @@ +{ + "name": "@actions/http-client", + "version": "2.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@actions/http-client", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "tunnel": "^0.0.6" + }, + "devDependencies": { + "@types/tunnel": "0.0.3", + "proxy": "^1.0.1" + } + }, + "node_modules/@types/node": { + "version": "12.12.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.31.tgz", + "integrity": "sha512-T+wnJno8uh27G9c+1T+a1/WYCHzLeDqtsGJkoEdSp2X8RTh3oOCZQcUnjAx90CS8cmmADX51O0FI/tu9s0yssg==", + "dev": true + }, + "node_modules/@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "dev": true, + "dependencies": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/basic-auth-parser": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/basic-auth-parser/-/basic-auth-parser-0.0.2.tgz", + "integrity": "sha1-zp5xp38jwSee7NJlmypGJEwVbkE=", + "dev": true + }, + "node_modules/camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "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==", + "dev": true + }, + "node_modules/proxy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/proxy/-/proxy-1.0.2.tgz", + "integrity": "sha512-KNac2ueWRpjbUh77OAFPZuNdfEqNynm9DD4xHT14CccGpW8wKZwEkN0yjlb7X9G9Z9F55N0Q+1z+WfgAhwYdzQ==", + "dev": true, + "dependencies": { + "args": "5.0.1", + "basic-auth-parser": "0.0.2", + "debug": "^4.1.1" + }, + "bin": { + "proxy": "bin/proxy.js" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + } + }, + "dependencies": { + "@types/node": { + "version": "12.12.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.31.tgz", + "integrity": "sha512-T+wnJno8uh27G9c+1T+a1/WYCHzLeDqtsGJkoEdSp2X8RTh3oOCZQcUnjAx90CS8cmmADX51O0FI/tu9s0yssg==", + "dev": true + }, + "@types/tunnel": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/tunnel/-/tunnel-0.0.3.tgz", + "integrity": "sha512-sOUTGn6h1SfQ+gbgqC364jLFBw2lnFqkgF3q0WovEHRLMrVD1sd5aufqi/aJObLekJO+Aq5z646U4Oxy6shXMA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "args": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/args/-/args-5.0.1.tgz", + "integrity": "sha512-1kqmFCFsPffavQFGt8OxJdIcETti99kySRUPMpOhaGjL6mRJn8HFU1OxKY5bMqfZKUwTQc1mZkAjmGYaVOHFtQ==", + "dev": true, + "requires": { + "camelcase": "5.0.0", + "chalk": "2.4.2", + "leven": "2.1.0", + "mri": "1.1.4" + } + }, + "basic-auth-parser": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/basic-auth-parser/-/basic-auth-parser-0.0.2.tgz", + "integrity": "sha1-zp5xp38jwSee7NJlmypGJEwVbkE=", + "dev": true + }, + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "dev": true + }, + "mri": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", + "integrity": "sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "proxy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/proxy/-/proxy-1.0.2.tgz", + "integrity": "sha512-KNac2ueWRpjbUh77OAFPZuNdfEqNynm9DD4xHT14CccGpW8wKZwEkN0yjlb7X9G9Z9F55N0Q+1z+WfgAhwYdzQ==", + "dev": true, + "requires": { + "args": "5.0.1", + "basic-auth-parser": "0.0.2", + "debug": "^4.1.1" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" + } + } +} diff --git a/packages/http-client/package.json b/packages/http-client/package.json new file mode 100644 index 00000000..c1de2213 --- /dev/null +++ b/packages/http-client/package.json @@ -0,0 +1,48 @@ +{ + "name": "@actions/http-client", + "version": "2.0.1", + "description": "Actions Http Client", + "keywords": [ + "github", + "actions", + "http" + ], + "homepage": "https://github.com/actions/toolkit/tree/main/packages/http-client", + "license": "MIT", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "directories": { + "lib": "lib", + "test": "__tests__" + }, + "files": [ + "lib", + "!.DS_Store" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/actions/toolkit.git", + "directory": "packages/http-client" + }, + "scripts": { + "audit-moderate": "npm install && npm audit --json --audit-level=moderate > audit.json", + "test": "echo \"Error: run tests from root\" && exit 1", + "build": "tsc", + "format": "prettier --write **/*.ts", + "format-check": "prettier --check **/*.ts", + "tsc": "tsc" + }, + "bugs": { + "url": "https://github.com/actions/toolkit/issues" + }, + "devDependencies": { + "@types/tunnel": "0.0.3", + "proxy": "^1.0.1" + }, + "dependencies": { + "tunnel": "^0.0.6" + } +} diff --git a/packages/http-client/src/auth.ts b/packages/http-client/src/auth.ts new file mode 100644 index 00000000..639adbe2 --- /dev/null +++ b/packages/http-client/src/auth.ts @@ -0,0 +1,86 @@ +import * as http from 'http' +import * as ifm from './interfaces' +import {HttpClientResponse} from './index' + +export class BasicCredentialHandler implements ifm.RequestHandler { + username: string + password: string + + constructor(username: string, password: string) { + this.username = username + this.password = password + } + + prepareRequest(options: http.RequestOptions): void { + if (!options.headers) { + throw Error('The request has no headers') + } + options.headers['Authorization'] = `Basic ${Buffer.from( + `${this.username}:${this.password}` + ).toString('base64')}` + } + + // This handler cannot handle 401 + canHandleAuthentication(): boolean { + return false + } + + async handleAuthentication(): Promise { + throw new Error('not implemented') + } +} + +export class BearerCredentialHandler implements ifm.RequestHandler { + token: string + + constructor(token: string) { + this.token = token + } + + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options: http.RequestOptions): void { + if (!options.headers) { + throw Error('The request has no headers') + } + options.headers['Authorization'] = `Bearer ${this.token}` + } + + // This handler cannot handle 401 + canHandleAuthentication(): boolean { + return false + } + + async handleAuthentication(): Promise { + throw new Error('not implemented') + } +} + +export class PersonalAccessTokenCredentialHandler + implements ifm.RequestHandler { + token: string + + constructor(token: string) { + this.token = token + } + + // currently implements pre-authorization + // TODO: support preAuth = false where it hooks on 401 + prepareRequest(options: http.RequestOptions): void { + if (!options.headers) { + throw Error('The request has no headers') + } + options.headers['Authorization'] = `Basic ${Buffer.from( + `PAT:${this.token}` + ).toString('base64')}` + } + + // This handler cannot handle 401 + canHandleAuthentication(): boolean { + return false + } + + async handleAuthentication(): Promise { + throw new Error('not implemented') + } +} diff --git a/packages/http-client/src/index.ts b/packages/http-client/src/index.ts new file mode 100644 index 00000000..f02c2754 --- /dev/null +++ b/packages/http-client/src/index.ts @@ -0,0 +1,773 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as http from 'http' +import * as https from 'https' +import * as ifm from './interfaces' +import * as net from 'net' +import * as pm from './proxy' +import * as tunnel from 'tunnel' + +export enum HttpCodes { + OK = 200, + MultipleChoices = 300, + MovedPermanently = 301, + ResourceMoved = 302, + SeeOther = 303, + NotModified = 304, + UseProxy = 305, + SwitchProxy = 306, + TemporaryRedirect = 307, + PermanentRedirect = 308, + BadRequest = 400, + Unauthorized = 401, + PaymentRequired = 402, + Forbidden = 403, + NotFound = 404, + MethodNotAllowed = 405, + NotAcceptable = 406, + ProxyAuthenticationRequired = 407, + RequestTimeout = 408, + Conflict = 409, + Gone = 410, + TooManyRequests = 429, + InternalServerError = 500, + NotImplemented = 501, + BadGateway = 502, + ServiceUnavailable = 503, + GatewayTimeout = 504 +} + +export enum Headers { + Accept = 'accept', + ContentType = 'content-type' +} + +export enum MediaTypes { + ApplicationJson = 'application/json' +} + +/** + * Returns the proxy URL, depending upon the supplied url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ +export function getProxyUrl(serverUrl: string): string { + const proxyUrl = pm.getProxyUrl(new URL(serverUrl)) + return proxyUrl ? proxyUrl.href : '' +} + +const HttpRedirectCodes: number[] = [ + HttpCodes.MovedPermanently, + HttpCodes.ResourceMoved, + HttpCodes.SeeOther, + HttpCodes.TemporaryRedirect, + HttpCodes.PermanentRedirect +] +const HttpResponseRetryCodes: number[] = [ + HttpCodes.BadGateway, + HttpCodes.ServiceUnavailable, + HttpCodes.GatewayTimeout +] +const RetryableHttpVerbs: string[] = ['OPTIONS', 'GET', 'DELETE', 'HEAD'] +const ExponentialBackoffCeiling = 10 +const ExponentialBackoffTimeSlice = 5 + +export class HttpClientError extends Error { + constructor(message: string, statusCode: number) { + super(message) + this.name = 'HttpClientError' + this.statusCode = statusCode + Object.setPrototypeOf(this, HttpClientError.prototype) + } + + statusCode: number + result?: any +} + +export class HttpClientResponse { + constructor(message: http.IncomingMessage) { + this.message = message + } + + message: http.IncomingMessage + async readBody(): Promise { + return new Promise(async resolve => { + let output = Buffer.alloc(0) + + this.message.on('data', (chunk: Buffer) => { + output = Buffer.concat([output, chunk]) + }) + + this.message.on('end', () => { + resolve(output.toString()) + }) + }) + } +} + +export function isHttps(requestUrl: string): boolean { + const parsedUrl: URL = new URL(requestUrl) + return parsedUrl.protocol === 'https:' +} + +export class HttpClient { + userAgent: string | undefined + handlers: ifm.RequestHandler[] + requestOptions: ifm.RequestOptions | undefined + + private _ignoreSslError = false + private _socketTimeout: number | undefined + private _allowRedirects = true + private _allowRedirectDowngrade = false + private _maxRedirects = 50 + private _allowRetries = false + private _maxRetries = 1 + private _agent: any + private _proxyAgent: any + private _keepAlive = false + private _disposed = false + + constructor( + userAgent?: string, + handlers?: ifm.RequestHandler[], + requestOptions?: ifm.RequestOptions + ) { + this.userAgent = userAgent + this.handlers = handlers || [] + this.requestOptions = requestOptions + if (requestOptions) { + if (requestOptions.ignoreSslError != null) { + this._ignoreSslError = requestOptions.ignoreSslError + } + + this._socketTimeout = requestOptions.socketTimeout + + if (requestOptions.allowRedirects != null) { + this._allowRedirects = requestOptions.allowRedirects + } + + if (requestOptions.allowRedirectDowngrade != null) { + this._allowRedirectDowngrade = requestOptions.allowRedirectDowngrade + } + + if (requestOptions.maxRedirects != null) { + this._maxRedirects = Math.max(requestOptions.maxRedirects, 0) + } + + if (requestOptions.keepAlive != null) { + this._keepAlive = requestOptions.keepAlive + } + + if (requestOptions.allowRetries != null) { + this._allowRetries = requestOptions.allowRetries + } + + if (requestOptions.maxRetries != null) { + this._maxRetries = requestOptions.maxRetries + } + } + } + + async options( + requestUrl: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise { + return this.request('OPTIONS', requestUrl, null, additionalHeaders || {}) + } + + async get( + requestUrl: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise { + return this.request('GET', requestUrl, null, additionalHeaders || {}) + } + + async del( + requestUrl: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise { + return this.request('DELETE', requestUrl, null, additionalHeaders || {}) + } + + async post( + requestUrl: string, + data: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise { + return this.request('POST', requestUrl, data, additionalHeaders || {}) + } + + async patch( + requestUrl: string, + data: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise { + return this.request('PATCH', requestUrl, data, additionalHeaders || {}) + } + + async put( + requestUrl: string, + data: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise { + return this.request('PUT', requestUrl, data, additionalHeaders || {}) + } + + async head( + requestUrl: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise { + return this.request('HEAD', requestUrl, null, additionalHeaders || {}) + } + + async sendStream( + verb: string, + requestUrl: string, + stream: NodeJS.ReadableStream, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise { + return this.request(verb, requestUrl, stream, additionalHeaders) + } + + /** + * Gets a typed object from an endpoint + * Be aware that not found returns a null. Other errors (4xx, 5xx) reject the promise + */ + async getJson( + requestUrl: string, + additionalHeaders: http.OutgoingHttpHeaders = {} + ): Promise> { + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader( + additionalHeaders, + Headers.Accept, + MediaTypes.ApplicationJson + ) + const res: HttpClientResponse = await this.get( + requestUrl, + additionalHeaders + ) + return this._processResponse(res, this.requestOptions) + } + + async postJson( + requestUrl: string, + obj: any, + additionalHeaders: http.OutgoingHttpHeaders = {} + ): Promise> { + const data: string = JSON.stringify(obj, null, 2) + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader( + additionalHeaders, + Headers.Accept, + MediaTypes.ApplicationJson + ) + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader( + additionalHeaders, + Headers.ContentType, + MediaTypes.ApplicationJson + ) + const res: HttpClientResponse = await this.post( + requestUrl, + data, + additionalHeaders + ) + return this._processResponse(res, this.requestOptions) + } + + async putJson( + requestUrl: string, + obj: any, + additionalHeaders: http.OutgoingHttpHeaders = {} + ): Promise> { + const data: string = JSON.stringify(obj, null, 2) + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader( + additionalHeaders, + Headers.Accept, + MediaTypes.ApplicationJson + ) + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader( + additionalHeaders, + Headers.ContentType, + MediaTypes.ApplicationJson + ) + const res: HttpClientResponse = await this.put( + requestUrl, + data, + additionalHeaders + ) + return this._processResponse(res, this.requestOptions) + } + + async patchJson( + requestUrl: string, + obj: any, + additionalHeaders: http.OutgoingHttpHeaders = {} + ): Promise> { + const data: string = JSON.stringify(obj, null, 2) + additionalHeaders[Headers.Accept] = this._getExistingOrDefaultHeader( + additionalHeaders, + Headers.Accept, + MediaTypes.ApplicationJson + ) + additionalHeaders[Headers.ContentType] = this._getExistingOrDefaultHeader( + additionalHeaders, + Headers.ContentType, + MediaTypes.ApplicationJson + ) + const res: HttpClientResponse = await this.patch( + requestUrl, + data, + additionalHeaders + ) + return this._processResponse(res, this.requestOptions) + } + + /** + * Makes a raw http request. + * All other methods such as get, post, patch, and request ultimately call this. + * Prefer get, del, post and patch + */ + async request( + verb: string, + requestUrl: string, + data: string | NodeJS.ReadableStream | null, + headers?: http.OutgoingHttpHeaders + ): Promise { + if (this._disposed) { + throw new Error('Client has already been disposed.') + } + + const parsedUrl = new URL(requestUrl) + let info: ifm.RequestInfo = this._prepareRequest(verb, parsedUrl, headers) + + // Only perform retries on reads since writes may not be idempotent. + const maxTries: number = + this._allowRetries && RetryableHttpVerbs.includes(verb) + ? this._maxRetries + 1 + : 1 + let numTries = 0 + + let response: HttpClientResponse | undefined + do { + response = await this.requestRaw(info, data) + + // Check if it's an authentication challenge + if ( + response && + response.message && + response.message.statusCode === HttpCodes.Unauthorized + ) { + let authenticationHandler: ifm.RequestHandler | undefined + + for (const handler of this.handlers) { + if (handler.canHandleAuthentication(response)) { + authenticationHandler = handler + break + } + } + + if (authenticationHandler) { + return authenticationHandler.handleAuthentication(this, info, data) + } else { + // We have received an unauthorized response but have no handlers to handle it. + // Let the response return to the caller. + return response + } + } + + let redirectsRemaining: number = this._maxRedirects + while ( + response.message.statusCode && + HttpRedirectCodes.includes(response.message.statusCode) && + this._allowRedirects && + redirectsRemaining > 0 + ) { + const redirectUrl: string | undefined = + response.message.headers['location'] + if (!redirectUrl) { + // if there's no location to redirect to, we won't + break + } + const parsedRedirectUrl = new URL(redirectUrl) + if ( + parsedUrl.protocol === 'https:' && + parsedUrl.protocol !== parsedRedirectUrl.protocol && + !this._allowRedirectDowngrade + ) { + throw new Error( + 'Redirect from HTTPS to HTTP protocol. This downgrade is not allowed for security reasons. If you want to allow this behavior, set the allowRedirectDowngrade option to true.' + ) + } + + // we need to finish reading the response before reassigning response + // which will leak the open socket. + await response.readBody() + + // strip authorization header if redirected to a different hostname + if (parsedRedirectUrl.hostname !== parsedUrl.hostname) { + for (const header in headers) { + // header names are case insensitive + if (header.toLowerCase() === 'authorization') { + delete headers[header] + } + } + } + + // let's make the request with the new redirectUrl + info = this._prepareRequest(verb, parsedRedirectUrl, headers) + response = await this.requestRaw(info, data) + redirectsRemaining-- + } + + if ( + !response.message.statusCode || + !HttpResponseRetryCodes.includes(response.message.statusCode) + ) { + // If not a retry code, return immediately instead of retrying + return response + } + + numTries += 1 + + if (numTries < maxTries) { + await response.readBody() + await this._performExponentialBackoff(numTries) + } + } while (numTries < maxTries) + + return response + } + + /** + * Needs to be called if keepAlive is set to true in request options. + */ + dispose(): void { + if (this._agent) { + this._agent.destroy() + } + + this._disposed = true + } + + /** + * Raw request. + * @param info + * @param data + */ + async requestRaw( + info: ifm.RequestInfo, + data: string | NodeJS.ReadableStream | null + ): Promise { + return new Promise((resolve, reject) => { + function callbackForResult(err?: Error, res?: HttpClientResponse): void { + if (err) { + reject(err) + } else if (!res) { + // If `err` is not passed, then `res` must be passed. + reject(new Error('Unknown error')) + } else { + resolve(res) + } + } + + this.requestRawWithCallback(info, data, callbackForResult) + }) + } + + /** + * Raw request with callback. + * @param info + * @param data + * @param onResult + */ + requestRawWithCallback( + info: ifm.RequestInfo, + data: string | NodeJS.ReadableStream | null, + onResult: (err?: Error, res?: HttpClientResponse) => void + ): void { + if (typeof data === 'string') { + if (!info.options.headers) { + info.options.headers = {} + } + info.options.headers['Content-Length'] = Buffer.byteLength(data, 'utf8') + } + + let callbackCalled = false + function handleResult(err?: Error, res?: HttpClientResponse): void { + if (!callbackCalled) { + callbackCalled = true + onResult(err, res) + } + } + + const req: http.ClientRequest = info.httpModule.request( + info.options, + (msg: http.IncomingMessage) => { + const res: HttpClientResponse = new HttpClientResponse(msg) + handleResult(undefined, res) + } + ) + + let socket: net.Socket + req.on('socket', sock => { + socket = sock + }) + + // If we ever get disconnected, we want the socket to timeout eventually + req.setTimeout(this._socketTimeout || 3 * 60000, () => { + if (socket) { + socket.end() + } + handleResult(new Error(`Request timeout: ${info.options.path}`)) + }) + + req.on('error', function(err) { + // err has statusCode property + // res should have headers + handleResult(err) + }) + + if (data && typeof data === 'string') { + req.write(data, 'utf8') + } + + if (data && typeof data !== 'string') { + data.on('close', function() { + req.end() + }) + + data.pipe(req) + } else { + req.end() + } + } + + /** + * Gets an http agent. This function is useful when you need an http agent that handles + * routing through a proxy server - depending upon the url and proxy environment variables. + * @param serverUrl The server URL where the request will be sent. For example, https://api.github.com + */ + getAgent(serverUrl: string): http.Agent { + const parsedUrl = new URL(serverUrl) + return this._getAgent(parsedUrl) + } + + private _prepareRequest( + method: string, + requestUrl: URL, + headers?: http.OutgoingHttpHeaders + ): ifm.RequestInfo { + const info: ifm.RequestInfo = {} + + info.parsedUrl = requestUrl + const usingSsl: boolean = info.parsedUrl.protocol === 'https:' + info.httpModule = usingSsl ? https : http + const defaultPort: number = usingSsl ? 443 : 80 + + info.options = {} + info.options.host = info.parsedUrl.hostname + info.options.port = info.parsedUrl.port + ? parseInt(info.parsedUrl.port) + : defaultPort + info.options.path = + (info.parsedUrl.pathname || '') + (info.parsedUrl.search || '') + info.options.method = method + info.options.headers = this._mergeHeaders(headers) + if (this.userAgent != null) { + info.options.headers['user-agent'] = this.userAgent + } + + info.options.agent = this._getAgent(info.parsedUrl) + + // gives handlers an opportunity to participate + if (this.handlers) { + for (const handler of this.handlers) { + handler.prepareRequest(info.options) + } + } + + return info + } + + private _mergeHeaders( + headers?: http.OutgoingHttpHeaders + ): http.OutgoingHttpHeaders { + if (this.requestOptions && this.requestOptions.headers) { + return Object.assign( + {}, + lowercaseKeys(this.requestOptions.headers), + lowercaseKeys(headers || {}) + ) + } + + return lowercaseKeys(headers || {}) + } + + private _getExistingOrDefaultHeader( + additionalHeaders: http.OutgoingHttpHeaders, + header: string, + _default: string + ): string | number | string[] { + let clientHeader: string | undefined + if (this.requestOptions && this.requestOptions.headers) { + clientHeader = lowercaseKeys(this.requestOptions.headers)[header] + } + return additionalHeaders[header] || clientHeader || _default + } + + private _getAgent(parsedUrl: URL): http.Agent { + let agent + const proxyUrl = pm.getProxyUrl(parsedUrl) + const useProxy = proxyUrl && proxyUrl.hostname + + if (this._keepAlive && useProxy) { + agent = this._proxyAgent + } + + if (this._keepAlive && !useProxy) { + agent = this._agent + } + + // if agent is already assigned use that agent. + if (agent) { + return agent + } + + const usingSsl = parsedUrl.protocol === 'https:' + let maxSockets = 100 + if (this.requestOptions) { + maxSockets = this.requestOptions.maxSockets || http.globalAgent.maxSockets + } + + // This is `useProxy` again, but we need to check `proxyURl` directly for TypeScripts's flow analysis. + if (proxyUrl && proxyUrl.hostname) { + const agentOptions = { + maxSockets, + keepAlive: this._keepAlive, + proxy: { + ...((proxyUrl.username || proxyUrl.password) && { + proxyAuth: `${proxyUrl.username}:${proxyUrl.password}` + }), + host: proxyUrl.hostname, + port: proxyUrl.port + } + } + + let tunnelAgent: Function + const overHttps = proxyUrl.protocol === 'https:' + if (usingSsl) { + tunnelAgent = overHttps ? tunnel.httpsOverHttps : tunnel.httpsOverHttp + } else { + tunnelAgent = overHttps ? tunnel.httpOverHttps : tunnel.httpOverHttp + } + + agent = tunnelAgent(agentOptions) + this._proxyAgent = agent + } + + // if reusing agent across request and tunneling agent isn't assigned create a new agent + if (this._keepAlive && !agent) { + const options = {keepAlive: this._keepAlive, maxSockets} + agent = usingSsl ? new https.Agent(options) : new http.Agent(options) + this._agent = agent + } + + // if not using private agent and tunnel agent isn't setup then use global agent + if (!agent) { + agent = usingSsl ? https.globalAgent : http.globalAgent + } + + if (usingSsl && this._ignoreSslError) { + // we don't want to set NODE_TLS_REJECT_UNAUTHORIZED=0 since that will affect request for entire process + // http.RequestOptions doesn't expose a way to modify RequestOptions.agent.options + // we have to cast it to any and change it directly + agent.options = Object.assign(agent.options || {}, { + rejectUnauthorized: false + }) + } + + return agent + } + + private async _performExponentialBackoff(retryNumber: number): Promise { + retryNumber = Math.min(ExponentialBackoffCeiling, retryNumber) + const ms: number = ExponentialBackoffTimeSlice * Math.pow(2, retryNumber) + return new Promise(resolve => setTimeout(() => resolve(), ms)) + } + + private async _processResponse( + res: HttpClientResponse, + options?: ifm.RequestOptions + ): Promise> { + return new Promise>(async (resolve, reject) => { + const statusCode = res.message.statusCode || 0 + + const response: ifm.TypedResponse = { + statusCode, + result: null, + headers: {} + } + + // not found leads to null obj returned + if (statusCode === HttpCodes.NotFound) { + resolve(response) + } + + // get the result from the body + + function dateTimeDeserializer(key: any, value: any): any { + if (typeof value === 'string') { + const a = new Date(value) + if (!isNaN(a.valueOf())) { + return a + } + } + + return value + } + + let obj: any + let contents: string | undefined + + try { + contents = await res.readBody() + if (contents && contents.length > 0) { + if (options && options.deserializeDates) { + obj = JSON.parse(contents, dateTimeDeserializer) + } else { + obj = JSON.parse(contents) + } + + response.result = obj + } + + response.headers = res.message.headers + } catch (err) { + // Invalid resource (contents not json); leaving result obj null + } + + // note that 3xx redirects are handled by the http layer. + if (statusCode > 299) { + let msg: string + + // if exception/error in body, attempt to get better error + if (obj && obj.message) { + msg = obj.message + } else if (contents && contents.length > 0) { + // it may be the case that the exception is in the body message as string + msg = contents + } else { + msg = `Failed request: (${statusCode})` + } + + const err = new HttpClientError(msg, statusCode) + err.result = response.result + + reject(err) + } else { + resolve(response) + } + }) + } +} + +const lowercaseKeys = (obj: {[index: string]: any}): any => + Object.keys(obj).reduce((c: any, k) => ((c[k.toLowerCase()] = obj[k]), c), {}) diff --git a/packages/http-client/src/interfaces.ts b/packages/http-client/src/interfaces.ts new file mode 100644 index 00000000..96b0fec7 --- /dev/null +++ b/packages/http-client/src/interfaces.ts @@ -0,0 +1,91 @@ +import * as http from 'http' +import * as https from 'https' +import {HttpClientResponse} from './index' + +export interface HttpClient { + options( + requestUrl: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise + get( + requestUrl: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise + del( + requestUrl: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise + post( + requestUrl: string, + data: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise + patch( + requestUrl: string, + data: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise + put( + requestUrl: string, + data: string, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise + sendStream( + verb: string, + requestUrl: string, + stream: NodeJS.ReadableStream, + additionalHeaders?: http.OutgoingHttpHeaders + ): Promise + request( + verb: string, + requestUrl: string, + data: string | NodeJS.ReadableStream, + headers: http.OutgoingHttpHeaders + ): Promise + requestRaw( + info: RequestInfo, + data: string | NodeJS.ReadableStream + ): Promise + requestRawWithCallback( + info: RequestInfo, + data: string | NodeJS.ReadableStream, + onResult: (err?: Error, res?: HttpClientResponse) => void + ): void +} + +export interface RequestHandler { + prepareRequest(options: http.RequestOptions): void + canHandleAuthentication(response: HttpClientResponse): boolean + handleAuthentication( + httpClient: HttpClient, + requestInfo: RequestInfo, + data: string | NodeJS.ReadableStream | null + ): Promise +} + +export interface RequestInfo { + options: http.RequestOptions + parsedUrl: URL + httpModule: typeof http | typeof https +} + +export interface RequestOptions { + headers?: http.OutgoingHttpHeaders + socketTimeout?: number + ignoreSslError?: boolean + allowRedirects?: boolean + allowRedirectDowngrade?: boolean + maxRedirects?: number + maxSockets?: number + keepAlive?: boolean + deserializeDates?: boolean + // Allows retries only on Read operations (since writes may not be idempotent) + allowRetries?: boolean + maxRetries?: number +} + +export interface TypedResponse { + statusCode: number + result: T | null + headers: http.IncomingHttpHeaders +} diff --git a/packages/http-client/src/proxy.ts b/packages/http-client/src/proxy.ts new file mode 100644 index 00000000..f13409b5 --- /dev/null +++ b/packages/http-client/src/proxy.ts @@ -0,0 +1,60 @@ +export function getProxyUrl(reqUrl: URL): URL | undefined { + const usingSsl = reqUrl.protocol === 'https:' + + if (checkBypass(reqUrl)) { + return undefined + } + + const proxyVar = (() => { + if (usingSsl) { + return process.env['https_proxy'] || process.env['HTTPS_PROXY'] + } else { + return process.env['http_proxy'] || process.env['HTTP_PROXY'] + } + })() + + if (proxyVar) { + return new URL(proxyVar) + } else { + return undefined + } +} + +export function checkBypass(reqUrl: URL): boolean { + if (!reqUrl.hostname) { + return false + } + + const noProxy = process.env['no_proxy'] || process.env['NO_PROXY'] || '' + if (!noProxy) { + return false + } + + // Determine the request port + let reqPort: number | undefined + if (reqUrl.port) { + reqPort = Number(reqUrl.port) + } else if (reqUrl.protocol === 'http:') { + reqPort = 80 + } else if (reqUrl.protocol === 'https:') { + reqPort = 443 + } + + // Format the request hostname and hostname with port + const upperReqHosts = [reqUrl.hostname.toUpperCase()] + if (typeof reqPort === 'number') { + upperReqHosts.push(`${upperReqHosts[0]}:${reqPort}`) + } + + // Compare request host against noproxy + for (const upperNoProxyItem of noProxy + .split(',') + .map(x => x.trim().toUpperCase()) + .filter(x => x)) { + if (upperReqHosts.some(x => x === upperNoProxyItem)) { + return true + } + } + + return false +} diff --git a/packages/http-client/tsconfig.json b/packages/http-client/tsconfig.json new file mode 100644 index 00000000..4abc2b1c --- /dev/null +++ b/packages/http-client/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./lib", + "rootDir": "./src", + "moduleResolution": "node" + }, + "include": [ + "./src" + ] +} \ No newline at end of file diff --git a/packages/tool-cache/RELEASES.md b/packages/tool-cache/RELEASES.md index c613f183..9fdd4898 100644 --- a/packages/tool-cache/RELEASES.md +++ b/packages/tool-cache/RELEASES.md @@ -1,5 +1,13 @@ # @actions/tool-cache Releases +### 2.0.1 +- Update to v2.0.1 of `@actions/http-client` [#1087](https://github.com/actions/toolkit/pull/1087) + +### 2.0.0 +- Update to v2.0.0 of `@actions/http-client` +- The type of the `headers` parameter in the exported function `downloadTool` has been narrowed from `{ [header: string]: any }` to `{ [header: string]: number | string | string[] | undefined; }` (that is, `http.OutgoingHttpHeaders`). + This is strictly a compile-time change for TypeScript consumers. Previous attempts to use a header value of a type other than those now accepted would have resulted in an error at run time. + ### 1.7.2 - Update `lockfileVersion` to `v2` in `package-lock.json [#1025](https://github.com/actions/toolkit/pull/1025) diff --git a/packages/tool-cache/package-lock.json b/packages/tool-cache/package-lock.json index 524d1345..1c7dfe7f 100644 --- a/packages/tool-cache/package-lock.json +++ b/packages/tool-cache/package-lock.json @@ -1,17 +1,17 @@ { "name": "@actions/tool-cache", - "version": "1.7.2", + "version": "2.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@actions/tool-cache", - "version": "1.7.2", + "version": "2.0.0", "license": "MIT", "dependencies": { "@actions/core": "^1.2.6", "@actions/exec": "^1.0.0", - "@actions/http-client": "^1.0.8", + "@actions/http-client": "^2.0.1", "@actions/io": "^1.1.1", "semver": "^6.1.0", "uuid": "^3.3.2" @@ -24,30 +24,41 @@ } }, "node_modules/@actions/core": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", - "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.0.tgz", + "integrity": "sha512-XirM+Zo/PFlA+1h+i4bkfvagujta+LIM2AOSzPbt8JqXbbuxb1HTB+FqIyaKmue9yiCx/JIJY6pXsOl3+T8JGw==", + "dependencies": { + "@actions/http-client": "^1.0.11" + } + }, + "node_modules/@actions/core/node_modules/@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "dependencies": { + "tunnel": "0.0.6" + } }, "node_modules/@actions/exec": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz", - "integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "dependencies": { "@actions/io": "^1.0.1" } }, "node_modules/@actions/http-client": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz", - "integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "dependencies": { - "tunnel": "0.0.6" + "tunnel": "^0.0.6" } }, "node_modules/@actions/io": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.1.tgz", - "integrity": "sha512-Qi4JoKXjmE0O67wAOH6y0n26QXhMKMFo7GD/4IXNVcrtLjUlGjGuVys6pQgwF3ArfGTQu0XpqaNr0YhED2RaRA==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.2.tgz", + "integrity": "sha512-d+RwPlMp+2qmBfeLYPLXuSRykDIFEwdTA0MMxzS9kh4kvP1ftrc/9fzy6pX6qAjthdXruHQ6/6kjT/DNo5ALuw==" }, "node_modules/@types/nock": { "version": "10.0.3", @@ -280,30 +291,43 @@ }, "dependencies": { "@actions/core": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz", - "integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.8.0.tgz", + "integrity": "sha512-XirM+Zo/PFlA+1h+i4bkfvagujta+LIM2AOSzPbt8JqXbbuxb1HTB+FqIyaKmue9yiCx/JIJY6pXsOl3+T8JGw==", + "requires": { + "@actions/http-client": "^1.0.11" + }, + "dependencies": { + "@actions/http-client": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz", + "integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==", + "requires": { + "tunnel": "0.0.6" + } + } + } }, "@actions/exec": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.3.tgz", - "integrity": "sha512-TogJGnueOmM7ntCi0ASTUj4LapRRtDfj57Ja4IhPmg2fls28uVOPbAn8N+JifaOumN2UG3oEO/Ixek2A4NcYSA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz", + "integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==", "requires": { "@actions/io": "^1.0.1" } }, "@actions/http-client": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.8.tgz", - "integrity": "sha512-G4JjJ6f9Hb3Zvejj+ewLLKLf99ZC+9v+yCxoYf9vSyH+WkzPLB2LuUtRMGNkooMqdugGBFStIKXOuvH1W+EctA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz", + "integrity": "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==", "requires": { - "tunnel": "0.0.6" + "tunnel": "^0.0.6" } }, "@actions/io": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.1.tgz", - "integrity": "sha512-Qi4JoKXjmE0O67wAOH6y0n26QXhMKMFo7GD/4IXNVcrtLjUlGjGuVys6pQgwF3ArfGTQu0XpqaNr0YhED2RaRA==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.2.tgz", + "integrity": "sha512-d+RwPlMp+2qmBfeLYPLXuSRykDIFEwdTA0MMxzS9kh4kvP1ftrc/9fzy6pX6qAjthdXruHQ6/6kjT/DNo5ALuw==" }, "@types/nock": { "version": "10.0.3", diff --git a/packages/tool-cache/package.json b/packages/tool-cache/package.json index da75109d..c7744d58 100644 --- a/packages/tool-cache/package.json +++ b/packages/tool-cache/package.json @@ -1,6 +1,6 @@ { "name": "@actions/tool-cache", - "version": "1.7.2", + "version": "2.0.1", "description": "Actions tool-cache lib", "keywords": [ "github", @@ -38,7 +38,7 @@ "dependencies": { "@actions/core": "^1.2.6", "@actions/exec": "^1.0.0", - "@actions/http-client": "^1.0.8", + "@actions/http-client": "^2.0.1", "@actions/io": "^1.1.1", "semver": "^6.1.0", "uuid": "^3.3.2" diff --git a/packages/tool-cache/src/tool-cache.ts b/packages/tool-cache/src/tool-cache.ts index 92fd519a..694d1252 100644 --- a/packages/tool-cache/src/tool-cache.ts +++ b/packages/tool-cache/src/tool-cache.ts @@ -8,12 +8,12 @@ import * as httpm from '@actions/http-client' import * as semver from 'semver' import * as stream from 'stream' import * as util from 'util' +import {ok} from 'assert' +import {OutgoingHttpHeaders} from 'http' import uuidV4 from 'uuid/v4' import {exec} from '@actions/exec/lib/exec' import {ExecOptions} from '@actions/exec/lib/interfaces' -import {ok} from 'assert' import {RetryHelper} from './retry-helper' -import {IHeaders} from '@actions/http-client/interfaces' export class HTTPError extends Error { constructor(readonly httpStatusCode: number | undefined) { @@ -39,7 +39,7 @@ export async function downloadTool( url: string, dest?: string, auth?: string, - headers?: IHeaders + headers?: OutgoingHttpHeaders ): Promise { dest = dest || path.join(_getTempDirectory(), uuidV4()) await io.mkdirP(path.dirname(dest)) @@ -82,7 +82,7 @@ async function downloadToolAttempt( url: string, dest: string, auth?: string, - headers?: IHeaders + headers?: OutgoingHttpHeaders ): Promise { if (fs.existsSync(dest)) { throw new Error(`Destination file path ${dest} already exists`) @@ -596,7 +596,7 @@ export async function getManifestFromRepo( const treeUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}` const http: httpm.HttpClient = new httpm.HttpClient('tool-cache') - const headers: IHeaders = {} + const headers: OutgoingHttpHeaders = {} if (auth) { core.debug('set auth') headers.authorization = auth diff --git a/scripts/create-package b/scripts/create-package index ed38d73a..29d07feb 100755 --- a/scripts/create-package +++ b/scripts/create-package @@ -9,5 +9,5 @@ if [[ -z "$name" ]]; then exit 1 fi -lerna create @actions/$name -cp packages/toolkit/tsconfig.json packages/$name/tsconfig.json \ No newline at end of file +npx lerna create @actions/$name +cp packages/core/tsconfig.json packages/$name/tsconfig.json \ No newline at end of file