Skip to content

Commit

Permalink
Sm/status-codes (#265)
Browse files Browse the repository at this point in the history
* feat: detailed status codes for deploy/retrieve

* feat: spinners for retrieve statuses

* refactor: partial success is success

* test: ut adds and bugs from tests

* test: remove only

* feat: better no results message for status

* fix: avoid undefined for push api version message

* feat: status code per command

* fix: zero is falsy, so ?? instead of ||

* refactor: no getString

* fix: handle warning-only "failures"

* chore: repo cleanup

* refactor: demote and defer isRest

* fix: don't modify tracking when no deployResult

* chore: bump sdr/stl

* refactor: shared logic for display api versions

* fix: deploy message only runs after CS, not validateddeployrequestid

Co-authored-by: Steve Hetzel <[email protected]>
  • Loading branch information
mshanemc and shetzel authored Nov 11, 2021
1 parent c8f878e commit 8bc0797
Show file tree
Hide file tree
Showing 22 changed files with 312 additions and 141 deletions.
Binary file removed .images/vscodeScreenshot.png
Binary file not shown.
3 changes: 2 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"request": "attach",
"name": "Attach",
"port": 9229,
"skipFiles": ["<node_internals>/**"]
"skipFiles": ["<node_internals>/**"],
"continueOnAttach": true
},
{
"name": "Run All Tests",
Expand Down
9 changes: 3 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,21 @@ All notable changes to this project will be documented in this file. See [standa

## [1.4.0](https://github.com/salesforcecli/plugin-source/compare/v1.3.1...v1.4.0) (2021-11-09)


### Features

* listmetadata and describemetadata ([b00a59a](https://github.com/salesforcecli/plugin-source/commit/b00a59a3c06cd81b121c306b6b9af3ee581a61ad))
- listmetadata and describemetadata ([b00a59a](https://github.com/salesforcecli/plugin-source/commit/b00a59a3c06cd81b121c306b6b9af3ee581a61ad))

### [1.3.1](https://github.com/salesforcecli/plugin-source/compare/v1.3.0...v1.3.1) (2021-10-28)


### Bug Fixes

* bump SDR to 5.1.1 ([#266](https://github.com/salesforcecli/plugin-source/issues/266)) ([ff6a4f7](https://github.com/salesforcecli/plugin-source/commit/ff6a4f74401668faec6404f941ce5cf73da426ff))
- bump SDR to 5.1.1 ([#266](https://github.com/salesforcecli/plugin-source/issues/266)) ([ff6a4f7](https://github.com/salesforcecli/plugin-source/commit/ff6a4f74401668faec6404f941ce5cf73da426ff))

## [1.3.0](https://github.com/salesforcecli/plugin-source/compare/v1.2.6...v1.3.0) (2021-10-28)


### Features

* source tracking beta commands ([b871774](https://github.com/salesforcecli/plugin-source/commit/b87177498c184580db7e3bd81f164ddc77e6de0b)), closes [#253](https://github.com/salesforcecli/plugin-source/issues/253) [#251](https://github.com/salesforcecli/plugin-source/issues/251) [#230](https://github.com/salesforcecli/plugin-source/issues/230) [#260](https://github.com/salesforcecli/plugin-source/issues/260) [#259](https://github.com/salesforcecli/plugin-source/issues/259)
- source tracking beta commands ([b871774](https://github.com/salesforcecli/plugin-source/commit/b87177498c184580db7e3bd81f164ddc77e6de0b)), closes [#253](https://github.com/salesforcecli/plugin-source/issues/253) [#251](https://github.com/salesforcecli/plugin-source/issues/251) [#230](https://github.com/salesforcecli/plugin-source/issues/230) [#260](https://github.com/salesforcecli/plugin-source/issues/260) [#259](https://github.com/salesforcecli/plugin-source/issues/259)

### [1.2.6](https://github.com/salesforcecli/plugin-source/compare/v1.2.5...v1.2.6) (2021-10-21)

Expand Down
154 changes: 79 additions & 75 deletions README.md

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion messages/retrieve.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,5 +47,10 @@
"columnNumberColumn": "COLUMN NUMBER",
"lineNumberColumn": "LINE NUMBER",
"errorColumn": "PROBLEM",
"wantsToRetrieveCustomFields": "Because you're retrieving one or more CustomFields, we're also retrieving the CustomObject to which it's associated."
"wantsToRetrieveCustomFields": "Because you're retrieving one or more CustomFields, we're also retrieving the CustomObject to which it's associated.",
"spinnerMessages": {
"componentSetBuild": "Preparing retrieve request",
"sendingRequest": "Sending request to org (metadata API version %s)",
"polling": "Waiting for the org to respond"
}
}
3 changes: 2 additions & 1 deletion messages/status.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
"remote": "list the changes that have been made in the scratch org",
"remoteLong": "Lists the changes that have been made in the scratch org."
},
"humanSuccess": "Source Status"
"humanSuccess": "Source Status",
"noResults": "No local or remote changes found."
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"@oclif/config": "^1",
"@salesforce/command": "^4.1.3",
"@salesforce/core": "^2.28.0",
"@salesforce/source-deploy-retrieve": "^5.1.1",
"@salesforce/source-tracking": "^0.4.1",
"@salesforce/source-deploy-retrieve": "^5.3.0",
"@salesforce/source-tracking": "^0.4.2",
"chalk": "^4.1.2",
"cli-ux": "^5.6.3",
"open": "^8.2.1",
Expand Down
12 changes: 10 additions & 2 deletions src/commands/force/source/beta/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,17 @@ export default class Pull extends SourceCommand {
}

protected resolveSuccess(): void {
const StatusCodeMap = new Map<RequestStatus, number>([
[RequestStatus.Succeeded, 0],
[RequestStatus.Canceled, 1],
[RequestStatus.Failed, 1],
[RequestStatus.InProgress, 69],
[RequestStatus.Pending, 69],
[RequestStatus.Canceling, 69],
]);
// there might not be a retrieveResult if we don't have anything to retrieve
if (this.retrieveResult && this.retrieveResult.response.status !== RequestStatus.Succeeded) {
this.setExitCode(1);
if (this.retrieveResult && this.retrieveResult.response.status) {
this.setExitCode(StatusCodeMap.get(this.retrieveResult.response.status) ?? 1);
}
}

Expand Down
64 changes: 44 additions & 20 deletions src/commands/force/source/beta/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Messages } from '@salesforce/core';
import { RequestStatus, ComponentStatus } from '@salesforce/source-deploy-retrieve';

import { SourceTracking, throwIfInvalid, replaceRenamedCommands } from '@salesforce/source-tracking';
import { DeployCommand } from '../../../../deployCommand';
import { DeployCommand, getVersionMessage } from '../../../../deployCommand';
import { PushResponse, PushResultFormatter } from '../../../../formatters/pushResultFormatter';
import { ProgressFormatter } from '../../../../formatters/progressFormatter';
import { DeployProgressBarFormatter } from '../../../../formatters/deployProgressBarFormatter';
Expand Down Expand Up @@ -51,11 +51,12 @@ export default class Push extends DeployCommand {
protected static requiresProject = true;
protected readonly lifecycleEventNames = ['predeploy', 'postdeploy'];

private isRest = false;
private tracking: SourceTracking;

public async run(): Promise<PushResponse[]> {
await this.deploy();
this.resolveSuccess();
await this.updateTrackingFiles();
return this.formatResult();
}

Expand All @@ -66,19 +67,20 @@ export default class Push extends DeployCommand {
toValidate: 'plugin-source',
command: replaceRenamedCommands('force:source:push'),
});
const waitDuration = this.getFlag<Duration>('wait');
this.isRest = await this.isRestDeploy();

const tracking = await SourceTracking.create({
this.tracking = await SourceTracking.create({
org: this.org,
project: this.project,
apiVersion: this.flags.apiversion as string,
});
if (!this.flags.forceoverwrite) {
processConflicts(await tracking.getConflicts(), this.ux, messages.getMessage('conflictMsg'));
processConflicts(await this.tracking.getConflicts(), this.ux, messages.getMessage('conflictMsg'));
}
const componentSet = await this.tracking.localChangesAsComponentSet();
const sourceApiVersion = await this.getSourceApiVersion();
if (sourceApiVersion) {
componentSet.sourceApiVersion = sourceApiVersion;
}
const componentSet = await tracking.localChangesAsComponentSet();
componentSet.sourceApiVersion = await this.getSourceApiVersion();

// there might have been components in local tracking, but they might be ignored by SDR or unresolvable.
// SDR will throw when you try to resolve them, so don't
Expand All @@ -89,13 +91,15 @@ export default class Push extends DeployCommand {

// fire predeploy event for sync and async deploys
await this.lifecycle.emit('predeploy', componentSet.toArray());
this.ux.log(`*** Pushing with ${this.isRest ? 'REST' : 'SOAP'} API v${componentSet.sourceApiVersion} ***`);

const isRest = await this.isRestDeploy();
this.ux.log(getVersionMessage('Pushing', componentSet, isRest));

const deploy = await componentSet.deploy({
usernameOrConnection: this.org.getUsername(),
apiOptions: {
ignoreWarnings: this.getFlag<boolean>('ignorewarnings', false),
rest: this.isRest,
rest: isRest,
testLevel: 'NoTestRun',
},
});
Expand All @@ -107,32 +111,52 @@ export default class Push extends DeployCommand {
: new DeployProgressStatusFormatter(this.logger, this.ux);
progressFormatter.progress(deploy);
}
this.deployResult = await deploy.pollStatus(500, waitDuration.seconds);
this.deployResult = await deploy.pollStatus(500, this.getFlag<Duration>('wait').seconds);

if (this.deployResult) {
// Only fire the postdeploy event when we have results. I.e., not async.
await this.lifecycle.emit('postdeploy', this.deployResult);
}
}

protected async updateTrackingFiles(): Promise<void> {
if (process.exitCode !== 0 || !this.deployResult) {
return;
}
const successes = this.deployResult
.getFileResponses()
.filter((fileResponse) => fileResponse.state !== ComponentStatus.Failed);
const successNonDeletes = successes.filter((fileResponse) => fileResponse.state !== ComponentStatus.Deleted);
const successDeletes = successes.filter((fileResponse) => fileResponse.state === ComponentStatus.Deleted);

if (this.deployResult) {
// Only fire the postdeploy event when we have results. I.e., not async.
await this.lifecycle.emit('postdeploy', this.deployResult);
}

await Promise.all([
tracking.updateLocalTracking({
this.tracking.updateLocalTracking({
files: successNonDeletes.map((fileResponse) => fileResponse.filePath),
deletedFiles: successDeletes.map((fileResponse) => fileResponse.filePath),
}),
tracking.updateRemoteTracking(successes),
this.tracking.updateRemoteTracking(successes),
]);
}

protected resolveSuccess(): void {
const StatusCodeMap = new Map<RequestStatus, number>([
[RequestStatus.Succeeded, 0],
[RequestStatus.Canceled, 1],
[RequestStatus.Failed, 1],
[RequestStatus.InProgress, 69],
[RequestStatus.Pending, 69],
[RequestStatus.Canceling, 69],
]);
// there might not be a deployResult if we exited early with an empty componentSet
if (this.deployResult && this.deployResult.response.status !== RequestStatus.Succeeded) {
this.setExitCode(1);
if (this.deployResult && this.deployResult.response.status) {
this.setExitCode(StatusCodeMap.get(this.deployResult.response.status) ?? 1);
// special override case for "only warnings about deleted things that are already deleted"
if (
this.deployResult.response.status === RequestStatus.Failed &&
this.deployResult.getFileResponses().every((fr) => fr.state !== 'Failed')
) {
this.setExitCode(0);
}
}
}

Expand Down
20 changes: 13 additions & 7 deletions src/commands/force/source/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { flags, FlagsConfig } from '@salesforce/command';
import { Messages } from '@salesforce/core';
import { AsyncResult, DeployResult, RequestStatus } from '@salesforce/source-deploy-retrieve';
import { Duration, env, once } from '@salesforce/kit';
import { getString, isString } from '@salesforce/ts-types';
import { DeployCommand } from '../../../deployCommand';
import { isString } from '@salesforce/ts-types';
import { DeployCommand, getVersionMessage } from '../../../deployCommand';
import { ComponentSetBuilder } from '../../../componentSetBuilder';
import { DeployCommandResult, DeployResultFormatter } from '../../../formatters/deployResultFormatter';
import { DeployAsyncResultFormatter, DeployCommandAsyncResult } from '../../../formatters/deployAsyncResultFormatter';
Expand Down Expand Up @@ -144,7 +144,6 @@ export class Deploy extends DeployCommand {
const waitDuration = this.getFlag<Duration>('wait');
this.isAsync = waitDuration.quantity === 0;
this.isRest = await this.isRestDeploy();
this.ux.log(`*** Deploying with ${this.isRest ? 'REST' : 'SOAP'} API ***`);

if (this.flags.validateddeployrequestid) {
this.deployResult = await this.deployRecentValidation();
Expand All @@ -166,6 +165,7 @@ export class Deploy extends DeployCommand {
});
// fire predeploy event for sync and async deploys
await this.lifecycle.emit('predeploy', this.componentSet.toArray());
this.ux.log(getVersionMessage('Deploying', this.componentSet, this.isRest));

const deploy = await this.componentSet.deploy({
usernameOrConnection: this.org.getUsername(),
Expand Down Expand Up @@ -205,11 +205,17 @@ export class Deploy extends DeployCommand {
* unsuccessful in oclif.
*/
protected resolveSuccess(): void {
const StatusCodeMap = new Map<RequestStatus, number>([
[RequestStatus.Succeeded, 0],
[RequestStatus.Canceled, 1],
[RequestStatus.Failed, 1],
[RequestStatus.SucceededPartial, 68],
[RequestStatus.InProgress, 69],
[RequestStatus.Pending, 69],
[RequestStatus.Canceling, 69],
]);
if (!this.isAsync) {
const status = getString(this.deployResult, 'response.status');
if (status !== RequestStatus.Succeeded) {
this.setExitCode(1);
}
this.setExitCode(StatusCodeMap.get(this.deployResult.response?.status) ?? 1);
}
}

Expand Down
25 changes: 19 additions & 6 deletions src/commands/force/source/retrieve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ import { join } from 'path';
import { flags, FlagsConfig } from '@salesforce/command';
import { Messages, SfdxProject } from '@salesforce/core';
import { Duration } from '@salesforce/kit';
import { getString } from '@salesforce/ts-types';
import { RetrieveResult, RequestStatus, ComponentSet } from '@salesforce/source-deploy-retrieve';
import { RetrieveResult, ComponentSet, RequestStatus } from '@salesforce/source-deploy-retrieve';
import { SourceCommand } from '../../../sourceCommand';
import {
RetrieveResultFormatter,
Expand Down Expand Up @@ -77,6 +76,7 @@ export class Retrieve extends SourceCommand {
}

protected async retrieve(): Promise<void> {
this.ux.startSpinner(messages.getMessage('spinnerMessages.componentSetBuild'));
this.componentSet = await ComponentSetBuilder.build({
apiversion: this.getFlag<string>('apiversion'),
sourceapiversion: await this.getSourceApiVersion(),
Expand All @@ -101,23 +101,36 @@ export class Retrieve extends SourceCommand {

await this.lifecycle.emit('preretrieve', this.componentSet.toArray());

this.ux.setSpinnerStatus(
messages.getMessage('spinnerMessages.sendingRequest', [
this.componentSet.sourceApiVersion || this.componentSet.apiVersion,
])
);
const mdapiRetrieve = await this.componentSet.retrieve({
usernameOrConnection: this.org.getUsername(),
merge: true,
output: this.project.getDefaultPackage().fullPath,
packageOptions: this.getFlag<string[]>('packagenames'),
});

this.ux.setSpinnerStatus(messages.getMessage('spinnerMessages.polling'));
this.retrieveResult = await mdapiRetrieve.pollStatus(1000, this.getFlag<Duration>('wait').seconds);

await this.lifecycle.emit('postretrieve', this.retrieveResult.getFileResponses());
this.ux.stopSpinner();
}

protected resolveSuccess(): void {
const status = getString(this.retrieveResult, 'response.status');
if (status !== RequestStatus.Succeeded) {
this.setExitCode(1);
}
const StatusCodeMap = new Map<RequestStatus, number>([
[RequestStatus.Succeeded, 0],
[RequestStatus.Canceled, 1],
[RequestStatus.Failed, 1],
[RequestStatus.InProgress, 69],
[RequestStatus.Pending, 69],
[RequestStatus.Canceling, 69],
]);

this.setExitCode(StatusCodeMap.get(this.retrieveResult.response.status) ?? 1);
}

protected async formatResult(): Promise<RetrieveCommandResult> {
Expand Down
29 changes: 29 additions & 0 deletions src/deployCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,32 @@ export abstract class DeployCommand extends SourceCommand {
return new ConfigFile({ isGlobal: true, filename: 'stash.json' });
}
}

export const getVersionMessage = (
action: 'Deploying' | 'Pushing',
componentSet: ComponentSet,
isRest: boolean
): string => {
// commands pass in the.componentSet, which may not exist in some tests
if (!componentSet) {
return;
}
// neither
if (!componentSet.sourceApiVersion && !componentSet.apiVersion) {
return `*** ${action} with ${isRest ? 'REST' : 'SOAP'} ***`;
}
// either OR both match (SDR will use either)
if (
!componentSet.sourceApiVersion ||
!componentSet.apiVersion ||
componentSet.sourceApiVersion === componentSet.apiVersion
) {
return `*** ${action} with ${isRest ? 'REST' : 'SOAP'} API v${
componentSet.apiVersion ?? componentSet.sourceApiVersion
} ***`;
}
// has both but they don't match
return `*** ${action} v${componentSet.sourceApiVersion} metadata with ${isRest ? 'REST' : 'SOAP'} API v${
componentSet.apiVersion
} connection ***`;
};
2 changes: 1 addition & 1 deletion src/formatters/deployResultFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export class DeployResultFormatter extends ResultFormatter {
}

protected displayFailures(): void {
if (this.hasStatus(RequestStatus.Failed)) {
if (this.hasStatus(RequestStatus.Failed) || this.hasStatus(RequestStatus.SucceededPartial)) {
const failures: Array<FileResponse | DeployMessage> = [];
const fileResponseFailures: Map<string, string> = new Map<string, string>();

Expand Down
3 changes: 1 addition & 2 deletions src/formatters/pushResultFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import * as chalk from 'chalk';
import { UX } from '@salesforce/command';
import { Logger, Messages, SfdxError } from '@salesforce/core';
import { getString } from '@salesforce/ts-types';
import {
DeployResult,
FileResponse,
Expand Down Expand Up @@ -66,7 +65,7 @@ export class PushResultFormatter extends ResultFormatter {
}

protected hasStatus(status: RequestStatus): boolean {
return getString(this.result, 'response.status') === status;
return this.result.response.status === status;
}

protected displaySuccesses(): void {
Expand Down
2 changes: 1 addition & 1 deletion src/formatters/resultFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export abstract class ResultFormatter {
// Command success is determined by the command so it can set the
// exit code on the process, which is done before formatting.
public isSuccess(): boolean {
return getNumber(process, 'exitCode', 0) === 0;
return [0, 69].includes(getNumber(process, 'exitCode', 0));
}

public isVerbose(): boolean {
Expand Down
Loading

0 comments on commit 8bc0797

Please sign in to comment.