diff --git a/.github/workflows/setup-gcloud-it.yml b/.github/workflows/setup-gcloud-it.yml index a011a24ca..cde2287b9 100644 --- a/.github/workflows/setup-gcloud-it.yml +++ b/.github/workflows/setup-gcloud-it.yml @@ -6,8 +6,8 @@ name: Integration Tests # kick off integration test runs for the time being. on: [push] jobs: - run: - name: setup-gcloud + versioned: + name: setup-gcloud versioned runs-on: ${{ matrix.operating-system }} strategy: fail-fast: false @@ -16,7 +16,7 @@ jobs: steps: - uses: actions/checkout@v2 - - name: setup-gcloud local + - name: setup-gcloud uses: ./setup-gcloud/ with: version: '278.0.0' @@ -26,3 +26,25 @@ jobs: - name: Integration Tests shell: bash run: ./setup-gcloud/__tests__/integration-tests.sh + latest: + name: setup-gcloud latest + runs-on: ${{ matrix.operating-system }} + strategy: + fail-fast: false + matrix: + operating-system: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v2 + + - name: setup-gcloud + uses: ./setup-gcloud/ + with: + version: 'latest' + service_account_email: ${{ secrets.SETUP_GCLOUD_IT_EMAIL }} + service_account_key: ${{ secrets.SETUP_GCLOUD_IT_KEY }} + + - name: Integration Tests + shell: bash + run: ./setup-gcloud/__tests__/integration-tests.sh + + diff --git a/.github/workflows/setup-gcloud.yml b/.github/workflows/setup-gcloud.yml index f401852bf..d9d2a1c94 100644 --- a/.github/workflows/setup-gcloud.yml +++ b/.github/workflows/setup-gcloud.yml @@ -5,6 +5,7 @@ jobs: name: setup-gcloud runs-on: ${{ matrix.operating-system }} strategy: + fail-fast: false matrix: operating-system: [ubuntu-latest, windows-latest, macos-latest] steps: diff --git a/setup-gcloud/README.md b/setup-gcloud/README.md index 5d7ca20fd..17656ad59 100644 --- a/setup-gcloud/README.md +++ b/setup-gcloud/README.md @@ -42,7 +42,7 @@ steps: - uses: actions/checkout@v1 - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master with: - version: '275.0.0' + version: '278.0.0' service_account_email: ${{ secrets.GCP_SA_EMAIL }} service_account_key: ${{ secrets.GCP_SA_KEY }} - run: gcloud info @@ -50,7 +50,7 @@ steps: ## Inputs -* `version`: The version of the gcloud to be installed. Example: `275.0.0` +* `version`: (Optional) The version of the gcloud to be installed. Example: `275.0.0`, Default: `latest` * `service_account_email`: (Optional) The service account email which will be used for authentication. diff --git a/setup-gcloud/__tests__/install-util.test.ts b/setup-gcloud/__tests__/install-util.test.ts index 1b08d3924..25387256a 100644 --- a/setup-gcloud/__tests__/install-util.test.ts +++ b/setup-gcloud/__tests__/install-util.test.ts @@ -44,7 +44,7 @@ describe('installGcloudSDK tests', () => { afterAll(async () => { await io.rmRF(toolDir); await io.rmRF(tempDir); - }); + }, TEST_TIMEOUT_MILLIS); it( 'Installs gcloud for current env', diff --git a/setup-gcloud/__tests__/version-util.test.ts b/setup-gcloud/__tests__/version-util.test.ts new file mode 100644 index 000000000..9756f8e04 --- /dev/null +++ b/setup-gcloud/__tests__/version-util.test.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Tests version-util. + */ + +import {getLatestGcloudSDKVersion} from '../src/version-util'; + +describe('getLatestGcloudSDKVersion test', () => { + it('Successfully retrieves latest version', async () => { + const semVerPattern = /^[0-9]+\.[0-9]+\.[0-9]+$/; + const result = await getLatestGcloudSDKVersion(); + expect(result).toBeDefined(); + expect(semVerPattern.test(result)).toBe(true); + }); +}); diff --git a/setup-gcloud/action.yml b/setup-gcloud/action.yml index a053bc6bf..5998450e0 100644 --- a/setup-gcloud/action.yml +++ b/setup-gcloud/action.yml @@ -3,8 +3,8 @@ description: 'Setup a gcloud environment and add it to the PATH' author: 'GoogleCloudPlatform' inputs: version: - description: 'The version of the gcloud SDK to be installed. Example: 275.0.0' - default: '275.0.0' + description: 'The version of the gcloud SDK to be installed. Example: 278.0.0' + default: 'latest' required: false service_account_email: description: 'The service account email which will be used for authentication.' diff --git a/setup-gcloud/dist/index.js b/setup-gcloud/dist/index.js index c3c4ddff8..db7c38fe1 100644 --- a/setup-gcloud/dist/index.js +++ b/setup-gcloud/dist/index.js @@ -360,6 +360,15 @@ function copyFile(srcFile, destFile, force) { * See the License for the specific language governing permissions and * limitations under the License. */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; @@ -369,6 +378,8 @@ var __importStar = (this && this.__importStar) || function (mod) { }; Object.defineProperty(exports, "__esModule", { value: true }); const httpm = __importStar(__webpack_require__(874)); +const attempt_1 = __webpack_require__(503); +const install_util_1 = __webpack_require__(962); /** * Formats the gcloud SDK release URL according to the specified arguments. * @@ -408,18 +419,28 @@ function formatReleaseURL(os, arch, version) { * @returns The verified gcloud SDK release URL. */ function getReleaseURL(os, arch, version) { - try { - const url = formatReleaseURL(os, arch, version); - const client = new httpm.HttpClient('github-actions-setup-gcloud'); - return client - .head(url) - .then(res => res.message.statusCode === 200 - ? Promise.resolve(url) - : Promise.reject(`error code: ${res.message.statusCode}`)); - } - catch (err) { - return Promise.reject(`Error trying to get gcloud SDK release URL: os: ${os} arch: ${arch} version: ${version}, err: ${err}`); - } + return __awaiter(this, void 0, void 0, function* () { + try { + const url = formatReleaseURL(os, arch, version); + const client = new httpm.HttpClient(install_util_1.GCLOUD_METRICS_LABEL); + return attempt_1.retry((context) => __awaiter(this, void 0, void 0, function* () { + const res = yield client.head(url); + if (res.message.statusCode === 200) { + return url; + } + else { + throw new Error(`error code: ${res.message.statusCode}`); + } + }), { + delay: 200, + factor: 2, + maxAttempts: 4, + }); + } + catch (err) { + throw new Error(`Error trying to get gcloud SDK release URL: os: ${os} arch: ${arch} version: ${version}, err: ${err}`); + } + }); } exports.getReleaseURL = getReleaseURL; @@ -1101,6 +1122,79 @@ function onceStrict (fn) { } +/***/ }), + +/***/ 71: +/***/ (function(__unusedmodule, exports, __webpack_require__) { + +"use strict"; + +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; + result["default"] = mod; + return result; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +/** + * Contains version utility functions. + */ +const httpm = __importStar(__webpack_require__(874)); +const attempt_1 = __webpack_require__(503); +const install_util_1 = __webpack_require__(962); +/** + * @returns The latest stable version of the gcloud SDK. + */ +function getLatestGcloudSDKVersion() { + return __awaiter(this, void 0, void 0, function* () { + const queryUrl = 'https://dl.google.com/dl/cloudsdk/channels/rapid/components-2.json'; + const client = new httpm.HttpClient(install_util_1.GCLOUD_METRICS_LABEL); + return yield attempt_1.retry((context) => __awaiter(this, void 0, void 0, function* () { + const res = yield client.get(queryUrl); + if (res.message.statusCode != 200) { + throw new Error(`Failed to retrieve gcloud SDK version, HTTP error code: ${res.message.statusCode} url: ${queryUrl}`); + } + const body = yield res.readBody(); + const responseObject = JSON.parse(body); + if (!responseObject.version) { + throw new Error(`Failed to retrieve gcloud SDK version, invalid response body: ${body}`); + } + return responseObject.version; + }), { + delay: 200, + factor: 2, + maxAttempts: 4, + }); + }); +} +exports.getLatestGcloudSDKVersion = getLatestGcloudSDKVersion; + + /***/ }), /***/ 87: @@ -5855,6 +5949,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); * Contains download utility functions. */ const toolCache = __importStar(__webpack_require__(533)); +const attempt_1 = __webpack_require__(503); /** * Downloads and extracts the tool at the specified URL. * @@ -5863,7 +5958,11 @@ const toolCache = __importStar(__webpack_require__(533)); */ function downloadAndExtractTool(url) { return __awaiter(this, void 0, void 0, function* () { - const downloadPath = yield toolCache.downloadTool(url); + const downloadPath = yield attempt_1.retry((context) => __awaiter(this, void 0, void 0, function* () { return toolCache.downloadTool(url); }), { + delay: 200, + factor: 2, + maxAttempts: 4, + }); let extractedPath; if (url.indexOf('.zip') != -1) { extractedPath = yield toolCache.extractZip(downloadPath); @@ -6977,6 +7076,158 @@ function getState(name) { exports.getState = getState; //# sourceMappingURL=core.js.map +/***/ }), + +/***/ 503: +/***/ (function(__unusedmodule, exports) { + +"use strict"; + +Object.defineProperty(exports, "__esModule", { value: true }); +function applyDefaults(options) { + if (!options) { + options = {}; + } + return { + delay: (options.delay === undefined) ? 200 : options.delay, + initialDelay: (options.initialDelay === undefined) ? 0 : options.initialDelay, + minDelay: (options.minDelay === undefined) ? 0 : options.minDelay, + maxDelay: (options.maxDelay === undefined) ? 0 : options.maxDelay, + factor: (options.factor === undefined) ? 0 : options.factor, + maxAttempts: (options.maxAttempts === undefined) ? 3 : options.maxAttempts, + timeout: (options.timeout === undefined) ? 0 : options.timeout, + jitter: (options.jitter === true), + handleError: (options.handleError === undefined) ? null : options.handleError, + handleTimeout: (options.handleTimeout === undefined) ? null : options.handleTimeout, + beforeAttempt: (options.beforeAttempt === undefined) ? null : options.beforeAttempt, + calculateDelay: (options.calculateDelay === undefined) ? null : options.calculateDelay + }; +} +async function sleep(delay) { + return new Promise((resolve, reject) => { + setTimeout(resolve, delay); + }); +} +exports.sleep = sleep; +function defaultCalculateDelay(context, options) { + let delay = options.delay; + if (delay === 0) { + // no delay between attempts + return 0; + } + if (options.factor) { + delay *= Math.pow(options.factor, context.attemptNum - 1); + if (options.maxDelay !== 0) { + delay = Math.min(delay, options.maxDelay); + } + } + if (options.jitter) { + // Jitter will result in a random value between `minDelay` and + // calculated delay for a given attempt. + // See https://www.awsarchitectureblog.com/2015/03/backoff.html + // We're using the "full jitter" strategy. + const min = Math.ceil(options.minDelay); + const max = Math.floor(delay); + delay = Math.floor(Math.random() * (max - min + 1)) + min; + } + return Math.round(delay); +} +exports.defaultCalculateDelay = defaultCalculateDelay; +async function retry(attemptFunc, attemptOptions) { + const options = applyDefaults(attemptOptions); + for (const prop of [ + 'delay', + 'initialDelay', + 'minDelay', + 'maxDelay', + 'maxAttempts', + 'timeout' + ]) { + const value = options[prop]; + if (!Number.isInteger(value) || (value < 0)) { + throw new Error(`Value for ${prop} must be an integer greater than or equal to 0`); + } + } + if ((options.factor.constructor !== Number) || (options.factor < 0)) { + throw new Error(`Value for factor must be a number greater than or equal to 0`); + } + if (options.delay < options.minDelay) { + throw new Error(`delay cannot be less than minDelay (delay: ${options.delay}, minDelay: ${options.minDelay}`); + } + const context = { + attemptNum: 0, + attemptsRemaining: options.maxAttempts ? options.maxAttempts : -1, + aborted: false, + abort() { + context.aborted = true; + } + }; + const calculateDelay = options.calculateDelay || defaultCalculateDelay; + async function makeAttempt() { + if (options.beforeAttempt) { + options.beforeAttempt(context, options); + } + if (context.aborted) { + const err = new Error(`Attempt aborted`); + err.code = 'ATTEMPT_ABORTED'; + throw err; + } + const onError = async (err) => { + if (options.handleError) { + options.handleError(err, context, options); + } + if (context.aborted || (context.attemptsRemaining === 0)) { + throw err; + } + // We are about to try again so increment attempt number + context.attemptNum++; + const delay = calculateDelay(context, options); + if (delay) { + await sleep(delay); + } + return makeAttempt(); + }; + if (context.attemptsRemaining > 0) { + context.attemptsRemaining--; + } + if (options.timeout) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + if (options.handleTimeout) { + resolve(options.handleTimeout(context, options)); + } + else { + const err = new Error(`Retry timeout (attemptNum: ${context.attemptNum}, timeout: ${options.timeout})`); + err.code = 'ATTEMPT_TIMEOUT'; + reject(err); + } + }, options.timeout); + attemptFunc(context, options).then((result) => { + clearTimeout(timer); + resolve(result); + }).catch((err) => { + clearTimeout(timer); + resolve(onError(err)); + }); + }); + } + else { + // No timeout provided so wait indefinitely for the returned promise + // to be resolved. + return attemptFunc(context, options).catch(onError); + } + } + const initialDelay = options.calculateDelay + ? options.calculateDelay(context, options) + : options.initialDelay; + if (initialDelay) { + await sleep(initialDelay); + } + return makeAttempt(); +} +exports.retry = retry; + + /***/ }), /***/ 533: @@ -8288,17 +8539,18 @@ const fs_1 = __webpack_require__(747); const tmp = __importStar(__webpack_require__(150)); const os = __importStar(__webpack_require__(87)); const format_url_1 = __webpack_require__(8); +const version_util_1 = __webpack_require__(71); const downloadUtil = __importStar(__webpack_require__(339)); const installUtil = __importStar(__webpack_require__(962)); function run() { return __awaiter(this, void 0, void 0, function* () { try { tmp.setGracefulCleanup(); - const version = core.getInput('version'); - if (!version) { - throw new Error('Missing required parameter: `version`'); + let version = core.getInput('version'); + if (!version || version == 'latest') { + version = yield version_util_1.getLatestGcloudSDKVersion(); } - // install the gcloud is not already present + // install the gcloud if not already present let toolPath = toolCache.find('gcloud', version); if (!toolPath) { toolPath = yield installGcloudSDK(version); @@ -8320,6 +8572,8 @@ function run() { }); }); yield fs_1.promises.writeFile(tmpKeyFilePath, js_base64_1.Base64.decode(serviceAccountKey)); + // A workaround for https://github.com/actions/toolkit/issues/229 + // Currently exec on windows runs as cmd shell. let toolCommand = 'gcloud'; if (process.platform == 'win32') { toolCommand = 'gcloud.cmd'; diff --git a/setup-gcloud/package-lock.json b/setup-gcloud/package-lock.json index 1321daead..c684a3961 100644 --- a/setup-gcloud/package-lock.json +++ b/setup-gcloud/package-lock.json @@ -467,6 +467,11 @@ "@types/yargs": "^13.0.0" } }, + "@lifeomic/attempt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lifeomic/attempt/-/attempt-3.0.0.tgz", + "integrity": "sha512-Ibk4Vfl46dSrhtH5fHsrTA4waAuyP7/qcr3uo0mO70azRc6LWgJILlMy3B1oOvyiN9jQcdqwsThaQkPKLiYKTg==" + }, "@octokit/endpoint": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-5.5.0.tgz", @@ -790,9 +795,9 @@ "dev": true }, "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", "dev": true }, "acorn-globals": { @@ -4005,6 +4010,14 @@ "whatwg-url": "^6.4.1", "ws": "^5.2.0", "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", + "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", + "dev": true + } } }, "jsesc": { diff --git a/setup-gcloud/package.json b/setup-gcloud/package.json index ee8ba6781..17c402484 100644 --- a/setup-gcloud/package.json +++ b/setup-gcloud/package.json @@ -29,6 +29,7 @@ "@actions/github": "^1.0.0", "@actions/io": "^1.0.0", "@actions/tool-cache": "^1.0.0", + "@lifeomic/attempt": "^3.0.0", "js-base64": "2.5.1", "semver": "^6.1.1", "tmp": "0.1.0", @@ -43,6 +44,8 @@ "@typescript-eslint/eslint-plugin": "^2.6.0", "@typescript-eslint/parser": "^2.6.0", "@zeit/ncc": "^0.20.5", + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", "eslint": "^6.6.0", "eslint-config-prettier": "^6.5.0", "eslint-plugin-prettier": "^3.1.1", diff --git a/setup-gcloud/src/download-util.ts b/setup-gcloud/src/download-util.ts index f357a29c7..4b839720a 100644 --- a/setup-gcloud/src/download-util.ts +++ b/setup-gcloud/src/download-util.ts @@ -18,6 +18,7 @@ * Contains download utility functions. */ import * as toolCache from '@actions/tool-cache'; +import {retry} from '@lifeomic/attempt'; /** * Downloads and extracts the tool at the specified URL. @@ -26,7 +27,14 @@ import * as toolCache from '@actions/tool-cache'; * @returns The path to the locally extracted tool. */ export async function downloadAndExtractTool(url: string): Promise { - const downloadPath = await toolCache.downloadTool(url); + const downloadPath = await retry( + async context => toolCache.downloadTool(url), + { + delay: 200, + factor: 2, + maxAttempts: 4, + }, + ); let extractedPath: string; if (url.indexOf('.zip') != -1) { extractedPath = await toolCache.extractZip(downloadPath); diff --git a/setup-gcloud/src/format-url.ts b/setup-gcloud/src/format-url.ts index 90aa0a443..272fcbf76 100644 --- a/setup-gcloud/src/format-url.ts +++ b/setup-gcloud/src/format-url.ts @@ -15,6 +15,8 @@ */ import * as httpm from 'typed-rest-client/HttpClient'; +import {retry} from '@lifeomic/attempt'; +import {GCLOUD_METRICS_LABEL} from './install-util'; /** * Formats the gcloud SDK release URL according to the specified arguments. @@ -59,25 +61,31 @@ function formatReleaseURL(os: string, arch: string, version: string): string { * @param version The version of the requested release. * @returns The verified gcloud SDK release URL. */ -export function getReleaseURL( +export async function getReleaseURL( os: string, arch: string, version: string, ): Promise { try { const url = formatReleaseURL(os, arch, version); - const client: httpm.HttpClient = new httpm.HttpClient( - 'github-actions-setup-gcloud', + const client = new httpm.HttpClient(GCLOUD_METRICS_LABEL); + return retry( + async context => { + const res = await client.head(url); + if (res.message.statusCode === 200) { + return url; + } else { + throw new Error(`error code: ${res.message.statusCode}`); + } + }, + { + delay: 200, + factor: 2, + maxAttempts: 4, + }, ); - return client - .head(url) - .then(res => - res.message.statusCode === 200 - ? Promise.resolve(url) - : Promise.reject(`error code: ${res.message.statusCode}`), - ); } catch (err) { - return Promise.reject( + throw new Error( `Error trying to get gcloud SDK release URL: os: ${os} arch: ${arch} version: ${version}, err: ${err}`, ); } diff --git a/setup-gcloud/src/setup-gcloud.ts b/setup-gcloud/src/setup-gcloud.ts index ea4b9ca26..c77883bad 100644 --- a/setup-gcloud/src/setup-gcloud.ts +++ b/setup-gcloud/src/setup-gcloud.ts @@ -23,6 +23,7 @@ import path from 'path'; import * as tmp from 'tmp'; import * as os from 'os'; import {getReleaseURL} from '../src/format-url'; +import {getLatestGcloudSDKVersion} from '../src/version-util'; import * as downloadUtil from './download-util'; import * as installUtil from './install-util'; @@ -30,12 +31,12 @@ async function run() { try { tmp.setGracefulCleanup(); - const version = core.getInput('version'); - if (!version) { - throw new Error('Missing required parameter: `version`'); + let version = core.getInput('version'); + if (!version || version == 'latest') { + version = await getLatestGcloudSDKVersion(); } - // install the gcloud is not already present + // install the gcloud if not already present let toolPath = toolCache.find('gcloud', version); if (!toolPath) { toolPath = await installGcloudSDK(version); diff --git a/setup-gcloud/src/version-util.ts b/setup-gcloud/src/version-util.ts new file mode 100644 index 000000000..9493d7989 --- /dev/null +++ b/setup-gcloud/src/version-util.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Contains version utility functions. + */ + +import * as httpm from 'typed-rest-client/HttpClient'; +import {retry} from '@lifeomic/attempt'; +import {GCLOUD_METRICS_LABEL} from './install-util'; + +/** + * @returns The latest stable version of the gcloud SDK. + */ +export async function getLatestGcloudSDKVersion(): Promise { + const queryUrl = + 'https://dl.google.com/dl/cloudsdk/channels/rapid/components-2.json'; + const client: httpm.HttpClient = new httpm.HttpClient(GCLOUD_METRICS_LABEL); + return await retry( + async context => { + const res = await client.get(queryUrl); + if (res.message.statusCode != 200) { + throw new Error( + `Failed to retrieve gcloud SDK version, HTTP error code: ${res.message.statusCode} url: ${queryUrl}`, + ); + } + + const body = await res.readBody(); + const responseObject = JSON.parse(body); + if (!responseObject.version) { + throw new Error( + `Failed to retrieve gcloud SDK version, invalid response body: ${body}`, + ); + } + return responseObject.version; + }, + { + delay: 200, + factor: 2, + maxAttempts: 4, + }, + ); +}