Skip to content

Commit f0cb569

Browse files
authored
Merge pull request #11 from salesforcecli/wr/deployretrieve
Wr/deployretrieve
2 parents aba798d + ca2076c commit f0cb569

File tree

13 files changed

+2512
-1357
lines changed

13 files changed

+2512
-1357
lines changed

command-snapshot.json

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,38 @@
1-
[]
1+
[
2+
{
3+
"command": "force:source:deploy",
4+
"plugin": "@salesforce/plugin-source",
5+
"flags": [
6+
"apiversion",
7+
"checkonly",
8+
"ignoreerrors",
9+
"ignorewarnings",
10+
"json",
11+
"loglevel",
12+
"manifest",
13+
"metadata",
14+
"runtests",
15+
"sourcepath",
16+
"targetusername",
17+
"testlevel",
18+
"validateddeployrequestid",
19+
"verbose",
20+
"wait"
21+
]
22+
},
23+
{
24+
"command": "force:source:retrieve",
25+
"plugin": "@salesforce/plugin-source",
26+
"flags": [
27+
"apiversion",
28+
"json",
29+
"loglevel",
30+
"manifest",
31+
"metadata",
32+
"packagenames",
33+
"sourcepath",
34+
"targetusername",
35+
"wait"
36+
]
37+
}
38+
]

messages/deploy.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"description": "deploy source to an org",
3+
"examples": [
4+
"$ sfdx force:source:deploy -p path/to/source",
5+
"$ sfdx force:source:deploy -p \"path/to/apex/classes/MyClass.cls,path/to/source/objects\"",
6+
"$ sfdx force:source:deploy -p \"path/to/objects/MyCustomObject/fields/MyField.field-meta.xml, path/to/apex/classes\"",
7+
"$ sfdx force:source:deploy -m ApexClass",
8+
"$ sfdx force:source:deploy -m ApexClass:MyApexClass",
9+
"$ sfdx force:source:deploy -m \"CustomObject,ApexClass\"",
10+
"$ sfdx force:source:deploy -m \"ApexClass, Profile:My Profile, Profile: AnotherProfile\"",
11+
"$ sfdx force:source:deploy -x path/to/package.xml",
12+
"$ sfdx force:source:deploy -m ApexClass -l RunLocalTests",
13+
"$ sfdx force:source:deploy -m ApexClass -l RunAllTestsInOrg -c",
14+
"$ sfdx force:source:deploy -q 0Af9A00000FTM6pSAH`"
15+
],
16+
"flags": {
17+
"sourcePath": "comma-separated list of source file paths to retrieve",
18+
"manifest": "file path for manifest (package.xml) of components to retrieve",
19+
"metadata": "comma-separated list of metadata component names",
20+
"wait": "wait time for command to finish in minutes",
21+
"packagename": "a comma-separated list of packages to retrieve",
22+
"verbose": "verbose output of retrieve result",
23+
"checkonly": "validate deploy but don’t save to the org",
24+
"testLevel": "deployment testing level",
25+
"runTests": "tests to run if --testlevel RunSpecifiedTests",
26+
"ignoreErrors": "ignore any errors and do not roll back deployment",
27+
"ignoreWarnings": "whether a warning will allow a deployment to complete successfully",
28+
"validateDeployRequestId": "request ID of the validated deployment to run a Quick Deploy"
29+
},
30+
"SourceRetrieveError": "Could not retrieve files in the sourcepath%s",
31+
"checkOnlySuccess": "\nSuccessfully validated the deployment"
32+
}

messages/retrieve.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"description": "retrieve source from an org",
3+
"examples": [
4+
"sfdx force:source:retrieve -p path/to/source",
5+
"sfdx force:source:retrieve -p \"path/to/apex/classes/MyClass.cls,path/to/source/objects\"",
6+
"sfdx force:source:retrieve -p \"path/to/objects/MyCustomObject/fields/MyField.field-meta.xml, path/to/apex/classes\"",
7+
"sfdx force:source:retrieve -m ApexClass",
8+
"sfdx force:source:retrieve -m ApexClass:MyApexClass",
9+
"sfdx force:source:retrieve -m \"CustomObject,ApexClass\"",
10+
"sfdx force:source:retrieve -x path/to/package.xml",
11+
"sfdx force:source:retrieve -n \"Package1, PackageName With Spaces, Package3\"",
12+
"sfdx force:source:retrieve -n MyPackageName -p path/to/apex/classes",
13+
"sfdx force:source:retrieve -n MyPackageName -x path/to/package.xml"
14+
],
15+
"flags": {
16+
"sourcePath": "comma-separated list of source file paths to retrieve",
17+
"manifestParamDescription": "file path for manifest (package.xml) of components to retrieve",
18+
"metadataParamDescription": "comma-separated list of metadata component names",
19+
"wait": "wait time for command to finish in minutes",
20+
"manifest": "file path for manifest (package.xml) of components to retrieve",
21+
"metadata": "comma-separated list of metadata component names",
22+
"packagename": "a comma-separated list of packages to retrieve",
23+
"verbose": "verbose output of retrieve result"
24+
},
25+
"SourceRetrieveError": "Could not retrieve files in the sourcepath%s",
26+
"retrieveTimeout": "Your retrieve request did not complete within the specified wait time [%s minutes]. Try again with a longer wait time.",
27+
"retrievedSourceHeader": "Retrieved Source",
28+
"fullNameTableColumn": "FULL NAME",
29+
"typeTableColumn": "TYPE",
30+
"workspacePathTableColumn": "PROJECT PATH",
31+
"NoResultsFound": "No results found",
32+
"metadataNotFoundWarning": "WARNING: The following metadata isn’t in your org. If it’s not new, someone deleted it from the org.",
33+
"columnNumberColumn": "COLUMN NUMBER",
34+
"lineNumberColumn": "LINE NUMBER",
35+
"errorColumn": "PROBLEM"
36+
}

package.json

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@
77
"dependencies": {
88
"@oclif/config": "^1",
99
"@salesforce/command": "^3.1.0",
10-
"@salesforce/core": "^2.18.0",
10+
"@salesforce/core": "^2.19.1",
11+
"@salesforce/source-deploy-retrieve": "^1.1.17",
12+
"chalk": "^4.1.0",
1113
"tslib": "^2"
1214
},
1315
"devDependencies": {
1416
"@oclif/dev-cli": "^1",
1517
"@oclif/plugin-command-snapshot": "^2.0.0",
1618
"@salesforce/dev-config": "^2.1.0",
19+
"@salesforce/cli-plugins-testkit": "^0.0.8",
1720
"@salesforce/dev-scripts": "^0.7.0",
1821
"@salesforce/plugin-command-reference": "^1.3.0",
19-
"@salesforce/prettier-config": "^0.0.1",
22+
"@salesforce/prettier-config": "^0.0.2",
2023
"@salesforce/ts-sinon": "1.3.0",
2124
"@typescript-eslint/eslint-plugin": "^4.2.0",
2225
"@typescript-eslint/parser": "^4.2.0",
@@ -73,8 +76,13 @@
7376
"@salesforce/plugin-command-reference"
7477
],
7578
"topics": {
76-
"hello": {
77-
"description": "Commands to say hello."
79+
"force": {
80+
"external": true,
81+
"subtopics": {
82+
"source": {
83+
"description": "commands to interact with source formatted metadata"
84+
}
85+
}
7886
}
7987
}
8088
},

src/commands/force/source/deploy.ts

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,191 @@
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 * as os from 'os';
8+
import * as path from 'path';
9+
import { flags, FlagsConfig } from '@salesforce/command';
10+
import { Lifecycle, Messages } from '@salesforce/core';
11+
import { SourceDeployResult } from '@salesforce/source-deploy-retrieve';
12+
import { Duration } from '@salesforce/kit';
13+
import { asString, asArray } from '@salesforce/ts-types';
14+
import * as chalk from 'chalk';
15+
import { SourceCommand } from '../../../sourceCommand';
16+
17+
Messages.importMessagesDirectory(__dirname);
18+
const messages = Messages.loadMessages('@salesforce/plugin-source', 'deploy');
19+
20+
export class deploy extends SourceCommand {
21+
public static readonly description = messages.getMessage('description');
22+
public static readonly examples = messages.getMessage('examples').split(os.EOL);
23+
public static readonly requiresProject = true;
24+
public static readonly requiresUsername = true;
25+
public static readonly flagsConfig: FlagsConfig = {
26+
checkonly: flags.boolean({
27+
char: 'c',
28+
description: messages.getMessage('flags.checkonly'),
29+
default: false,
30+
}),
31+
wait: flags.minutes({
32+
char: 'w',
33+
default: Duration.minutes(SourceCommand.DEFAULT_SRC_WAIT_MINUTES),
34+
min: Duration.minutes(SourceCommand.MINIMUM_SRC_WAIT_MINUTES),
35+
description: messages.getMessage('flags.wait'),
36+
}),
37+
testlevel: flags.enum({
38+
char: 'l',
39+
description: messages.getMessage('flags.testLevel'),
40+
options: ['NoTestRun', 'RunSpecifiedTests', 'RunLocalTests', 'RunAllTestsInOrg'],
41+
default: 'NoTestRun',
42+
}),
43+
runtests: flags.array({
44+
char: 'r',
45+
description: messages.getMessage('flags.runTests'),
46+
default: [],
47+
}),
48+
ignoreerrors: flags.boolean({
49+
char: 'o',
50+
description: messages.getMessage('flags.ignoreErrors'),
51+
default: false,
52+
}),
53+
ignorewarnings: flags.boolean({
54+
char: 'g',
55+
description: messages.getMessage('flags.ignoreWarnings'),
56+
default: false,
57+
}),
58+
validateddeployrequestid: flags.id({
59+
char: 'q',
60+
description: messages.getMessage('flags.validateDeployRequestId'),
61+
exclusive: [
62+
'manifest',
63+
'metadata',
64+
'sourcepath',
65+
'checkonly',
66+
'testlevel',
67+
'runtests',
68+
'ignoreerrors',
69+
'ignorewarnings',
70+
],
71+
}),
72+
verbose: flags.builtin({
73+
description: messages.getMessage('flags.verbose'),
74+
}),
75+
metadata: flags.array({
76+
char: 'm',
77+
description: messages.getMessage('flags.metadata'),
78+
exclusive: ['manifest', 'sourcepath'],
79+
}),
80+
sourcepath: flags.array({
81+
char: 'p',
82+
description: messages.getMessage('flags.sourcePath'),
83+
exclusive: ['manifest', 'metadata'],
84+
}),
85+
manifest: flags.filepath({
86+
char: 'x',
87+
description: messages.getMessage('flags.manifest'),
88+
exclusive: ['metadata', 'sourcepath'],
89+
}),
90+
};
91+
protected readonly lifecycleEventNames = ['predeploy', 'postdeploy'];
92+
93+
public async run(): Promise<SourceDeployResult> {
94+
if (this.flags.validatedeployrequestid) {
95+
// TODO: return this.doDeployRecentValidation();
96+
}
97+
const hookEmitter = Lifecycle.getInstance();
98+
99+
const cs = await this.createComponentSet({
100+
sourcepath: asArray<string>(this.flags.sourcepath),
101+
manifest: asString(this.flags.manifest),
102+
metadata: asArray<string>(this.flags.metadata),
103+
});
104+
105+
await hookEmitter.emit('predeploy', { packageXmlPath: cs.getPackageXml() });
106+
107+
const results = await cs.deploy(this.org.getUsername(), {
108+
wait: (this.flags.wait as Duration).milliseconds,
109+
apiOptions: {
110+
// TODO: build out more api options
111+
checkOnly: this.flags.checkonly as boolean,
112+
ignoreWarnings: this.flags.ignorewarnings as boolean,
113+
runTests: this.flags.runtests as string[],
114+
},
115+
});
116+
117+
await hookEmitter.emit('postdeploy', results);
118+
119+
this.print(results);
120+
121+
return results;
122+
}
123+
124+
private printComponentFailures(result: SourceDeployResult): void {
125+
if (result.status === 'Failed' && result.components) {
126+
// sort by filename then fullname
127+
const failures = result.components.sort((i, j) => {
128+
if (i.component.type.directoryName === j.component.type.directoryName) {
129+
// if the have the same directoryName then sort by fullName
130+
return i.component.fullName < j.component.fullName ? 1 : -1;
131+
}
132+
return i.component.type.directoryName < j.component.type.directoryName ? 1 : -1;
133+
});
134+
this.ux.log('');
135+
this.ux.styledHeader(chalk.red(`Component Failures [${failures.length}]`));
136+
this.ux.table(failures, {
137+
// TODO: these accessors are temporary until library JSON fixes
138+
columns: [
139+
{ key: 'component.type.name', label: 'Type' },
140+
{ key: 'diagnostics[0].filePath', label: 'File' },
141+
{ key: 'component.name', label: 'Name' },
142+
{ key: 'diagnostics[0].message', label: 'Problem' },
143+
],
144+
});
145+
this.ux.log('');
146+
}
147+
}
148+
149+
private printComponentSuccess(result: SourceDeployResult): void {
150+
if (result.success && result.components) {
151+
if (result.components.length > 0) {
152+
// sort by type then filename then fullname
153+
const files = result.components.sort((i, j) => {
154+
if (i.component.type.name === j.component.type.name) {
155+
// same metadata type, according to above comment sort on filename
156+
if (i.component.type.directoryName === j.component.type.directoryName) {
157+
// same filename's according to comment sort by fullName
158+
return i.component.fullName < j.component.fullName ? 1 : -1;
159+
}
160+
return i.component.type.directoryName < j.component.type.directoryName ? 1 : -1;
161+
}
162+
return i.component.type.name < j.component.type.name ? 1 : -1;
163+
});
164+
// get relative path for table output
165+
files.forEach((file) => {
166+
if (file.component.content) {
167+
file.component.content = path.relative(process.cwd(), file.component.content);
168+
}
169+
});
170+
this.ux.log('');
171+
this.ux.styledHeader(chalk.blue('Deployed Source'));
172+
this.ux.table(files, {
173+
// TODO: these accessors are temporary until library JSON fixes
174+
columns: [
175+
{ key: 'component.name', label: 'FULL NAME' },
176+
{ key: 'component.type.name', label: 'TYPE' },
177+
{ key: 'component.content', label: 'PROJECT PATH' },
178+
],
179+
});
180+
}
181+
}
182+
}
183+
184+
private print(result: SourceDeployResult): SourceDeployResult {
185+
this.printComponentSuccess(result);
186+
this.printComponentFailures(result);
187+
// TODO: this.printTestResults(result); <- this has WI @W-8903671@
188+
if (result.success && this.flags.checkonly) {
189+
this.log(messages.getMessage('checkOnlySuccess'));
190+
}
191+
192+
return result;
193+
}
194+
}

0 commit comments

Comments
 (0)