salesforce-alm
Version:
This package contains tools, and APIs, for an improved salesforce.com developer experience.
305 lines (303 loc) • 13 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
*/
const path = require("path");
const os = require("os");
const ts_types_1 = require("@salesforce/ts-types");
const core_1 = require("@salesforce/core");
const archiver = require("archiver");
// 3pp
const BBPromise = require("bluebird");
// Local
const kit_1 = require("@salesforce/kit");
const logger = require("../core/logApi");
const almError = require("../core/almError");
const consts = require("../core/constants");
const StashApi = require("../core/stash");
const Stash = require("../core/stash");
const DeployReport = require("./mdapiDeployReportApi");
const mdApiUtil_1 = require("./mdApiUtil");
const DEPLOY_ERROR_EXIT_CODE = 1;
// convert params (lowercase) to expected deploy options (camelcase)
const convertParamsToDeployOptions = function ({ rollbackonerror, testlevel, runtests, autoUpdatePackage, checkonly, ignorewarnings, singlepackage, }) {
const deployOptions = {};
deployOptions.rollbackOnError = rollbackonerror;
if (testlevel) {
deployOptions.testLevel = testlevel;
}
if (runtests) {
deployOptions.runTests = runtests.split(',');
}
if (autoUpdatePackage) {
deployOptions.autoUpdatePackage = autoUpdatePackage;
}
if (ignorewarnings) {
deployOptions.ignoreWarnings = ignorewarnings;
}
if (checkonly) {
deployOptions.checkOnly = checkonly;
}
if (singlepackage) {
deployOptions.singlePackage = true;
}
return deployOptions;
};
/**
* API that wraps Metadata API to deploy source - directory or zip - to given org.
*
* @param force
* @constructor
*/
class MdDeployApi {
constructor(org, pollIntervalStrategy, stashTarget = StashApi.Commands.MDAPI_DEPLOY) {
this.scratchOrg = org;
this.force = org.force;
this.logger = logger.child('md-deploy');
this.timer = process.hrtime();
this._fsStatAsync = BBPromise.promisify(core_1.fs.stat);
// if source:deploy or source:push is the command, create a source report
if (stashTarget === Stash.Commands.SOURCE_DEPLOY) {
this._reporter = new DeployReport(org, pollIntervalStrategy, Stash.Commands.SOURCE_DEPLOY);
}
else {
// create the default mdapi report
this._reporter = new DeployReport(org, pollIntervalStrategy);
}
this.stashTarget = stashTarget;
}
_getElapsedTime() {
const elapsed = process.hrtime(this.timer);
this.timer = process.hrtime();
return (elapsed[0] * 1000 + elapsed[1] / 1000000).toFixed(3);
}
_zip(dir, zipfile) {
const file = path.parse(dir);
const outFile = zipfile || path.join(os.tmpdir() || '.', `${file.base}.zip`);
const output = core_1.fs.createWriteStream(outFile);
return new BBPromise((resolve, reject) => {
const archive = archiver('zip', { zlib: { level: 9 } });
archive.on('end', () => {
this.logger.debug(`${archive.pointer()} bytes written to ${outFile} using ${this._getElapsedTime()}ms`);
resolve(outFile);
});
archive.on('error', (err) => {
this._logError(err);
reject(err);
});
archive.pipe(output);
archive.directory(dir, file.base);
archive.finalize();
});
}
_log(message) {
if (this.loggingEnabled) {
this.logger.log(message);
}
}
_logError(message) {
if (this.loggingEnabled) {
this.logger.error(message);
}
}
_getMetadata({ deploydir, zipfile }) {
// either zip root dir or pass given zip filepath
return deploydir ? this._zip(deploydir, zipfile) : zipfile;
}
async _sendMetadata(zipPath, options) {
zipPath = path.resolve(zipPath);
const zipStream = this._createReadStream(zipPath);
// REST is the default unless:
// 1. SOAP is specified with the soapdeploy flag on the command
// 2. The restDeploy SFDX config setting is explicitly false.
if (await mdApiUtil_1.MetadataTransportInfo.isRestDeploy(options)) {
this._log('*** Deploying with REST ***');
return this.force.mdapiRestDeploy(this.scratchOrg, zipStream, convertParamsToDeployOptions(options));
}
else {
this._log('*** Deploying with SOAP ***');
return this.force.mdapiSoapDeploy(this.scratchOrg, zipStream, convertParamsToDeployOptions(options));
}
}
_createReadStream(zipPath) {
return core_1.fs.createReadStream(zipPath);
}
deploy(options) {
// Logging is enabled if the output is not json and logging is not disabled
// set .logginEnabled = true? for source
this.loggingEnabled = options.source || options.verbose || (!options.json && !options.disableLogging);
options.wait = +(options.wait || consts.DEFAULT_MDAPI_WAIT_MINUTES);
// ignoreerrors is a boolean flag and is preferred over the deprecated rollbackonerror flag
// KEEP THIS code for legacy with commands calling mdapiDeployApi directly.
if (options.ignoreerrors !== undefined) {
options.rollbackonerror = !options.ignoreerrors;
}
else if (options.rollbackonerror !== undefined) {
// eslint-disable-next-line no-self-assign
options.rollbackonerror = options.rollbackonerror;
}
else {
options.rollbackonerror = true;
}
if (options.validateddeployrequestid) {
return this._doDeployRecentValidation(options);
}
return this._doDeploy(options);
}
validate(context) {
const options = context.flags;
const deploydir = options.deploydir;
const zipfile = options.zipfile;
const validateddeployrequestid = options.validateddeployrequestid;
const validationPromises = [];
// 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'));
}
if (options.rollbackonerror && !ts_types_1.isBoolean(options.rollbackonerror)) {
// This should never get called since rollbackonerror is no longer an options
// but keep for legacy. We want to default to true, so anything that isn't false.
options.rollbackonerror = options.rollbackonerror.toLowerCase() !== 'false';
}
if (!(deploydir || zipfile || validateddeployrequestid)) {
return BBPromise.reject(almError('MissingRequiredParameter', 'deploydir|zipfile|validateddeployrequestid'));
}
if (validateddeployrequestid && !(validateddeployrequestid.length == 18 || validateddeployrequestid.length == 15)) {
return BBPromise.reject(almError('mdDeployCommandCliInvalidRequestIdError', validateddeployrequestid));
}
try {
mdApiUtil_1.MetadataTransportInfo.validateExclusiveFlag(options, 'deploydir', 'jobid');
mdApiUtil_1.MetadataTransportInfo.validateExclusiveFlag(options, 'zipfile', 'jobid');
mdApiUtil_1.MetadataTransportInfo.validateExclusiveFlag(options, 'checkonly', 'jobid');
mdApiUtil_1.MetadataTransportInfo.validateExclusiveFlag(options, 'rollbackonerror', 'ignoreerrors');
mdApiUtil_1.MetadataTransportInfo.validateExclusiveFlag(options, 'soapdeploy', 'jobid');
}
catch (e) {
return BBPromise.reject(e);
}
// Validate required options
if (deploydir) {
// Validate that the deploy root is a directory.
validationPromises.push(this._validateFileStat(deploydir, (fileData) => fileData.isDirectory(), BBPromise.resolve, almError('InvalidArgumentDirectoryPath', ['deploydir', deploydir])));
}
else if (zipfile) {
// Validate that the zipfile is a file.
validationPromises.push(this._validateFileStat(zipfile, (fileData) => fileData.isFile(), BBPromise.resolve, almError('InvalidArgumentFilePath', ['zipfile', zipfile])));
}
return BBPromise.all(validationPromises).then(() => BBPromise.resolve(options));
}
// Accepts:
// pathToValidate: a file path to validate
// validationFunc: function that is called with the result of a fs.stat(), should return true or false
// successFunc: function that returns a promise.
// error: an Error object that will be thrown if the validationFunc returns false.
// Returns:
// Successfull Validation: The result of a call to successFunc.
// Failed Validation: A rejected promise with the specified error, or a PathDoesNotExist
// error if the file read fails.
_validateFileStat(pathToValidate, validationFunc, successFunc, error) {
return this._fsStatAsync(pathToValidate)
.then((data) => {
if (validationFunc(data)) {
return successFunc();
}
else {
return BBPromise.reject(error);
}
})
.catch((err) => {
err = err.code === 'ENOENT' ? almError('PathDoesNotExist', pathToValidate) : err;
return BBPromise.reject(err);
});
}
async _doDeployStatus(result, options) {
options.deprecatedStatusRequest = options.jobid ? true : false;
options.jobid = options.jobid || result.id;
if (await mdApiUtil_1.MetadataTransportInfo.isRestDeployWithWaitZero(options)) {
options.result = result.deployResult;
}
else {
options.result = result;
options.result.status = options.result.state;
}
return this._reporter.report(options);
}
async _setStashVars(result, options) {
await StashApi.setValues({
jobid: result.id,
targetusername: options.targetusername,
}, this.stashTarget);
return result;
}
async _doDeploy(options) {
try {
const zipPath = await this._getMetadata(options);
let sendMetadataResponse;
if (!options.jobid) {
sendMetadataResponse = await this._sendMetadata(zipPath, options);
}
const stashedVars = await this._setStashVars(sendMetadataResponse, options);
const deployStats = await this._doDeployStatus(stashedVars, options);
return await this._throwErrorIfDeployFailed(deployStats);
}
catch (err) {
if (err.name === 'sf:MALFORMED_ID') {
throw almError('mdDeployCommandCliInvalidJobIdError', options.jobid);
}
else if (options.testlevel !== 'NoTestRun' &&
ts_types_1.getObject(err, 'result.details.runTestResult') &&
ts_types_1.getString(err, 'result.details.runTestResult.numFailures') !== '0') {
// if the deployment was running tests, and there were test failures
err.name = 'testFailure';
throw err;
}
else {
throw err;
}
}
}
async _doDeployRecentValidation(options) {
let result;
try {
if (!options.jobid) {
const body = await mdApiUtil_1.mdapiDeployRecentValidation(this.scratchOrg, options);
result = {};
result.id = body;
result.state = 'Queued';
}
result = await this._setStashVars(result, options);
result = await this._doDeployStatus(result, options);
this._throwErrorIfDeployFailed(result);
return result;
}
catch (err) {
if (err.name === 'sf:MALFORMED_ID') {
throw almError('mdDeployCommandCliInvalidJobIdError', options.jobid);
}
else {
throw err;
}
}
}
_throwErrorIfDeployFailed(result) {
if (['Failed', 'Canceled'].includes(result.status)) {
const err = result.status === 'Canceled' ? almError('mdapiDeployCanceled') : 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;
}
_minToMs(min) {
return min * 60000;
}
}
module.exports = MdDeployApi;
//# sourceMappingURL=mdapiDeployApi.js.map