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: deploys don't require a project #619

Merged
merged 14 commits into from
May 17, 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
},
"devDependencies": {
"@oclif/plugin-command-snapshot": "^3.3.14",
"@salesforce/cli-plugins-testkit": "^3.3.2",
"@salesforce/cli-plugins-testkit": "^3.4.0",
"@salesforce/dev-config": "^3.1.0",
"@salesforce/dev-scripts": "^4.3.1",
"@salesforce/plugin-command-reference": "^2.4.4",
Expand Down Expand Up @@ -146,6 +146,7 @@
"test:nuts:convert": "nyc mocha \"test/nuts/convert/*.nut.ts\" --slow 4500 --timeout 1200000 --parallel --retries 0 --jobs 20",
"test:nuts:deb": "nyc mocha \"test/nuts/digitalExperienceBundle/*.nut.ts\" --slow 4500 --timeout 1200000 --parallel --retries 0 --jobs 20",
"test:nuts:delete": "nyc mocha \"test/nuts/delete/*.nut.ts\" --slow 4500 --timeout 1200000 --parallel --retries 0 --jobs 20",
"test:nuts:deploy": "nyc mocha \"test/nuts/deploy/*.nut.ts\" --slow 4500 --timeout 1200000 --parallel --retries 0 --jobs 20",
"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",
"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",
"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",
Expand Down Expand Up @@ -269,4 +270,4 @@
"output": []
}
}
}
}
4 changes: 4 additions & 0 deletions schemas/project-deploy-cancel.json
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@
{
"type": "string",
"const": "Queued"
},
{
"type": "string",
"const": "Nothing to deploy"
}
]
},
Expand Down
4 changes: 4 additions & 0 deletions schemas/project-deploy-quick.json
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@
{
"type": "string",
"const": "Queued"
},
{
"type": "string",
"const": "Nothing to deploy"
}
]
},
Expand Down
4 changes: 4 additions & 0 deletions schemas/project-deploy-report.json
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@
{
"type": "string",
"const": "Queued"
},
{
"type": "string",
"const": "Nothing to deploy"
}
]
},
Expand Down
4 changes: 4 additions & 0 deletions schemas/project-deploy-resume.json
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@
{
"type": "string",
"const": "Queued"
},
{
"type": "string",
"const": "Nothing to deploy"
}
]
},
Expand Down
4 changes: 4 additions & 0 deletions schemas/project-deploy-start.json
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@
{
"type": "string",
"const": "Queued"
},
{
"type": "string",
"const": "Nothing to deploy"
}
]
},
Expand Down
4 changes: 4 additions & 0 deletions schemas/project-deploy-validate.json
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,10 @@
{
"type": "string",
"const": "Queued"
},
{
"type": "string",
"const": "Nothing to deploy"
}
]
},
Expand Down
13 changes: 10 additions & 3 deletions src/commands/project/deploy/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { DEPLOY_STATUS_CODES_DESCRIPTIONS } from '../../../utils/errorCodes';
import { ConfigVars } from '../../../configMeta';
import { coverageFormattersFlag, fileOrDirFlag, testLevelFlag, testsFlag } from '../../../utils/flags';
import { writeConflictTable } from '../../../utils/conflicts';
import { getOptionalProject } from '../../../utils/project';

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

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

public async run(): Promise<DeployResultJson> {
const { flags } = await this.parse(DeployMetadata);
const project = await getOptionalProject();

if (
this.project.getSfProjectJson().getContents()['pushPackageDirectoriesSequentially'] &&
project?.getSfProjectJson().getContents()['pushPackageDirectoriesSequentially'] &&
// flag exclusivity is handled correctly above - but to avoid short-circuiting the check, we need to check all of them
!flags.manifest &&
!flags.metadata &&
Expand All @@ -186,9 +188,14 @@ export default class DeployMetadata extends SfCommand<DeployResultJson> {
api,
},
this.config.bin,
this.project
project
);

if (!deploy) {
this.log('No changes to deploy');
return { status: 'Nothing to deploy', files: [] };
}

const action = flags['dry-run'] ? 'Deploying (dry-run)' : 'Deploying';
this.log(getVersionMessage(action, componentSet, api));
if (!deploy.id) {
Expand Down
5 changes: 4 additions & 1 deletion src/commands/project/deploy/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,10 +127,13 @@ export default class DeployMetadataValidate extends SfCommand<DeployResultJson>
api,
},
this.config.bin,
this.project
this.project,
undefined,
true
);

this.log(getVersionMessage('Validating Deployment', componentSet, api));

if (!deploy.id) {
throw new SfError('The deploy id is not available.');
}
Expand Down
184 changes: 115 additions & 69 deletions src/commands/project/retrieve/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@
import { rm } from 'fs/promises';
import { join, resolve } from 'path';

import { EnvironmentVariable, Messages, OrgConfigProperties, SfError } from '@salesforce/core';
import { RetrieveResult, ComponentSetBuilder, RetrieveSetOptions } from '@salesforce/source-deploy-retrieve';

import { EnvironmentVariable, Messages, OrgConfigProperties, SfError, SfProject } from '@salesforce/core';
import {
RetrieveResult,
ComponentSetBuilder,
RetrieveSetOptions,
ComponentSet,
FileResponse,
} from '@salesforce/source-deploy-retrieve';
import { SfCommand, toHelpSection, Flags } from '@salesforce/sf-plugins-core';
import { getString } from '@salesforce/ts-types';
import { SourceTracking, SourceConflictError } from '@salesforce/source-tracking';
import { Duration } from '@salesforce/kit';
import { Interfaces } from '@oclif/core';

import { DEFAULT_ZIP_FILE_NAME, ensuredDirFlag, zipFileFlag } from '../../../utils/flags';
import { RetrieveResultFormatter } from '../../../formatters/retrieveResultFormatter';
import { MetadataRetrieveResultFormatter } from '../../../formatters/metadataRetrieveResultFormatter';
Expand All @@ -26,11 +33,11 @@ Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'retrieve.metadata');
const mdTransferMessages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'metadata.transfer');

type Format = 'source' | 'metadata';
export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
public static readonly summary = messages.getMessage('summary');
public static readonly description = messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');
public static readonly requiresProject = true;
public static readonly aliases = ['retrieve:metadata'];
public static readonly deprecateAliases = true;

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

protected retrieveResult!: RetrieveResult;

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

this.spinner.start(messages.getMessage('spinner.start'));
const format = flags['target-metadata-dir'] ? 'metadata' : 'source';
const stl = await SourceTracking.create({
org: flags['target-org'],
project: this.project,
subscribeSDREvents: true,
ignoreConflicts: format === 'metadata' || flags['ignore-conflicts'],
});
const isChanges = !flags['source-dir'] && !flags['manifest'] && !flags['metadata'];
const { componentSetFromNonDeletes, fileResponsesFromDelete } = isChanges
? await stl.maybeApplyRemoteDeletesToLocal(true)
: {
componentSetFromNonDeletes: await ComponentSetBuilder.build({
apiversion: flags['api-version'],
sourcepath: flags['source-dir'],
packagenames: flags['package-name'],
...(flags.manifest
? {
manifest: {
manifestPath: flags.manifest,
directoryPaths: await getPackageDirs(),
},
}
: {}),
...(flags.metadata
? { metadata: { metadataEntries: flags.metadata, directoryPaths: await getPackageDirs() } }
: {}),
}),
fileResponsesFromDelete: [],
};
// stl sets version based on config/files--if the command overrides it, we need to update
if (isChanges && flags['api-version']) {
componentSetFromNonDeletes.apiVersion = flags['api-version'];
}

const { componentSetFromNonDeletes, fileResponsesFromDelete = [] } = await buildRetrieveAndDeleteTargets(
flags,
format
);
const retrieveOpts = await buildRetrieveOptions(flags, format, zipFileName);

this.spinner.status = messages.getMessage('spinner.sending', [
componentSetFromNonDeletes.sourceApiVersion ?? componentSetFromNonDeletes.apiVersion,
]);

const zipFileName = flags['zip-file-name'] ?? DEFAULT_ZIP_FILE_NAME;
const retrieveOpts: RetrieveSetOptions = {
usernameOrConnection:
flags['target-org'].getUsername() ?? flags['target-org'].getConnection(flags['api-version']),
merge: true,
output: this.project.getDefaultPackage().fullPath,
packageOptions: flags['package-name'],
format,
...(format === 'metadata'
? {
singlePackage: flags['single-package'],
unzip: flags.unzip,
zipFileName,
output: flags['target-metadata-dir'],
}
: {}),
};

const retrieve = await componentSetFromNonDeletes.retrieve(retrieveOpts);

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

retrieve.onUpdate((data) => {
this.spinner.status = mdTransferMessages.getMessage(data.status);
});

// any thing else should stop the progress bar
retrieve.onFinish((data) => this.spinner.stop(mdTransferMessages.getMessage(data.response.status)));

retrieve.onCancel((data) => this.spinner.stop(mdTransferMessages.getMessage(data?.status ?? 'Canceled')));

retrieve.onError((error: Error) => {
this.spinner.stop(error.name);
throw error;
Expand Down Expand Up @@ -242,17 +203,102 @@ export default class RetrieveMetadata extends SfCommand<RetrieveResultJson> {
}

protected catch(error: Error | SfError): Promise<SfCommand.Error> {
if (error instanceof SourceConflictError) {
if (!this.jsonEnabled()) {
writeConflictTable(error.data);
// set the message and add plugin-specific actions
return super.catch({
...error,
message: messages.getMessage('error.Conflicts'),
actions: messages.getMessages('error.Conflicts.Actions', [this.config.bin]),
});
}
if (!this.jsonEnabled() && error instanceof SourceConflictError) {
writeConflictTable(error.data);
// set the message and add plugin-specific actions
return super.catch({
...error,
message: messages.getMessage('error.Conflicts'),
actions: messages.getMessages('error.Conflicts.Actions', [this.config.bin]),
});
}

return super.catch(error);
}
}

type RetrieveAndDeleteTargets = {
/** componentSet that can be used to retrieve known changes */
componentSetFromNonDeletes: ComponentSet;
/** optional Array of artificially constructed FileResponses from the deletion of local files */
fileResponsesFromDelete?: FileResponse[];
};

const buildRetrieveAndDeleteTargets = async (
flags: Interfaces.InferredFlags<typeof RetrieveMetadata.flags>,
format: Format
): Promise<RetrieveAndDeleteTargets> => {
const isChanges = !flags['source-dir'] && !flags['manifest'] && !flags['metadata'] && !flags['target-metadata-dir'];

if (isChanges) {
const stl = await SourceTracking.create({
org: flags['target-org'],
project: await SfProject.resolve(),
subscribeSDREvents: true,
ignoreConflicts: format === 'metadata' || flags['ignore-conflicts'],
});
const result = await stl.maybeApplyRemoteDeletesToLocal(true);
// STL returns a componentSet that gets these from the project/config.
// if the command has a flag, we'll override
if (flags['api-version']) {
result.componentSetFromNonDeletes.apiVersion = flags['api-version'];
}
return result;
} else {
return {
componentSetFromNonDeletes: await ComponentSetBuilder.build({
apiversion: flags['api-version'],
sourcepath: flags['source-dir'],
packagenames: flags['package-name'],
...(flags.manifest
? {
manifest: {
manifestPath: flags.manifest,
// if mdapi format, there might not be a project
directoryPaths: format === 'metadata' ? [] : await getPackageDirs(),
},
}
: {}),
...(flags.metadata
? {
metadata: {
metadataEntries: flags.metadata,
// if mdapi format, there might not be a project
directoryPaths: format === 'metadata' ? [] : await getPackageDirs(),
},
}
: {}),
}),
};
}
};

/**
*
*
* @param flags
* @param project
* @param format 'metadata' or 'source'
* @returns RetrieveSetOptions (an object that can be passed as the options for a ComponentSet retrieve)
*/
const buildRetrieveOptions = async (
flags: Interfaces.InferredFlags<typeof RetrieveMetadata.flags>,
format: Format,
zipFileName: string
): Promise<RetrieveSetOptions> => ({
usernameOrConnection: flags['target-org'].getUsername() ?? flags['target-org'].getConnection(flags['api-version']),
merge: true,
packageOptions: flags['package-name'],
format,
...(format === 'metadata'
? {
singlePackage: flags['single-package'],
unzip: flags.unzip,
zipFileName,
// known to exist because that's how `format` becomes 'metadata'
output: flags['target-metadata-dir'] as string,
}
: {
output: (await SfProject.resolve()).getDefaultPackage().fullPath,
}),
});