Skip to content

Commit

Permalink
feat: display replacements on push/deploy (#628)
Browse files Browse the repository at this point in the history
* feat: display replacements on push/deploy

* chore: bump sdr

* chore: update lockfile and snapshot
  • Loading branch information
mshanemc authored Nov 1, 2022
1 parent 8995644 commit 6fd2045
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 14 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@salesforce/command": "^5.2.13",
"@salesforce/core": "^3.26.2",
"@salesforce/kit": "^1.7.1",
"@salesforce/source-deploy-retrieve": "^7.4.0",
"@salesforce/source-deploy-retrieve": "^7.5.0",
"@salesforce/source-tracking": "^2.2.10",
"chalk": "^4.1.2",
"got": "^11.8.3",
Expand Down Expand Up @@ -188,4 +188,4 @@
"publishConfig": {
"access": "public"
}
}
}
23 changes: 22 additions & 1 deletion src/formatters/deployResultFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import * as path from 'path';
import * as chalk from 'chalk';
import { UX } from '@salesforce/command';
import { Logger, Messages, SfError } from '@salesforce/core';
Expand All @@ -30,6 +31,7 @@ export interface DeployCommandResult extends MdDeployResult {
outboundFiles: string[];
deploys: MetadataApiDeployStatus[];
deletes?: MetadataApiDeployStatus[];
replacements?: Record<string, string[]>;
}

export class DeployResultFormatter extends ResultFormatter {
Expand All @@ -56,7 +58,9 @@ export class DeployResultFormatter extends ResultFormatter {
json.coverage = this.getCoverageFileInfo();
json.junit = this.getJunitFileInfo();
}

if (this.result.replacements?.size) {
json.replacements = Object.fromEntries(this.result.replacements);
}
return json;
}

Expand Down Expand Up @@ -86,6 +90,7 @@ export class DeployResultFormatter extends ResultFormatter {
this.displayFailures();
this.displayTestResults();
this.displayOutputFileLocations();
this.displayReplacements();

// Throw a DeployFailed error unless the deployment was successful.
if (!this.isSuccess()) {
Expand Down Expand Up @@ -117,6 +122,22 @@ export class DeployResultFormatter extends ResultFormatter {
return get(this.result, 'response', {}) as MetadataApiDeployStatus;
}

protected displayReplacements(): void {
if (this.isVerbose() && this.result.replacements?.size) {
this.ux.log('');
this.ux.styledHeader(chalk.blue('Metadata Replacements'));
const replacements = Array.from(this.result.replacements.entries()).flatMap(([filepath, stringsReplaced]) =>
stringsReplaced.map((replaced) => ({
filePath: path.relative(process.cwd(), filepath),
replaced,
}))
);
this.ux.table(replacements, {
filePath: { header: 'PROJECT PATH' },
replaced: { header: 'TEXT REPLACED' },
});
}
}
protected displaySuccesses(): void {
if (this.isSuccess() && this.fileResponses?.length) {
const successes = this.fileResponses.filter((f) => !['Failed', 'Deleted'].includes(f.state));
Expand Down
46 changes: 41 additions & 5 deletions src/formatters/source/pushResultFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { resolve as pathResolve } from 'path';
import { relative, resolve as pathResolve } from 'path';
import * as chalk from 'chalk';
import { UX } from '@salesforce/command';
import { Logger, Messages, SfError } from '@salesforce/core';
Expand All @@ -24,11 +24,14 @@ import { ResultFormatter, ResultFormatterOptions } from '../resultFormatter';
Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-source', 'push');

export type PushResponse = { pushedSource: Array<Pick<FileResponse, 'filePath' | 'fullName' | 'state' | 'type'>> };
export type PushResponse = {
pushedSource: Array<Pick<FileResponse, 'filePath' | 'fullName' | 'state' | 'type'>>;
replacements?: Record<string, string[]>;
};

export class PushResultFormatter extends ResultFormatter {
protected fileResponses: FileResponse[];

protected replacements: Map<string, string[]>;
public constructor(
logger: Logger,
ux: UX,
Expand All @@ -39,6 +42,7 @@ export class PushResultFormatter extends ResultFormatter {
) {
super(logger, ux, options);
this.fileResponses = this.correctFileResponses();
this.replacements = mergeReplacements(results);
}

/**
Expand All @@ -65,9 +69,9 @@ export class PushResultFormatter extends ResultFormatter {
const toReturn = this.isQuiet()
? this.fileResponses.filter((fileResponse) => fileResponse.state === ComponentStatus.Failed)
: this.fileResponses;

return {
pushedSource: toReturn.map(({ state, fullName, type, filePath }) => ({ state, fullName, type, filePath })),
...(!this.isQuiet() && this.replacements.size ? { replacements: Object.fromEntries(this.replacements) } : {}),
};
}

Expand All @@ -82,7 +86,7 @@ export class PushResultFormatter extends ResultFormatter {
public display(): void {
this.displaySuccesses();
this.displayFailures();

this.displayReplacements();
// Throw a DeployFailed error unless the deployment was successful.
if (!this.isSuccess()) {
// Add error message directly on the DeployResult (e.g., a GACK)
Expand Down Expand Up @@ -163,6 +167,23 @@ export class PushResultFormatter extends ResultFormatter {
}
}

protected displayReplacements(): void {
if (!this.isQuiet() && this.replacements.size) {
this.ux.log('');
this.ux.styledHeader(chalk.blue('Metadata Replacements'));
const replacements = Array.from(this.replacements.entries()).flatMap(([filepath, stringsReplaced]) =>
stringsReplaced.map((replaced) => ({
filePath: relative(process.cwd(), filepath),
replaced,
}))
);
this.ux.table(replacements, {
filePath: { header: 'PROJECT PATH' },
replaced: { header: 'TEXT REPLACED' },
});
}
}

protected displayFailures(): void {
const failures: Array<FileResponse | DeployMessage> = [];
const fileResponseFailures: Map<string, string> = new Map<string, string>();
Expand Down Expand Up @@ -219,3 +240,18 @@ export class PushResultFormatter extends ResultFormatter {
}
}
}

export const mergeReplacements = (results: DeployResult[]): DeployResult['replacements'] => {
const merged = new Map<string, string[]>();
const replacements = results.filter((result) => result.replacements?.size).map((result) => result.replacements);
replacements.forEach((replacement) => {
replacement.forEach((value, key) => {
if (!merged.has(key)) {
merged.set(key, value);
} else {
merged.set(key, Array.from(new Set([...merged.get(key), ...value])));
}
});
});
return merged;
};
78 changes: 77 additions & 1 deletion test/formatters/deployResultFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as sinon from 'sinon';
import { expect } from 'chai';
import { Logger } from '@salesforce/core';
import { UX } from '@salesforce/command';
import { FileResponse } from '@salesforce/source-deploy-retrieve';
import { DeployResult, FileResponse } from '@salesforce/source-deploy-retrieve';
import { stubInterface } from '@salesforce/ts-sinon';
import { getDeployResult } from '../commands/source/deployResponses';
import { DeployCommandResult, DeployResultFormatter } from '../../src/formatters/deployResultFormatter';
Expand Down Expand Up @@ -53,6 +53,7 @@ describe('DeployResultFormatter', () => {

afterEach(() => {
sandbox.restore();
process.exitCode = undefined;
});

describe('getJson', () => {
Expand Down Expand Up @@ -87,6 +88,36 @@ describe('DeployResultFormatter', () => {
const formatter = new DeployResultFormatter(logger, ux as UX, {}, deployResultPartialSuccess);
expect(formatter.getJson()).to.deep.equal(expectedPartialSuccessResponse);
});

describe('replacements', () => {
it('includes expected json property when there are replacements', () => {
const resultWithReplacements = {
...(JSON.parse(JSON.stringify(deployResultSuccess)) as DeployResult),
replacements: new Map<string, string[]>([['MyApexClass.cls', ['foo', 'bar']]]),
};
const formatter = new DeployResultFormatter(logger, ux as UX, {}, resultWithReplacements as DeployResult);
const json = formatter.getJson();

expect(json.replacements).to.deep.equal({ 'MyApexClass.cls': ['foo', 'bar'] });
});
it('omits json property when there are no replacements', () => {
const resultWithoutReplacements = {
...(JSON.parse(JSON.stringify(deployResultSuccess)) as DeployResult),
};
const formatter = new DeployResultFormatter(logger, ux as UX, {}, resultWithoutReplacements as DeployResult);
const json = formatter.getJson();
expect(json.replacements).to.be.undefined;
});
it('omits json property when replacements exists but is empty', () => {
const resultWithEmptyReplacements = {
...(JSON.parse(JSON.stringify(deployResultSuccess)) as DeployResult),
replacements: new Map<string, string[]>(),
};
const formatter = new DeployResultFormatter(logger, ux as UX, {}, resultWithEmptyReplacements as DeployResult);
const json = formatter.getJson();
expect(json.replacements).to.be.undefined;
});
});
});

describe('display', () => {
Expand Down Expand Up @@ -183,5 +214,50 @@ describe('DeployResultFormatter', () => {
expect(styledHeaderStub.args[0][0]).to.include('Deployed Source');
expect(styledHeaderStub.args[1][0]).to.include('Component Failures');
});

describe('replacements', () => {
it('omits replacements when there are none', async () => {
process.exitCode = 0;
const resultWithoutReplacements = {
...deployResultSuccess,
} as DeployResult;
const formatter = new DeployResultFormatter(logger, ux as UX, { verbose: true }, resultWithoutReplacements);

formatter.display();

expect(logStub.callCount, 'logStub.callCount').to.equal(2);
expect(tableStub.callCount, 'tableStub.callCount').to.equal(1);
expect(styledHeaderStub.args[0][0]).to.include('Deployed Source');
});
it('displays replacements on verbose', async () => {
process.exitCode = 0;

const resultWithReplacements = {
...deployResultSuccess,
replacements: new Map<string, string[]>([['MyApexClass.cls', ['foo', 'bar']]]),
} as DeployResult;
const formatter = new DeployResultFormatter(logger, ux as UX, { verbose: true }, resultWithReplacements);
formatter.display();

expect(logStub.callCount, 'logStub.callCount').to.equal(3);
// expect(tableStub.callCount, 'tableStub.callCount').to.equal(2);
expect(styledHeaderStub.args[0][0]).to.include('Deployed Source');
expect(styledHeaderStub.args[1][0]).to.include('Metadata Replacements');
});
it('omits replacements unless verbose', async () => {
process.exitCode = 0;

const resultWithReplacements = {
...deployResultSuccess,
replacements: new Map<string, string[]>([['MyApexClass.cls', ['foo', 'bar']]]),
} as DeployResult;
const formatter = new DeployResultFormatter(logger, ux as UX, {}, resultWithReplacements);
formatter.display();

expect(logStub.callCount, 'logStub.callCount').to.equal(2);
expect(tableStub.callCount, 'tableStub.callCount').to.equal(1);
expect(styledHeaderStub.args[0][0]).to.include('Deployed Source');
});
});
});
});
66 changes: 65 additions & 1 deletion test/formatters/pushResultFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,16 @@ import { Logger } from '@salesforce/core';
import { UX } from '@salesforce/command';
import * as sinon from 'sinon';
import { stubInterface } from '@salesforce/ts-sinon';
import { DeployResult } from '@salesforce/source-deploy-retrieve';
import { getDeployResult } from '../commands/source/deployResponses';
import { PushResultFormatter } from '../../src/formatters/source/pushResultFormatter';
import { PushResultFormatter, mergeReplacements } from '../../src/formatters/source/pushResultFormatter';

describe('PushResultFormatter', () => {
const logger = Logger.childFromRoot('deployTestLogger').useMemoryLogging();
const deployResultSuccess = [getDeployResult('successSync')];
const deployResultSuccessWithReplacements = [
{ ...getDeployResult('successSync'), replacements: new Map<string, string[]>([['foo', ['bar', 'baz']]]) },
] as DeployResult[];
const deployResultFailure = [getDeployResult('failed')];

const sandbox = sinon.createSandbox();
Expand Down Expand Up @@ -59,6 +63,22 @@ describe('PushResultFormatter', () => {
},
]);
});
it('returns expected json for success with replaements', () => {
process.exitCode = 0;
const formatter = new PushResultFormatter(logger, new UX(logger), {}, deployResultSuccessWithReplacements);
const result = formatter.getJson();
expect(result.pushedSource).to.deep.equal([
{
filePath: 'classes/ProductController.cls',
fullName: 'ProductController',
state: 'Changed',
type: 'ApexClass',
},
]);
expect(result.replacements).to.deep.equal({
foo: ['bar', 'baz'],
});
});
it('returns expected json for failure', () => {
const formatter = new PushResultFormatter(logger, new UX(logger), {}, deployResultFailure);
process.exitCode = 1;
Expand All @@ -80,6 +100,18 @@ describe('PushResultFormatter', () => {
process.exitCode = 0;
const formatter = new PushResultFormatter(logger, new UX(logger), { quiet: true }, deployResultSuccess);
expect(formatter.getJson().pushedSource).to.deep.equal([]);
expect(formatter.getJson().replacements).to.be.undefined;
});
it('omits replacements', () => {
process.exitCode = 0;
const formatter = new PushResultFormatter(
logger,
new UX(logger),
{ quiet: true },
deployResultSuccessWithReplacements
);
expect(formatter.getJson().pushedSource).to.deep.equal([]);
expect(formatter.getJson().replacements).to.be.undefined;
});
it('honors quiet flag for json failure', () => {
const formatter = new PushResultFormatter(logger, new UX(logger), { quiet: true }, deployResultFailure);
Expand All @@ -102,6 +134,15 @@ describe('PushResultFormatter', () => {
expect(headerStub.callCount, JSON.stringify(headerStub.args)).to.equal(1);
expect(tableStub.callCount, JSON.stringify(tableStub.args)).to.equal(1);
});
it('returns expected output for success with replacements', () => {
process.exitCode = 0;
const formatter = new PushResultFormatter(logger, uxMock as UX, {}, deployResultSuccessWithReplacements);
formatter.display();
expect(headerStub.callCount, JSON.stringify(headerStub.args)).to.equal(2);
expect(headerStub.args[0][0]).to.include('Pushed Source');
expect(headerStub.args[1][0]).to.include('Metadata Replacements');
expect(tableStub.callCount, JSON.stringify(tableStub.args)).to.equal(2);
});
it('should output as expected for a deploy failure (GACK)', async () => {
const errorMessage =
'UNKNOWN_EXCEPTION: An unexpected error occurred. Please include this ErrorId if you contact support: 1730955361-49792 (-1117026034)';
Expand Down Expand Up @@ -142,5 +183,28 @@ describe('PushResultFormatter', () => {
}
});
});

describe('replacement merging when multiple pushes', () => {
it('merges the replacements from 2 pushes', () => {
const deployResultSuccessWithReplacements1 = {
...getDeployResult('successSync'),
replacements: new Map<string, string[]>([
['foo', ['bar']],
['quux', ['baz']],
]),
} as DeployResult;
const deployResultSuccessWithReplacements2 = {
...getDeployResult('successSync'),
replacements: new Map<string, string[]>([['foo', ['baz']]]),
} as DeployResult;
const result = mergeReplacements([deployResultSuccessWithReplacements1, deployResultSuccessWithReplacements2]);
expect(result).to.deep.equal(
new Map<string, string[]>([
['foo', ['bar', 'baz']],
['quux', ['baz']],
])
);
});
});
});
});
Loading

0 comments on commit 6fd2045

Please sign in to comment.