From e6fcf5c62a81655b5831c3f70b9c8e0d65fe4fdc Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Thu, 8 Apr 2021 14:03:29 -0600 Subject: [PATCH] fix: add progress bar to deploy --- package.json | 1 + src/commands/force/source/deploy.ts | 82 ++++++++++++++++++++++++----- test/commands/source/deploy.test.ts | 49 +++++++++++++++++ yarn.lock | 4 +- 4 files changed, 120 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 45ca0170a..608926207 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@salesforce/core": "^2.20.8", "@salesforce/source-deploy-retrieve": "1.1.21", "chalk": "^4.1.0", + "cli-ux": "^5.5.1", "tslib": "^2" }, "devDependencies": { diff --git a/src/commands/force/source/deploy.ts b/src/commands/force/source/deploy.ts index a0066531e..d9bdeabb5 100644 --- a/src/commands/force/source/deploy.ts +++ b/src/commands/force/source/deploy.ts @@ -9,15 +9,19 @@ import * as os from 'os'; import * as path from 'path'; import { flags, FlagsConfig } from '@salesforce/command'; import { Lifecycle, Messages } from '@salesforce/core'; -import { DeployResult } from '@salesforce/source-deploy-retrieve'; +import { DeployResult, MetadataApiDeploy } from '@salesforce/source-deploy-retrieve'; import { Duration } from '@salesforce/kit'; import { asString, asArray, getBoolean, JsonCollection } from '@salesforce/ts-types'; import * as chalk from 'chalk'; +import cli from 'cli-ux'; +import { env } from '@salesforce/kit'; import { SourceCommand } from '../../../sourceCommand'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/plugin-source', 'deploy'); +type TestLevel = 'NoTestRun' | 'RunSpecifiedTests' | 'RunLocalTests' | 'RunAllTestsInOrg'; + export class Deploy extends SourceCommand { public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessage('examples').split(os.EOL); @@ -111,19 +115,24 @@ export class Deploy extends SourceCommand { await hookEmitter.emit('predeploy', { packageXmlPath: cs.getPackageXml() }); - const results = await cs - .deploy({ - usernameOrConnection: this.org.getUsername(), - apiOptions: { - ignoreWarnings: getBoolean(this.flags, 'ignorewarnings', false), - rollbackOnError: !getBoolean(this.flags, 'ignoreerrors', false), - checkOnly: getBoolean(this.flags, 'checkonly', false), - runTests: asArray(this.flags.runtests), - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - testLevel: this.flags.testlevel, - }, - }) - .start(); + const deploy = cs.deploy({ + usernameOrConnection: this.org.getUsername(), + apiOptions: { + ignoreWarnings: getBoolean(this.flags, 'ignorewarnings', false), + rollbackOnError: !getBoolean(this.flags, 'ignoreerrors', false), + checkOnly: getBoolean(this.flags, 'checkonly', false), + runTests: asArray(this.flags.runtests), + testLevel: this.flags.testlevel as TestLevel, + }, + }); + + // if SFDX_USE_PROGRESS_BAR is true and no --json flag use progress bar, if not, skip + if (env.getBoolean('SFDX_USE_PROGRESS_BAR', true) && !this.flags.json) { + this.progress(deploy); + } + + const results = await deploy.start(); + await hookEmitter.emit('postdeploy', results); // skip a lot of steps that would do nothing @@ -134,6 +143,51 @@ export class Deploy extends SourceCommand { return results; } + private progress(deploy: MetadataApiDeploy): void { + // cli.progress doesn't have typings + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const progressBar = cli.progress({ + format: 'SOURCE PROGRESS | {bar} | {value}/{total} Components', + barCompleteChar: '\u2588', + barIncompleteChar: '\u2591', + linewrap: true, + }); + let printOnce = true; + deploy.onUpdate((data) => { + // the numCompTot. isn't computed right away, wait to start until we know how many we have + if (data.numberComponentsTotal && printOnce) { + this.ux.log(`Job ID | ${data.id}`); + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + progressBar.start(data.numberComponentsTotal + data.numberTestsTotal); + printOnce = false; + } + + // the numTestsTot. isn't computed until validated as tests by the server, update the PB once we know + if (data.numberTestsTotal) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + progressBar.setTotal(data.numberComponentsTotal + data.numberTestsTotal); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call + progressBar.update(data.numberComponentsDeployed + data.numberTestsCompleted); + }); + + deploy.onFinish(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + progressBar.stop(); + }); + + deploy.onCancel(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + progressBar.stop(); + }); + + deploy.onError(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access + progressBar.stop(); + }); + } + private printComponentFailures(result: DeployResult): void { if (result.response.status === 'Failed' && result.components) { // sort by filename then fullname diff --git a/test/commands/source/deploy.test.ts b/test/commands/source/deploy.test.ts index 77b39f024..d8947a5c0 100644 --- a/test/commands/source/deploy.test.ts +++ b/test/commands/source/deploy.test.ts @@ -24,6 +24,7 @@ describe('force:source:deploy', () => { // Stubs let createComponentSetStub: sinon.SinonStub; + let progressStub: sinon.SinonStub; let deployStub: sinon.SinonStub; let startStub: sinon.SinonStub; let lifecycleEmitStub: sinon.SinonStub; @@ -44,12 +45,15 @@ describe('force:source:deploy', () => { getUsername: () => username, }, createComponentSet: createComponentSetStub, + progress: progressStub, + print: () => {}, }) as Promise; }; beforeEach(() => { startStub = sandbox.stub().returns(stubbedResults); deployStub = sandbox.stub().returns({ start: startStub }); + progressStub = sandbox.stub(); createComponentSetStub = sandbox.stub().returns({ deploy: deployStub, getPackageXml: () => packageXml, @@ -107,6 +111,10 @@ describe('force:source:deploy', () => { expect(lifecycleEmitStub.secondCall.args[1]).to.deep.equal(stubbedResults); }; + const ensureProgressBar = (callCount: number) => { + expect(progressStub.callCount).to.equal(callCount); + }; + it('should pass along sourcepath', async () => { const sourcepath = ['somepath']; const result = await run({ sourcepath, json: true }); @@ -114,6 +122,7 @@ describe('force:source:deploy', () => { ensureCreateComponentSetArgs({ sourcepath }); ensureDeployArgs(); ensureHookArgs(); + ensureProgressBar(0); }); it('should pass along metadata', async () => { @@ -123,6 +132,7 @@ describe('force:source:deploy', () => { ensureCreateComponentSetArgs({ metadata }); ensureDeployArgs(); ensureHookArgs(); + ensureProgressBar(0); }); it('should pass along manifest', async () => { @@ -132,6 +142,7 @@ describe('force:source:deploy', () => { ensureCreateComponentSetArgs({ manifest }); ensureDeployArgs(); ensureHookArgs(); + ensureProgressBar(0); }); it('should pass along apiversion', async () => { @@ -142,6 +153,7 @@ describe('force:source:deploy', () => { ensureCreateComponentSetArgs({ apiversion, manifest }); ensureDeployArgs(); ensureHookArgs(); + ensureProgressBar(0); }); it('should pass along all deploy options', async () => { @@ -170,5 +182,42 @@ describe('force:source:deploy', () => { }, }); ensureHookArgs(); + ensureProgressBar(0); + }); + + it('should NOT call progress bar because of environment variable', async () => { + try { + process.env.SFDX_USE_PROGRESS_BAR = 'false'; + const sourcepath = ['somepath']; + const result = await run({ sourcepath }); + expect(result).to.deep.equal(stubbedResults); + ensureCreateComponentSetArgs({ sourcepath }); + ensureDeployArgs(); + ensureHookArgs(); + ensureProgressBar(0); + } finally { + delete process.env.SFDX_USE_PROGRESS_BAR; + } + }); + + it('should NOT call progress bar because of --json', async () => { + const sourcepath = ['somepath']; + const result = await run({ sourcepath, json: true }); + expect(result).to.deep.equal(stubbedResults); + expect(progressStub.called).to.be.false; + ensureCreateComponentSetArgs({ sourcepath }); + ensureDeployArgs(); + ensureHookArgs(); + ensureProgressBar(0); + }); + + it('should call progress bar', async () => { + const sourcepath = ['somepath']; + const result = await run({ sourcepath }); + expect(result).to.deep.equal(stubbedResults); + ensureCreateComponentSetArgs({ sourcepath }); + ensureDeployArgs(); + ensureHookArgs(); + ensureProgressBar(1); }); }); diff --git a/yarn.lock b/yarn.lock index 1237243bc..5b84eee45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1722,9 +1722,9 @@ cli-ux@^4.9.3: treeify "^1.1.0" tslib "^1.9.3" -cli-ux@^5.2.1: +cli-ux@^5.2.1, cli-ux@^5.5.1: version "5.5.1" - resolved "https://registry.npmjs.org/cli-ux/-/cli-ux-5.5.1.tgz#99d28dae0c3ef7845fa2ea56e066a1d5fcceca9e" + resolved "https://registry.yarnpkg.com/cli-ux/-/cli-ux-5.5.1.tgz#99d28dae0c3ef7845fa2ea56e066a1d5fcceca9e" integrity sha512-t3DT1U1C3rArLGYLpKa3m9dr/8uKZRI8HRm/rXKL7UTjm4c+Yd9zHNWg1tP8uaJkUbhmvx5SQHwb3VWpPUVdHQ== dependencies: "@oclif/command" "^1.6.0"