Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,14 +93,20 @@ jobs:
equal: ['linux', <<parameters.os>>]
steps:
- run: yarn add $CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME#$CIRCLE_SHA1
- run:
# STL could also have an SDR inside it. Take it out to prevent conflicts
name: remove sdr from source-tracking
command: |
npm install shx -g
shx rm -rf node_modules/@salesforce/source-deploy-retrieve
working_directory: node_modules/@salesforce/source-tracking
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a common thing to do for circular deps?
Does this not mean that for the tests source-tracking would be using the current version of SDR, but after deploy it'd be using whatever version it specifies in it's package.json?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they're not circular, just transitive. plugin-source depends on SDR and STL and STL depends on SDR.

When we build the CLI, we bump both SDR and STL via yarn resolutions to their latest version.

This tests the changes on this branch everywhere that SDR is used. Imagine a bug in SDR that causes problems for STL, but you don't catch it because the nuts are using an STL with a different (bug-free) SDR. Wouldn't want to miss that!

- run:
name: install/build <<parameters.external_project_git_url>> in node_modules
# why doesn't SDR put the metadataRegistry.json in the lib when run from inside a node module? I don't know.
# prevent dependency conflicts between plugin's top-level imports and imported SDR's deps by deleting them
# If there are real conflicts, we'll catch them when bumping a version in the plugin (same nuts)
command: |
yarn install
npm install shx -g
shx rm -rf node_modules/@salesforce/kit
shx rm -rf node_modules/@typescript-eslint
shx rm -rf node_modules/eslint-plugin-header
Expand Down
46 changes: 23 additions & 23 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,61 +25,61 @@
"node": ">=14.0.0"
},
"dependencies": {
"@salesforce/core": "^3.21.1",
"@salesforce/kit": "^1.5.41",
"@salesforce/core": "^3.22.0",
"@salesforce/kit": "^1.5.42",
"@salesforce/ts-types": "^1.5.20",
"archiver": "^5.3.0",
"fast-xml-parser": "^3.17.4",
"archiver": "^5.3.1",
"fast-xml-parser": "^3.21.1",
"got": "^11.8.5",
"graceful-fs": "^4.2.8",
"ignore": "^5.1.8",
"graceful-fs": "^4.2.10",
"ignore": "^5.2.0",
"mime": "2.6.0",
"proxy-agent": "^5.0.0",
"proxy-from-env": "^1.1.0",
"unzipper": "0.10.11",
"xmldom-sfdx-encoding": "^0.1.29"
"xmldom-sfdx-encoding": "^0.1.30"
},
"devDependencies": {
"@salesforce/dev-config": "^3.0.1",
"@salesforce/dev-scripts": "^2.0.1",
"@salesforce/dev-scripts": "^2.0.3",
"@salesforce/prettier-config": "^0.0.2",
"@salesforce/ts-sinon": "^1.1.2",
"@types/archiver": "^5.1.1",
"@salesforce/ts-sinon": "^1.3.21",
"@types/archiver": "^5.3.1",
"@types/deep-equal-in-any-order": "^1.0.1",
"@types/mime": "2.0.3",
"@types/mkdirp": "0.5.2",
"@types/shelljs": "^0.8.11",
"@types/unzipper": "^0.10.5",
"@types/proxy-from-env": "^1.0.1",
"@types/shelljs": "^0.8.9",
"@types/unzipper": "^0.10.3",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"chai": "^4.2.0",
"commitizen": "^3.0.5",
"chai": "^4.3.6",
"commitizen": "^3.1.2",
"cz-conventional-changelog": "^2.1.0",
"deep-equal-in-any-order": "^1.1.19",
"deepmerge": "^4.2.2",
"eslint": "^7.32.0",
"eslint-config-prettier": "^6.11.0",
"eslint-config-prettier": "^6.15.0",
"eslint-config-salesforce": "^0.1.6",
"eslint-config-salesforce-license": "^0.1.6",
"eslint-config-salesforce-typescript": "^0.2.8",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-jsdoc": "^35.1.3",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^35.5.1",
"eslint-plugin-prettier": "^4.0.0",
"husky": "^7.0.4",
"jsforce": "2.0.0-beta.10",
"lint-staged": "^10.2.11",
"mocha": "^9.1.3",
"lint-staged": "^10.5.4",
"mocha": "^9.2.2",
"mocha-junit-reporter": "^1.23.3",
"nyc": "^15.1.0",
"prettier": "^2.0.5",
"pretty-quick": "^3.1.0",
"prettier": "^2.7.1",
"pretty-quick": "^3.1.3",
"shelljs": "0.8.5",
"shx": "^0.3.2",
"shx": "^0.3.4",
"sinon": "10.0.0",
"ts-node": "^10.8.1",
"typescript": "^4.1.3"
"typescript": "^4.7.4"
},
"scripts": {
"build": "sf-build",
Expand Down
38 changes: 32 additions & 6 deletions src/resolve/connectionResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { Connection, Logger } from '@salesforce/core';
import { retry, NotRetryableError, RetryError } from 'ts-retry-promise';
import { RegistryAccess, registry as defaultRegistry, MetadataType } from '../registry';
import { standardValueSet } from '../registry/standardvalueset';
import { FileProperties, StdValueSetRecord, ListMetadataQuery } from '../client/types';
Expand Down Expand Up @@ -108,10 +109,28 @@ export class ConnectionResolver {
if (query.type === defaultRegistry.types.standardvalueset.name && members.length === 0) {
const standardValueSetPromises = standardValueSet.fullnames.map(async (standardValueSetFullName) => {
try {
const standardValueSetRecord: StdValueSetRecord = await this.connection.singleRecordQuery(
`SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = '${standardValueSetFullName}'`,
{ tooling: true }
);
// The 'singleRecordQuery' method was having connection errors, using `retry` resolves this
// Note that this type of connection retry logic may someday be added to jsforce v2
// Once that happens this logic could be reverted
const standardValueSetRecord: StdValueSetRecord = await retry(async () => {
try {
return await this.connection.singleRecordQuery(
`SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = '${standardValueSetFullName}'`,
{ tooling: true }
);
} catch (err) {
// We exit the retry loop with `NotRetryableError` if we get an (expected) unsupported metadata type error
const error = err as Error;
if (error.message.includes('either inaccessible or not supported in Metadata API')) {
this.logger.debug('Expected error:', error.message);
throw new NotRetryableError(error.message);
}

// Otherwise throw the err so we can retry again
throw err;
}
});

return (
standardValueSetRecord.Metadata.standardValue.length && {
fullName: standardValueSetRecord.MasterLabel,
Expand All @@ -126,8 +145,15 @@ export class ConnectionResolver {
lastModifiedDate: '',
}
);
} catch (error) {
this.logger.debug((error as Error).message);
} catch (err) {
// error.message here will be overwritten by 'ts-retry-promise'
// Example error.message from the library: "All retries failed" or "Met not retryable error"
// 'ts-retry-promise' exposes the actual error on `error.lastError`
const error = err as RetryError;

if (error.lastError && error.lastError.message) {
this.logger.debug(error.lastError.message);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a few unit tests to cover the conditions added here?

  • retries and succeeds
  • Hits the retry limit (seemed a bit like exercising the retry lib, but would be good to have a test to verify what happens when it hits the limit). Could just stub the failure condition early verse having to actually call until the lib fails.
  • Not Retryable error thrown

It would make the unit testing a bit cleaner if you refactored the standardValueSetRecord specific handling out into a method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some tests @gbockus-sf 👍

}
});
for await (const standardValueSetResult of standardValueSetPromises) {
Expand Down
48 changes: 47 additions & 1 deletion test/resolve/connectionResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import { expect } from 'chai';
import { MockTestOrgData, testSetup } from '@salesforce/core/lib/testSetup';
import { createSandbox, SinonSandbox } from 'sinon';
import { Connection } from '@salesforce/core';
import { Connection, Logger } from '@salesforce/core';
import { mockConnection } from '../mock/client';
import { ConnectionResolver } from '../../src/resolve';
import { MetadataComponent, registry } from '../../src/';
Expand Down Expand Up @@ -258,6 +258,52 @@ describe('ConnectionResolver', () => {
];
expect(result.components).to.deep.equal(expected);
});

it('should retry (ten times) if unexpected error occurs', async () => {
const loggerStub = sandboxStub.stub(Logger.prototype, 'debug');

sandboxStub.stub(connection.metadata, 'list');

const query = "SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = 'AccountOwnership'";

const mockToolingQuery = sandboxStub.stub(connection, 'singleRecordQuery');
mockToolingQuery.withArgs(query).rejects(new Error('Something happened. Oh no.'));

const resolver = new ConnectionResolver(connection);
const result = await resolver.resolve();
const expected: MetadataComponent[] = [];

// filter over queries and find ones called with `query`
const retries = mockToolingQuery.args.filter((call) => call[0] === query);

expect(retries.length).to.equal(11); // first call plus 10 retries
expect(loggerStub.calledOnce).to.be.true;
expect(loggerStub.args[0][0]).to.equal('Something happened. Oh no.');
expect(result.components).to.deep.equal(expected);
});

it('should not retry query if expected unsupported metadata error is encountered', async () => {
const loggerStub = sandboxStub.stub(Logger.prototype, 'debug');

sandboxStub.stub(connection.metadata, 'list');

const errorMessage = 'WorkTypeGroupAddInfo is either inaccessible or not supported in Metadata API';

const mockToolingQuery = sandboxStub.stub(connection, 'singleRecordQuery');
mockToolingQuery
.withArgs("SELECT Id, MasterLabel, Metadata FROM StandardValueSet WHERE MasterLabel = 'WorkTypeGroupAddInfo'")
.rejects(new Error(errorMessage));

const resolver = new ConnectionResolver(connection);
const result = await resolver.resolve();
const expected: MetadataComponent[] = [];

expect(loggerStub.calledOnce).to.be.true;
expect(loggerStub.args[0][0]).to.equal('Expected error:');
expect(loggerStub.args[0][1]).to.equal(errorMessage);
expect(result.components).to.deep.equal(expected);
});

it('should resolve no managed components', async () => {
const metadataQueryStub = sandboxStub.stub(connection.metadata, 'list');

Expand Down
Loading