Skip to content

Commit 81393a1

Browse files
Merge pull request #619 from salesforcecli/sm/projectless-deploys
fix: deploys don't require a project
2 parents f55b6d7 + 5f0fe08 commit 81393a1

28 files changed

+454
-113
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
},
2020
"devDependencies": {
2121
"@oclif/plugin-command-snapshot": "^3.3.14",
22-
"@salesforce/cli-plugins-testkit": "^3.3.2",
22+
"@salesforce/cli-plugins-testkit": "^3.4.0",
2323
"@salesforce/dev-config": "^3.1.0",
2424
"@salesforce/dev-scripts": "^4.3.1",
2525
"@salesforce/plugin-command-reference": "^2.4.4",
@@ -146,6 +146,7 @@
146146
"test:nuts:convert": "nyc mocha \"test/nuts/convert/*.nut.ts\" --slow 4500 --timeout 1200000 --parallel --retries 0 --jobs 20",
147147
"test:nuts:deb": "nyc mocha \"test/nuts/digitalExperienceBundle/*.nut.ts\" --slow 4500 --timeout 1200000 --parallel --retries 0 --jobs 20",
148148
"test:nuts:delete": "nyc mocha \"test/nuts/delete/*.nut.ts\" --slow 4500 --timeout 1200000 --parallel --retries 0 --jobs 20",
149+
"test:nuts:deploy": "nyc mocha \"test/nuts/deploy/*.nut.ts\" --slow 4500 --timeout 1200000 --parallel --retries 0 --jobs 20",
149150
"test:nuts:deploy:metadata:manifest": "cross-env PLUGIN_DEPLOY_RETRIEVE_SEED_FILTER=deploy.metadata.manifest ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 1200000 --parallel --retries 0 --jobs 20",
150151
"test:nuts:deploy:metadata:metadata": "cross-env PLUGIN_DEPLOY_RETRIEVE_SEED_FILTER=deploy.metadata.metadata ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 1200000 --parallel --retries 0 --jobs 20",
151152
"test:nuts:deploy:metadata:metadata-dir": "cross-env PLUGIN_DEPLOY_RETRIEVE_SEED_FILTER=deploy.metadata.metadata-dir ts-node ./test/nuts/generateNuts.ts && mocha \"test/nuts/generated/*.nut.ts\" --slow 4500 --timeout 1200000 --parallel --retries 0 --jobs 20",
@@ -269,4 +270,4 @@
269270
"output": []
270271
}
271272
}
272-
}
273+
}

schemas/project-deploy-cancel.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@
483483
{
484484
"type": "string",
485485
"const": "Queued"
486+
},
487+
{
488+
"type": "string",
489+
"const": "Nothing to deploy"
486490
}
487491
]
488492
},

schemas/project-deploy-quick.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@
483483
{
484484
"type": "string",
485485
"const": "Queued"
486+
},
487+
{
488+
"type": "string",
489+
"const": "Nothing to deploy"
486490
}
487491
]
488492
},

schemas/project-deploy-report.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@
483483
{
484484
"type": "string",
485485
"const": "Queued"
486+
},
487+
{
488+
"type": "string",
489+
"const": "Nothing to deploy"
486490
}
487491
]
488492
},

schemas/project-deploy-resume.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@
483483
{
484484
"type": "string",
485485
"const": "Queued"
486+
},
487+
{
488+
"type": "string",
489+
"const": "Nothing to deploy"
486490
}
487491
]
488492
},

schemas/project-deploy-start.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@
483483
{
484484
"type": "string",
485485
"const": "Queued"
486+
},
487+
{
488+
"type": "string",
489+
"const": "Nothing to deploy"
486490
}
487491
]
488492
},

schemas/project-deploy-validate.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@
483483
{
484484
"type": "string",
485485
"const": "Queued"
486+
},
487+
{
488+
"type": "string",
489+
"const": "Nothing to deploy"
486490
}
487491
]
488492
},

src/commands/project/deploy/start.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { DEPLOY_STATUS_CODES_DESCRIPTIONS } from '../../../utils/errorCodes';
1919
import { ConfigVars } from '../../../configMeta';
2020
import { coverageFormattersFlag, fileOrDirFlag, testLevelFlag, testsFlag } from '../../../utils/flags';
2121
import { writeConflictTable } from '../../../utils/conflicts';
22+
import { getOptionalProject } from '../../../utils/project';
2223

2324
Messages.importMessagesDirectory(__dirname);
2425
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'deploy.metadata');
@@ -29,7 +30,6 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
2930
public static readonly description = messages.getMessage('description');
3031
public static readonly summary = messages.getMessage('summary');
3132
public static readonly examples = messages.getMessages('examples');
32-
public static readonly requiresProject = true;
3333
public static readonly aliases = ['deploy:metadata'];
3434
public static readonly deprecateAliases = true;
3535

@@ -163,8 +163,10 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
163163

164164
public async run(): Promise<DeployResultJson> {
165165
const { flags } = await this.parse(DeployMetadata);
166+
const project = await getOptionalProject();
167+
166168
if (
167-
this.project.getSfProjectJson().getContents()['pushPackageDirectoriesSequentially'] &&
169+
project?.getSfProjectJson().getContents()['pushPackageDirectoriesSequentially'] &&
168170
// flag exclusivity is handled correctly above - but to avoid short-circuiting the check, we need to check all of them
169171
!flags.manifest &&
170172
!flags.metadata &&
@@ -186,9 +188,14 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
186188
api,
187189
},
188190
this.config.bin,
189-
this.project
191+
project
190192
);
191193

194+
if (!deploy) {
195+
this.log('No changes to deploy');
196+
return { status: 'Nothing to deploy', files: [] };
197+
}
198+
192199
const action = flags['dry-run'] ? 'Deploying (dry-run)' : 'Deploying';
193200
this.log(getVersionMessage(action, componentSet, api));
194201
if (!deploy.id) {

src/commands/project/deploy/validate.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,13 @@ export default class DeployMetadataValidate extends SfCommand<DeployResultJson>
127127
api,
128128
},
129129
this.config.bin,
130-
this.project
130+
this.project,
131+
undefined,
132+
true
131133
);
132134

133135
this.log(getVersionMessage('Validating Deployment', componentSet, api));
136+
134137
if (!deploy.id) {
135138
throw new SfError('The deploy id is not available.');
136139
}

src/commands/project/retrieve/start.ts

Lines changed: 115 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,20 @@
88
import { rm } from 'fs/promises';
99
import { join, resolve } from 'path';
1010

11-
import { EnvironmentVariable, Messages, OrgConfigProperties, SfError } from '@salesforce/core';
12-
import { RetrieveResult, ComponentSetBuilder, RetrieveSetOptions } from '@salesforce/source-deploy-retrieve';
13-
11+
import { EnvironmentVariable, Messages, OrgConfigProperties, SfError, SfProject } from '@salesforce/core';
12+
import {
13+
RetrieveResult,
14+
ComponentSetBuilder,
15+
RetrieveSetOptions,
16+
ComponentSet,
17+
FileResponse,
18+
} from '@salesforce/source-deploy-retrieve';
1419
import { SfCommand, toHelpSection, Flags } from '@salesforce/sf-plugins-core';
1520
import { getString } from '@salesforce/ts-types';
1621
import { SourceTracking, SourceConflictError } from '@salesforce/source-tracking';
1722
import { Duration } from '@salesforce/kit';
23+
import { Interfaces } from '@oclif/core';
24+
1825
import { DEFAULT_ZIP_FILE_NAME, ensuredDirFlag, zipFileFlag } from '../../../utils/flags';
1926
import { RetrieveResultFormatter } from '../../../formatters/retrieveResultFormatter';
2027
import { MetadataRetrieveResultFormatter } from '../../../formatters/metadataRetrieveResultFormatter';
@@ -26,11 +33,11 @@ Messages.importMessagesDirectory(__dirname);
2633
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'retrieve.metadata');
2734
const mdTransferMessages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'metadata.transfer');
2835

36+
type Format = 'source' | 'metadata';
2937
export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
3038
public static readonly summary = messages.getMessage('summary');
3139
public static readonly description = messages.getMessage('description');
3240
public static readonly examples = messages.getMessages('examples');
33-
public static readonly requiresProject = true;
3441
public static readonly aliases = ['retrieve:metadata'];
3542
public static readonly deprecateAliases = true;
3643

@@ -127,79 +134,33 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
127134

128135
protected retrieveResult!: RetrieveResult;
129136

130-
// eslint-disable-next-line complexity
131137
public async run(): Promise<RetrieveResultJson> {
132138
const { flags } = await this.parse(RetrieveMetadata);
139+
const format: Format = flags['target-metadata-dir'] ? 'metadata' : 'source';
140+
const zipFileName = flags['zip-file-name'] ?? DEFAULT_ZIP_FILE_NAME;
133141

134142
this.spinner.start(messages.getMessage('spinner.start'));
135-
const format = flags['target-metadata-dir'] ? 'metadata' : 'source';
136-
const stl = await SourceTracking.create({
137-
org: flags['target-org'],
138-
project: this.project,
139-
subscribeSDREvents: true,
140-
ignoreConflicts: format === 'metadata' || flags['ignore-conflicts'],
141-
});
142-
const isChanges = !flags['source-dir'] && !flags['manifest'] && !flags['metadata'];
143-
const { componentSetFromNonDeletes, fileResponsesFromDelete } = isChanges
144-
? await stl.maybeApplyRemoteDeletesToLocal(true)
145-
: {
146-
componentSetFromNonDeletes: await ComponentSetBuilder.build({
147-
apiversion: flags['api-version'],
148-
sourcepath: flags['source-dir'],
149-
packagenames: flags['package-name'],
150-
...(flags.manifest
151-
? {
152-
manifest: {
153-
manifestPath: flags.manifest,
154-
directoryPaths: await getPackageDirs(),
155-
},
156-
}
157-
: {}),
158-
...(flags.metadata
159-
? { metadata: { metadataEntries: flags.metadata, directoryPaths: await getPackageDirs() } }
160-
: {}),
161-
}),
162-
fileResponsesFromDelete: [],
163-
};
164-
// stl sets version based on config/files--if the command overrides it, we need to update
165-
if (isChanges && flags['api-version']) {
166-
componentSetFromNonDeletes.apiVersion = flags['api-version'];
167-
}
143+
144+
const { componentSetFromNonDeletes, fileResponsesFromDelete = [] } = await buildRetrieveAndDeleteTargets(
145+
flags,
146+
format
147+
);
148+
const retrieveOpts = await buildRetrieveOptions(flags, format, zipFileName);
149+
168150
this.spinner.status = messages.getMessage('spinner.sending', [
169151
componentSetFromNonDeletes.sourceApiVersion ?? componentSetFromNonDeletes.apiVersion,
170152
]);
171153

172-
const zipFileName = flags['zip-file-name'] ?? DEFAULT_ZIP_FILE_NAME;
173-
const retrieveOpts: RetrieveSetOptions = {
174-
usernameOrConnection:
175-
flags['target-org'].getUsername() ?? flags['target-org'].getConnection(flags['api-version']),
176-
merge: true,
177-
output: this.project.getDefaultPackage().fullPath,
178-
packageOptions: flags['package-name'],
179-
format,
180-
...(format === 'metadata'
181-
? {
182-
singlePackage: flags['single-package'],
183-
unzip: flags.unzip,
184-
zipFileName,
185-
output: flags['target-metadata-dir'],
186-
}
187-
: {}),
188-
};
189-
190154
const retrieve = await componentSetFromNonDeletes.retrieve(retrieveOpts);
191155

192156
this.spinner.status = messages.getMessage('spinner.polling');
193157

194158
retrieve.onUpdate((data) => {
195159
this.spinner.status = mdTransferMessages.getMessage(data.status);
196160
});
197-
198161
// any thing else should stop the progress bar
199162
retrieve.onFinish((data) => this.spinner.stop(mdTransferMessages.getMessage(data.response.status)));
200-
201163
retrieve.onCancel((data) => this.spinner.stop(mdTransferMessages.getMessage(data?.status ?? 'Canceled')));
202-
203164
retrieve.onError((error: Error) => {
204165
this.spinner.stop(error.name);
205166
throw error;
@@ -242,17 +203,102 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
242203
}
243204

244205
protected catch(error: Error | SfError): Promise<SfCommand.Error> {
245-
if (error instanceof SourceConflictError) {
246-
if (!this.jsonEnabled()) {
247-
writeConflictTable(error.data);
248-
// set the message and add plugin-specific actions
249-
return super.catch({
250-
...error,
251-
message: messages.getMessage('error.Conflicts'),
252-
actions: messages.getMessages('error.Conflicts.Actions', [this.config.bin]),
253-
});
254-
}
206+
if (!this.jsonEnabled() && error instanceof SourceConflictError) {
207+
writeConflictTable(error.data);
208+
// set the message and add plugin-specific actions
209+
return super.catch({
210+
...error,
211+
message: messages.getMessage('error.Conflicts'),
212+
actions: messages.getMessages('error.Conflicts.Actions', [this.config.bin]),
213+
});
255214
}
215+
256216
return super.catch(error);
257217
}
258218
}
219+
220+
type RetrieveAndDeleteTargets = {
221+
/** componentSet that can be used to retrieve known changes */
222+
componentSetFromNonDeletes: ComponentSet;
223+
/** optional Array of artificially constructed FileResponses from the deletion of local files */
224+
fileResponsesFromDelete?: FileResponse[];
225+
};
226+
227+
const buildRetrieveAndDeleteTargets = async (
228+
flags: Interfaces.InferredFlags<typeof RetrieveMetadata.flags>,
229+
format: Format
230+
): Promise<RetrieveAndDeleteTargets> => {
231+
const isChanges = !flags['source-dir'] && !flags['manifest'] && !flags['metadata'] && !flags['target-metadata-dir'];
232+
233+
if (isChanges) {
234+
const stl = await SourceTracking.create({
235+
org: flags['target-org'],
236+
project: await SfProject.resolve(),
237+
subscribeSDREvents: true,
238+
ignoreConflicts: format === 'metadata' || flags['ignore-conflicts'],
239+
});
240+
const result = await stl.maybeApplyRemoteDeletesToLocal(true);
241+
// STL returns a componentSet that gets these from the project/config.
242+
// if the command has a flag, we'll override
243+
if (flags['api-version']) {
244+
result.componentSetFromNonDeletes.apiVersion = flags['api-version'];
245+
}
246+
return result;
247+
} else {
248+
return {
249+
componentSetFromNonDeletes: await ComponentSetBuilder.build({
250+
apiversion: flags['api-version'],
251+
sourcepath: flags['source-dir'],
252+
packagenames: flags['package-name'],
253+
...(flags.manifest
254+
? {
255+
manifest: {
256+
manifestPath: flags.manifest,
257+
// if mdapi format, there might not be a project
258+
directoryPaths: format === 'metadata' ? [] : await getPackageDirs(),
259+
},
260+
}
261+
: {}),
262+
...(flags.metadata
263+
? {
264+
metadata: {
265+
metadataEntries: flags.metadata,
266+
// if mdapi format, there might not be a project
267+
directoryPaths: format === 'metadata' ? [] : await getPackageDirs(),
268+
},
269+
}
270+
: {}),
271+
}),
272+
};
273+
}
274+
};
275+
276+
/**
277+
*
278+
*
279+
* @param flags
280+
* @param project
281+
* @param format 'metadata' or 'source'
282+
* @returns RetrieveSetOptions (an object that can be passed as the options for a ComponentSet retrieve)
283+
*/
284+
const buildRetrieveOptions = async (
285+
flags: Interfaces.InferredFlags<typeof RetrieveMetadata.flags>,
286+
format: Format,
287+
zipFileName: string
288+
): Promise<RetrieveSetOptions> => ({
289+
usernameOrConnection: flags['target-org'].getUsername() ?? flags['target-org'].getConnection(flags['api-version']),
290+
merge: true,
291+
packageOptions: flags['package-name'],
292+
format,
293+
...(format === 'metadata'
294+
? {
295+
singlePackage: flags['single-package'],
296+
unzip: flags.unzip,
297+
zipFileName,
298+
// known to exist because that's how `format` becomes 'metadata'
299+
output: flags['target-metadata-dir'] as string,
300+
}
301+
: {
302+
output: (await SfProject.resolve()).getDefaultPackage().fullPath,
303+
}),
304+
});

0 commit comments

Comments
 (0)