UNPKG

salesforce-alm

Version:

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

305 lines (303 loc) 13 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 */ 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