Skip to content

Commit e18c197

Browse files
Wr/destructive deploy (#230)
* fix: implementing destructive change deploy, 1 NUT * chore: add NUTs, fix UTs * chore: post-review board updates * chore: fix linting * chore: minor updates to flag logic, redo NUT query method * chore: remove unnecessary checks in NUTs * chore: bump SDR to 5, include deploy:destructive in NUTs
1 parent 9542a72 commit e18c197

13 files changed

+246
-23
lines changed

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ workflows:
7878
- 'yarn test:nuts:manifest:create'
7979
- 'yarn test:nuts:retrieve'
8080
- 'yarn test:nuts:specialTypes'
81+
- 'yarn test:nuts:deploy:destructive'
8182
- release-management/release-package:
8283
sign: true
8384
github-release: true

command-snapshot.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"loglevel",
3434
"manifest",
3535
"metadata",
36+
"postdestructivechanges",
37+
"predestructivechanges",
3638
"runtests",
3739
"soapdeploy",
3840
"sourcepath",

messages/deploy.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"description": "deploy source to an org\nUse this command to deploy source (metadata that’s in source format) to an org.\nTo take advantage of change tracking with scratch orgs, use \"sfdx force:source:push\".\nTo deploy metadata that’s in metadata format, use \"sfdx force:mdapi:deploy\".\n\nThe source you deploy overwrites the corresponding metadata in your org. This command does not attempt to merge your source with the versions in your org.\n\nTo run the command asynchronously, set --wait to 0, which immediately returns the job ID. This way, you can continue to use the CLI.\nTo check the status of the job, use force:source:deploy:report.\n\nIf the comma-separated list you’re supplying contains spaces, enclose the entire comma-separated list in one set of double quotes. On Windows, if the list contains commas, also enclose the entire list in one set of double quotes.\n",
2+
"description": "deploy source to an org\nUse this command to deploy source (metadata that’s in source format) to an org.\nTo take advantage of change tracking with scratch orgs, use \"sfdx force:source:push\".\nTo deploy metadata that’s in metadata format, use \"sfdx force:mdapi:deploy\".\n\nThe source you deploy overwrites the corresponding metadata in your org. This command does not attempt to merge your source with the versions in your org.\n\nTo run the command asynchronously, set --wait to 0, which immediately returns the job ID. This way, you can continue to use the CLI.\nTo check the status of the job, use force:source:deploy:report.\n\nIf the comma-separated list you’re supplying contains spaces, enclose the entire comma-separated list in one set of double quotes. On Windows, if the list contains commas, also enclose the entire list in one set of double quotes.\n If you use the --manifest, --predestructivechanges, or --postdestructivechanges parameters, run the force:source:manifest:create command to easily generate the different types of manifest files.",
33
"examples": [
44
"To deploy the source files in a directory:\n\t $ sfdx force:source:deploy -p path/to/source",
55
"To deploy a specific Apex class and the objects whose source is in a directory: \n\t$ sfdx force:source:deploy -p \"path/to/apex/classes/MyClass.cls,path/to/source/objects\"",
@@ -11,7 +11,9 @@
1111
"To deploy all components listed in a manifest:\n $ sfdx force:source:deploy -x path/to/package.xml",
1212
"To run the tests that aren’t in any managed packages as part of a deployment:\n $ sfdx force:source:deploy -m ApexClass -l RunLocalTests",
1313
"To check whether a deployment would succeed (to prepare for Quick Deploy):\n $ sfdx force:source:deploy -m ApexClass -l RunAllTestsInOrg -c",
14-
"To deploy an already validated deployment (Quick Deploy):\n $ sfdx force:source:deploy -q 0Af9A00000FTM6pSAH`,"
14+
"To deploy an already validated deployment (Quick Deploy):\n $ sfdx force:source:deploy -q 0Af9A00000FTM6pSAH`",
15+
"To run a destructive operation before the deploy occurs:\n $ sfdx force:source:deploy --manifest package.xml --predestructivechanges destructiveChangesPre.xml",
16+
"To run a destructive operation after the deploy occurs:\n $ sfdx force:source:deploy --manifest package.xml --postdestructivechanges destructiveChangesPost.xml"
1517
],
1618
"flags": {
1719
"sourcePath": "comma-separated list of source file paths to deploy",
@@ -25,7 +27,9 @@
2527
"ignoreErrors": "ignore any errors and do not roll back deployment",
2628
"ignoreWarnings": "whether a warning will allow a deployment to complete successfully",
2729
"validateDeployRequestId": "deploy request ID of the validated deployment to run a Quick Deploy",
28-
"soapDeploy": "deploy metadata with SOAP API instead of REST API"
30+
"soapDeploy": "deploy metadata with SOAP API instead of REST API",
31+
"predestructivechanges": "file path for a manifest (destructiveChangesPre.xml) of components to delete before the deploy",
32+
"postdestructivechanges": "file path for a manifest (destructiveChangesPost.xml) of components to delete after the deploy"
2933
},
3034
"flagsLong": {
3135
"sourcePath": [

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"@oclif/config": "^1",
99
"@salesforce/command": "^4.1.3",
1010
"@salesforce/core": "^2.28.0",
11-
"@salesforce/source-deploy-retrieve": "^4.5.7",
11+
"@salesforce/source-deploy-retrieve": "^5.0.0",
1212
"chalk": "^4.1.2",
1313
"cli-ux": "^5.6.3",
1414
"open": "^8.2.1",
@@ -152,6 +152,7 @@
152152
"test:nuts:retrieve": "cross-env PLUGIN_SOURCE_SEED_FILTER=retrieve ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 600000 --parallel --retries 0",
153153
"test:nuts:specialTypes": "mocha \"test/nuts/territory2.nut.ts\" \"test/nuts/folderTypes.nut.ts\" --slow 4500 --timeout 600000 --retries 0 --parallel",
154154
"test:nuts:territory2": "mocha \"test/nuts/territory2.nut.ts\" --slow 4500 --timeout 600000 --retries 0",
155+
"test:nuts:deploy:destructive": "mocha \"test/nuts/deployDestructive.nut.ts\" --slow 3000 --timeout 600000 --parallel --retries 0",
155156
"version": "oclif-dev readme"
156157
},
157158
"husky": {

src/commands/force/source/delete.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,13 @@ import * as fs from 'fs';
99
import { confirm } from 'cli-ux/lib/prompt';
1010
import { flags, FlagsConfig } from '@salesforce/command';
1111
import { Messages } from '@salesforce/core';
12-
import { ComponentSet, MetadataComponent, RequestStatus, SourceComponent } from '@salesforce/source-deploy-retrieve';
12+
import {
13+
ComponentSet,
14+
DestructiveChangesType,
15+
MetadataComponent,
16+
RequestStatus,
17+
SourceComponent,
18+
} from '@salesforce/source-deploy-retrieve';
1319
import { Duration, env, once } from '@salesforce/kit';
1420
import { getString } from '@salesforce/ts-types';
1521
import { DeployCommand } from '../../../deployCommand';
@@ -119,10 +125,10 @@ export class Delete extends DeployCommand {
119125
const cs = new ComponentSet([]);
120126
this.components.map((component) => {
121127
if (component instanceof SourceComponent) {
122-
cs.add(component, true);
128+
cs.add(component, DestructiveChangesType.POST);
123129
} else {
124130
// a remote-only delete
125-
cs.add(new SourceComponent({ name: component.fullName, type: component.type }), true);
131+
cs.add(new SourceComponent({ name: component.fullName, type: component.type }), DestructiveChangesType.POST);
126132
}
127133
});
128134
this.componentSet = cs;

src/commands/force/source/deploy.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,11 @@ import * as os from 'os';
88
import { flags, FlagsConfig } from '@salesforce/command';
99
import { Messages } from '@salesforce/core';
1010
import { AsyncResult, DeployResult, RequestStatus } from '@salesforce/source-deploy-retrieve';
11-
import { Duration } from '@salesforce/kit';
11+
import { Duration, env, once } from '@salesforce/kit';
1212
import { getString, isString } from '@salesforce/ts-types';
13-
import { env, once } from '@salesforce/kit';
1413
import { DeployCommand } from '../../../deployCommand';
1514
import { ComponentSetBuilder } from '../../../componentSetBuilder';
16-
import { DeployResultFormatter, DeployCommandResult } from '../../../formatters/deployResultFormatter';
15+
import { DeployCommandResult, DeployResultFormatter } from '../../../formatters/deployResultFormatter';
1716
import { DeployAsyncResultFormatter, DeployCommandAsyncResult } from '../../../formatters/deployAsyncResultFormatter';
1817
import { ProgressFormatter } from '../../../formatters/progressFormatter';
1918
import { DeployProgressBarFormatter } from '../../../formatters/deployProgressBarFormatter';
@@ -107,6 +106,14 @@ export class Deploy extends DeployCommand {
107106
longDescription: messages.getMessage('flagsLong.manifest'),
108107
exclusive: ['metadata', 'sourcepath'],
109108
}),
109+
predestructivechanges: flags.filepath({
110+
description: messages.getMessage('flags.predestructivechanges'),
111+
dependsOn: ['manifest'],
112+
}),
113+
postdestructivechanges: flags.filepath({
114+
description: messages.getMessage('flags.postdestructivechanges'),
115+
dependsOn: ['manifest'],
116+
}),
110117
};
111118
protected xorFlags = ['manifest', 'metadata', 'sourcepath', 'validateddeployrequestid'];
112119
protected readonly lifecycleEventNames = ['predeploy', 'postdeploy'];
@@ -149,6 +156,8 @@ export class Deploy extends DeployCommand {
149156
manifest: this.flags.manifest && {
150157
manifestPath: this.getFlag<string>('manifest'),
151158
directoryPaths: this.getPackageDirs(),
159+
destructiveChangesPre: this.getFlag<string>('predestructivechanges'),
160+
destructiveChangesPost: this.getFlag<string>('postdestructivechanges'),
152161
},
153162
metadata: this.flags.metadata && {
154163
metadataEntries: this.getFlag<string[]>('metadata'),

src/componentSetBuilder.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77

88
import * as path from 'path';
99
import { ComponentSet, RegistryAccess } from '@salesforce/source-deploy-retrieve';
10-
import { fs, SfdxError, Logger } from '@salesforce/core';
10+
import { fs, Logger, SfdxError } from '@salesforce/core';
1111

1212
export type ManifestOption = {
1313
manifestPath: string;
1414
directoryPaths: string[];
15+
destructiveChangesPre?: string;
16+
destructiveChangesPost?: string;
1517
};
1618
export type MetadataOption = {
1719
metadataEntries: string[];
@@ -21,6 +23,7 @@ export type ComponentSetOptions = {
2123
packagenames?: string[];
2224
sourcepath?: string[];
2325
manifest?: ManifestOption;
26+
2427
metadata?: MetadataOption;
2528
apiversion?: string;
2629
sourceapiversion?: string;
@@ -68,6 +71,8 @@ export class ComponentSetBuilder {
6871
manifestPath: manifest.manifestPath,
6972
resolveSourcePaths: options.manifest.directoryPaths,
7073
forceAddWildcards: true,
74+
destructivePre: options.manifest.destructiveChangesPre,
75+
destructivePost: options.manifest.destructiveChangesPost,
7176
});
7277
}
7378

src/deployCommand.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import { ComponentSet, DeployResult, MetadataApiDeployStatus } from '@salesforce/source-deploy-retrieve';
9-
import { SfdxError, ConfigFile, ConfigAggregator, PollingClient, StatusResult } from '@salesforce/core';
9+
import { ConfigAggregator, ConfigFile, PollingClient, SfdxError, StatusResult } from '@salesforce/core';
1010
import { AnyJson, asString, getBoolean } from '@salesforce/ts-types';
1111
import { Duration, once } from '@salesforce/kit';
1212
import { SourceCommand } from './sourceCommand';
@@ -21,7 +21,6 @@ export abstract class DeployCommand extends SourceCommand {
2121
});
2222

2323
protected deployResult: DeployResult;
24-
2524
/**
2625
* Request a report of an in-progess or completed deployment.
2726
*

src/formatters/deployResultFormatter.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
import * as chalk from 'chalk';
99
import { UX } from '@salesforce/command';
1010
import { Logger, Messages, SfdxError } from '@salesforce/core';
11-
import { get, getBoolean, getString, getNumber, asString } from '@salesforce/ts-types';
11+
import { asString, get, getBoolean, getNumber, getString } from '@salesforce/ts-types';
1212
import {
13-
DeployResult,
1413
CodeCoverage,
14+
DeployMessage,
15+
DeployResult,
1516
FileResponse,
1617
MetadataApiDeployStatus,
1718
RequestStatus,
18-
DeployMessage,
1919
} from '@salesforce/source-deploy-retrieve';
2020
import { ResultFormatter, ResultFormatterOptions, toArray } from './resultFormatter';
2121

@@ -76,6 +76,7 @@ export class DeployResultFormatter extends ResultFormatter {
7676
throw new SfdxError(messages.getMessage('deployCanceled', [canceledByName]), 'DeployFailed');
7777
}
7878
this.displaySuccesses();
79+
this.displayDeletions();
7980
this.displayFailures();
8081
this.displayTestResults();
8182

@@ -107,7 +108,7 @@ export class DeployResultFormatter extends ResultFormatter {
107108

108109
protected displaySuccesses(): void {
109110
if (this.isSuccess() && this.fileResponses?.length) {
110-
const successes = this.fileResponses.filter((f) => f.state !== 'Failed');
111+
const successes = this.fileResponses.filter((f) => !['Failed', 'Deleted'].includes(f.state));
111112
if (!successes.length) {
112113
return;
113114
}
@@ -126,6 +127,25 @@ export class DeployResultFormatter extends ResultFormatter {
126127
}
127128
}
128129

130+
protected displayDeletions(): void {
131+
const deletions = this.fileResponses.filter((f) => f.state === 'Deleted');
132+
if (!deletions.length) {
133+
return;
134+
}
135+
this.sortFileResponses(deletions);
136+
this.asRelativePaths(deletions);
137+
138+
this.ux.log('');
139+
this.ux.styledHeader(chalk.blue('Deleted Source'));
140+
this.ux.table(deletions, {
141+
columns: [
142+
{ key: 'fullName', label: 'FULL NAME' },
143+
{ key: 'type', label: 'TYPE' },
144+
{ key: 'filePath', label: 'PROJECT PATH' },
145+
],
146+
});
147+
}
148+
129149
protected displayFailures(): void {
130150
if (this.hasStatus(RequestStatus.Failed)) {
131151
const failures: Array<FileResponse | DeployMessage> = [];

test/commands/source/componentSetBuilder.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,8 @@ describe('ComponentSetBuilder', () => {
321321
forceAddWildcards: true,
322322
manifestPath: options.manifest.manifestPath,
323323
resolveSourcePaths: [packageDir1],
324+
destructivePre: undefined,
325+
destructivePost: undefined,
324326
});
325327
expect(compSet.size).to.equal(1);
326328
expect(compSet.has(apexClassComponent)).to.equal(true);
@@ -348,6 +350,8 @@ describe('ComponentSetBuilder', () => {
348350
forceAddWildcards: true,
349351
manifestPath: options.manifest.manifestPath,
350352
resolveSourcePaths: [packageDir1, packageDir2],
353+
destructivePre: undefined,
354+
destructivePost: undefined,
351355
});
352356
expect(compSet.size).to.equal(2);
353357
expect(compSet.has(apexClassComponent)).to.equal(true);

test/commands/source/deploy.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import * as sinon from 'sinon';
1010
import { expect } from 'chai';
1111
import { MetadataApiDeployOptions } from '@salesforce/source-deploy-retrieve';
1212
import { fromStub, stubInterface, stubMethod } from '@salesforce/ts-sinon';
13-
import { ConfigAggregator, Lifecycle, Org, SfdxProject, Messages } from '@salesforce/core';
13+
import { ConfigAggregator, Lifecycle, Messages, Org, SfdxProject } from '@salesforce/core';
1414
import { UX } from '@salesforce/command';
1515
import { IConfig } from '@oclif/config';
1616
import { Deploy } from '../../../src/commands/force/source/deploy';
1717
import { DeployCommandResult, DeployResultFormatter } from '../../../src/formatters/deployResultFormatter';
1818
import {
19-
DeployCommandAsyncResult,
2019
DeployAsyncResultFormatter,
20+
DeployCommandAsyncResult,
2121
} from '../../../src/formatters/deployAsyncResultFormatter';
2222
import { ComponentSetBuilder, ComponentSetOptions } from '../../../src/componentSetBuilder';
2323
import { DeployProgressBarFormatter } from '../../../src/formatters/deployProgressBarFormatter';
@@ -223,6 +223,8 @@ describe('force:source:deploy', () => {
223223
manifest: {
224224
manifestPath: manifest,
225225
directoryPaths: [defaultDir],
226+
destructiveChangesPost: undefined,
227+
destructiveChangesPre: undefined,
226228
},
227229
});
228230
ensureDeployArgs();
@@ -240,6 +242,8 @@ describe('force:source:deploy', () => {
240242
manifest: {
241243
manifestPath: manifest,
242244
directoryPaths: [defaultDir],
245+
destructiveChangesPost: undefined,
246+
destructiveChangesPre: undefined,
243247
},
244248
});
245249
ensureDeployArgs();
@@ -258,6 +262,8 @@ describe('force:source:deploy', () => {
258262
manifest: {
259263
manifestPath: manifest,
260264
directoryPaths: [defaultDir],
265+
destructiveChangesPost: undefined,
266+
destructiveChangesPre: undefined,
261267
},
262268
});
263269
ensureDeployArgs();
@@ -284,6 +290,8 @@ describe('force:source:deploy', () => {
284290
manifest: {
285291
manifestPath: manifest,
286292
directoryPaths: [defaultDir],
293+
destructiveChangesPost: undefined,
294+
destructiveChangesPre: undefined,
287295
},
288296
});
289297

0 commit comments

Comments
 (0)