Skip to content

Commit ffbdf90

Browse files
authored
Merge pull request #717 from salesforcecli/sh/enhance-org-delete
fix: provide better errors when deleting sandboxes
2 parents f1c6622 + ff05b22 commit ffbdf90

File tree

12 files changed

+1073
-405
lines changed

12 files changed

+1073
-405
lines changed

.github/workflows/create-github-release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ on:
66
- main
77
- prerelease/**
88
tags-ignore:
9-
- "*"
9+
- '*'
1010
workflow_dispatch:
1111
inputs:
1212
prerelease:
1313
type: string
14-
description: "Name to use for the prerelease: beta, dev, etc. NOTE: If this is already set in the package.json, it does not need to be passed in here."
14+
description: 'Name to use for the prerelease: beta, dev, etc. NOTE: If this is already set in the package.json, it does not need to be passed in here.'
1515

1616
jobs:
1717
release:

messages/delete_sandbox.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Delete a sandbox.
66

77
Salesforce CLI marks the org for deletion in the production org that contains the sandbox licenses and then deletes all local references to the org from your computer.
88
Specify a sandbox with either the username you used when you logged into it, or the alias you gave the sandbox when you created it. Run "<%= config.bin %> org list" to view all your orgs, including sandboxes, and their aliases.
9+
Both the sandbox and the associated production org must already be authenticated with the CLI to successfully delete the sandbox.
910

1011
# examples
1112

@@ -17,7 +18,7 @@ Specify a sandbox with either the username you used when you logged into it, or
1718

1819
<%= config.bin %> <%= command.id %> --target-org [email protected]
1920

20-
- Delete the sandbox without prompting to confirm :
21+
- Delete the sandbox without prompting to confirm:
2122

2223
<%= config.bin %> <%= command.id %> --target-org my-sandbox --no-prompt
2324

@@ -41,6 +42,11 @@ Successfully marked sandbox %s for deletion.
4142

4243
There is no sandbox with the username %s.
4344

44-
# error.isNotSandbox
45+
# error.unknownSandbox
4546

46-
The target org, %s, is not a sandbox.
47+
The org with username: %s is not known by the CLI to be a sandbox
48+
49+
# error.unknownSandbox.actions
50+
51+
Re-authenticate the sandbox with the CLI and try again.
52+
Ensure the CLI has authenticated with the sandbox's production org.

src/commands/org/delete/sandbox.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
import { dirname } from 'node:path';
88
import { fileURLToPath } from 'node:url';
9-
import { AuthInfo, AuthRemover, Messages, Org, StateAggregator } from '@salesforce/core';
9+
import { AuthInfo, AuthRemover, Messages, Org, SfError, StateAggregator } from '@salesforce/core';
1010
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
1111
import { orgThatMightBeDeleted } from '../../../shared/flags.js';
1212

@@ -18,7 +18,7 @@ export interface SandboxDeleteResponse {
1818
username: string;
1919
}
2020

21-
export default class EnvDeleteSandbox extends SfCommand<SandboxDeleteResponse> {
21+
export default class DeleteSandbox extends SfCommand<SandboxDeleteResponse> {
2222
public static readonly summary = messages.getMessage('summary');
2323
public static readonly description = messages.getMessage('description');
2424
public static readonly examples = messages.getMessages('examples');
@@ -36,14 +36,30 @@ export default class EnvDeleteSandbox extends SfCommand<SandboxDeleteResponse> {
3636
};
3737

3838
public async run(): Promise<SandboxDeleteResponse> {
39-
const flags = (await this.parse(EnvDeleteSandbox)).flags;
39+
const flags = (await this.parse(DeleteSandbox)).flags;
4040
const username = flags['target-org'];
41+
let orgId: string;
4142

42-
const orgId = (await AuthInfo.create({ username })).getFields().orgId as string;
43-
const isSandbox = await (await StateAggregator.getInstance()).sandboxes.hasFile(orgId);
43+
try {
44+
const sbxAuthFields = (await AuthInfo.create({ username })).getFields();
45+
orgId = sbxAuthFields.orgId as string;
46+
} catch (error) {
47+
if (error instanceof SfError && error.name === 'NamedOrgNotFoundError') {
48+
error.actions = [
49+
`Ensure the alias or username for the ${username} org is correct.`,
50+
`Ensure the ${username} org has been authenticated with the CLI.`,
51+
];
52+
}
53+
throw error;
54+
}
55+
56+
// The StateAggregator identifies sandbox auth files with a pattern of
57+
// <sandbox_ID>.sandbox.json. E.g., 00DZ0000009T3VZMA0.sandbox.json
58+
const stateAggregator = await StateAggregator.getInstance();
59+
const cliCreatedSandbox = await stateAggregator.sandboxes.hasFile(orgId);
4460

45-
if (!isSandbox) {
46-
throw messages.createError('error.isNotSandbox', [username]);
61+
if (!cliCreatedSandbox) {
62+
throw messages.createError('error.unknownSandbox', [username]);
4763
}
4864

4965
if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [username])))) {

src/commands/org/delete/scratch.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export interface ScratchDeleteResponse {
1919
username: string;
2020
}
2121

22-
export default class EnvDeleteScratch extends SfCommand<ScratchDeleteResponse> {
22+
export default class DeleteScratch extends SfCommand<ScratchDeleteResponse> {
2323
public static readonly summary = messages.getMessage('summary');
2424
public static readonly description = messages.getMessage('description');
2525
public static readonly examples = messages.getMessages('examples');
@@ -37,7 +37,7 @@ export default class EnvDeleteScratch extends SfCommand<ScratchDeleteResponse> {
3737
};
3838

3939
public async run(): Promise<ScratchDeleteResponse> {
40-
const flags = (await this.parse(EnvDeleteScratch)).flags;
40+
const flags = (await this.parse(DeleteScratch)).flags;
4141
const resolvedUsername = flags['target-org'];
4242
const orgId = (await AuthInfo.create({ username: resolvedUsername })).getFields().orgId as string;
4343

src/shared/sandboxReporter.ts

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77
import { StatusEvent, ResultEvent } from '@salesforce/core';
8-
import { Duration } from '@salesforce/kit';
8+
import { getSecondsToHuman } from './timeUtils.js';
99

1010
export class SandboxReporter {
1111
public static sandboxProgress(update: StatusEvent): string {
1212
const { remainingWait, interval, sandboxProcessObj, waitingOnAuth } = update;
1313

14-
const waitTime: string = SandboxReporter.getSecondsToHuman(remainingWait);
14+
const waitTime: string = getSecondsToHuman(remainingWait);
1515
const waitTimeMsg = `Sleeping ${interval} seconds. Will wait ${waitTime} more before timing out.`;
1616
const sandboxIdentifierMsg = `${sandboxProcessObj.SandboxName}(${sandboxProcessObj.Id})`;
1717
const waitingOnAuthMessage: string = waitingOnAuth ? ', waiting on JWT auth' : '';
@@ -44,16 +44,4 @@ export class SandboxReporter {
4444

4545
return { sandboxReadyForUse, data };
4646
}
47-
48-
private static getSecondsToHuman(waitTimeInSec: number): string {
49-
const hours = Duration.hours(Math.floor(waitTimeInSec / 3600));
50-
const minutes = Duration.minutes(Math.floor((waitTimeInSec % 3600) / 60));
51-
const seconds = Duration.seconds(Math.floor(waitTimeInSec % 60));
52-
53-
const hDisplay: string = hours.hours > 0 ? hours.toString() + ' ' : '';
54-
const mDisplay: string = minutes.minutes > 0 ? minutes.toString() + ' ' : '';
55-
const sDisplay: string = seconds.seconds > 0 ? seconds.toString() : '';
56-
57-
return (hDisplay + mDisplay + sDisplay).trim();
58-
}
5947
}

test/nut/scratchDelete.nut.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
1111
import { assert, expect } from 'chai';
1212
import { ScratchDeleteResponse } from '../../src/commands/org/delete/scratch.js';
1313

14-
describe('env:delete:scratch NUTs', () => {
14+
describe('org:delete:scratch NUTs', () => {
1515
const scratchOrgAlias = 'scratch-org';
1616
const scratchOrgAlias2 = 'scratch-org-2';
1717
const scratchOrgAlias3 = 'scratch-org-3';

test/unit/force/org/clone.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import requestFunctions from '../../../../src/shared/sandboxRequest.js';
1616

1717
config.truncateThreshold = 0;
1818

19-
describe('org:clone', () => {
19+
describe('[DEPRECATED] force:org:clone', () => {
2020
const $$ = new TestContext();
2121

2222
beforeEach(async () => {

test/unit/force/org/delete.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import { MockTestOrgData, TestContext } from '@salesforce/core/lib/testSetup.js'
1212
import { config, expect } from 'chai';
1313
import { stubPrompter, stubSfCommandUx } from '@salesforce/sf-plugins-core';
1414
import { SandboxAccessor } from '@salesforce/core/lib/stateAggregator/accessors/sandboxAccessor.js';
15-
import { Delete } from '../../../../src/commands/force/org/delete.js';
15+
import { Delete as LegacyDelete } from '../../../../src/commands/force/org/delete.js';
1616

1717
config.truncateThreshold = 0;
1818
Messages.importMessagesDirectory(dirname(fileURLToPath(import.meta.url)));
1919
const messages = Messages.loadMessages('@salesforce/plugin-org', 'delete');
2020

21-
describe('org:delete', () => {
21+
describe('[DEPRECATED] force:org:delete', () => {
2222
const $$ = new TestContext();
2323
const testOrg = new MockTestOrgData();
2424
const testHub = new MockTestOrgData();
@@ -36,7 +36,7 @@ describe('org:delete', () => {
3636
it('will throw an error when no default set', async () => {
3737
await $$.stubConfig({});
3838
try {
39-
await Delete.run();
39+
await LegacyDelete.run();
4040
expect.fail('should have thrown an error');
4141
} catch (e) {
4242
const err = e as SfError;
@@ -47,7 +47,7 @@ describe('org:delete', () => {
4747

4848
it('will prompt before attempting to delete', async () => {
4949
await $$.stubConfig({ 'target-org': testOrg.username });
50-
const res = await Delete.run([]);
50+
const res = await LegacyDelete.run([]);
5151
expect(prompterStubs.confirm.calledOnce).to.equal(true);
5252
expect(prompterStubs.confirm.firstCall.args[0]).to.equal(
5353
messages.getMessage('confirmDelete', ['scratch', testOrg.username])
@@ -58,7 +58,7 @@ describe('org:delete', () => {
5858
it('will resolve a default alias', async () => {
5959
await $$.stubConfig({ 'target-org': 'myAlias' });
6060
$$.stubAliases({ myAlias: testOrg.username });
61-
const res = await Delete.run([]);
61+
const res = await LegacyDelete.run([]);
6262
expect(prompterStubs.confirm.calledOnce).to.equal(true);
6363
expect(prompterStubs.confirm.firstCall.args[0]).to.equal(
6464
messages.getMessage('confirmDelete', ['scratch', testOrg.username])
@@ -68,7 +68,7 @@ describe('org:delete', () => {
6868

6969
it('will determine sandbox vs scratch org and delete sandbox', async () => {
7070
$$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(true);
71-
const res = await Delete.run(['--target-org', testOrg.username]);
71+
const res = await LegacyDelete.run(['--target-org', testOrg.username]);
7272
expect(prompterStubs.confirm.calledOnce).to.equal(true);
7373
expect(prompterStubs.confirm.firstCall.args[0]).to.equal(
7474
messages.getMessage('confirmDelete', ['sandbox', testOrg.username])
@@ -79,7 +79,7 @@ describe('org:delete', () => {
7979
it('will NOT prompt before deleting scratch org when flag is provided', async () => {
8080
$$.SANDBOX.stub(Org.prototype, 'isSandbox').resolves(false);
8181
$$.SANDBOX.stub(Org.prototype, 'delete').resolves();
82-
const res = await Delete.run(['--noprompt', '--target-org', testOrg.username]);
82+
const res = await LegacyDelete.run(['--noprompt', '--target-org', testOrg.username]);
8383
expect(prompterStubs.confirm.calledOnce).to.equal(false);
8484
expect(sfCommandUxStubs.log.callCount).to.equal(1);
8585
expect(sfCommandUxStubs.log.getCalls().flatMap((call) => call.args)).to.deep.include(
@@ -91,7 +91,7 @@ describe('org:delete', () => {
9191
it('will NOT prompt before deleting sandbox when flag is provided', async () => {
9292
$$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(true);
9393
$$.SANDBOX.stub(Org.prototype, 'delete').resolves();
94-
const res = await Delete.run(['--noprompt', '--target-org', testOrg.username]);
94+
const res = await LegacyDelete.run(['--noprompt', '--target-org', testOrg.username]);
9595
expect(prompterStubs.confirm.calledOnce).to.equal(false);
9696
expect(sfCommandUxStubs.log.callCount).to.equal(1);
9797
expect(sfCommandUxStubs.log.getCalls().flatMap((call) => call.args)).to.deep.include(
@@ -103,7 +103,7 @@ describe('org:delete', () => {
103103
it('will catch the ScratchOrgNotFound and wrap correctly', async () => {
104104
$$.SANDBOX.stub(Org.prototype, 'isSandbox').resolves(false);
105105
$$.SANDBOX.stub(Org.prototype, 'delete').throws(new SfError('bah!', 'ScratchOrgNotFound'));
106-
const res = await Delete.run(['--noprompt', '--target-org', testOrg.username]);
106+
const res = await LegacyDelete.run(['--noprompt', '--target-org', testOrg.username]);
107107
expect(prompterStubs.confirm.calledOnce).to.equal(false);
108108
expect(res).to.deep.equal({ orgId: testOrg.orgId, username: testOrg.username });
109109
expect(sfCommandUxStubs.log.getCalls().flatMap((call) => call.args)).to.deep.include(
@@ -114,7 +114,7 @@ describe('org:delete', () => {
114114
it('will catch the SandboxNotFound and wrap correctly', async () => {
115115
$$.SANDBOX.stub(SandboxAccessor.prototype, 'hasFile').resolves(true);
116116
$$.SANDBOX.stub(Org.prototype, 'delete').throws(new SfError('bah!', 'SandboxNotFound'));
117-
const res = await Delete.run(['--noprompt', '--target-org', testOrg.username]);
117+
const res = await LegacyDelete.run(['--noprompt', '--target-org', testOrg.username]);
118118
expect(prompterStubs.confirm.called).to.equal(false);
119119
expect(res).to.deep.equal({ orgId: testOrg.orgId, username: testOrg.username });
120120
expect(

test/unit/force/org/sandboxCreate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ config.truncateThreshold = 0;
3030
Messages.importMessagesDirectory(dirname(fileURLToPath(import.meta.url)));
3131
const messages = Messages.loadMessages('@salesforce/plugin-org', 'create');
3232

33-
describe('org:create (sandbox paths)', () => {
33+
describe('[DEPRECATED] force:org:create (sandbox paths)', () => {
3434
const $$ = new TestContext();
3535
const testOrg = new MockTestOrgData();
3636
// stubs

test/unit/force/org/scratchOrgCreate.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const CREATE_RESULT = {
3636
warnings: [],
3737
};
3838

39-
describe('org:create', () => {
39+
describe('[DEPRECATED] force:org:create', () => {
4040
const $$ = new TestContext();
4141
const testHub = new MockTestOrgData();
4242
testHub.isDevHub = true;

0 commit comments

Comments
 (0)