Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: allow expired orgs to be deleted #640

Merged
merged 3 commits into from
Apr 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions messages/delete.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ The force:org:delete command is deprecated. Use org:delete:scratch or org:delete

# description

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.
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.

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

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

No prompt to confirm deletion.

# missingUsername

Unable to determine the username of the org to delete. Specify the username with the --target-org | -o flag.

# flags.target-org.summary

Username or alias of the target org.

# flags.targetdevhubusername

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

# commandSandboxSuccess

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.
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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,4 @@
"output": []
}
}
}
}
57 changes: 37 additions & 20 deletions src/commands/force/org/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,8 @@
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import {
Flags,
SfCommand,
requiredOrgFlagWithDeprecations,
orgApiVersionFlagWithDeprecations,
loglevel,
} from '@salesforce/sf-plugins-core';
import { Messages } from '@salesforce/core';
import { Flags, loglevel, orgApiVersionFlagWithDeprecations, SfCommand } from '@salesforce/sf-plugins-core';
import { AuthInfo, AuthRemover, Messages, Org, StateAggregator } from '@salesforce/core';

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

public async run(): Promise<DeleteResult> {
const { flags } = await this.parse(Delete);
const username = flags['target-org'].getUsername() ?? 'unknown username';
const orgId = flags['target-org'].getOrgId();
// the connection version can be set before using it to isSandbox and delete
flags['target-org'].getConnection(flags['api-version']);
const isSandbox = await flags['target-org'].isSandbox();
const resolvedUsername =
// from -o alias -> -o username -> [default username]
(await StateAggregator.getInstance()).aliases.getUsername(flags['target-org'] ?? '') ??
flags['target-org'] ??
(this.configAggregator.getPropertyValue('target-org') as string);

if (!resolvedUsername) {
throw messages.createError('missingUsername');
}

const orgId = (await AuthInfo.create({ username: resolvedUsername })).getFields().orgId as string;
const isSandbox = await (await StateAggregator.getInstance()).sandboxes.hasFile(orgId);

// read the config file for the org to be deleted, if it has a PROD_ORG_USERNAME entry, it's a sandbox
// we either need permission to proceed without a prompt OR get the user to confirm
if (
flags['no-prompt'] ||
(await this.confirm(messages.getMessage('confirmDelete', [isSandbox ? 'sandbox' : 'scratch', username])))
(await this.confirm(messages.getMessage('confirmDelete', [isSandbox ? 'sandbox' : 'scratch', resolvedUsername])))
) {
let alreadyDeleted = false;
let successMessageKey = 'commandSandboxSuccess';
try {
const org = await Org.create({ aliasOrUsername: resolvedUsername });

// will determine if it's a scratch org or sandbox and will delete from the appropriate parent org (DevHub or Production)
await flags['target-org'].delete();
await org.delete();
} catch (e) {
if (e instanceof Error && e.name === 'ScratchOrgNotFound') {
if (e instanceof Error && e.name === 'DomainNotFoundError') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where does error happen from core? Asking because .delete doesn't need to auth to the org, but does need to auth the hub and there could be a DomainNotFoundError coming from the hub. Then you could get into a weird place where the scratch org never got deleted

// the org has expired, so remote operations won't work
// let's clean up the files locally
const authRemover = await AuthRemover.create();
await authRemover.removeAuth(resolvedUsername);
} else if (e instanceof Error && e.name === 'ScratchOrgNotFound') {
alreadyDeleted = true;
} else if (e instanceof Error && e.name === 'SandboxNotFound') {
successMessageKey = 'sandboxConfigOnlySuccess';
Expand All @@ -80,12 +97,12 @@ export class Delete extends SfCommand<DeleteResult> {

this.log(
isSandbox
? messages.getMessage(successMessageKey, [username])
? messages.getMessage(successMessageKey, [resolvedUsername])
: messages.getMessage(alreadyDeleted ? 'deleteOrgConfigOnlyCommandSuccess' : 'deleteOrgCommandSuccess', [
username,
resolvedUsername,
])
);
}
return { username, orgId };
return { username: resolvedUsername, orgId };
}
}
33 changes: 24 additions & 9 deletions src/commands/org/delete/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { Messages, SfError } from '@salesforce/core';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { AuthInfo, AuthRemover, Messages, Org, SfError, StateAggregator } from '@salesforce/core';
import { Flags, SfCommand } from '@salesforce/sf-plugins-core';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-org', 'delete_sandbox');
Expand All @@ -14,16 +14,19 @@ export interface SandboxDeleteResponse {
orgId: string;
username: string;
}

export default class EnvDeleteSandbox extends SfCommand<SandboxDeleteResponse> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');
public static readonly aliases = ['env:delete:sandbox'];
public static readonly deprecateAliases = true;
public static readonly flags = {
'target-org': Flags.requiredOrg({
summary: messages.getMessage('flags.target-org.summary'),
'target-org': Flags.string({
// we're recreating the flag without all the validation
// eslint-disable-next-line sf-plugin/dash-o
char: 'o',
summary: messages.getMessage('flags.target-org.summary'),
required: true,
}),
'no-prompt': Flags.boolean({
Expand All @@ -34,28 +37,40 @@ export default class EnvDeleteSandbox extends SfCommand<SandboxDeleteResponse> {

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

if (!(await org.isSandbox())) {
const orgId = (await AuthInfo.create({ username })).getFields().orgId as string;
const isSandbox = await (await StateAggregator.getInstance()).sandboxes.hasFile(orgId);

if (!isSandbox) {
throw messages.createError('error.isNotSandbox', [username]);
}

if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [username])))) {
try {
const org = await Org.create({ aliasOrUsername: username });
await org.delete();
this.logSuccess(messages.getMessage('success', [username]));
} catch (e) {
if (e instanceof Error && e.name === 'SandboxNotFound') {
if (e instanceof Error && e.name === 'DomainNotFoundError') {
// the org has expired, so remote operations won't work
// let's clean up the files locally
const authRemover = await AuthRemover.create();
await authRemover.removeAuth(username);
this.logSuccess(messages.getMessage('success.Idempotent', [username]));
} else if (e instanceof Error && e.name === 'SandboxNotFound') {
this.logSuccess(messages.getMessage('success.Idempotent', [username]));
} else {
throw e;
}
}
}
return { username, orgId: org.getOrgId() };
return { username, orgId };
}
}
34 changes: 26 additions & 8 deletions src/commands/org/delete/scratch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { Messages } from '@salesforce/core';
import { AuthInfo, AuthRemover, Messages, Org, StateAggregator } from '@salesforce/core';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';

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

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

if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [org.getUsername()])))) {
if (flags['no-prompt'] || (await this.confirm(messages.getMessage('prompt.confirm', [resolvedUsername])))) {
try {
const org = await Org.create({ aliasOrUsername: resolvedUsername });

await org.delete();
this.logSuccess(messages.getMessage('success', [org.getUsername()]));
return { username: org.getUsername() as string, orgId: org.getOrgId() };
} catch (e) {
if (e instanceof Error && e.name === 'ScratchOrgNotFound') {
this.logSuccess(messages.getMessage('success.Idempotent', [org.getUsername()]));
if (e instanceof Error && e.name === 'DomainNotFoundError') {
// the org has expired, so remote operations won't work
// let's clean up the files locally
const authRemover = await AuthRemover.create();
await authRemover.removeAuth(resolvedUsername);
this.logSuccess(messages.getMessage('success', [resolvedUsername]));
} else if (e instanceof Error && e.name === 'ScratchOrgNotFound') {
this.logSuccess(messages.getMessage('success.Idempotent', [resolvedUsername]));
} else {
throw e;
}
}
}
return { username: org.getUsername() as string, orgId: org.getOrgId() };
return { username: resolvedUsername, orgId };
}
}
5 changes: 0 additions & 5 deletions test/nut/scratchDelete.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,6 @@ describe('env:delete:scratch NUTs', () => {
}
});

it('should see default username in help', () => {
const output = execCmd<ScratchDeleteResponse>('env:delete:scratch --help', { ensureExitCode: 0 }).shellOutput;
expect(output).to.include(session.orgs.get('default')?.username);
});

it('should delete the 1st scratch org by alias', () => {
const command = `env:delete:scratch --target-org ${scratchOrgAlias} --no-prompt --json`;
const output = execCmd<ScratchDeleteResponse>(command, { ensureExitCode: 0 }).jsonOutput?.result;
Expand Down
Loading