UNPKG

@salesforce/plugin-release-management

Version:
425 lines 16.4 kB
"use strict"; /* * Copyright (c) 2020, salesforce.com, inc. * All rights reserved. * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ Object.defineProperty(exports, "__esModule", { value: true }); const path = require("path"); const os = require("os"); const fs = require("fs/promises"); const shelljs_1 = require("shelljs"); const core_1 = require("@oclif/core"); const command_1 = require("@salesforce/command"); const core_2 = require("@salesforce/core"); const ts_types_1 = require("@salesforce/ts-types"); const got_1 = require("got"); const chalk = require("chalk"); const types_1 = require("../../../types"); const amazonS3_1 = require("../../../amazonS3"); core_2.Messages.importMessagesDirectory(__dirname); const messages = core_2.Messages.load('@salesforce/plugin-release-management', 'cli.install.test', [ 'description', 'examples', 'cliFlag', 'methodFlag', 'channelFlag', 'outputFileFlag', ]); var Method; (function (Method) { let Type; (function (Type) { Type["INSTALLER"] = "installer"; Type["NPM"] = "npm"; Type["TARBALL"] = "tarball"; })(Type = Method.Type || (Method.Type = {})); class Base { constructor(options) { this.options = options; } async execute() { const { service, available } = await this.ping(); if (!available) { core_1.CliUx.ux.warn(`${service} is not currently available.`); const results = { [this.options.method]: {}, }; for (const cli of this.getTargets()) { results[this.options.method][cli] = false; } return results; } switch (process.platform) { case 'darwin': { return this.darwin(); } case 'win32': { return this.win32(); } case 'linux': { return this.linux(); } default: break; } return {}; } async ping() { return Promise.resolve({ available: true, service: 'Service' }); } logResult(cli, success) { const msg = success ? chalk.green('true') : chalk.red('false'); core_1.CliUx.ux.log(`${chalk.bold(`${cli} Success`)}: ${msg}`); } getTargets() { return Base.TEST_TARGETS[this.options.cli]; } } Base.TEST_TARGETS = { [types_1.CLI.SF]: [types_1.CLI.SF], [types_1.CLI.SFDX]: [types_1.CLI.SFDX, types_1.CLI.SF], }; Method.Base = Base; })(Method || (Method = {})); class Tarball extends Method.Base { constructor(options) { super(options); this.options = options; this.paths = { darwin: ['x64.tar.gz', 'x64.tar.xz'], win32: [ 'x64.tar.gz', 'x86.tar.gz', // .xz is not supported by powershell's tar command // 'x64.tar.xz', // 'x86.tar.xz' ], linux: ['x64.tar.gz', 'x64.tar.xz'], 'linux-arm': ['arm.tar.gz', 'arm.tar.xz'], }; this.s3 = new amazonS3_1.AmazonS3({ cli: options.cli, channel: options.channel }); } async darwin() { return this.installAndTest('darwin'); } async win32() { return this.installAndTest('win32'); } async linux() { return this.installAndTest('linux'); } async ping() { return this.s3.ping(); } async installAndTest(platform) { const tarballs = this.getTarballs(platform); const results = {}; for (const [tarball, location] of Object.entries(tarballs)) { try { await this.s3.download(tarball, location); const extracted = await this.extract(location); const testResults = this.test(extracted); for (const [cli, success] of Object.entries(testResults)) { this.logResult(cli, success); } results[tarball] = testResults; } catch { results[tarball] = {}; for (const cli of this.getTargets()) { results[tarball][cli] = false; } } core_1.CliUx.ux.log(); } return results; } getTarballs(platform) { const paths = platform === 'linux' && os.arch().includes('arm') ? this.paths['linux-arm'] : this.paths[platform]; const s3Tarballs = paths.map((p) => { return `${this.s3.directory}/channels/${this.options.channel}/${this.options.cli}-${platform}-${p}`; }); const tarballs = {}; for (const tarball of s3Tarballs) { const name = path.basename(tarball); const location = path.join(this.options.directory, name); tarballs[tarball] = location; } return tarballs; } async extract(file) { const dir = path.join(this.options.directory, path.basename(file).replace(/\./g, '-')); await fs.mkdir(dir, { recursive: true }); return new Promise((resolve, reject) => { core_1.CliUx.ux.action.start(`Unpacking ${chalk.cyan(path.basename(file))} to ${dir}`); const cmd = process.platform === 'win32' ? `tar -xf ${file} -C ${dir} --strip-components 1 --exclude node_modules/.bin` : `tar -xf ${file} -C ${dir} --strip-components 1`; const opts = process.platform === 'win32' ? { shell: 'powershell.exe' } : {}; (0, shelljs_1.exec)(cmd, { ...opts, silent: true }, (code, stdout, stderr) => { if (code === 0) { core_1.CliUx.ux.action.stop(); core_1.CliUx.ux.log(stdout); resolve(dir); } else { core_1.CliUx.ux.log('stdout:', stdout); core_1.CliUx.ux.log('stderr:', stderr); reject(); } }); }); } test(directory) { const results = {}; for (const cli of this.getTargets()) { const executable = path.join(directory, 'bin', cli); core_1.CliUx.ux.log(`Testing ${chalk.cyan(executable)}`); const result = process.platform === 'win32' ? (0, shelljs_1.exec)(`cmd /c "${executable}.cmd" --version`) : (0, shelljs_1.exec)(`${executable} --version`); results[cli] = result.code === 0; } return results; } } class Npm extends Method.Base { constructor(options) { super(options); this.options = options; const name = options.cli === types_1.CLI.SF ? '@salesforce/cli' : 'sfdx-cli'; const tag = options.channel === types_1.Channel.STABLE ? 'latest' : 'latest-rc'; this.package = `${name}@${tag}`; } async darwin() { return this.installAndTest(); } async win32() { return this.installAndTest(); } async linux() { return this.installAndTest(); } async ping() { // I'm not confident that this is the best way to preempt any issues related to Npm's availability. Mainly // because I couldn't find any documetation related to what status indicators might be used and when. const response = await got_1.default.get(Npm.STATUS_URL).json(); return { service: 'Npm', available: ['none', 'minor'].includes(response.status.indicator) }; } async installAndTest() { try { await this.install(); } catch { const results = {}; for (const cli of this.getTargets()) { results[cli] = false; } return { [this.package]: results }; } const testResults = this.test(); for (const [cli, success] of Object.entries(testResults)) { this.logResult(cli, success); } core_1.CliUx.ux.log(); return { [this.package]: testResults }; } async install() { core_1.CliUx.ux.action.start(`Installing: ${chalk.cyan(this.package)}`); return new Promise((resolve, reject) => { (0, shelljs_1.exec)(`npm install ${this.package}`, { silent: true, cwd: this.options.directory }, (code, stdout, stderr) => { if (code === 0) { core_1.CliUx.ux.action.stop(); core_1.CliUx.ux.log(stdout); resolve(); } else { core_1.CliUx.ux.action.stop('Failed'); core_1.CliUx.ux.log(stdout); core_1.CliUx.ux.log(stderr); reject(); } }); }); } test() { const results = {}; const executable = path.join(this.options.directory, 'node_modules', '.bin', this.options.cli); core_1.CliUx.ux.log(`Testing ${chalk.cyan(executable)}`); const result = process.platform === 'win32' ? (0, shelljs_1.exec)(`cmd /c "${executable}" --version`) : (0, shelljs_1.exec)(`${executable} --version`); results[this.options.cli] = result.code === 0; return results; } } Npm.STATUS_URL = 'https://status.npmjs.org/api/v2/status.json'; class Installer extends Method.Base { constructor(options) { super(options); this.options = options; this.s3 = new amazonS3_1.AmazonS3({ cli: options.cli, channel: options.channel }); } async darwin() { const pkg = `${this.options.cli}.pkg`; const url = `${this.s3.directory}/channels/${this.options.channel}/${pkg}`; const location = path.join(this.options.directory, pkg); await this.s3.download(url, location); const result = (0, shelljs_1.exec)(`sudo installer -pkg ${location} -target /`); const results = {}; if (result.code === 0) { const testResults = this.nixTest(); for (const [cli, success] of Object.entries(testResults)) { this.logResult(cli, success); } results[url] = testResults; } else { results[url] = {}; for (const cli of this.getTargets()) { this.logResult(this.options.cli, false); results[url][cli] = false; } } core_1.CliUx.ux.log(); return results; } async win32() { const executables = [`${this.options.cli}-x64.exe`, `${this.options.cli}-x86.exe`]; const results = {}; for (const exe of executables) { const url = `${this.s3.directory}/channels/${this.options.channel}/${exe}`; const location = path.join(this.options.directory, exe); await this.s3.download(url, location); const installLocation = `C:\\install-test\\${this.options.cli}\\${exe.includes('x86') ? 'x86' : 'x64'}`; const cmd = `Start-Process -Wait -FilePath "${location}" -ArgumentList "/S", "/D=${installLocation}" -PassThru`; core_1.CliUx.ux.log(`Installing ${chalk.cyan(exe)} to ${installLocation}...`); const result = (0, shelljs_1.exec)(cmd, { shell: 'powershell.exe' }); if (result.code === 0) { const testResults = this.win32Test(installLocation); for (const [cli, success] of Object.entries(testResults)) { this.logResult(cli, success); } results[url] = testResults; } else { results[url] = {}; for (const cli of this.getTargets()) { this.logResult(this.options.cli, false); results[url][cli] = false; } } } return results; } // eslint-disable-next-line @typescript-eslint/require-await async linux() { throw new Error('Installers not supported for linux.'); } async ping() { return this.s3.ping(); } win32Test(installLocation) { const results = {}; for (const cli of this.getTargets()) { const binaryPath = path.join(installLocation, 'bin', `${cli}.cmd`); core_1.CliUx.ux.log(`Testing ${chalk.cyan(binaryPath)}`); const result = (0, shelljs_1.exec)(`cmd /c "${binaryPath}" --version`); results[cli] = result.code === 0 && binaryPath.includes('x86') ? result.stdout.includes('win32-x86') : result.stdout.includes('win32-x64'); } return results; } nixTest() { const results = {}; for (const cli of this.getTargets()) { const binaryPath = `/usr/local/bin/${cli}`; core_1.CliUx.ux.log(`Testing ${chalk.cyan(binaryPath)}`); const result = (0, shelljs_1.exec)(`${binaryPath} --version`); results[cli] = result.code === 0; } return results; } } class Test extends command_1.SfdxCommand { async run() { const cli = (0, ts_types_1.ensure)(this.flags.cli); const method = (0, ts_types_1.ensure)(this.flags.method); const channel = (0, ts_types_1.ensure)(this.flags.channel); const outputFile = (0, ts_types_1.ensure)(this.flags['output-file']); const directory = await this.makeWorkingDir(cli, channel, method); core_1.CliUx.ux.log(`Working Directory: ${directory}`); core_1.CliUx.ux.log(); let results = {}; switch (method) { case 'tarball': { const tarball = new Tarball({ cli, channel, directory, method }); results = await tarball.execute(); break; } case 'npm': { const npm = new Npm({ cli, channel, directory, method }); results = await npm.execute(); break; } case 'installer': { const installer = new Installer({ cli, channel, directory, method }); results = await installer.execute(); break; } default: break; } const hasFailures = Object.values(results) .flatMap(Object.values) .some((r) => !r); if (hasFailures) process.exitCode = 1; const fileData = JSON.stringify({ status: process.exitCode ?? 0, results }, null, 2); await fs.writeFile(outputFile, fileData, { encoding: 'utf8', mode: '600', }); core_1.CliUx.ux.styledJSON(results); core_1.CliUx.ux.log(`Results written to ${outputFile}`); } async makeWorkingDir(cli, channel, method) { const tmpDir = path.join(os.tmpdir(), 'cli-install-test', cli, channel, method); // ensure that we are starting with a clean directory try { await fs.rm(tmpDir, { recursive: true, force: true }); } catch { // error means that folder doesn't exist which is okay } await fs.mkdir(tmpDir, { recursive: true }); return tmpDir; } } exports.default = Test; Test.description = messages.getMessage('description'); Test.examples = messages.getMessage('examples').split(os.EOL); Test.flagsConfig = { cli: command_1.flags.string({ description: messages.getMessage('cliFlag'), options: Object.values(types_1.CLI), char: 'c', required: true, }), method: command_1.flags.string({ description: messages.getMessage('methodFlag'), options: Object.values(Method.Type), char: 'm', required: true, }), channel: command_1.flags.string({ description: messages.getMessage('channelFlag'), options: Object.values(types_1.Channel), default: 'stable', }), 'output-file': command_1.flags.string({ description: messages.getMessage('outputFileFlag'), default: 'test-results.json', }), }; //# sourceMappingURL=test.js.map