Skip to content

Commit 337845d

Browse files
authored
Merge pull request #640 from salesforcecli/wr/deleteOrg
fix: allow expired orgs to be deleted
2 parents f0d8282 + cb328b8 commit 337845d

File tree

7 files changed

+137
-59
lines changed

7 files changed

+137
-59
lines changed

messages/delete.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ The force:org:delete command is deprecated. Use org:delete:scratch or org:delete
88

99
# description
1010

11-
Salesforce CLI marks the org for deletion in either the Dev Hub org (for scratch orgs) or production org (for sandboxes) and then deletes all local references to the org from your computer.
11+
Salesforce CLI marks the org for deletion in either the Dev Hub org (for scratch orgs) or production org (for sandboxes)
12+
and then deletes all local references to the org from your computer.
1213

1314
To mark the org for deletion without being prompted to confirm, specify --noprompt.
1415

@@ -22,6 +23,14 @@ To mark the org for deletion without being prompted to confirm, specify --noprom
2223

2324
No prompt to confirm deletion.
2425

26+
# missingUsername
27+
28+
Unable to determine the username of the org to delete. Specify the username with the --target-org | -o flag.
29+
30+
# flags.target-org.summary
31+
32+
Username or alias of the target org.
33+
2534
# flags.targetdevhubusername
2635

2736
The targetdevhubusername flag exists only for backwards compatibility. It is not necessary and has no effect.
@@ -48,4 +57,5 @@ Successfully marked scratch org %s for deletion
4857

4958
# commandSandboxSuccess
5059

51-
The sandbox org %s has been successfully removed from your list of CLI authorized orgs. If you created the sandbox with one of the force:org commands, it has also been marked for deletion.
60+
The sandbox org %s has been successfully removed from your list of CLI authorized orgs. If you created the sandbox with
61+
one of the force:org commands, it has also been marked for deletion.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,4 +236,4 @@
236236
"output": []
237237
}
238238
}
239-
}
239+
}

src/commands/force/org/delete.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,8 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import {
8-
Flags,
9-
SfCommand,
10-
requiredOrgFlagWithDeprecations,
11-
orgApiVersionFlagWithDeprecations,
12-
loglevel,
13-
} from '@salesforce/sf-plugins-core';
14-
import { Messages } from '@salesforce/core';
7+
import { Flags, loglevel, orgApiVersionFlagWithDeprecations, SfCommand } from '@salesforce/sf-plugins-core';
8+
import { AuthInfo, AuthRemover, Messages, Org, StateAggregator } from '@salesforce/core';
159

1610
Messages.importMessagesDirectory(__dirname);
1711
const messages = Messages.loadMessages('@salesforce/plugin-org', 'delete');
@@ -30,7 +24,15 @@ export class Delete extends SfCommand<DeleteResult> {
3024
message: messages.getMessage('deprecation'),
3125
};
3226
public static readonly flags = {
33-
'target-org': requiredOrgFlagWithDeprecations,
27+
'target-org': Flags.string({
28+
// not required because the user could be assuming the default config
29+
aliases: ['targetusername', 'u'],
30+
deprecateAliases: true,
31+
// we're recreating the flag without all the validation
32+
// eslint-disable-next-line sf-plugin/dash-o
33+
char: 'o',
34+
summary: messages.getMessage('flags.target-org.summary'),
35+
}),
3436
targetdevhubusername: Flags.string({
3537
summary: messages.getMessage('flags.targetdevhubusername'),
3638
char: 'v',
@@ -52,24 +54,39 @@ export class Delete extends SfCommand<DeleteResult> {
5254

5355
public async run(): Promise<DeleteResult> {
5456
const { flags } = await this.parse(Delete);
55-
const username = flags['target-org'].getUsername() ?? 'unknown username';
56-
const orgId = flags['target-org'].getOrgId();
57-
// the connection version can be set before using it to isSandbox and delete
58-
flags['target-org'].getConnection(flags['api-version']);
59-
const isSandbox = await flags['target-org'].isSandbox();
57+
const resolvedUsername =
58+
// from -o alias -> -o username -> [default username]
59+
(await StateAggregator.getInstance()).aliases.getUsername(flags['target-org'] ?? '') ??
60+
flags['target-org'] ??
61+
(this.configAggregator.getPropertyValue('target-org') as string);
62+
63+
if (!resolvedUsername) {
64+
throw messages.createError('missingUsername');
65+
}
66+
67+
const orgId = (await AuthInfo.create({ username: resolvedUsername })).getFields().orgId as string;
68+
const isSandbox = await (await StateAggregator.getInstance()).sandboxes.hasFile(orgId);
69+
6070
// read the config file for the org to be deleted, if it has a PROD_ORG_USERNAME entry, it's a sandbox
6171
// we either need permission to proceed without a prompt OR get the user to confirm
6272
if (
6373
flags['no-prompt'] ||
64-
(await this.confirm(messages.getMessage('confirmDelete', [isSandbox ? 'sandbox' : 'scratch', username])))
74+
(await this.confirm(messages.getMessage('confirmDelete', [isSandbox ? 'sandbox' : 'scratch', resolvedUsername])))
6575
) {
6676
let alreadyDeleted = false;
6777
let successMessageKey = 'commandSandboxSuccess';
6878
try {
79+
const org = await Org.create({ aliasOrUsername: resolvedUsername });
80+
6981
// will determine if it's a scratch org or sandbox and will delete from the appropriate parent org (DevHub or Production)
70-
await flags['target-org'].delete();
82+
await org.delete();
7183
} catch (e) {
72-
if (e instanceof Error && e.name === 'ScratchOrgNotFound') {
84+
if (e instanceof Error && e.name === 'DomainNotFoundError') {
85+
// the org has expired, so remote operations won't work
86+
// let's clean up the files locally
87+
const authRemover = await AuthRemover.create();
88+
await authRemover.removeAuth(resolvedUsername);
89+
} else if (e instanceof Error && e.name === 'ScratchOrgNotFound') {
7390
alreadyDeleted = true;
7491
} else if (e instanceof Error && e.name === 'SandboxNotFound') {
7592
successMessageKey = 'sandboxConfigOnlySuccess';
@@ -80,12 +97,12 @@ export class Delete extends SfCommand<DeleteResult> {
8097

8198
this.log(
8299
isSandbox
83-
? messages.getMessage(successMessageKey, [username])
100+
? messages.getMessage(successMessageKey, [resolvedUsername])
84101
: messages.getMessage(alreadyDeleted ? 'deleteOrgConfigOnlyCommandSuccess' : 'deleteOrgCommandSuccess', [
85-
username,
102+
resolvedUsername,
86103
])
87104
);
88105
}
89-
return { username, orgId };
106+
return { username: resolvedUsername, orgId };
90107
}
91108
}

src/commands/org/delete/sandbox.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
* Licensed under the BSD 3-Clause license.
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
7-
import { Messages, SfError } from '@salesforce/core';
8-
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
7+
import { AuthInfo, AuthRemover, Messages, Org, SfError, StateAggregator } from '@salesforce/core';
8+
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';
99

1010
Messages.importMessagesDirectory(__dirname);
1111
const messages = Messages.loadMessages('@salesforce/plugin-org', 'delete_sandbox');
@@ -14,16 +14,19 @@ export interface SandboxDeleteResponse {
1414
orgId: string;
1515
username: string;
1616
}
17+
1718
export default class EnvDeleteSandbox extends SfCommand<SandboxDeleteResponse> {
1819
public static readonly summary = messages.getMessage('summary');
1920
public static readonly description = messages.getMessage('description');
2021
public static readonly examples = messages.getMessages('examples');
2122
public static readonly aliases = ['env:delete:sandbox'];
2223
public static readonly deprecateAliases = true;
2324
public static readonly flags = {
24-
'target-org': Flags.requiredOrg({
25-
summary: messages.getMessage('flags.target-org.summary'),
25+
'target-org': Flags.string({
26+
// we're recreating the flag without all the validation
27+
// eslint-disable-next-line sf-plugin/dash-o
2628
char: 'o',
29+
summary: messages.getMessage('flags.target-org.summary'),
2730
required: true,
2831
}),
2932
'no-prompt': Flags.boolean({
@@ -34,28 +37,40 @@ export default class EnvDeleteSandbox extends SfCommand<SandboxDeleteResponse> {
3437

3538
public async run(): Promise<SandboxDeleteResponse> {
3639
const flags = (await this.parse(EnvDeleteSandbox)).flags;
37-
const org = flags['target-org'];
38-
const username = org.getUsername();
40+
const username = // from -o alias -> -o username -> [default username]
41+
(await StateAggregator.getInstance()).aliases.getUsername(flags['target-org'] ?? '') ??
42+
flags['target-org'] ??
43+
(this.configAggregator.getPropertyValue('target-org') as string);
3944
if (!username) {
4045
throw new SfError('The org does not have a username.');
4146
}
4247

43-
if (!(await org.isSandbox())) {
48+
const orgId = (await AuthInfo.create({ username })).getFields().orgId as string;
49+
const isSandbox = await (await StateAggregator.getInstance()).sandboxes.hasFile(orgId);
50+
51+
if (!isSandbox) {
4452
throw messages.createError('error.isNotSandbox', [username]);
4553
}
4654

4755
if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [username])))) {
4856
try {
57+
const org = await Org.create({ aliasOrUsername: username });
4958
await org.delete();
5059
this.logSuccess(messages.getMessage('success', [username]));
5160
} catch (e) {
52-
if (e instanceof Error && e.name === 'SandboxNotFound') {
61+
if (e instanceof Error && e.name === 'DomainNotFoundError') {
62+
// the org has expired, so remote operations won't work
63+
// let's clean up the files locally
64+
const authRemover = await AuthRemover.create();
65+
await authRemover.removeAuth(username);
66+
this.logSuccess(messages.getMessage('success.Idempotent', [username]));
67+
} else if (e instanceof Error && e.name === 'SandboxNotFound') {
5368
this.logSuccess(messages.getMessage('success.Idempotent', [username]));
5469
} else {
5570
throw e;
5671
}
5772
}
5873
}
59-
return { username, orgId: org.getOrgId() };
74+
return { username, orgId };
6075
}
6176
}

src/commands/org/delete/scratch.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +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 { Messages } from '@salesforce/core';
8+
import { AuthInfo, AuthRemover, Messages, Org, StateAggregator } from '@salesforce/core';
99
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
1010

1111
Messages.importMessagesDirectory(__dirname);
@@ -23,10 +23,14 @@ export default class EnvDeleteScratch extends SfCommand<ScratchDeleteResponse> {
2323
public static readonly aliases = ['env:delete:scratch'];
2424
public static readonly deprecateAliases = true;
2525
public static readonly flags = {
26-
'target-org': Flags.requiredOrg({
26+
'target-org': Flags.string({
27+
// not required because the user could be assuming the default config
28+
aliases: ['targetusername', 'u'],
29+
deprecateAliases: true,
30+
// we're recreating the flag without all the validation
31+
// eslint-disable-next-line sf-plugin/dash-o
2732
char: 'o',
2833
summary: messages.getMessage('flags.target-org.summary'),
29-
required: true,
3034
}),
3135
'no-prompt': Flags.boolean({
3236
char: 'p',
@@ -36,20 +40,34 @@ export default class EnvDeleteScratch extends SfCommand<ScratchDeleteResponse> {
3640

3741
public async run(): Promise<ScratchDeleteResponse> {
3842
const flags = (await this.parse(EnvDeleteScratch)).flags;
39-
const org = flags['target-org'];
43+
const resolvedUsername =
44+
// from -o alias -> -o username -> [default username]
45+
(await StateAggregator.getInstance()).aliases.getUsername(flags['target-org'] ?? '') ??
46+
flags['target-org'] ??
47+
(this.configAggregator.getPropertyValue('target-org') as string);
48+
const orgId = (await AuthInfo.create({ username: resolvedUsername })).getFields().orgId as string;
4049

41-
if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [org.getUsername()])))) {
50+
if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [resolvedUsername])))) {
4251
try {
52+
const org = await Org.create({ aliasOrUsername: resolvedUsername });
53+
4354
await org.delete();
4455
this.logSuccess(messages.getMessage('success', [org.getUsername()]));
56+
return { username: org.getUsername() as string, orgId: org.getOrgId() };
4557
} catch (e) {
46-
if (e instanceof Error && e.name === 'ScratchOrgNotFound') {
47-
this.logSuccess(messages.getMessage('success.Idempotent', [org.getUsername()]));
58+
if (e instanceof Error && e.name === 'DomainNotFoundError') {
59+
// the org has expired, so remote operations won't work
60+
// let's clean up the files locally
61+
const authRemover = await AuthRemover.create();
62+
await authRemover.removeAuth(resolvedUsername);
63+
this.logSuccess(messages.getMessage('success', [resolvedUsername]));
64+
} else if (e instanceof Error && e.name === 'ScratchOrgNotFound') {
65+
this.logSuccess(messages.getMessage('success.Idempotent', [resolvedUsername]));
4866
} else {
4967
throw e;
5068
}
5169
}
5270
}
53-
return { username: org.getUsername() as string, orgId: org.getOrgId() };
71+
return { username: resolvedUsername, orgId };
5472
}
5573
}

test/nut/scratchDelete.nut.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,6 @@ describe('env:delete:scratch NUTs', () => {
5050
}
5151
});
5252

53-
it('should see default username in help', () => {
54-
const output = execCmd<ScratchDeleteResponse>('env:delete:scratch --help', { ensureExitCode: 0 }).shellOutput;
55-
expect(output).to.include(session.orgs.get('default')?.username);
56-
});
57-
5853
it('should delete the 1st scratch org by alias', () => {
5954
const command = `env:delete:scratch --target-org ${scratchOrgAlias} --no-prompt --json`;
6055
const output = execCmd<ScratchDeleteResponse>(command, { ensureExitCode: 0 }).jsonOutput?.result;

0 commit comments

Comments
 (0)