Skip to content
5 changes: 2 additions & 3 deletions messages/sandboxbase.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# sandboxSuccess

The sandbox org %s was successful.
Your sandbox is ready.

# sandboxSuccess.actions

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

# checkSandboxStatus

Expand Down
24 changes: 12 additions & 12 deletions src/commands/org/create/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp

this.debug('Create started with args %s ', this.flags);
this.validateFlags();

return this.createSandbox();
}

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

private async createSandbox(): Promise<SandboxCommandResponse> {
const lifecycle = Lifecycle.getInstance();

this.prodOrg = this.flags['target-org'];

this.registerLifecycleListeners(lifecycle, {
isAsync: this.flags.async,
setDefault: this.flags['set-default'],
alias: this.flags.alias,
prodOrg: this.prodOrg,
tracksSource: this.flags['no-track-source'] === true ? false : undefined,
});
const sandboxReq = await this.createSandboxRequest();
await this.confirmSandboxReq({
...sandboxReq,
});
this.initSandboxProcessData(sandboxReq);

if (!this.flags.async) {
this.spinner.start('Sandbox Create');
}
this.registerLifecycleListenersAndMSO(lifecycle, {
mso: {
title: 'Sandbox Create',
},
isAsync: this.flags.async,
setDefault: this.flags['set-default'],
alias: this.flags.alias,
prodOrg: this.prodOrg,
tracksSource: this.flags['no-track-source'] === true ? false : undefined,
});

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

Expand All @@ -217,12 +217,12 @@ export default class CreateSandbox extends SandboxCommandBase<SandboxCommandResp
});
this.latestSandboxProgressObj = sandboxProcessObject;
this.saveSandboxProgressConfig();

if (this.flags.async) {
process.exitCode = 68;
}
return this.getSandboxCommandResponse();
} catch (err) {
this.spinner.stop();
if (this.pollingTimeOut && this.latestSandboxProgressObj) {
void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined);
process.exitCode = 68;
Expand Down
14 changes: 8 additions & 6 deletions src/commands/org/refresh/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,21 @@ export default class RefreshSandbox extends SandboxCommandBase<SandboxCommandRes
await this.confirmSandboxRefresh(this.sbxConfig);

const lifecycle = Lifecycle.getInstance();
this.registerLifecycleListeners(lifecycle, { isAsync: this.flags['async'], prodOrg: this.prodOrg });
this.registerLifecycleListenersAndMSO(lifecycle, {
mso: {
refresh: true,
title: 'Sandbox Refresh',
},
isAsync: this.flags['async'],
prodOrg: this.prodOrg,
});

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

try {
if (!this.flags.async) {
this.spinner.start('Sandbox Refresh');
}

const sandboxProcessObject = await this.prodOrg.refreshSandbox(updateableSandboxInfo, {
wait: this.flags['wait'],
interval: this.flags['poll-interval'],
Expand All @@ -158,7 +161,6 @@ export default class RefreshSandbox extends SandboxCommandBase<SandboxCommandRes
}
return this.getSandboxCommandResponse();
} catch (err) {
this.spinner.stop();
if (this.pollingTimeOut && this.latestSandboxProgressObj) {
void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined);
process.exitCode = 68;
Expand Down
18 changes: 11 additions & 7 deletions src/commands/org/resume/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

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

this.registerLifecycleListeners(lifecycle, {
this.registerLifecycleListenersAndMSO(lifecycle, {
mso: {
title: 'Resume Sandbox',
refresh: this.sandboxRequestData.action === 'Refresh',
},
isAsync: false,
alias: this.sandboxRequestData.alias,
setDefault: this.sandboxRequestData.setDefault,
Expand All @@ -139,10 +144,6 @@ export default class ResumeSandbox extends SandboxCommandBase<SandboxCommandResp

const sandboxReq = this.createResumeSandboxRequest();

if (this.flags.wait?.seconds && this.flags.wait.seconds > 0) {
this.spinner.start(`Resume ${this.sandboxRequestData.action ?? 'Create/Refresh'}`);
}

this.debug('Calling resume with ResumeSandboxRequest: %s ', sandboxReq);

try {
Expand All @@ -152,7 +153,6 @@ export default class ResumeSandbox extends SandboxCommandBase<SandboxCommandResp
});
return this.getSandboxCommandResponse();
} catch (err) {
this.spinner.stop();
if (this.latestSandboxProgressObj && this.pollingTimeOut) {
void lifecycle.emit(SandboxEvents.EVENT_ASYNC_RESULT, undefined);
process.exitCode = 68;
Expand Down Expand Up @@ -185,12 +185,16 @@ export default class ResumeSandbox extends SandboxCommandBase<SandboxCommandResp
const entries = this.sandboxRequestConfig.entries() as Array<[string, SandboxRequestCacheEntry]>;
const sce = entries.find(([, e]) => e?.sandboxProcessObject?.Id === this.flags['job-id'])?.[1];
sandboxRequestCacheEntry = sce;
if (sandboxRequestCacheEntry === undefined) {
this.warn(
`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.`
);
}
}

// If the action is in the cache entry, use it.
if (sandboxRequestCacheEntry?.action) {
this.action = sandboxRequestCacheEntry?.action;
this.sandboxProgress.action = sandboxRequestCacheEntry?.action;
}

return {
Expand Down
129 changes: 57 additions & 72 deletions src/shared/sandboxCommandBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 os from 'node:os';

import { SfCommand } from '@salesforce/sf-plugins-core';
import { Config } from '@oclif/core';
import {
Expand All @@ -21,8 +20,7 @@ import {
SandboxUserAuthResponse,
StatusEvent,
} from '@salesforce/core';
import { SandboxProgress } from './sandboxProgress.js';
import { State } from './stagedProgress.js';
import { SandboxStages } from './sandboxStages.js';

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

export abstract class SandboxCommandBase<T> extends SfCommand<T> {
protected sandboxProgress: SandboxProgress;
protected stages!: SandboxStages;
protected latestSandboxProgressObj?: SandboxProcessObject;
protected sandboxAuth?: SandboxUserAuthResponse;
protected prodOrg?: Org;
Expand All @@ -47,10 +45,9 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
this.action =
this.constructor.name === 'RefreshSandbox'
? 'Refresh'
: this.constructor.name === 'CreateSandbox'
: ['CreateSandbox', 'ResumeSandbox'].includes(this.constructor.name)
? 'Create'
: 'Create/Refresh';
this.sandboxProgress = new SandboxProgress({ action: this.action });
}
protected async getSandboxRequestConfig(): Promise<SandboxRequestCache> {
if (!this.sandboxRequestConfig) {
Expand Down Expand Up @@ -85,72 +82,81 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
return true;
}

protected registerLifecycleListeners(
protected registerLifecycleListenersAndMSO(
lifecycle: Lifecycle,
options: { isAsync: boolean; alias?: string; setDefault?: boolean; prodOrg?: Org; tracksSource?: boolean }
options: {
mso: { title: string; refresh?: boolean };
isAsync: boolean;
alias?: string;
setDefault?: boolean;
prodOrg?: Org;
tracksSource?: boolean;
}
): void {
this.stages = new SandboxStages({
refresh: options.mso.refresh ?? false,
jsonEnabled: this.jsonEnabled(),
title: options.isAsync ? `${options.mso.title} (async)` : options.mso.title,
});

this.stages.start();

lifecycle.on('POLLING_TIME_OUT', async () => {
this.pollingTimeOut = true;
this.stages.stop();
return Promise.resolve(this.updateSandboxRequestData());
});

lifecycle.on(SandboxEvents.EVENT_RESUME, async (results: SandboxProcessObject) => {
this.stages.start();
this.latestSandboxProgressObj = results;
this.sandboxProgress.markPreviousStagesAsCompleted(
results.Status !== 'Completed' ? results.Status : 'Authenticating'
);
this.stages.update(this.latestSandboxProgressObj);

return Promise.resolve(this.updateSandboxRequestData());
});

lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results?: SandboxProcessObject) => {
this.latestSandboxProgressObj = results ?? this.latestSandboxProgressObj;
this.updateSandboxRequestData();
if (!options.isAsync) {
this.spinner.stop();
}
// things that require data on latestSandboxProgressObj
if (this.latestSandboxProgressObj) {
const progress = this.sandboxProgress.getSandboxProgress({
sandboxProcessObj: this.latestSandboxProgressObj,
sandboxRes: undefined,
});
const currentStage = progress.status;
this.sandboxProgress.markPreviousStagesAsCompleted(currentStage);
this.updateStage(currentStage, 'inProgress');
this.updateProgress(
{ sandboxProcessObj: this.latestSandboxProgressObj, sandboxRes: undefined },
options.isAsync
);
lifecycle.on(SandboxEvents.EVENT_ASYNC_RESULT, async (results: SandboxProcessObject | undefined) => {
// this event is fired by commands on poll timeout without any payload,
// we want to make sure to only update state if there's payload (event from sfdx-core).
if (results) {
this.latestSandboxProgressObj = results;
this.stages.update(this.latestSandboxProgressObj);
this.updateSandboxRequestData();
}

this.stages.stop('async');
if (this.pollingTimeOut) {
this.warn(messages.getMessage('warning.ClientTimeoutWaitingForSandboxProcess', [this.action.toLowerCase()]));
}
this.log(this.sandboxProgress.formatProgressStatus(false));
return Promise.resolve(this.info(messages.getMessage('checkSandboxStatus', this.getCheckSandboxStatusParams())));
});

lifecycle.on(SandboxEvents.EVENT_STATUS, async (results: StatusEvent) => {
// this starts MSO for:
// * org create/create sandbox
this.stages.start();
this.latestSandboxProgressObj = results.sandboxProcessObj;
this.updateSandboxRequestData();
const progress = this.sandboxProgress.getSandboxProgress(results);
const currentStage = progress.status;
this.updateStage(currentStage, 'inProgress');
return Promise.resolve(this.updateProgress(results, options.isAsync));

this.stages.update(this.latestSandboxProgressObj);

return Promise.resolve();
});

lifecycle.on(SandboxEvents.EVENT_AUTH, async (results: SandboxUserAuthResponse) => {
this.sandboxUsername = results.authUserName;
this.stages.auth();
this.sandboxAuth = results;
return Promise.resolve();
});

lifecycle.on(SandboxEvents.EVENT_RESULT, async (results: ResultEvent) => {
this.latestSandboxProgressObj = results.sandboxProcessObj;
this.sandboxUsername = results.sandboxRes.authUserName;
this.updateSandboxRequestData();
this.sandboxProgress.markPreviousStagesAsCompleted();
this.updateProgress(results, options.isAsync);
if (!options.isAsync) {
this.progress.stop();
}

this.stages.update(results.sandboxProcessObj);

if (results.sandboxRes?.authUserName) {
const authInfo = await AuthInfo.create({ username: results.sandboxRes?.authUserName });
await authInfo.handleAliasAndDefaultSettings({
Expand All @@ -160,8 +166,9 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
setTracksSource: await this.calculateTrackingSetting(options.tracksSource),
});
}
this.stages.stop();

this.removeSandboxProgressConfig();
this.updateProgress(results, options.isAsync);
this.reportResults(results);
});

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

protected reportResults(results: ResultEvent): void {
this.log();
this.styledHeader(`Sandbox Org ${this.action} Status`);
this.log(this.sandboxProgress.formatProgressStatus(false));
this.logSuccess(
[
messages.getMessage('sandboxSuccess', [this.action.toLowerCase()]),
messages.getMessages('sandboxSuccess.actions', [
results.sandboxRes?.authUserName,
this.config.bin,
results.sandboxRes?.authUserName,
]),
messages.getMessage('sandboxSuccess'),
messages.getMessages('sandboxSuccess.actions', [this.config.bin, results.sandboxRes?.authUserName]),
].join(os.EOL)
);
}

protected updateProgress(
event: StatusEvent | (Omit<ResultEvent, 'sandboxRes'> & { sandboxRes?: ResultEvent['sandboxRes'] }),
isAsync: boolean
): void {
const sandboxProgress = this.sandboxProgress.getSandboxProgress(event);
this.sandboxUsername = (event as ResultEvent).sandboxRes?.authUserName;
this.sandboxProgress.statusData = {
sandboxUsername: this.sandboxUsername,
sandboxProgress,
sandboxProcessObj: event.sandboxProcessObj,
};
if (!isAsync) {
this.spinner.status = this.sandboxProgress.formatProgressStatus();
}
}

protected updateStage(stage: string | undefined, state: State): void {
if (stage) {
this.sandboxProgress.transitionStages(stage, state);
}
}

protected updateSandboxRequestData(): void {
if (this.sandboxRequestData && this.latestSandboxProgressObj) {
this.sandboxRequestData.sandboxProcessObject = this.latestSandboxProgressObj;
Expand Down Expand Up @@ -262,6 +240,13 @@ export abstract class SandboxCommandBase<T> extends SfCommand<T> {
return { ...(this.latestSandboxProgressObj as SandboxProcessObject), SandboxUsername: sbxUsername };
}

protected catch(error: Error): Promise<never> {
if (this.stages) {
this.stages.stop('failed');
}

return super.catch(error);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected async finally(_: Error | undefined): Promise<any> {
const lifecycle = Lifecycle.getInstance();
Expand Down
Loading
Loading