diff --git a/README.md b/README.md index 37823f534..9703e3f5c 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ sfdx plugins # Usage + ```sh-session $ npm install -g @salesforce/plugin-source $ sfdx COMMAND @@ -86,16 +87,18 @@ USAGE $ sfdx COMMAND ... ``` + # Commands -* [`sfdx force:source:convert [-r ] [-d ] [-n ] [-p | -x | -m ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourceconvert--r-directory--d-directory--n-string--p-array---x-string---m-array---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) -* [`sfdx force:source:deploy [--soapdeploy] [-w ] [-q | -x | -m | -p | -c | -l NoTestRun|RunSpecifiedTests|RunLocalTests|RunAllTestsInOrg | -r | -o | -g] [-u ] [--apiversion ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeploy---soapdeploy--w-minutes--q-id---x-filepath---m-array---p-array---c---l-notestrunrunspecifiedtestsrunlocaltestsrunalltestsinorg---r-array---o---g--u-string---apiversion-string---verbose---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) -* [`sfdx force:source:deploy:cancel [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeploycancel--w-minutes--i-id--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) -* [`sfdx force:source:deploy:report [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeployreport--w-minutes--i-id--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) -* [`sfdx force:source:retrieve [-p | -x | -m ] [-w ] [-n ] [-u ] [-a ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourceretrieve--p-array---x-filepath---m-array--w-minutes--n-array--u-string--a-string---verbose---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) + +- [`sfdx force:source:convert [-r ] [-d ] [-n ] [-p | -x | -m ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourceconvert--r-directory--d-directory--n-string--p-array---x-string---m-array---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +- [`sfdx force:source:deploy [--soapdeploy] [-w ] [-q | -x | -m | -p | -c | -l NoTestRun|RunSpecifiedTests|RunLocalTests|RunAllTestsInOrg | -r | -o | -g] [-u ] [--apiversion ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeploy---soapdeploy--w-minutes--q-id---x-filepath---m-array---p-array---c---l-notestrunrunspecifiedtestsrunlocaltestsrunalltestsinorg---r-array---o---g--u-string---apiversion-string---verbose---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +- [`sfdx force:source:deploy:cancel [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeploycancel--w-minutes--i-id--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +- [`sfdx force:source:deploy:report [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourcedeployreport--w-minutes--i-id--u-string---apiversion-string---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) +- [`sfdx force:source:retrieve [-p | -x | -m ] [-w ] [-n ] [-u ] [-a ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]`](#sfdx-forcesourceretrieve--p-array---x-filepath---m-array--w-minutes--n-array--u-string--a-string---verbose---json---loglevel-tracedebuginfowarnerrorfataltracedebuginfowarnerrorfatal) ## `sfdx force:source:convert [-r ] [-d ] [-n ] [-p | -x | -m ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL]` @@ -105,7 +108,7 @@ convert source into Metadata API format convert source into Metadata API format USAGE - $ sfdx force:source:convert [-r ] [-d ] [-n ] [-p | -x | -m ] + $ sfdx force:source:convert [-r ] [-d ] [-n ] [-p | -x | -m ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -148,8 +151,8 @@ deploy source to an org deploy source to an org USAGE - $ sfdx force:source:deploy [--soapdeploy] [-w ] [-q | -x | -m | -p | -c | -l - NoTestRun|RunSpecifiedTests|RunLocalTests|RunAllTestsInOrg | -r | -o | -g] [-u ] [--apiversion + $ sfdx force:source:deploy [--soapdeploy] [-w ] [-q | -x | -m | -p | -c | -l + NoTestRun|RunSpecifiedTests|RunLocalTests|RunAllTestsInOrg | -r | -o | -g] [-u ] [--apiversion ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -223,7 +226,7 @@ cancel a source deployment cancel a source deployment USAGE - $ sfdx force:source:deploy:cancel [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel + $ sfdx force:source:deploy:cancel [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -261,7 +264,7 @@ check the status of a metadata deployment check the status of a metadata deployment USAGE - $ sfdx force:source:deploy:report [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel + $ sfdx force:source:deploy:report [-w ] [-i ] [-u ] [--apiversion ] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -304,7 +307,7 @@ retrieve source from an org retrieve source from an org USAGE - $ sfdx force:source:retrieve [-p | -x | -m ] [-w ] [-n ] [-u ] [-a + $ sfdx force:source:retrieve [-p | -x | -m ] [-w ] [-n ] [-u ] [-a ] [--verbose] [--json] [--loglevel trace|debug|info|warn|error|fatal|TRACE|DEBUG|INFO|WARN|ERROR|FATAL] OPTIONS @@ -350,4 +353,5 @@ EXAMPLES ``` _See code: [src/commands/force/source/retrieve.ts](https://github.com/salesforcecli/plugin-source/blob/v0.0.18/src/commands/force/source/retrieve.ts)_ + diff --git a/command-snapshot.json b/command-snapshot.json index dd4119839..df07390f2 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -34,7 +34,7 @@ { "command": "force:source:deploy:report", "plugin": "@salesforce/plugin-source", - "flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "wait"] + "flags": ["apiversion", "jobid", "json", "loglevel", "targetusername", "wait", "verbose"] }, { "command": "force:source:retrieve", diff --git a/messages/deploy.json b/messages/deploy.json index 359795567..391873ab9 100644 --- a/messages/deploy.json +++ b/messages/deploy.json @@ -30,5 +30,8 @@ "checkOnlySuccess": "Successfully validated the deployment. %s components deployed and %s tests run.\nUse the --verbose parameter to see detailed output.", "MissingDeployId": "No deploy ID was provided or found in deploy history", "deployCanceled": "The deployment has been canceled by %s", - "deployFailed": "Deploy failed." + "deployFailed": "Deploy failed.", + "asyncDeployQueued": "Deploy has been queued.", + "asyncDeployCancel": "Run sfdx force:source:deploy:cancel -i %s to cancel the deploy.", + "asyncDeployReport": "Run sfdx force:source:deploy:report -i %s to get the latest status." } diff --git a/messages/report.json b/messages/report.json index 7d7c45a67..e16f9c2dc 100644 --- a/messages/report.json +++ b/messages/report.json @@ -13,6 +13,7 @@ ], "flags": { "jobid": "job ID of the deployment you want to check; defaults to your most recent CLI deployment if not specified", - "wait": "wait time for command to finish in minutes" + "wait": "wait time for command to finish in minutes", + "verbose": "verbose output of deploy result" } } diff --git a/package.json b/package.json index 344cac92e..44b2f6dd8 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "@salesforce/plugin-source", "description": "Commands to interact with source formatted metadata", - "version": "0.0.19", + "version": "0.1.19", "author": "Salesforce", "bugs": "https://github.com/forcedotcom/cli/issues", "dependencies": { "@oclif/config": "^1", "@salesforce/command": "^3.1.3", "@salesforce/core": "^2.23.4", - "@salesforce/source-deploy-retrieve": "^2.1.5", + "@salesforce/source-deploy-retrieve": "^3.0.0", "chalk": "^4.1.0", "cli-ux": "^5.5.1", "tslib": "^2" diff --git a/src/commands/force/source/convert.ts b/src/commands/force/source/convert.ts index 5519542a5..948e6732a 100644 --- a/src/commands/force/source/convert.ts +++ b/src/commands/force/source/convert.ts @@ -80,7 +80,7 @@ export class Convert extends SourceCommand { paths.push(this.project.getDefaultPackage().path); } - const cs = await ComponentSetBuilder.build({ + this.componentSet = await ComponentSetBuilder.build({ sourcepath: paths, manifest: manifest && { manifestPath: this.getFlag('manifest'), @@ -93,7 +93,7 @@ export class Convert extends SourceCommand { }); const converter = new MetadataConverter(); - this.convertResult = await converter.convert(cs.getSourceComponents().toArray(), 'metadata', { + this.convertResult = await converter.convert(this.componentSet.getSourceComponents().toArray(), 'metadata', { type: 'directory', outputDirectory: this.getFlag('outputdir'), packageName: this.getFlag('packagename'), diff --git a/src/commands/force/source/deploy.ts b/src/commands/force/source/deploy.ts index 965af27c6..6288eee2c 100644 --- a/src/commands/force/source/deploy.ts +++ b/src/commands/force/source/deploy.ts @@ -8,18 +8,15 @@ import * as os from 'os'; import { flags, FlagsConfig } from '@salesforce/command'; import { Messages } from '@salesforce/core'; -import { DeployResult, MetadataApiDeploy } from '@salesforce/source-deploy-retrieve'; +import { AsyncResult, DeployResult, MetadataApiDeploy } from '@salesforce/source-deploy-retrieve'; import { Duration } from '@salesforce/kit'; import { getString, isString } from '@salesforce/ts-types'; -import { env } from '@salesforce/kit'; +import { env, once } from '@salesforce/kit'; import { RequestStatus } from '@salesforce/source-deploy-retrieve/lib/src/client/types'; import { DeployCommand } from '../../../deployCommand'; import { ComponentSetBuilder } from '../../../componentSetBuilder'; -import { - DeployResultFormatter, - DeployCommandResult, - DeployCommandAsyncResult, -} from '../../../formatters/deployResultFormatter'; +import { DeployResultFormatter, DeployCommandResult } from '../../../formatters/deployResultFormatter'; +import { DeployAsyncResultFormatter, DeployCommandAsyncResult } from '../../../formatters/deployAsyncResultFormatter'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-source', 'deploy'); @@ -102,6 +99,12 @@ export class Deploy extends DeployCommand { private isAsync = false; private isRest = false; + private asyncDeployResult: AsyncResult; + + private updateDeployId = once((id) => { + this.displayDeployId(id); + this.setStash(id); + }); public async run(): Promise { await this.deploy(); @@ -114,15 +117,15 @@ export class Deploy extends DeployCommand { // 2. asynchronous - deploy metadata and immediately return. // 3. recent validation - deploy metadata that's already been validated by the org protected async deploy(): Promise { - this.isAsync = this.getFlag('wait').quantity === 0; + const waitDuration = this.getFlag('wait'); + this.isAsync = waitDuration.quantity === 0; this.isRest = await this.isRestDeploy(); this.ux.log(`*** Deploying with ${this.isRest ? 'REST' : 'SOAP'} API ***`); if (this.flags.validateddeployrequestid) { this.deployResult = await this.deployRecentValidation(); } else { - // the deployment involves a component set - const cs = await ComponentSetBuilder.build({ + this.componentSet = await ComponentSetBuilder.build({ apiversion: this.getFlag('apiversion'), sourcepath: this.getFlag('sourcepath'), manifest: this.flags.manifest && { @@ -135,57 +138,63 @@ export class Deploy extends DeployCommand { }, }); // fire predeploy event for sync and async deploys - await this.lifecycle.emit('predeploy', cs.toArray()); - if (this.isAsync) { - // This is an async deploy. We just kick off the request. - throw Error('ASYNC DEPLOYS NOT IMPLEMENTED YET'); - } else { - const deploy = cs.deploy({ - usernameOrConnection: this.org.getUsername(), - apiOptions: { - ignoreWarnings: this.getFlag('ignorewarnings', false), - rollbackOnError: !this.getFlag('ignoreerrors', false), - checkOnly: this.getFlag('checkonly', false), - runTests: this.getFlag('runtests'), - testLevel: this.getFlag('testlevel', 'NoTestRun'), - }, - }); + await this.lifecycle.emit('predeploy', this.componentSet.toArray()); + const deploy = await this.componentSet.deploy({ + usernameOrConnection: this.org.getUsername(), + apiOptions: { + ignoreWarnings: this.getFlag('ignorewarnings', false), + rollbackOnError: !this.getFlag('ignoreerrors', false), + checkOnly: this.getFlag('checkonly', false), + runTests: this.getFlag('runtests'), + testLevel: this.getFlag('testlevel'), + rest: this.isRest, + }, + }); + this.asyncDeployResult = { id: deploy.id }; + this.updateDeployId(deploy.id); + + if (!this.isAsync) { // if SFDX_USE_PROGRESS_BAR is unset or true (default true) AND we're not print JSON output if (env.getBoolean('SFDX_USE_PROGRESS_BAR', true) && !this.isJsonOutput()) { this.initProgressBar(); this.progress(deploy); } - - this.deployResult = await deploy.start(); + this.deployResult = await deploy.pollStatus(500, waitDuration.seconds); } } - await this.lifecycle.emit('postdeploy', this.deployResult); - - const deployId = getString(this.deployResult, 'response.id'); - if (deployId) { - this.displayDeployId(deployId); - const file = this.getStash(); - // TODO: I think we should stash the ID as soon as we know it. - this.logger.debug(`Stashing deploy ID: ${deployId}`); - await file.write({ [DeployCommand.STASH_KEY]: { jobid: deployId } }); + if (this.deployResult) { + // Only fire the postdeploy event when we have results. I.e., not async. + await this.lifecycle.emit('postdeploy', this.deployResult); } } + /** + * Checks the response status to determine whether the deploy was successful. + * Async deploys are successful unless an error is thrown, which resolves as + * unsuccessful in oclif. + */ protected resolveSuccess(): void { - const status = getString(this.deployResult, 'response.status'); - if (status !== RequestStatus.Succeeded) { - this.setExitCode(1); + if (!this.isAsync) { + const status = getString(this.deployResult, 'response.status'); + if (status !== RequestStatus.Succeeded) { + this.setExitCode(1); + } } } protected formatResult(): DeployCommandResult | DeployCommandAsyncResult { const formatterOptions = { verbose: this.getFlag('verbose', false), - async: this.isAsync, }; - const formatter = new DeployResultFormatter(this.logger, this.ux, formatterOptions, this.deployResult); + + let formatter: DeployAsyncResultFormatter | DeployResultFormatter; + if (this.isAsync) { + formatter = new DeployAsyncResultFormatter(this.logger, this.ux, formatterOptions, this.asyncDeployResult); + } else { + formatter = new DeployResultFormatter(this.logger, this.ux, formatterOptions, this.deployResult); + } // Only display results to console when JSON flag is unset. if (!this.isJsonOutput()) { @@ -199,15 +208,8 @@ export class Deploy extends DeployCommand { const conn = this.org.getConnection(); const id = this.getFlag('validateddeployrequestid'); - // TODO: This is an async call so we need to poll unless `--wait 0` - // See mdapiCheckStatusApi.ts for the toolbelt polling impl. const response = await conn.deployRecentValidation({ id, rest: this.isRest }); - if (!this.isAsync) { - // Remove this and add polling if we need to poll in the plugin. - throw Error('deployRecentValidation polling not yet implemented'); - } - // This is the deploy ID of the deployRecentValidation response, not // the already validated deploy ID (i.e., validateddeployrequestid). let validatedDeployId: string; @@ -218,26 +220,27 @@ export class Deploy extends DeployCommand { // REST API validatedDeployId = (response as { id: string }).id; } + this.updateDeployId(validatedDeployId); - return this.report(validatedDeployId); + return this.isAsync ? this.report(validatedDeployId) : this.poll(validatedDeployId); } private progress(deploy: MetadataApiDeploy): void { - let started = false; + const startProgressBar = once((componentTotal: number) => { + this.progressBar.start(componentTotal); + }); + deploy.onUpdate((data) => { // the numCompTot. isn't computed right away, wait to start until we know how many we have - if (data.numberComponentsTotal && !started) { - this.displayDeployId(data.id); - this.progressBar.start(data.numberComponentsTotal + data.numberTestsTotal); - started = true; + if (data.numberComponentsTotal) { + startProgressBar(data.numberComponentsTotal + data.numberTestsTotal); + this.progressBar.update(data.numberComponentsDeployed + data.numberTestsCompleted); } // the numTestsTot. isn't computed until validated as tests by the server, update the PB once we know - if (data.numberTestsTotal) { + if (data.numberTestsTotal && data.numberComponentsTotal) { this.progressBar.setTotal(data.numberComponentsTotal + data.numberTestsTotal); } - - this.progressBar.update(data.numberComponentsDeployed + data.numberTestsCompleted); }); // any thing else should stop the progress bar diff --git a/src/commands/force/source/deploy/report.ts b/src/commands/force/source/deploy/report.ts index 611b260be..12c2aae58 100644 --- a/src/commands/force/source/deploy/report.ts +++ b/src/commands/force/source/deploy/report.ts @@ -6,7 +6,7 @@ */ import * as os from 'os'; -import { Messages } from '@salesforce/core'; +import { Messages, SfdxProject } from '@salesforce/core'; import { flags, FlagsConfig } from '@salesforce/command'; import { Duration } from '@salesforce/kit'; import { DeployCommand } from '../../../../deployCommand'; @@ -14,6 +14,7 @@ import { DeployReportCommandResult, DeployReportResultFormatter, } from '../../../../formatters/deployReportResultFormatter'; +import { ComponentSetBuilder } from '../../../../componentSetBuilder'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-source', 'report'); @@ -33,6 +34,9 @@ export class Report extends DeployCommand { char: 'i', description: messages.getMessage('flags.jobid'), }), + verbose: flags.builtin({ + description: messages.getMessage('flags.verbose'), + }), }; public async run(): Promise { @@ -43,6 +47,20 @@ export class Report extends DeployCommand { protected async doReport(): Promise { const deployId = this.resolveDeployId(this.getFlag('jobid')); + + // If the verbose flag is set, AND the command was executed from within + // an SFDX project, we need to build a ComponentSet so we have mapped + // source file output. + if (this.getFlag('verbose')) { + let sourcepath: string[]; + try { + this.project = await SfdxProject.resolve(); + sourcepath = this.project.getUniquePackageDirectories().map((pDir) => pDir.fullPath); + } catch (err) { + // ignore the error. this was just to get improved command output. + } + this.componentSet = await ComponentSetBuilder.build({ sourcepath }); + } this.deployResult = await this.report(deployId); } @@ -54,7 +72,11 @@ export class Report extends DeployCommand { protected resolveSuccess(): void {} protected formatResult(): DeployReportCommandResult { - const formatter = new DeployReportResultFormatter(this.logger, this.ux, {}, this.deployResult); + const formatterOptions = { + verbose: this.getFlag('verbose', false), + }; + const formatter = new DeployReportResultFormatter(this.logger, this.ux, formatterOptions, this.deployResult); + if (!this.isJsonOutput()) { formatter.display(); } diff --git a/src/commands/force/source/retrieve.ts b/src/commands/force/source/retrieve.ts index 3acefeedd..4bb1b54e3 100644 --- a/src/commands/force/source/retrieve.ts +++ b/src/commands/force/source/retrieve.ts @@ -69,7 +69,7 @@ export class Retrieve extends SourceCommand { } protected async retrieve(): Promise { - const cs = await ComponentSetBuilder.build({ + this.componentSet = await ComponentSetBuilder.build({ apiversion: this.getFlag('apiversion'), packagenames: this.getFlag('packagenames'), sourcepath: this.getFlag('sourcepath'), @@ -83,16 +83,15 @@ export class Retrieve extends SourceCommand { }, }); - await this.lifecycle.emit('preretrieve', cs.toArray()); + await this.lifecycle.emit('preretrieve', this.componentSet.toArray()); - this.retrieveResult = await cs - .retrieve({ - usernameOrConnection: this.org.getUsername(), - merge: true, - output: this.project.getDefaultPackage().fullPath, - packageNames: this.getFlag('packagenames'), - }) - .start(); + const mdapiRetrieve = await this.componentSet.retrieve({ + usernameOrConnection: this.org.getUsername(), + merge: true, + output: this.project.getDefaultPackage().fullPath, + packageNames: this.getFlag('packagenames'), + }); + this.retrieveResult = await mdapiRetrieve.pollStatus(1000, this.getFlag('wait').seconds); await this.lifecycle.emit('postretrieve', this.retrieveResult.response); } diff --git a/src/deployCommand.ts b/src/deployCommand.ts index d418d83af..3ff20fd52 100644 --- a/src/deployCommand.ts +++ b/src/deployCommand.ts @@ -9,12 +9,18 @@ import { ComponentSet, DeployResult } from '@salesforce/source-deploy-retrieve'; import { SfdxError, ConfigFile, ConfigAggregator, PollingClient, StatusResult } from '@salesforce/core'; import { MetadataApiDeployStatus } from '@salesforce/source-deploy-retrieve/lib/src/client/types'; import { AnyJson, asString, getBoolean } from '@salesforce/ts-types'; -import { Duration } from '@salesforce/kit'; +import { Duration, once } from '@salesforce/kit'; import { SourceCommand } from './sourceCommand'; export abstract class DeployCommand extends SourceCommand { protected static readonly STASH_KEY = 'SOURCE_DEPLOY'; - protected deployIdDisplayed = false; + + protected displayDeployId = once((id: string) => { + if (!this.isJsonOutput()) { + this.ux.log(`Deploy ID: ${id}`); + } + }); + protected deployResult: DeployResult; /** @@ -30,23 +36,26 @@ export abstract class DeployCommand extends SourceCommand { const res = await this.org.getConnection().metadata.checkDeployStatus(deployId, true); const deployStatus = res as unknown as MetadataApiDeployStatus; - return new DeployResult(deployStatus, new ComponentSet()); + const componentSet = this.componentSet || new ComponentSet(); + return new DeployResult(deployStatus, componentSet); } - protected getStash(): ConfigFile<{ isGlobal: true; filename: 'stash.json' }> { - return new ConfigFile({ isGlobal: true, filename: 'stash.json' }); + protected setStash(deployId: string): void { + const file = this.getStash(); + this.logger.debug(`Stashing deploy ID: ${deployId} in ${file.getPath()}`); + file.writeSync({ [DeployCommand.STASH_KEY]: { jobid: deployId } }); } protected resolveDeployId(id: string): string { if (id) { return id; } else { - // try and read from the ~/.sfdx/stash.json file for the most recent deploy ID try { - this.logger.debug('Reading from ~/.sfdx/stash.json for the deploy id'); const stash = this.getStash(); stash.readSync(true); - return asString((stash.get(DeployCommand.STASH_KEY) as { jobid: string }).jobid); + const deployId = asString((stash.get(DeployCommand.STASH_KEY) as { jobid: string }).jobid); + this.logger.debug(`Using deploy ID: ${deployId} from ${stash.getPath()}`); + return deployId; } catch (err: unknown) { const error = err as Error & { code: string }; if (error.code === 'ENOENT') { @@ -57,13 +66,6 @@ export abstract class DeployCommand extends SourceCommand { } } - protected displayDeployId(id: string): void { - if (!this.isJsonOutput() && !this.deployIdDisplayed) { - this.ux.log(`Deploy ID: ${id}`); - this.deployIdDisplayed = true; - } - } - // 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. @@ -81,11 +83,12 @@ export abstract class DeployCommand extends SourceCommand { return false; } else if (restDeployConfig === 'true') { this.logger.debug('restDeploy SFDX config === true. Using REST'); + return true; } else { - this.logger.debug('soapdeploy flag unset. restDeploy SFDX config unset. Defaulting to REST'); + this.logger.debug('soapdeploy flag unset. restDeploy SFDX config unset. Defaulting to SOAP'); } - return true; + return false; } protected async poll(deployId: string, options?: Partial): Promise { @@ -100,10 +103,12 @@ export abstract class DeployCommand extends SourceCommand { }; }, }; - const pollingOptions = { ...defaultOptions, ...options }; - const pollingClient = await PollingClient.create(pollingOptions); return pollingClient.subscribe() as unknown as Promise; } + + private getStash(): ConfigFile<{ isGlobal: true; filename: 'stash.json' }> { + return new ConfigFile({ isGlobal: true, filename: 'stash.json' }); + } } diff --git a/src/formatters/deployAsyncResultFormatter.ts b/src/formatters/deployAsyncResultFormatter.ts new file mode 100644 index 000000000..f85b63804 --- /dev/null +++ b/src/formatters/deployAsyncResultFormatter.ts @@ -0,0 +1,69 @@ +/* + * 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 { EOL } from 'os'; +import { UX } from '@salesforce/command'; +import { Logger, Messages } from '@salesforce/core'; +import { cloneJson } from '@salesforce/kit'; +import { AsyncResult } from '@salesforce/source-deploy-retrieve'; +import { ResultFormatter, ResultFormatterOptions } from './resultFormatter'; + +Messages.importMessagesDirectory(__dirname); +const messages = Messages.loadMessages('@salesforce/plugin-source', 'deploy'); + +export interface DeployCommandAsyncResult extends DeployAsyncStatus { + outboundFiles: string[]; + deploys: DeployAsyncStatus[]; +} +// Per the AsyncResult MDAPI docs, only `id` is required/used. The rest is here for +// backwards compatibility. +// https://developer.salesforce.com/docs/atlas.en-us.api_meta.meta/api_meta/meta_asyncresult.htm +export interface DeployAsyncStatus { + done: boolean; + id: string; + state: 'Queued'; + status: 'Queued'; + timedOut: boolean; +} + +export class DeployAsyncResultFormatter extends ResultFormatter { + protected result: AsyncResult; + + public constructor(logger: Logger, ux: UX, options: ResultFormatterOptions, result: AsyncResult) { + super(logger, ux, options); + this.result = result; + } + + /** + * Get the JSON output from the AsyncDeployResult. + * + * @returns a DeployCommandAsyncResult. + */ + public getJson(): DeployCommandAsyncResult { + const asyncResult = { + id: this.result.id, + done: false, + state: 'Queued', + status: 'Queued', + timedOut: true, + } as DeployCommandAsyncResult; + const resultClone = cloneJson(asyncResult); + asyncResult.outboundFiles = []; + asyncResult.deploys = [resultClone]; + + return asyncResult; + } + + /** + * Displays async deploy results in human format. + */ + public display(): void { + this.ux.log(messages.getMessage('asyncDeployQueued'), EOL); + this.ux.log(messages.getMessage('asyncDeployCancel', [this.result.id])); + this.ux.log(messages.getMessage('asyncDeployReport', [this.result.id])); + } +} diff --git a/src/formatters/deployReportResultFormatter.ts b/src/formatters/deployReportResultFormatter.ts index 17562d95e..9efe5d95c 100644 --- a/src/formatters/deployReportResultFormatter.ts +++ b/src/formatters/deployReportResultFormatter.ts @@ -6,12 +6,15 @@ */ import { MetadataApiDeployStatus } from '@salesforce/source-deploy-retrieve/lib/src/client/types'; +import { getString } from '@salesforce/ts-types'; import { DeployResultFormatter } from './deployResultFormatter'; export type DeployReportCommandResult = MetadataApiDeployStatus; export class DeployReportResultFormatter extends DeployResultFormatter { public display(): void { + const status = getString(this, 'result.response.status', 'unknown'); + this.ux.log(`Status: ${status}`); if (!this.isVerbose()) { const componentsTotal = this.getNumResult('numberComponentsTotal'); if (componentsTotal) { diff --git a/src/formatters/deployResultFormatter.ts b/src/formatters/deployResultFormatter.ts index 7f9daafc0..77da7b21a 100644 --- a/src/formatters/deployResultFormatter.ts +++ b/src/formatters/deployResultFormatter.ts @@ -5,7 +5,6 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import * as path from 'path'; import * as chalk from 'chalk'; import { UX } from '@salesforce/command'; import { Logger, Messages, SfdxError } from '@salesforce/core'; @@ -27,20 +26,6 @@ export interface DeployCommandResult extends MetadataApiDeployStatus { deploys: MetadataApiDeployStatus[]; } -// For async deploy command results it looks like this: -export interface DeployCommandAsyncResult extends DeployAsyncStatus { - deployedSource: FileResponse[]; - outboundFiles: string[]; - deploys: DeployAsyncStatus[]; -} -export interface DeployAsyncStatus { - done: boolean; - id: string; - state: 'Queued'; - status: 'Queued'; - timedOut: boolean; -} - export class DeployResultFormatter extends ResultFormatter { protected result: DeployResult; protected fileResponses: FileResponse[]; @@ -52,35 +37,26 @@ export class DeployResultFormatter extends ResultFormatter { } /** - * Get the JSON output from the DeployResult. The returned JSON shape - * varies based on: - * - * 1. Standard synchronous deploy - * 2. Asynchronous deploy (wait=0) + * Get the JSON output from the DeployResult. * * @returns a JSON formatted result matching the provided type. */ - public getJson(): DeployCommandResult | DeployCommandAsyncResult { - const json = this.getResponse() as DeployCommandResult | DeployCommandAsyncResult; + public getJson(): DeployCommandResult { + const json = this.getResponse() as DeployCommandResult; json.deployedSource = this.fileResponses; json.outboundFiles = []; // to match toolbelt version json.deploys = [Object.assign({}, this.getResponse())]; // to match toolbelt version - if (this.isAsync()) { - // json = this.getResponse(); // <-- TODO: ensure the response matches toolbelt - return json as DeployCommandAsyncResult; - } - - return json as DeployCommandResult; + return json; } /** * Displays deploy results in human format. Output can vary based on: * - * 1. Standard synchronous deploy (no tests run) - * 2. Asynchronous deploy (wait=0) + * 1. Verbose option * 3. Checkonly deploy (checkonly=true) * 4. Deploy with test results + * 5. Canceled status */ public display(): void { // Display check-only, non-verbose success @@ -109,10 +85,19 @@ export class DeployResultFormatter extends ResultFormatter { return getString(this.result, 'response.status') === status; } - protected hasComponents(): boolean { + // Returns true if the components returned in the server response + // were mapped to local source in the ComponentSet. + protected hasMappedComponents(): boolean { return getNumber(this.result, 'components.size', 0) > 0; } + // Returns true if the server response contained components. + protected hasComponents(): boolean { + const successes = getNumber(this.result, 'response.details.componentSuccesses.length', 0) > 0; + const failures = getNumber(this.result, 'response.details.componentFailures.length', 0) > 0; + return successes || failures; + } + protected isRunTestsEnabled(): boolean { return getBoolean(this.result, 'response.runTestsEnabled', false); } @@ -131,27 +116,12 @@ export class DeployResultFormatter extends ResultFormatter { protected displaySuccesses(): void { if (this.isSuccess() && this.hasComponents()) { - // sort by type then filename then fullname - const files = this.fileResponses.sort((i, j) => { - if (i.fullName === j.fullName) { - // same metadata type, according to above comment sort on filename - if (i.filePath === j.filePath) { - // same filename's according to comment sort by fullName - return i.fullName < j.fullName ? 1 : -1; - } - return i.filePath < j.filePath ? 1 : -1; - } - return i.type < j.type ? 1 : -1; - }); - // get relative path for table output - files.forEach((file) => { - if (file.filePath) { - file.filePath = path.relative(process.cwd(), file.filePath); - } - }); + this.sortFileResponses(this.fileResponses); + this.asRelativePaths(this.fileResponses); + this.ux.log(''); this.ux.styledHeader(chalk.blue('Deployed Source')); - this.ux.table(files, { + this.ux.table(this.fileResponses, { columns: [ { key: 'fullName', label: 'FULL NAME' }, { key: 'type', label: 'TYPE' }, @@ -163,16 +133,10 @@ export class DeployResultFormatter extends ResultFormatter { protected displayFailures(): void { if (this.hasStatus(RequestStatus.Failed) && this.hasComponents()) { - // sort by filename then fullname - const failures = this.fileResponses - .filter((fileResponse) => fileResponse.state === 'Failed') - .sort((i, j) => { - if (i.filePath === j.filePath) { - // if they have the same directoryName then sort by fullName - return i.fullName < j.fullName ? 1 : -1; - } - return i.filePath < j.filePath ? 1 : -1; - }); + const failures = this.fileResponses.filter((f) => f.state === 'Failed'); + this.sortFileResponses(failures); + this.asRelativePaths(failures); + this.ux.log(''); this.ux.styledHeader(chalk.red(`Component Failures [${failures.length}]`)); // TODO: do we really need the project path or file path in the table? diff --git a/src/formatters/resultFormatter.ts b/src/formatters/resultFormatter.ts index c0e8a89de..6abd2bf40 100644 --- a/src/formatters/resultFormatter.ts +++ b/src/formatters/resultFormatter.ts @@ -5,13 +5,14 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ +import * as path from 'path'; import { UX } from '@salesforce/command'; import { Logger } from '@salesforce/core'; +import { FileResponse } from '@salesforce/source-deploy-retrieve'; import { getBoolean, getNumber } from '@salesforce/ts-types'; export interface ResultFormatterOptions { verbose?: boolean; - async?: boolean; waitTime?: number; } @@ -32,14 +33,32 @@ export abstract class ResultFormatter { return getNumber(process, 'exitCode', 0) === 0; } - public isAsync(): boolean { - return getBoolean(this.options, 'async', false); - } - public isVerbose(): boolean { return getBoolean(this.options, 'verbose', false); } + // Sort by type > filePath > fullName + protected sortFileResponses(fileResponses: FileResponse[]): void { + fileResponses.sort((i, j) => { + if (i.type === j.type) { + if (i.filePath === j.filePath) { + return i.fullName > j.fullName ? 1 : -1; + } + return i.filePath > j.filePath ? 1 : -1; + } + return i.type > j.type ? 1 : -1; + }); + } + + // Convert absolute paths to relative for better table output. + protected asRelativePaths(fileResponses: FileResponse[]): void { + fileResponses.forEach((file) => { + if (file.filePath) { + file.filePath = path.relative(process.cwd(), file.filePath); + } + }); + } + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ public abstract getJson(): any; public abstract display(): void; diff --git a/src/formatters/retrieveResultFormatter.ts b/src/formatters/retrieveResultFormatter.ts index c6e9adda1..e3b396d7f 100644 --- a/src/formatters/retrieveResultFormatter.ts +++ b/src/formatters/retrieveResultFormatter.ts @@ -10,12 +10,7 @@ import { UX } from '@salesforce/command'; import { Logger, Messages } from '@salesforce/core'; import { get, getString, getNumber } from '@salesforce/ts-types'; import { RetrieveResult, MetadataApiRetrieveStatus } from '@salesforce/source-deploy-retrieve'; -import { - FileResponse, - FileProperties, - RequestStatus, - RetrieveMessage, -} from '@salesforce/source-deploy-retrieve/lib/src/client/types'; +import { FileResponse, RequestStatus, RetrieveMessage } from '@salesforce/source-deploy-retrieve/lib/src/client/types'; import { ResultFormatter, ResultFormatterOptions } from './resultFormatter'; Messages.importMessagesDirectory(__dirname); @@ -71,20 +66,24 @@ export class RetrieveResultFormatter extends ResultFormatter { this.ux.styledHeader(blue(messages.getMessage('retrievedSourceHeader'))); if (this.isSuccess()) { - const fileProps = get(this.result, 'response.fileProperties', []) as FileProperties | FileProperties[]; - const fileProperties: FileProperties[] = Array.isArray(fileProps) ? fileProps : [fileProps]; - if (fileProperties.length) { + if (this.fileResponses?.length) { + this.sortFileResponses(this.fileResponses); + this.asRelativePaths(this.fileResponses); const columns = [ { key: 'fullName', label: 'FULL NAME' }, { key: 'type', label: 'TYPE' }, - { key: 'fileName', label: 'PROJECT PATH' }, + { key: 'filePath', label: 'PROJECT PATH' }, ]; - this.ux.table(fileProperties, { columns }); + this.ux.table(this.fileResponses, { columns }); } else { this.ux.log(messages.getMessage('NoResultsFound')); } } else { - this.ux.log('Retrieve Failed due to: (check this.result.response.messages)'); + const unknownMsg: RetrieveMessage[] = [{ fileName: 'unknown', problem: 'unknown' }]; + const responseMsgs = get(this.result, 'response.messages', unknownMsg) as RetrieveMessage | RetrieveMessage[]; + const errMsgs = Array.isArray(responseMsgs) ? responseMsgs : [responseMsgs]; + const errMsgsForDisplay = errMsgs.reduce((p, c) => `${p}\n${c.fileName}: ${c.problem}`, ''); + this.ux.log(`Retrieve Failed due to: ${errMsgsForDisplay}`); } // if (results.status === 'SucceededPartial' && results.successes.length && results.failures.length) { diff --git a/src/sourceCommand.ts b/src/sourceCommand.ts index 577f89fd0..d85fbd576 100644 --- a/src/sourceCommand.ts +++ b/src/sourceCommand.ts @@ -7,6 +7,7 @@ import { SfdxCommand } from '@salesforce/command'; import { Lifecycle } from '@salesforce/core'; +import { ComponentSet } from '@salesforce/source-deploy-retrieve'; import { get, getBoolean } from '@salesforce/ts-types'; import cli from 'cli-ux'; @@ -23,6 +24,8 @@ export abstract class SourceCommand extends SfdxCommand { protected progressBar?: ProgressBar; protected lifecycle = Lifecycle.getInstance(); + protected componentSet?: ComponentSet; + protected isJsonOutput(): boolean { return getBoolean(this.flags, 'json', false); } diff --git a/test/commands/source/deploy.test.ts b/test/commands/source/deploy.test.ts index b6dd499b9..e2ba136f9 100644 --- a/test/commands/source/deploy.test.ts +++ b/test/commands/source/deploy.test.ts @@ -15,6 +15,10 @@ import { UX } from '@salesforce/command'; import { IConfig } from '@oclif/config'; import { Deploy } from '../../../src/commands/force/source/deploy'; import { DeployCommandResult, DeployResultFormatter } from '../../../src/formatters/deployResultFormatter'; +import { + DeployCommandAsyncResult, + DeployAsyncResultFormatter, +} from '../../../src/formatters/deployAsyncResultFormatter'; import { ComponentSetBuilder, ComponentSetOptions } from '../../../src/componentSetBuilder'; import { getDeployResult } from './deployResponses'; import { exampleSourceComponent } from './testConsts'; @@ -32,13 +36,32 @@ describe('force:source:deploy', () => { expectedResults.outboundFiles = []; expectedResults.deploys = [deployResult.response]; + const expectedAsyncResults: DeployCommandAsyncResult = { + done: false, + id: deployResult.response.id, + state: 'Queued', + status: 'Queued', + timedOut: true, + outboundFiles: [], + deploys: [ + { + done: false, + id: deployResult.response.id, + state: 'Queued', + status: 'Queued', + timedOut: true, + }, + ], + }; + // Stubs let buildComponentSetStub: sinon.SinonStub; let initProgressBarStub: sinon.SinonStub; let progressStub: sinon.SinonStub; let deployStub: sinon.SinonStub; - let startStub: sinon.SinonStub; + let pollStub: sinon.SinonStub; let lifecycleEmitStub: sinon.SinonStub; + let formatterDisplayStub: sinon.SinonStub; class TestDeploy extends Deploy { public async runIt() { @@ -74,14 +97,17 @@ describe('force:source:deploy', () => { initProgressBarStub = stubMethod(sandbox, cmd, 'initProgressBar'); progressStub = stubMethod(sandbox, cmd, 'progress'); stubMethod(sandbox, UX.prototype, 'log'); - stubMethod(sandbox, DeployResultFormatter.prototype, 'display'); stubMethod(sandbox, Deploy.prototype, 'deployRecentValidation').resolves({}); + formatterDisplayStub = stubMethod(sandbox, DeployResultFormatter.prototype, 'display'); return cmd.runIt(); }; beforeEach(() => { - startStub = sandbox.stub().returns(deployResult); - deployStub = sandbox.stub().returns({ start: startStub }); + pollStub = sandbox.stub().resolves(deployResult); + deployStub = sandbox.stub().resolves({ + pollStatus: pollStub, + id: deployResult.response.id, + }); buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ deploy: deployStub, getPackageXml: () => packageXml, @@ -120,6 +146,7 @@ describe('force:source:deploy', () => { checkOnly: false, runTests: [], testLevel: 'NoTestRun', + rest: false, }, }; if (overrides?.apiOptions) { @@ -383,4 +410,30 @@ describe('force:source:deploy', () => { it('should stop progress bar onCancel'); it('should stop progress bar onError'); }); + + it('should return JSON format and not display for a synchronous deploy', async () => { + const result = await runDeployCmd(['--sourcepath', 'somepath', '--json']); + expect(formatterDisplayStub.calledOnce).to.equal(false); + expect(result).to.deep.equal(expectedResults); + }); + + it('should return JSON format and not display for an asynchronous deploy', async () => { + const formatterAsyncDisplayStub = stubMethod(sandbox, DeployAsyncResultFormatter.prototype, 'display'); + const result = await runDeployCmd(['--sourcepath', 'somepath', '--json', '--wait', '0']); + expect(formatterAsyncDisplayStub.calledOnce).to.equal(false); + expect(result).to.deep.equal(expectedAsyncResults); + }); + + it('should return JSON format and display for a synchronous deploy', async () => { + const result = await runDeployCmd(['--sourcepath', 'somepath']); + expect(formatterDisplayStub.calledOnce).to.equal(true); + expect(result).to.deep.equal(expectedResults); + }); + + it('should return JSON format and display for an asynchronous deploy', async () => { + const formatterAsyncDisplayStub = stubMethod(sandbox, DeployAsyncResultFormatter.prototype, 'display'); + const result = await runDeployCmd(['--sourcepath', 'somepath', '--wait', '0']); + expect(formatterAsyncDisplayStub.calledOnce).to.equal(true); + expect(result).to.deep.equal(expectedAsyncResults); + }); }); diff --git a/test/commands/source/retrieve.test.ts b/test/commands/source/retrieve.test.ts index 9655feded..aaba05079 100644 --- a/test/commands/source/retrieve.test.ts +++ b/test/commands/source/retrieve.test.ts @@ -37,7 +37,7 @@ describe('force:source:retrieve', () => { // Stubs let buildComponentSetStub: sinon.SinonStub; let retrieveStub: sinon.SinonStub; - let startStub: sinon.SinonStub; + let pollStub: sinon.SinonStub; let lifecycleEmitStub: sinon.SinonStub; class TestRetrieve extends Retrieve { @@ -77,8 +77,11 @@ describe('force:source:retrieve', () => { }; beforeEach(() => { - startStub = sandbox.stub().returns(retrieveResult); - retrieveStub = sandbox.stub().returns({ start: startStub }); + pollStub = sandbox.stub().resolves(retrieveResult); + retrieveStub = sandbox.stub().resolves({ + pollStatus: pollStub, + retrieveId: retrieveResult.response.id, + }); buildComponentSetStub = stubMethod(sandbox, ComponentSetBuilder, 'build').resolves({ retrieve: retrieveStub, getPackageXml: () => packageXml, diff --git a/test/commands/source/retrieveResponses.ts b/test/commands/source/retrieveResponses.ts index 2d935d111..f16f1094c 100644 --- a/test/commands/source/retrieveResponses.ts +++ b/test/commands/source/retrieveResponses.ts @@ -83,12 +83,14 @@ export const getRetrieveResult = ( getFileResponses() { let fileProps = response.fileProperties; fileProps = Array.isArray(fileProps) ? fileProps : [fileProps]; - return fileProps.map((comp) => ({ - fullName: comp.fullName, - filePath: comp.fileName, - state: 'Changed', - type: comp.type, - })); + return fileProps + .filter((p) => p.type !== 'Package') + .map((comp) => ({ + fullName: comp.fullName, + filePath: comp.fileName, + state: 'Changed', + type: comp.type, + })); }, } as RetrieveResult; }; diff --git a/test/formatters/retrieveResultFormatter.test.ts b/test/formatters/retrieveResultFormatter.test.ts index 1888b3128..d779b52ba 100644 --- a/test/formatters/retrieveResultFormatter.test.ts +++ b/test/formatters/retrieveResultFormatter.test.ts @@ -108,9 +108,8 @@ describe('RetrieveResultFormatter', () => { expect(logStub.called).to.equal(false); expect(tableStub.called).to.equal(true); expect(styledHeaderStub.firstCall.args[0]).to.contain('Retrieved Source'); - // NOTE: THIS SHOULD CHANGE TO BE THE fileResponses after the async PR is merged. - const fileProps = retrieveResultSuccess.response.fileProperties; - expect(tableStub.firstCall.args[0]).to.deep.equal(fileProps); + const fileResponses = retrieveResultSuccess.getFileResponses(); + expect(tableStub.firstCall.args[0]).to.deep.equal(fileResponses); }); it('should output as expected for an InProgress', async () => { diff --git a/test/nuts/seeds/deploy.async.seed.ts b/test/nuts/seeds/deploy.async.seed.ts index dc502e64a..27dc3b8d1 100644 --- a/test/nuts/seeds/deploy.async.seed.ts +++ b/test/nuts/seeds/deploy.async.seed.ts @@ -6,13 +6,15 @@ */ import { SourceTestkit } from '@salesforce/source-testkit'; +import { getBoolean, getString } from '@salesforce/ts-types'; +import { expect } from '@salesforce/command/lib/test'; import { TEST_REPOS_MAP } from '../testMatrix'; // DO NOT TOUCH. generateNuts.ts will insert these values const REPO = TEST_REPOS_MAP.get('%REPO_URL%'); const EXECUTABLE = '%EXECUTABLE%'; -context.skip('Async Deploy NUTs [name: %REPO_NAME%] [exec: %EXECUTABLE%]', () => { +context('Async Deploy NUTs [name: %REPO_NAME%] [exec: %EXECUTABLE%]', () => { let testkit: SourceTestkit; before(async () => { @@ -21,6 +23,8 @@ context.skip('Async Deploy NUTs [name: %REPO_NAME%] [exec: %EXECUTABLE%]', () => executable: EXECUTABLE, nut: __filename, }); + // an initial deploy to initialize testkit source tracking + await testkit.deploy({ args: `--sourcepath ${testkit.packageNames.join(',')}` }); }); after(async () => { @@ -33,34 +37,46 @@ context.skip('Async Deploy NUTs [name: %REPO_NAME%] [exec: %EXECUTABLE%]', () => } }); - it('should deploy the entire project', async () => { - await testkit.deploy({ args: `--sourcepath ${testkit.packageNames.join(',')}` }); - await testkit.expect.filesToBeDeployed(testkit.packageGlobs); - }); - describe('async deploy', () => { it('should return an id immediately when --wait is set to 0 and deploy:report should report results', async () => { + // delete the lwc test stubs which will cause errors with the source tracking/globbing + await testkit.deleteGlobs(['force-app/test/**/*']); + const deploy = await testkit.deploy({ args: `--sourcepath ${testkit.packageNames.join(',')} --wait 0`, }); + // test the stashed deploy id + const report = await testkit.deployReport(); testkit.expect.toHaveProperty(deploy.result, 'id'); testkit.expect.toHavePropertyAndNotValue(deploy.result, 'status', 'Succeeded'); - const report = await testkit.deployReport({ args: `-i ${deploy.result.id}` }); - testkit.expect.toHavePropertyAndValue(report.result, 'status', 'Succeeded'); - await testkit.expect.filesToBeDeployed(testkit.packageGlobs, [], 'force:source:deploy:report'); + const status = getBoolean(report.result, 'done'); + if (status) { + // if the deploy finished, expect changes and a 'succeeded' status + testkit.expect.toHavePropertyAndValue(report.result, 'status', 'Succeeded'); + await testkit.expect.filesToBeDeployed(testkit.packageGlobs, [ + 'force-app/main/default/objects/Account/fields/asdf__c', + ]); + } else { + // the deploy could be InProgress, Pending, or Queued, at this point + expect(['Pending', 'InProgress', 'Queued']).to.include(getString(report.result, 'status')); + await testkit.expect.filesToNotBeDeployed(testkit.packageGlobs); + } }); - it('should return an id immediately when --wait is set to 0 and deploy:cancel should cancel the deploy', async () => { - const deploy = await testkit.deploy({ - args: `--sourcepath ${testkit.packageNames.join(',')} --wait 0`, - }); - testkit.expect.toHaveProperty(deploy.result, 'id'); - testkit.expect.toHavePropertyAndNotValue(deploy.result, 'status', 'Succeeded'); + // sample-multiple-package-project deploys too quickly with SDR to cancel + if (REPO.gitUrl.includes('dreamhouse')) { + it('should return an id immediately when --wait is set to 0 and deploy:cancel should cancel the deploy', async () => { + await testkit.deleteGlobs(['force-app/test/**/*']); - await testkit.deployCancel({ args: `-i ${deploy.result.id}` }); - await testkit.expect.someFilesToNotBeDeployed(testkit.packageGlobs, 'force:source:deploy:cancel'); - }); + const deploy = await testkit.deploy({ + args: `--sourcepath ${testkit.packageNames.join(',')} --wait 0`, + }); + await testkit.deployCancel({ args: `-i ${deploy.result.id}` }); + + testkit.expect.toHaveProperty(deploy.result, 'id'); + }); + } }); }); diff --git a/yarn.lock b/yarn.lock index d5a455908..0dbbf7890 100644 --- a/yarn.lock +++ b/yarn.lock @@ -822,13 +822,13 @@ unzipper "0.10.11" xmldom-sfdx-encoding "^0.1.29" -"@salesforce/source-deploy-retrieve@^2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-2.1.5.tgz#4605c48c148a0bd4201402221b34d5ecdb8cc82f" - integrity sha512-4bv6COVgfWpTEH7UXnNii+bDBp96UeSXa6CkDC9zQWSbePvBCUC3YBBC2W80BRyejSaDkYCNhAJn5NoPTxkJ/A== +"@salesforce/source-deploy-retrieve@^3.0.0": + version "3.0.0" + resolved "https://registry.npmjs.org/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-3.0.0.tgz#9edcd4c91a392132ae4436861f910d3acbbe3502" + integrity sha512-GWJS1N+sEh+0/oCuwXX5qNAB1gaxK0bCy+YFfN/8ZMbT1ge0oEBCKFf6H7MW9LXsNFUtW/2mCwMxwOz6jyX7SQ== dependencies: "@salesforce/core" "2.23.2" - "@salesforce/kit" "1.5.0" + "@salesforce/kit" "^1.5.0" "@salesforce/ts-types" "^1.4.2" archiver "4.0.1" fast-xml-parser "^3.17.4"