Skip to content

Commit

Permalink
fix: mdapi deploy negative one (#487)
Browse files Browse the repository at this point in the history
* fix: mdapi deploy negative one

* fix: ternary tweaks

* fix: -1 for mdapi:report, 0 for mdapi:deploy, -1 for retrieve

* refactor: consistent use of duration/freq for mdapi

* test: adjust for new parameter structure

* fix: remove ugly debug statement

* refactor: consistent use of pollStatus object param

* fix: correct default for deploy:report

* fix: allow -1 for retrieve report

* refactor: report can be async

Co-authored-by: Eric Willhoit <[email protected]>
  • Loading branch information
mshanemc and iowillhoit authored May 17, 2022
1 parent bfdae7a commit c7e2494
Show file tree
Hide file tree
Showing 9 changed files with 67 additions and 20 deletions.
4 changes: 2 additions & 2 deletions messages/md.deploy.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"flags": {
"checkOnly": "validate deploy but don’t save to the org",
"deployDir": "root of directory tree of files to deploy",
"wait": "wait time for command to finish in minutes (default: %s)",
"wait": "wait time for command to finish in minutes. Use -1 to wait indefinitely",
"jobId": "job ID of the deployment to check; required if you’ve never deployed using Salesforce CLI; if not specified, defaults to your most recent CLI deployment",
"testLevel": "deployment testing level",
"runTests": "tests to run if --testlevel RunSpecifiedTests",
Expand All @@ -32,7 +32,7 @@
"flagsLong": {
"checkOnly": "IMPORTANT: Where possible, we changed noninclusive terms to align with our company value of Equality. We maintained certain terms to avoid any effect on customer implementations.\n\nValidates the deployed metadata and runs all Apex tests, but prevents the deployment from being saved to the org.\nIf you change a field type from Master-Detail to Lookup or vice versa, that change isn’t supported when using the --checkonly parameter to test a deployment (validation). This kind of change isn’t supported for test deployments to avoid the risk of data loss or corruption. If a change that isn’t supported for test deployments is included in a deployment package, the test deployment fails and issues an error.\nIf your deployment package changes a field type from Master-Detail to Lookup or vice versa, you can still validate the changes prior to deploying to Production by performing a full deployment to another test Sandbox. A full deployment includes a validation of the changes as part of the deployment process.\nNote: A Metadata API deployment that includes Master-Detail relationships deletes all detail records in the Recycle Bin in the following cases.\n1. For a deployment with a new Master-Detail field, soft delete (send to the Recycle Bin) all detail records before proceeding to deploy the Master-Detail field, or the deployment fails. During the deployment, detail records are permanently deleted from the Recycle Bin and cannot be recovered.\n2. For a deployment that converts a Lookup field relationship to a Master-Detail relationship, detail records must reference a master record or be soft-deleted (sent to the Recycle Bin) for the deployment to succeed. However, a successful deployment permanently deletes any detail records in the Recycle Bin.",
"deployDir": "The root of the directory tree that contains the files to deploy. The root must contain a valid package.xml file describing the entities in the directory structure. Required to initiate a deployment if you don’t use --zipfile. If you specify both --zipfile and --deploydir, a zip file of the contents of the --deploydir directory is written to the location specified by --zipfile.",
"wait": "The number of minutes to wait for the command to complete. The default is –1 (no limit).",
"wait": "The number of minutes to wait for the command to complete. The default is 0 (returns immediately).",
"jobId": "The job ID (id field value for AsyncResult) of the deployment you want to check. The job ID is required if you haven’t previously deployed using Salesforce CLI. If you deploy using Salesforce CLI and don’t specify a job ID, we use the ID of the most recent metadata deployment.",
"testLevel": "Specifies which level of deployment tests to run. Valid values are:\nNoTestRun—No tests are run. This test level applies only to deployments to development environments, such as sandbox, Developer Edition, or trial orgs. This test level is the default for development environments.\nRunSpecifiedTests—Runs only the tests that you specify in the --runtests option. Code coverage requirements differ from the default coverage requirements when using this test level. Executed tests must comprise a minimum of 75% code coverage for each class and trigger in the deployment package. This coverage is computed for each class and trigger individually and is different than the overall coverage percentage.\nRunLocalTests—All tests in your org are run, except the ones that originate from installed managed and unlocked packages. This test level is the default for production deployments that include Apex classes or triggers.\nRunAllTestsInOrg—All tests in your org are run, including tests of managed packages.\nIf you don’t specify a test level, the default behavior depends on the contents of your deployment package. For more information, see “Running Tests in a Deployment” in the Metadata API Developer Guide.",
"runTests": "Lists the Apex classes containing the deployment tests to run. Use this parameter when you set --testlevel to RunSpecifiedTests.",
Expand Down
2 changes: 1 addition & 1 deletion messages/md.deployreport.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"flags": {
"verbose": "verbose output of deploy results",
"jobId": "job ID of the deployment to check; required if you’ve never deployed using Salesforce CLI; if not specified, defaults to your most recent CLI deployment",
"wait": "wait time for command to finish in minutes (default: %s)",
"wait": "wait time for command to finish in minutes. Use -1 to poll indefinitely",
"concise": "omit success messages for smaller JSON output"
},
"flagsLong": {
Expand Down
7 changes: 4 additions & 3 deletions src/commands/force/mdapi/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ export class Deploy extends DeployCommand {
}

protected async deploy(): Promise<void> {
const waitDuration = this.getFlag<Duration>('wait');
const waitFlag = this.getFlag<Duration>('wait');
const waitDuration = waitFlag.minutes === -1 ? Duration.days(7) : waitFlag;

this.isAsync = waitDuration.quantity === 0;
this.isRest = await this.isRestDeploy();

Expand Down Expand Up @@ -143,7 +145,6 @@ export class Deploy extends DeployCommand {

// we might not know the source api version without unzipping a zip file, so we don't use componentSet
this.ux.log(getVersionMessage('Deploying', undefined, this.isRest));
this.logger.debug('Deploy result: %o', deploy);

if (!this.isAsync) {
if (!this.isJsonOutput()) {
Expand All @@ -153,7 +154,7 @@ export class Deploy extends DeployCommand {
progressFormatter.progress(deploy);
}
this.displayDeployId(deploy.id);
this.deployResult = await deploy.pollStatus(500, waitDuration.seconds);
this.deployResult = await deploy.pollStatus({ frequency: Duration.milliseconds(500), timeout: waitDuration });
}
}

Expand Down
20 changes: 15 additions & 5 deletions src/commands/force/mdapi/deploy/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export class Report extends DeployCommand {
public static readonly flagsConfig: FlagsConfig = {
wait: flags.minutes({
char: 'w',
default: Duration.minutes(DeployCommand.DEFAULT_WAIT_MINUTES),
min: Duration.minutes(1),
description: messages.getMessage('flags.wait', [DeployCommand.DEFAULT_WAIT_MINUTES]),
longDescription: messages.getMessage('flagsLong.wait', [DeployCommand.DEFAULT_WAIT_MINUTES]),
default: Duration.minutes(0),
min: Duration.minutes(-1),
description: messages.getMessage('flags.wait'),
longDescription: messages.getMessage('flagsLong.wait'),
}),
jobid: flags.id({
char: 'i',
Expand All @@ -58,6 +58,16 @@ export class Report extends DeployCommand {
if (this.flags.verbose) {
this.ux.log(messages.getMessage('usernameOutput', [this.org.getUsername()]));
}
const waitFlag = this.getFlag<Duration>('wait');
const waitDuration = waitFlag.minutes === -1 ? Duration.days(7) : waitFlag;

this.isAsync = waitDuration.quantity === 0;

if (this.isAsync) {
this.deployResult = await this.report(this.getFlag<string>('jobid'));
return;
}

const deployId = this.resolveDeployId(this.getFlag<string>('jobid'));
this.displayDeployId(deployId);

Expand All @@ -68,7 +78,7 @@ export class Report extends DeployCommand {
: new DeployProgressStatusFormatter(this.logger, this.ux);
progressFormatter.progress(deploy);
}
this.deployResult = await deploy.pollStatus(500, this.getFlag<Duration>('wait').seconds);
this.deployResult = await deploy.pollStatus({ frequency: Duration.milliseconds(500), timeout: waitDuration });
}

// this is different from the source:report uses report error codes (unfortunately)
Expand Down
8 changes: 6 additions & 2 deletions src/commands/force/mdapi/retrieve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ export class Retrieve extends SourceCommand {
const singlePackage = this.getFlag<boolean>('singlepackage');
this.zipFileName = this.resolveZipFileName(this.getFlag<string>('zipfilename'));
this.unzip = this.getFlag<boolean>('unzip');
this.wait = this.getFlag<Duration>('wait');
const waitFlag = this.getFlag<Duration>('wait');
this.wait = waitFlag.minutes === -1 ? Duration.days(7) : waitFlag;
this.isAsync = this.wait.quantity === 0;

if (singlePackage && packagenames?.length > 1) {
Expand Down Expand Up @@ -167,7 +168,10 @@ export class Retrieve extends SourceCommand {
this.ux.stopSpinner('queued');
} else {
this.ux.setSpinnerStatus(spinnerMessages.getMessage('retrieve.polling'));
this.retrieveResult = await this.mdapiRetrieve.pollStatus(1000, this.wait.seconds);
this.retrieveResult = await this.mdapiRetrieve.pollStatus({
frequency: Duration.milliseconds(1000),
timeout: this.wait,
});
this.ux.stopSpinner();
}
}
Expand Down
5 changes: 4 additions & 1 deletion src/commands/force/mdapi/retrieve/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class Report extends SourceCommand {
char: 'w',
description: messages.getMessage('flags.wait'),
longDescription: messages.getMessage('flagsLong.wait'),
min: -1,
default: Duration.minutes(1440), // 24 hours is a reasonable default versus -1 (no timeout)
}),
verbose: flags.builtin({
Expand Down Expand Up @@ -99,7 +100,9 @@ export class Report extends SourceCommand {
this.unzip = this.getFlag<boolean>('unzip');
}

this.wait = this.getFlag<Duration>('wait');
const waitFlag = this.getFlag<Duration>('wait');
this.wait = waitFlag.minutes === -1 ? Duration.days(7) : waitFlag;

this.isAsync = this.wait.quantity === 0;

this.ux.startSpinner(spinnerMessages.getMessage('retrieve.main', [this.org.getUsername()]));
Expand Down
5 changes: 4 additions & 1 deletion src/deployCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,12 @@ export abstract class DeployCommand extends SourceCommand {
}

protected async poll(deployId: string, options?: Partial<PollingClient.Options>): Promise<DeployResult> {
const waitFlag = this.getFlag<Duration>('wait');
const waitDuration = waitFlag.minutes === -1 ? Duration.days(7) : waitFlag;

const defaultOptions: PollingClient.Options = {
frequency: options?.frequency ?? Duration.seconds(1),
timeout: options?.timeout ?? (this.flags.wait as Duration),
timeout: options?.timeout ?? waitDuration,
poll: async (): Promise<StatusResult> => {
const deployResult = await this.report(deployId);
return {
Expand Down
13 changes: 9 additions & 4 deletions test/commands/mdapi/retrieve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { fromStub, stubInterface, stubMethod } from '@salesforce/ts-sinon';
import { IConfig } from '@oclif/config';
import { UX } from '@salesforce/command';
import { ComponentSetBuilder, ComponentSetOptions, RetrieveOptions } from '@salesforce/source-deploy-retrieve';
import { Duration } from '@salesforce/kit';
import { Retrieve } from '../../../src/commands/force/mdapi/retrieve';
import { Stash, StashData } from '../../../src/stash';
import { getRetrieveResult } from '../source/retrieveResponses';
Expand Down Expand Up @@ -170,8 +171,10 @@ describe('force:mdapi:retrieve', () => {
ensureStashSet();
expect(fsStatStub.called).to.be.true;
// should use the default polling timeout of 1440 minutes (86400 seconds)
expect(pollStub.firstCall.args[0]).to.equal(1000);
expect(pollStub.firstCall.args[1]).to.equal(86400);
expect(pollStub.firstCall.args[0]).to.deep.equal({
frequency: Duration.milliseconds(1000),
timeout: Duration.minutes(1440),
});
});

it('should pass along unpackaged', async () => {
Expand Down Expand Up @@ -264,8 +267,10 @@ describe('force:mdapi:retrieve', () => {
ensureHookArgs();
ensureStashSet();
expect(fsStatStub.called).to.be.true;
expect(pollStub.firstCall.args[0]).to.equal(1000);
expect(pollStub.firstCall.args[1]).to.equal(300);
expect(pollStub.firstCall.args[0]).to.deep.equal({
frequency: Duration.milliseconds(1000),
timeout: Duration.minutes(5),
});
});

it('should display expected output', async () => {
Expand Down
23 changes: 22 additions & 1 deletion test/nuts/mdapi.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,17 @@ describe('mdapi NUTs', () => {
});
});

it('async report from stash', () => {
// we can't know the exit code so don't use ensureExitCode
const reportCommandResponse = execCmd<MdDeployResult>(
'force:mdapi:deploy:report --wait 0 -u nonDefaultOrg --json'
).jsonOutput.result;

// this output is a change from mdapi:deploy:report which returned NOTHING after the progress bar
expect(reportCommandResponse).to.have.property('status');
expect(['Pending', 'Succeeded', 'Failed', 'InProgress'].includes(reportCommandResponse.status));
});

it('request non-verbose deploy report without a deployId', () => {
const reportCommandResponse = execCmd('force:mdapi:deploy:report --wait 200 -u nonDefaultOrg', {
ensureExitCode: 0,
Expand All @@ -449,6 +460,16 @@ describe('mdapi NUTs', () => {
// has the basic table output
expect(reportCommandResponse).to.include('Deployed Source');
});

it('async report without a deployId', () => {
const reportCommandResponse = execCmd('force:mdapi:deploy:report --wait 0 -u nonDefaultOrg', {
ensureExitCode: 0,
}).shellOutput.stdout;

// this output is a change from mdapi:deploy:report which returned NOTHING after the progress bar
expect(reportCommandResponse).to.include('Status: Succeeded', reportCommandResponse);
expect(reportCommandResponse).to.include('Deployed: ', reportCommandResponse);
});
});

describe('Deploy directory using default org and request report using jobid parameter from a different org', () => {
Expand Down Expand Up @@ -477,7 +498,7 @@ describe('mdapi NUTs', () => {
).jsonOutput.result;
});
it('should deploy validated Id', () => {
execCmd(`force:mdapi:deploy --wait 200 --validateddeployrequestid ${deployCommandResponse.id}`, {
execCmd(`force:mdapi:deploy --wait -1 --validateddeployrequestid ${deployCommandResponse.id}`, {
ensureExitCode: 0,
});
});
Expand Down

0 comments on commit c7e2494

Please sign in to comment.