UNPKG

@salesforce/plugin-release-management

Version:
430 lines 16.2 kB
/* * 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 */ import path from 'node:path'; import os from 'node:os'; import fs from 'node:fs/promises'; import shelljs from 'shelljs'; import { Flags, SfCommand, Ux } from '@salesforce/sf-plugins-core'; import { Messages } from '@salesforce/core'; import { ensure } from '@salesforce/ts-types'; import got from 'got'; import chalk from 'chalk'; import { Channel, CLI } from '../../../types.js'; import { AmazonS3, download } from '../../../amazonS3.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-release-management', 'cli.install.test'); const ux = new Ux(); var Method; (function (Method) { let Type; (function (Type) { Type["INSTALLER"] = "installer"; Type["NPM"] = "npm"; Type["TARBALL"] = "tarball"; })(Type = Method.Type || (Method.Type = {})); class Base { options; static TEST_TARGETS = { [CLI.SF]: [CLI.SF], [CLI.SFDX]: [CLI.SFDX, CLI.SF], }; constructor(options) { this.options = options; } async execute() { const { service, available } = await this.ping(); if (!available) { 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 {}; } // eslint-disable-next-line class-methods-use-this async ping() { return Promise.resolve({ available: true, service: 'Service' }); } // eslint-disable-next-line class-methods-use-this logResult(cli, success) { const msg = success ? chalk.green('true') : chalk.red('false'); ux.log(`${chalk.bold(`${cli} Success`)}: ${msg}`); } getTargets() { return Base.TEST_TARGETS[this.options.cli]; } } Method.Base = Base; })(Method || (Method = {})); class Tarball extends Method.Base { options; s3; 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'], }; constructor(options) { super(options); this.options = options; this.s3 = new 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 { // eslint-disable-next-line no-await-in-loop await download(tarball, location); // eslint-disable-next-line no-await-in-loop 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; } } 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) => `${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) => { ux.spinner.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' } : {}; shelljs.exec(cmd, { ...opts, silent: true }, (code, stdout, stderr) => { if (code === 0) { ux.spinner.stop(); ux.log(stdout); resolve(dir); } else { ux.log('stdout:', stdout); ux.log('stderr:', stderr); reject(); } }); }); } test(directory) { const results = {}; for (const cli of this.getTargets()) { const executable = path.join(directory, 'bin', cli); ux.log(`Testing ${chalk.cyan(executable)}`); const result = process.platform === 'win32' ? shelljs.exec(`cmd /c "${executable}.cmd" --version`) : shelljs.exec(`${executable} --version`); results[cli] = result.code === 0; } return results; } } class Npm extends Method.Base { options; static STATUS_URL = 'https://status.npmjs.org/api/v2/status.json'; package; constructor(options) { super(options); this.options = options; const name = options.cli === CLI.SF ? '@salesforce/cli' : 'sfdx-cli'; const tag = options.channel === Channel.STABLE ? 'latest' : 'latest-rc'; this.package = `${name}@${tag}`; } async darwin() { return this.installAndTest(); } async win32() { return this.installAndTest(); } async linux() { return this.installAndTest(); } // eslint-disable-next-line class-methods-use-this 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 documentation related to what status indicators might be used and when. const response = await got.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); } ux.log(); return { [this.package]: testResults }; } async install() { ux.spinner.start(`Installing: ${chalk.cyan(this.package)}`); return new Promise((resolve, reject) => { shelljs.exec(`npm install ${this.package}`, { silent: true, cwd: this.options.directory }, (code, stdout, stderr) => { if (code === 0) { ux.spinner.stop(); ux.log(stdout); resolve(); } else { ux.spinner.stop('Failed'); ux.log(stdout); ux.log(stderr); reject(); } }); }); } test() { const results = {}; const executable = path.join(this.options.directory, 'node_modules', '.bin', this.options.cli); ux.log(`Testing ${chalk.cyan(executable)}`); const result = process.platform === 'win32' ? shelljs.exec(process.platform === 'win32' ? `cmd /c "${executable}" --version` : `${executable} --version`) : shelljs.exec(`${executable} --version`); results[this.options.cli] = result.code === 0; return results; } } class Installer extends Method.Base { options; s3; constructor(options) { super(options); this.options = options; this.s3 = new 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 download(url, location); const result = shelljs.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; } } 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); // eslint-disable-next-line no-await-in-loop await 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`; ux.log(`Installing ${chalk.cyan(exe)} to ${installLocation}...`); const result = shelljs.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, class-methods-use-this 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`); ux.log(`Testing ${chalk.cyan(binaryPath)}`); const result = shelljs.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}`; ux.log(`Testing ${chalk.cyan(binaryPath)}`); const result = shelljs.exec(`${binaryPath} --version`); results[cli] = result.code === 0; } return results; } } export default class Test extends SfCommand { static description = messages.getMessage('description'); static summary = messages.getMessage('description'); static examples = messages.getMessages('examples'); static flags = { cli: Flags.string({ summary: messages.getMessage('flags.cli.summary'), options: Object.values(CLI), char: 'c', required: true, }), method: Flags.string({ summary: messages.getMessage('flags.method.summary'), options: Object.values(Method.Type), char: 'm', required: true, }), channel: Flags.string({ summary: messages.getMessage('flags.channel.summary'), options: Object.values(Channel), default: 'stable', }), 'output-file': Flags.string({ summary: messages.getMessage('flags.output-file.summary'), default: 'test-results.json', }), }; async run() { const { flags } = await this.parse(Test); const cli = ensure(flags.cli); const method = ensure(flags.method); const channel = ensure(flags.channel); const outputFile = ensure(flags['output-file']); const directory = await makeWorkingDir(cli, channel, method); ux.log(`Working Directory: ${directory}`); ux.log(); let results = {}; switch (method) { case Method.Type.TARBALL: { const tarball = new Tarball({ cli, channel, directory, method }); results = await tarball.execute(); break; } case Method.Type.NPM: { const npm = new Npm({ cli, channel, directory, method }); results = await npm.execute(); break; } case Method.Type.INSTALLER: { const installer = new Installer({ cli, channel, directory, method }); results = await installer.execute(); 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', }); ux.styledJSON(results); ux.log(`Results written to ${outputFile}`); } } const makeWorkingDir = async (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; }; //# sourceMappingURL=test.js.map