UNPKG

salesforce-alm

Version:

This package contains tools, and APIs, for an improved salesforce.com developer experience.

406 lines (404 loc) 18.3 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 */ // 3pp const util = require("util"); const BBPromise = require("bluebird"); const _ = require("lodash"); const cli_ux_1 = require("cli-ux"); // Local const kit_1 = require("@salesforce/kit"); const core_1 = require("@salesforce/core"); const logger = require("../core/logApi"); const almError = require("../core/almError"); const Messages = require("../messages"); const messages = Messages(); const consts = require("../core/constants"); const Stash = require("../core/stash"); const CheckStatus = require("./mdapiCheckStatusApi"); // for messages in FCT/messages/ core_1.Messages.importMessagesDirectory(__dirname); const DEPLOY_ERROR_EXIT_CODE = 1; /** * API that wraps Metadata API to deploy source - directory or zip - to given org. * * @param force * @constructor */ class MdDeployReportApi { constructor(org, pollIntervalStrategy, stashkey = Stash.Commands.MDAPI_DEPLOY) { this.stashkey = stashkey; this.scratchOrg = org; this.logger = logger.child('md-deploy-report'); this._print = this._print.bind(this); this.pollIntervalStrategy = pollIntervalStrategy; // if SFDX_USE_PROGRESS_BAR is true use progress bar, if not use old output this.useProgressBar = kit_1.env.getBoolean('SFDX_USE_PROGRESS_BAR', true); this.progressBar = cli_ux_1.default.progress({ format: `${stashkey.split('_')[0]} PROGRESS | {bar} | {value}/{total} Components`, barCompleteChar: '\u2588', barIncompleteChar: '\u2591', linewrap: true, }); } _log(message) { if (this.loggingEnabled) { this.logger.log(message); } } _logError(message) { if (this.loggingEnabled) { this.logger.error(message); } } _printComponentFailures(result) { var _a; if ((_a = result === null || result === void 0 ? void 0 : result.details) === null || _a === void 0 ? void 0 : _a.componentFailures) { if (!util.isArray(result.details.componentFailures)) { result.details.componentFailures = [result.details.componentFailures]; } if (this.useProgressBar) { this.progressBar.stop(); } // sort by filename then fullname const failures = _.chain(result.details.componentFailures) .sortBy([ function (o) { return o.fileName ? o.fileName.toUpperCase() : o.fileName; }, ]) .sortBy([ function (o) { return o.fullName ? o.fullName.toUpperCase() : o.fullName; }, ]) .value(); this.logger.log(''); this.logger.styledHeader(this.logger.color.red(`Component Failures [${failures.length}]`)); this.logger.table(failures, { columns: [ { key: 'problemType', label: 'Type' }, { key: 'fileName', label: 'File' }, { key: 'fullName', label: 'Name' }, { key: 'problem', label: 'Problem' }, ], }); this.logger.log(''); } } _printTests(result) { if (result.details && result.details.runTestResult) { if (result.details.runTestResult.failures) { if (!util.isArray(result.details.runTestResult.failures)) { result.details.runTestResult.failures = [result.details.runTestResult.failures]; } const tests = _.chain(result.details.runTestResult.failures) .sortBy([ function (o) { return o.methodName.toUpperCase(); }, ]) .sortBy([ function (o) { return o.name.toUpperCase(); }, ]) .value(); this.progressBar.update(result.numberComponentsDeployed + result.numberTestsCompleted); this._log(''); this.logger.styledHeader(this.logger.color.red(`Test Failures [${result.details.runTestResult.numFailures}]`)); this.logger.table(tests, { columns: [ { key: 'name', label: 'Name' }, { key: 'methodName', label: 'Method' }, { key: 'message', label: 'Message' }, { key: 'stackTrace', label: 'Stacktrace' }, ], }); } if (result.details && result.details.runTestResult.successes) { if (!util.isArray(result.details.runTestResult.successes)) { result.details.runTestResult.successes = [result.details.runTestResult.successes]; } const tests = _.chain(result.details.runTestResult.successes) .sortBy([ function (o) { return o.methodName.toUpperCase(); }, ]) .sortBy([ function (o) { return o.name.toUpperCase(); }, ]) .value(); this.progressBar.update(result.numberComponentsDeployed + result.numberTestsCompleted); this._log(''); this.logger.styledHeader(this.logger.color.green(`Test Success [${result.details.runTestResult.successes.length}]`)); this.logger.table(tests, { columns: [ { key: 'name', label: 'Name' }, { key: 'methodName', label: 'Method' }, ], }); } if (result.details && result.details.runTestResult.codeCoverage) { if (!util.isArray(result.details.runTestResult.codeCoverage)) { result.details.runTestResult.codeCoverage = [result.details.runTestResult.codeCoverage]; } const coverage = _.chain(result.details.runTestResult.codeCoverage) .sortBy([ function (o) { return o.name.toUpperCase(); }, ]) .value(); this._log(''); this.logger.styledHeader(this.logger.color.blue('Apex Code Coverage')); this.logger.table(coverage, { columns: [ { key: 'name', label: 'Name' }, { key: 'numLocations', label: '% Covered', format: (numLocations, row) => { numLocations = parseInt(numLocations); const numLocationsNotCovered = parseInt(row.numLocationsNotCovered); let color = this.logger.color.green; // Is 100% too high of a bar? if (numLocationsNotCovered > 0) { color = this.logger.color.red; } let pctCovered = 100; const coverageDecimal = parseFloat(((numLocations - numLocationsNotCovered) / numLocations).toFixed(2)); if (numLocations > 0) { pctCovered = coverageDecimal * 100; } return color(`${pctCovered}%`); }, }, { key: 'locationsNotCovered', label: 'Uncovered Lines', format: (locationsNotCovered) => { if (!locationsNotCovered) { return ''; } if (!util.isArray(locationsNotCovered)) { locationsNotCovered = [locationsNotCovered]; } const uncoveredLines = []; locationsNotCovered.forEach((uncoveredLine) => { uncoveredLines.push(uncoveredLine.line); }); return uncoveredLines.join(','); }, }, ], }); } if (result.details.runTestResult.successes || result.details.runTestResult.failures) { this._log(''); this._log(`Total Test Time: ${result.details.runTestResult.totalTime}`); } } } _printComponentSuccess(result, options) { if (options.verbose && result.details && result.details.componentSuccesses) { if (!util.isArray(result.details.componentSuccesses)) { result.details.componentSuccesses = [result.details.componentSuccesses]; } if (result.details.componentSuccesses.length > 0) { // sort by type then filename then fullname const files = result.details.componentSuccesses.length > 0 ? _.chain(result.details.componentSuccesses) .sortBy([ function (o) { return o.fullName ? o.fullName.toUpperCase() : o.fullName; }, ]) .sortBy([ function (o) { return o.fileName ? o.fileName.toUpperCase() : o.fileName; }, ]) .sortBy([ function (o) { return o.componentType ? o.componentType.toUpperCase() : o.componentType; }, ]) .value() : []; this._log(''); this.logger.styledHeader(this.logger.color.blue(`Components Deployed [${result.numberComponentsDeployed}]`)); this.logger.table(files, { columns: [ { key: 'componentType', label: 'Type' }, { key: 'fileName', label: 'File' }, { key: 'fullName', label: 'Name' }, { key: 'id', label: 'Id' }, ], }); } } } _print(options, result) { if (this.loggingEnabled && !options.json) { if (this.useProgressBar) { const total = result.numberComponentsTotal + result.numberTestsTotal; const actionsDone = result.numberComponentsDeployed + result.numberTestsCompleted; // If the metadata deploy isn't picked up yet, there will be no total of components or tests yet so we // need to start the progressBar again. We shouldn't do it every time because it resets the time on the // progressBar. Also, the total can change when a deploy ha succeeded... for whatever reason. if (this.progressBar.isActive && total === this.progressBar.total) { // Don't update the progress bar if nothing has changed. Removes unnecessary noise in non-tty environments if (actionsDone !== this.progressBar.value) { this.progressBar.update(actionsDone); } } else { this.progressBar.start(total, actionsDone); } } else { this._printOldOutput(result); } if (result.timedOut) { if (this.useProgressBar) { this.progressBar.stop(); } this._log(messages.getMessage('mdDeployCommandCliWaitTimeExceededError', [options.wait])); return result; } } if (result.completedDate) { /** This should be called only for mdapi:deploy. The result of source:deploy is handled differently*/ if (this.stashkey === 'MDAPI_DEPLOY') { this._printComponentSuccess(result, options); this._printComponentFailures(result); } // source:deploy handles success and errors separately, it doesn't handle test output this._printTests(result); if (options.checkonly) { this._printComponentFailures(result); if (!result.numberComponentErrors) { const coreMessages = core_1.Messages.loadMessages('salesforce-alm', 'source_deploy'); this._log(coreMessages.getMessage('sourceDeployCheckOnlySuccess')); } } if (this.useProgressBar) { this.progressBar.stop(); } } return result; } report(options) { // Logging is enabled if the output is not json and logging is not disabled this.loggingEnabled = options.source || options.verbose || (!options.json && !options.disableLogging); options.wait = +(options.wait || consts.DEFAULT_MDAPI_WAIT_MINUTES); if (this.useProgressBar) { this._log(`Job ID | ${options.jobid}`); } return BBPromise.resolve() .then(() => this._doDeployStatus(options)) .then((result) => this._throwErrorIfDeployFailed(result)) .catch((err) => { if (err.name === 'sf:MALFORMED_ID') { throw almError('mdDeployCommandCliInvalidJobIdError', options.jobid); } else { throw err; } }); } async validate(context) { const options = context.flags; const stashedValues = await Stash.list(this.stashkey); if (!options.jobid) { options.jobid = options.jobid || stashedValues.jobid; } if (!options.jobid) { return BBPromise.reject(almError('MissingRequiredParameter', 'jobid')); } // Wait must be a number that is greater than zero or equal to -1. const validWaitValue = !isNaN(+options.wait) && (+options.wait === -1 || +options.wait >= 0); if (options.wait && !validWaitValue) { return BBPromise.reject(almError('mdapiCliInvalidWaitError')); } return BBPromise.resolve(options); } _doDeployStatus(options) { const jobid = options.jobid; const org = this.scratchOrg; if (options.result && options.wait == 0 && !options.deprecatedStatusRequest) { // this will always be a timeout condition since we never call CheckStatus.handleStatus() options.result.timedOut = true; this._print(options, options.result); return options.result; } return new CheckStatus(options.wait, consts.DEFAULT_MDAPI_POLL_INTERVAL_MILLISECONDS, this._print.bind(this, options), org.force.mdapiCheckDeployStatus.bind(org.force, org, jobid), this.pollIntervalStrategy) .handleStatus() .then((res) => { if (this.useProgressBar) { this.progressBar.stop(); } return res; }); } _throwErrorIfDeployFailed(result) { if (result.status === 'Failed') { const err = almError('mdapiDeployFailed'); this._setExitCode(DEPLOY_ERROR_EXIT_CODE); kit_1.set(err, 'result', result); return BBPromise.reject(err); } return BBPromise.resolve(result); } _setExitCode(code) { process.exitCode = code; } _printOldOutput(result) { const deployStart = new Date(result.createdDate).getTime(); const deployEnd = new Date(result.completedDate).getTime(); const totalDeployTime = deployEnd - deployStart; const processingHeader = this.logger.color.yellow('Status'); const successHeader = this.logger.color.green('Result'); const failureHeader = this.logger.color.red('Result'); this._log(''); if (!result.done) { this.logger.styledHeader(processingHeader); } else { if (result.completedDate) { this._log(`Deployment finished in ${totalDeployTime}ms`); } this._log(''); const header = result.success ? successHeader : failureHeader; this.logger.styledHeader(header); } this._log(''); this._log(`Status: ${result.status}`); this._log(`jobid: ${result.id}`); if (result.status !== 'Queued') { const successfulComponentsMessage = result.checkOnly ? `Components checked: ${result.numberComponentsDeployed}` : `Components deployed: ${result.numberComponentsDeployed}`; if (result.completedDate) { this._log(`Completed: ${result.completedDate}`); } this._log(`Component errors: ${result.numberComponentErrors}`); this._log(successfulComponentsMessage); this._log(`Components total: ${result.numberComponentsTotal}`); this._log(`Tests errors: ${result.numberTestErrors}`); this._log(`Tests completed: ${result.numberTestsCompleted}`); this._log(`Tests total: ${result.numberTestsTotal}`); this._log(`Check only: ${result.checkOnly}`); } } } module.exports = MdDeployReportApi; //# sourceMappingURL=mdapiDeployReportApi.js.map