Skip to content

Commit f09be60

Browse files
committed
feat: abstract out multi stage output
1 parent 090fbe1 commit f09be60

File tree

3 files changed

+198
-145
lines changed

3 files changed

+198
-145
lines changed

src/commands/project/deploy/report.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export default class DeployMetadataReport extends SfCommand<DeployResultJson> {
7070
const jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id'], false);
7171

7272
const deployOpts = cache.maybeGet(jobId);
73-
const wait = flags['wait'];
73+
const { wait } = flags;
7474
const org = deployOpts?.['target-org']
7575
? await Org.create({ aliasOrUsername: deployOpts['target-org'] })
7676
: flags['target-org'];

src/commands/project/deploy/start.ts

Lines changed: 25 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77

88
import { MultiStageOutput } from '@oclif/multi-stage-output';
99
import { EnvironmentVariable, Lifecycle, Messages, OrgConfigProperties, SfError } from '@salesforce/core';
10-
import { DeployVersionData, MetadataApiDeployStatus, RequestStatus } from '@salesforce/source-deploy-retrieve';
10+
import { DeployVersionData, MetadataApiDeployStatus } from '@salesforce/source-deploy-retrieve';
1111
import { Duration } from '@salesforce/kit';
1212
import { SfCommand, toHelpSection, Flags } from '@salesforce/sf-plugins-core';
1313
import { SourceConflictError, SourceMemberPollingEvent } from '@salesforce/source-tracking';
14+
import { DeployStages } from '../../../utils/multiStageOutput.js';
1415
import { AsyncDeployResultFormatter } from '../../../formatters/asyncDeployResultFormatter.js';
1516
import { DeployResultFormatter } from '../../../formatters/deployResultFormatter.js';
1617
import { DeployResultJson, TestLevel } from '../../../utils/types.js';
@@ -24,23 +25,13 @@ import { getOptionalProject } from '../../../utils/project.js';
2425

2526
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
2627
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'deploy.metadata');
27-
const mdTransferMessages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'metadata.transfer');
2828

2929
const exclusiveFlags = ['manifest', 'source-dir', 'metadata', 'metadata-dir'];
3030
const mdapiFormatFlags = 'Metadata API Format';
3131
const sourceFormatFlags = 'Source Format';
3232
const testFlags = 'Test';
3333
const destructiveFlags = 'Delete';
3434

35-
function round(value: number, precision: number): number {
36-
const multiplier = Math.pow(10, precision || 0);
37-
return Math.round(value * multiplier) / multiplier;
38-
}
39-
40-
function formatProgress(current: number, total: number): string {
41-
return `${current}/${total} (${round((current / total) * 100, 0)}%)`;
42-
}
43-
4435
export default class DeployMetadata extends SfCommand<DeployResultJson> {
4536
public static readonly description = messages.getMessage('description');
4637
public static readonly summary = messages.getMessage('summary');
@@ -193,6 +184,8 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
193184
targetOrg: string;
194185
}>;
195186

187+
protected stages!: DeployStages;
188+
196189
public async run(): Promise<DeployResultJson> {
197190
const { flags } = await this.parse(DeployMetadata);
198191
const project = await getOptionalProject();
@@ -216,96 +209,26 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
216209
const username = flags['target-org'].getUsername();
217210
const title = flags['dry-run'] ? 'Deploying Metadata (dry-run)' : 'Deploying Metadata';
218211

219-
this.ms = new MultiStageOutput<{
220-
mdapiDeploy: MetadataApiDeployStatus;
221-
sourceMemberPolling: SourceMemberPollingEvent;
222-
status: string;
223-
apiData: DeployVersionData;
224-
targetOrg: string;
225-
}>({
212+
this.stages = new DeployStages({
226213
title,
227-
stages: [
228-
'Preparing',
229-
'Waiting for the org to respond',
230-
'Deploying Metadata',
231-
'Running Tests',
232-
'Updating Source Tracking',
233-
'Done',
234-
],
235214
jsonEnabled: this.jsonEnabled(),
236-
preStagesBlock: [
237-
{
238-
type: 'message',
239-
get: (data) =>
240-
data?.apiData &&
241-
messages.getMessage('apiVersionMsgDetailed', [
242-
flags['dry-run'] ? 'Deploying (dry-run)' : 'Deploying',
243-
// technically manifestVersion can be undefined, but only on raw mdapi deployments.
244-
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
245-
flags['metadata-dir'] ? '<version specified in manifest>' : `v${data.apiData.manifestVersion}`,
246-
username,
247-
data.apiData.apiVersion,
248-
data.apiData.webService,
249-
]),
250-
},
251-
],
252-
postStagesBlock: [
253-
{
254-
label: 'Status',
255-
get: (data) => data?.status,
256-
bold: true,
257-
type: 'dynamic-key-value',
258-
},
259-
{
260-
label: 'Deploy ID',
261-
get: (data) => data?.mdapiDeploy?.id,
262-
type: 'static-key-value',
263-
},
264-
{
265-
label: 'Target Org',
266-
get: (data) => data?.targetOrg,
267-
type: 'static-key-value',
268-
},
269-
],
270-
stageSpecificBlock: [
271-
{
272-
label: 'Components',
273-
get: (data) =>
274-
data?.mdapiDeploy?.numberComponentsTotal
275-
? formatProgress(
276-
data?.mdapiDeploy?.numberComponentsDeployed ?? 0,
277-
data?.mdapiDeploy?.numberComponentsTotal
278-
)
279-
: undefined,
280-
stage: 'Deploying Metadata',
281-
type: 'dynamic-key-value',
282-
},
283-
{
284-
label: 'Tests',
285-
get: (data) =>
286-
data?.mdapiDeploy?.numberTestsTotal && data?.mdapiDeploy?.numberTestsCompleted
287-
? formatProgress(data?.mdapiDeploy?.numberTestsCompleted, data?.mdapiDeploy?.numberTestsTotal)
288-
: undefined,
289-
stage: 'Running Tests',
290-
type: 'dynamic-key-value',
291-
},
292-
{
293-
label: 'Members',
294-
get: (data) =>
295-
data?.sourceMemberPolling &&
296-
formatProgress(
297-
data.sourceMemberPolling.original - data.sourceMemberPolling.remaining,
298-
data.sourceMemberPolling.original
299-
),
300-
stage: 'Updating Source Tracking',
301-
type: 'dynamic-key-value',
302-
},
303-
],
304215
});
305216

306217
const lifecycle = Lifecycle.getInstance();
307218
lifecycle.on('apiVersionDeploy', async (apiData: DeployVersionData) =>
308-
Promise.resolve(this.ms.updateData({ apiData }))
219+
Promise.resolve(
220+
this.stages.update({
221+
apiMessage: messages.getMessage('apiVersionMsgDetailed', [
222+
flags['dry-run'] ? 'Deploying (dry-run)' : 'Deploying',
223+
// technically manifestVersion can be undefined, but only on raw mdapi deployments.
224+
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
225+
flags['metadata-dir'] ? '<version specified in manifest>' : `v${apiData.manifestVersion}`,
226+
username,
227+
apiData.apiVersion,
228+
apiData.webService,
229+
]),
230+
})
231+
)
309232
);
310233

311234
const { deploy } = await executeDeploy(
@@ -317,8 +240,10 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
317240
project
318241
);
319242

243+
this.stages.start(username, deploy);
244+
320245
if (!deploy) {
321-
this.ms.stop();
246+
this.stages.stop();
322247
this.log('No changes to deploy');
323248
return { status: 'Nothing to deploy', files: [] };
324249
}
@@ -328,8 +253,8 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
328253
}
329254

330255
if (flags.async) {
331-
this.ms.goto('Done', { status: 'Queued', targetOrg: username });
332-
this.ms.stop();
256+
this.stages.done({ status: 'Queued', username });
257+
this.stages.stop();
333258
if (flags['coverage-formatters']) {
334259
this.warn(messages.getMessage('asyncCoverageJunitWarning'));
335260
}
@@ -338,50 +263,6 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
338263
return asyncFormatter.getJson();
339264
}
340265

341-
this.ms.goto('Preparing', { targetOrg: username });
342-
343-
// for sourceMember polling events
344-
lifecycle.on<SourceMemberPollingEvent>('sourceMemberPollingEvent', (event: SourceMemberPollingEvent) =>
345-
Promise.resolve(this.ms.goto('Updating Source Tracking', { sourceMemberPolling: event }))
346-
);
347-
348-
deploy.onUpdate((data) => {
349-
if (
350-
data.numberComponentsDeployed === data.numberComponentsTotal &&
351-
data.numberTestsTotal > 0 &&
352-
data.numberComponentsDeployed > 0
353-
) {
354-
this.ms.goto('Running Tests', { mdapiDeploy: data, status: mdTransferMessages.getMessage(data?.status) });
355-
} else if (data.status === RequestStatus.Pending) {
356-
this.ms.goto('Waiting for the org to respond', {
357-
mdapiDeploy: data,
358-
status: mdTransferMessages.getMessage(data?.status),
359-
});
360-
} else {
361-
this.ms.goto('Deploying Metadata', { mdapiDeploy: data, status: mdTransferMessages.getMessage(data?.status) });
362-
}
363-
});
364-
365-
deploy.onFinish((data) => {
366-
this.ms.goto('Done', { mdapiDeploy: data.response, status: mdTransferMessages.getMessage(data.response.status) });
367-
this.ms.stop();
368-
});
369-
370-
deploy.onCancel((data) => {
371-
this.ms.updateData({ mdapiDeploy: data, status: mdTransferMessages.getMessage(data?.status ?? 'Canceled') });
372-
373-
this.ms.stop(new Error('Deploy canceled'));
374-
});
375-
376-
deploy.onError((error: Error) => {
377-
if (error.message.includes('client has timed out')) {
378-
this.ms.updateData({ status: 'Client Timeout' });
379-
}
380-
381-
this.ms.stop(error);
382-
throw error;
383-
});
384-
385266
const result = await deploy.pollStatus({ timeout: flags.wait });
386267
process.exitCode = determineExitCode(result);
387268
const formatter = new DeployResultFormatter(result, flags);
@@ -399,8 +280,8 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
399280
protected catch(error: Error | SfError): Promise<never> {
400281
if (error instanceof SourceConflictError && error.data) {
401282
if (!this.jsonEnabled()) {
402-
this.ms.updateData({ status: 'Failed' });
403-
this.ms.stop(error);
283+
this.stages.update({ status: 'Failed' });
284+
this.stages.stop(error);
404285
writeConflictTable(error.data);
405286
// set the message and add plugin-specific actions
406287
return super.catch({

0 commit comments

Comments
 (0)