77
88import { MultiStageOutput } from '@oclif/multi-stage-output' ;
99import { 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' ;
1111import { Duration } from '@salesforce/kit' ;
1212import { SfCommand , toHelpSection , Flags } from '@salesforce/sf-plugins-core' ;
1313import { SourceConflictError , SourceMemberPollingEvent } from '@salesforce/source-tracking' ;
14+ import { DeployStages } from '../../../utils/multiStageOutput.js' ;
1415import { AsyncDeployResultFormatter } from '../../../formatters/asyncDeployResultFormatter.js' ;
1516import { DeployResultFormatter } from '../../../formatters/deployResultFormatter.js' ;
1617import { DeployResultJson , TestLevel } from '../../../utils/types.js' ;
@@ -24,23 +25,13 @@ import { getOptionalProject } from '../../../utils/project.js';
2425
2526Messages . importMessagesDirectoryFromMetaUrl ( import . meta. url ) ;
2627const messages = Messages . loadMessages ( '@salesforce/plugin-deploy-retrieve' , 'deploy.metadata' ) ;
27- const mdTransferMessages = Messages . loadMessages ( '@salesforce/plugin-deploy-retrieve' , 'metadata.transfer' ) ;
2828
2929const exclusiveFlags = [ 'manifest' , 'source-dir' , 'metadata' , 'metadata-dir' ] ;
3030const mdapiFormatFlags = 'Metadata API Format' ;
3131const sourceFormatFlags = 'Source Format' ;
3232const testFlags = 'Test' ;
3333const 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-
4435export 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