@salesforce/plugin-release-management
Version:
A plugin for preparing and publishing npm packages
425 lines • 16.4 kB
JavaScript
"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