Skip to content

Commit c1e706b

Browse files
feat: use MSO in sandbox commands (#1297)
* feat: update mso * feat: update mso * fix: mso for sandbox * fix: mso * fix: mso * feat: update mso * fix: delete comment * fix: update mso resume * feat: update mso with create/refresh/resume sandbox * chore: refactor * fix: set sandbox username * test: update sandbox NUT * test: log failure --------- Co-authored-by: Cristian Dominguez <[email protected]>
1 parent c67955d commit c1e706b

File tree

15 files changed

+172
-815
lines changed

15 files changed

+172
-815
lines changed

messages/sandboxbase.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
# sandboxSuccess
22

3-
The sandbox org %s was successful.
3+
Your sandbox is ready.
44

55
# sandboxSuccess.actions
66

7-
The username for the sandbox is %s.
8-
You can open the org by running "%s org open -o %s"
7+
You can open it by running "%s org open -o %s"
98

109
# checkSandboxStatus
1110

src/commands/org/create/sandbox.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp
124124

125125
this.debug('Create started with args %s ', this.flags);
126126
this.validateFlags();
127+
127128
return this.createSandbox();
128129
}
129130

@@ -187,25 +188,24 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp
187188

188189
private async createSandbox(): Promise<SandboxCommandResponse> {
189190
const lifecycle = Lifecycle.getInstance();
190-
191191
this.prodOrg = this.flags['target-org'];
192192

193-
this.registerLifecycleListeners(lifecycle, {
194-
isAsync: this.flags.async,
195-
setDefault: this.flags['set-default'],
196-
alias: this.flags.alias,
197-
prodOrg: this.prodOrg,
198-
tracksSource: this.flags['no-track-source'] === true ? false : undefined,
199-
});
200193
const sandboxReq = await this.createSandboxRequest();
201194
await this.confirmSandboxReq({
202195
...sandboxReq,
203196
});
204197
this.initSandboxProcessData(sandboxReq);
205198

206-
if (!this.flags.async) {
207-
this.spinner.start('Sandbox Create');
208-
}
199+
this.registerLifecycleListenersAndMSO(lifecycle, {
200+
mso: {
201+
title: 'Sandbox Create',
202+
},
203+
isAsync: this.flags.async,
204+
setDefault: this.flags['set-default'],
205+
alias: this.flags.alias,
206+
prodOrg: this.prodOrg,
207+
tracksSource: this.flags['no-track-source'] === true ? false : undefined,
208+
});
209209

210210
this.debug('Calling create with SandboxRequest: %s ', sandboxReq);
211211

@@ -217,12 +217,12 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp
217217
});
218218
this.latestSandboxProgressObj = sandboxProcessObject;
219219
this.saveSandboxProgressConfig();
220+
220221
if (this.flags.async) {
221222
process.exitCode = 68;
222223
}
223224
return this.getSandboxCommandResponse();
224225
} catch (err) {
225-
this.spinner.stop();
226226
if (this.pollingTimeOut && this.latestSandboxProgressObj) {
227227
void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined);
228228
process.exitCode = 68;

src/commands/org/refresh/sandbox.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,18 +126,21 @@ export default class RefreshSandbox extends SandboxCommandBase<SandboxCommandRes
126126
await this.confirmSandboxRefresh(this.sbxConfig);
127127

128128
const lifecycle = Lifecycle.getInstance();
129-
this.registerLifecycleListeners(lifecycle, { isAsync: this.flags['async'], prodOrg: this.prodOrg });
129+
this.registerLifecycleListenersAndMSO(lifecycle, {
130+
mso: {
131+
refresh: true,
132+
title: 'Sandbox Refresh',
133+
},
134+
isAsync: this.flags['async'],
135+
prodOrg: this.prodOrg,
136+
});
130137

131138
// remove uneditable fields before refresh
132139
const updateableSandboxInfo = omit(this.sbxConfig, uneditableFields);
133140
this.debug('Calling refresh with SandboxInfo: %s ', updateableSandboxInfo);
134141
this.initSandboxProcessData(this.sbxConfig);
135142

136143
try {
137-
if (!this.flags.async) {
138-
this.spinner.start('Sandbox Refresh');
139-
}
140-
141144
const sandboxProcessObject = await this.prodOrg.refreshSandbox(updateableSandboxInfo, {
142145
wait: this.flags['wait'],
143146
interval: this.flags['poll-interval'],
@@ -158,7 +161,6 @@ export default class RefreshSandbox extends SandboxCommandBase<SandboxCommandRes
158161
}
159162
return this.getSandboxCommandResponse();
160163
} catch (err) {
161-
this.spinner.stop();
162164
if (this.pollingTimeOut && this.latestSandboxProgressObj) {
163165
void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined);
164166
process.exitCode = 68;

src/commands/org/resume/sandbox.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8+
import { EOL } from 'node:os';
89
import { Flags } from '@salesforce/sf-plugins-core';
910
import {
1011
StateAggregator,
@@ -117,7 +118,11 @@ export default class ResumeSandbox extends SandboxCommandBase<SandboxCommandResp
117118
this.flags['target-org'] = this.prodOrg;
118119
const lifecycle = Lifecycle.getInstance();
119120

120-
this.registerLifecycleListeners(lifecycle, {
121+
this.registerLifecycleListenersAndMSO(lifecycle, {
122+
mso: {
123+
title: 'Resume Sandbox',
124+
refresh: this.sandboxRequestData.action === 'Refresh',
125+
},
121126
isAsync: false,
122127
alias: this.sandboxRequestData.alias,
123128
setDefault: this.sandboxRequestData.setDefault,
@@ -139,10 +144,6 @@ export default class ResumeSandbox extends SandboxCommandBase<SandboxCommandResp
139144

140145
const sandboxReq = this.createResumeSandboxRequest();
141146

142-
if (this.flags.wait?.seconds && this.flags.wait.seconds > 0) {
143-
this.spinner.start(`Resume ${this.sandboxRequestData.action ?? 'Create/Refresh'}`);
144-
}
145-
146147
this.debug('Calling resume with ResumeSandboxRequest: %s ', sandboxReq);
147148

148149
try {
@@ -152,7 +153,6 @@ export default class ResumeSandbox extends SandboxCommandBase<SandboxCommandResp
152153
});
153154
return this.getSandboxCommandResponse();
154155
} catch (err) {
155-
this.spinner.stop();
156156
if (this.latestSandboxProgressObj && this.pollingTimeOut) {
157157
void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined);
158158
process.exitCode = 68;
@@ -185,12 +185,16 @@ export default class ResumeSandbox extends SandboxCommandBase<SandboxCommandResp
185185
const entries = this.sandboxRequestConfig.entries() as Array<[string, SandboxRequestCacheEntry]>;
186186
const sce = entries.find(([, e]) => e?.sandboxProcessObject?.Id === this.flags['job-id'])?.[1];
187187
sandboxRequestCacheEntry = sce;
188+
if (sandboxRequestCacheEntry === undefined) {
189+
this.warn(
190+
`Could not find a cache entry for ${this.flags['job-id']}.${EOL}If you are resuming a sandbox operation from a different machine note that we cannot set the alias/set-default flag values as those are saved locally.`
191+
);
192+
}
188193
}
189194

190195
// If the action is in the cache entry, use it.
191196
if (sandboxRequestCacheEntry?.action) {
192197
this.action = sandboxRequestCacheEntry?.action;
193-
this.sandboxProgress.action = sandboxRequestCacheEntry?.action;
194198
}
195199

196200
return {

src/shared/sandboxCommandBase.ts

Lines changed: 57 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import os from 'node:os';
8-
98
import { SfCommand } from '@salesforce/sf-plugins-core';
109
import { Config } from '@oclif/core';
1110
import {
@@ -21,8 +20,7 @@ import {
2120
SandboxUserAuthResponse,
2221
StatusEvent,
2322
} from '@salesforce/core';
24-
import { SandboxProgress } from './sandboxProgress.js';
25-
import { State } from './stagedProgress.js';
23+
import { SandboxStages } from './sandboxStages.js';
2624

2725
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
2826
const messages = Messages.loadMessages('@salesforce/plugin-org', 'sandboxbase');
@@ -32,7 +30,7 @@ export type SandboxCommandResponse = SandboxProcessObject & {
3230
};
3331

3432
export abstract class SandboxCommandBase<T> extends SfCommand<T> {
35-
protected sandboxProgress: SandboxProgress;
33+
protected stages!: SandboxStages;
3634
protected latestSandboxProgressObj?: SandboxProcessObject;
3735
protected sandboxAuth?: SandboxUserAuthResponse;
3836
protected prodOrg?: Org;
@@ -47,10 +45,9 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
4745
this.action =
4846
this.constructor.name === 'RefreshSandbox'
4947
? 'Refresh'
50-
: this.constructor.name === 'CreateSandbox'
48+
: ['CreateSandbox', 'ResumeSandbox'].includes(this.constructor.name)
5149
? 'Create'
5250
: 'Create/Refresh';
53-
this.sandboxProgress = new SandboxProgress({ action: this.action });
5451
}
5552
protected async getSandboxRequestConfig(): Promise<SandboxRequestCache> {
5653
if (!this.sandboxRequestConfig) {
@@ -85,72 +82,81 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
8582
return true;
8683
}
8784

88-
protected registerLifecycleListeners(
85+
protected registerLifecycleListenersAndMSO(
8986
lifecycle: Lifecycle,
90-
options: { isAsync: boolean; alias?: string; setDefault?: boolean; prodOrg?: Org; tracksSource?: boolean }
87+
options: {
88+
mso: { title: string; refresh?: boolean };
89+
isAsync: boolean;
90+
alias?: string;
91+
setDefault?: boolean;
92+
prodOrg?: Org;
93+
tracksSource?: boolean;
94+
}
9195
): void {
96+
this.stages = new SandboxStages({
97+
refresh: options.mso.refresh ?? false,
98+
jsonEnabled: this.jsonEnabled(),
99+
title: options.isAsync ? `${options.mso.title} (async)` : options.mso.title,
100+
});
101+
102+
this.stages.start();
103+
92104
lifecycle.on('POLLING_TIME_OUT', async () => {
93105
this.pollingTimeOut = true;
106+
this.stages.stop();
94107
return Promise.resolve(this.updateSandboxRequestData());
95108
});
96109

97110
lifecycle.on(SandboxEvents.EVENT_RESUME, async (results: SandboxProcessObject) => {
111+
this.stages.start();
98112
this.latestSandboxProgressObj = results;
99-
this.sandboxProgress.markPreviousStagesAsCompleted(
100-
results.Status !== 'Completed' ? results.Status : 'Authenticating'
101-
);
113+
this.stages.update(this.latestSandboxProgressObj);
114+
102115
return Promise.resolve(this.updateSandboxRequestData());
103116
});
104117

105-
lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results?: SandboxProcessObject) => {
106-
this.latestSandboxProgressObj = results ?? this.latestSandboxProgressObj;
107-
this.updateSandboxRequestData();
108-
if (!options.isAsync) {
109-
this.spinner.stop();
110-
}
111-
// things that require data on latestSandboxProgressObj
112-
if (this.latestSandboxProgressObj) {
113-
const progress = this.sandboxProgress.getSandboxProgress({
114-
sandboxProcessObj: this.latestSandboxProgressObj,
115-
sandboxRes: undefined,
116-
});
117-
const currentStage = progress.status;
118-
this.sandboxProgress.markPreviousStagesAsCompleted(currentStage);
119-
this.updateStage(currentStage, 'inProgress');
120-
this.updateProgress(
121-
{ sandboxProcessObj: this.latestSandboxProgressObj, sandboxRes: undefined },
122-
options.isAsync
123-
);
118+
lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results: SandboxProcessObject | undefined) => {
119+
// this event is fired by commands on poll timeout without any payload,
120+
// we want to make sure to only update state if there's payload (event from sfdx-core).
121+
if (results) {
122+
this.latestSandboxProgressObj = results;
123+
this.stages.update(this.latestSandboxProgressObj);
124+
this.updateSandboxRequestData();
124125
}
126+
127+
this.stages.stop('async');
125128
if (this.pollingTimeOut) {
126129
this.warn(messages.getMessage('warning.ClientTimeoutWaitingForSandboxProcess', [this.action.toLowerCase()]));
127130
}
128-
this.log(this.sandboxProgress.formatProgressStatus(false));
129131
return Promise.resolve(this.info(messages.getMessage('checkSandboxStatus', this.getCheckSandboxStatusParams())));
130132
});
131133

132134
lifecycle.on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => {
135+
// this starts MSO for:
136+
// * org create/create sandbox
137+
this.stages.start();
133138
this.latestSandboxProgressObj = results.sandboxProcessObj;
134139
this.updateSandboxRequestData();
135-
const progress = this.sandboxProgress.getSandboxProgress(results);
136-
const currentStage = progress.status;
137-
this.updateStage(currentStage, 'inProgress');
138-
return Promise.resolve(this.updateProgress(results, options.isAsync));
140+
141+
this.stages.update(this.latestSandboxProgressObj);
142+
143+
return Promise.resolve();
139144
});
140145

141146
lifecycle.on(SandboxEvents.EVENT_AUTH, async (results: SandboxUserAuthResponse) => {
147+
this.sandboxUsername = results.authUserName;
148+
this.stages.auth();
142149
this.sandboxAuth = results;
143150
return Promise.resolve();
144151
});
145152

146153
lifecycle.on(SandboxEvents.EVENT_RESULT, async (results: ResultEvent) => {
147154
this.latestSandboxProgressObj = results.sandboxProcessObj;
155+
this.sandboxUsername = results.sandboxRes.authUserName;
148156
this.updateSandboxRequestData();
149-
this.sandboxProgress.markPreviousStagesAsCompleted();
150-
this.updateProgress(results, options.isAsync);
151-
if (!options.isAsync) {
152-
this.progress.stop();
153-
}
157+
158+
this.stages.update(results.sandboxProcessObj);
159+
154160
if (results.sandboxRes?.authUserName) {
155161
const authInfo = await AuthInfo.create({ username: results.sandboxRes?.authUserName });
156162
await authInfo.handleAliasAndDefaultSettings({
@@ -160,8 +166,9 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
160166
setTracksSource: await this.calculateTrackingSetting(options.tracksSource),
161167
});
162168
}
169+
this.stages.stop();
170+
163171
this.removeSandboxProgressConfig();
164-
this.updateProgress(results, options.isAsync);
165172
this.reportResults(results);
166173
});
167174

@@ -185,43 +192,14 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
185192
}
186193

187194
protected reportResults(results: ResultEvent): void {
188-
this.log();
189-
this.styledHeader(`Sandbox Org ${this.action} Status`);
190-
this.log(this.sandboxProgress.formatProgressStatus(false));
191195
this.logSuccess(
192196
[
193-
messages.getMessage('sandboxSuccess', [this.action.toLowerCase()]),
194-
messages.getMessages('sandboxSuccess.actions', [
195-
results.sandboxRes?.authUserName,
196-
this.config.bin,
197-
results.sandboxRes?.authUserName,
198-
]),
197+
messages.getMessage('sandboxSuccess'),
198+
messages.getMessages('sandboxSuccess.actions', [this.config.bin, results.sandboxRes?.authUserName]),
199199
].join(os.EOL)
200200
);
201201
}
202202

203-
protected updateProgress(
204-
event: StatusEvent | (Omit<ResultEvent, 'sandboxRes'> & { sandboxRes?: ResultEvent['sandboxRes'] }),
205-
isAsync: boolean
206-
): void {
207-
const sandboxProgress = this.sandboxProgress.getSandboxProgress(event);
208-
this.sandboxUsername = (event as ResultEvent).sandboxRes?.authUserName;
209-
this.sandboxProgress.statusData = {
210-
sandboxUsername: this.sandboxUsername,
211-
sandboxProgress,
212-
sandboxProcessObj: event.sandboxProcessObj,
213-
};
214-
if (!isAsync) {
215-
this.spinner.status = this.sandboxProgress.formatProgressStatus();
216-
}
217-
}
218-
219-
protected updateStage(stage: string | undefined, state: State): void {
220-
if (stage) {
221-
this.sandboxProgress.transitionStages(stage, state);
222-
}
223-
}
224-
225203
protected updateSandboxRequestData(): void {
226204
if (this.sandboxRequestData && this.latestSandboxProgressObj) {
227205
this.sandboxRequestData.sandboxProcessObject = this.latestSandboxProgressObj;
@@ -262,6 +240,13 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
262240
return { ...(this.latestSandboxProgressObj as SandboxProcessObject), SandboxUsername: sbxUsername };
263241
}
264242

243+
protected catch(error: Error): Promise<never> {
244+
if (this.stages) {
245+
this.stages.stop('failed');
246+
}
247+
248+
return super.catch(error);
249+
}
265250
// eslint-disable-next-line @typescript-eslint/no-explicit-any
266251
protected async finally(_: Error | undefined): Promise<any> {
267252
const lifecycle = Lifecycle.getInstance();

0 commit comments

Comments
 (0)