Skip to content

Commit 3ebdc07

Browse files
authored
feat: add deploy size and count warnings (#1435)
1 parent bd3d017 commit 3ebdc07

File tree

2 files changed

+132
-3
lines changed

2 files changed

+132
-3
lines changed

src/client/metadataApiDeploy.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { format } from 'node:util';
99
import { isString } from '@salesforce/ts-types';
1010
import JSZip from 'jszip';
1111
import fs from 'graceful-fs';
12-
import { Lifecycle, Messages, SfError } from '@salesforce/core';
12+
import { Lifecycle, Messages, SfError, envVars } from '@salesforce/core';
1313
import { ensureArray } from '@salesforce/kit';
1414
import { RegistryAccess } from '../registry/registryAccess';
1515
import { ReplacementEvent } from '../convert/types';
@@ -234,6 +234,7 @@ export class MetadataApiDeploy extends MetadataTransfer<
234234
this.logger.debug(zipMessage);
235235
await LifecycleInstance.emit('apiVersionDeploy', { webService, manifestVersion, apiVersion });
236236
await LifecycleInstance.emit('deployZipData', { zipSize: this.zipSize, zipFileCount });
237+
await this.warnIfDeployThresholdExceeded(this.zipSize, zipFileCount);
237238

238239
return this.isRestDeploy
239240
? connection.metadata.deployRest(zipBuffer, optionsWithoutRest)
@@ -311,6 +312,41 @@ export class MetadataApiDeploy extends MetadataTransfer<
311312
return deployResult;
312313
}
313314

315+
// By default, an 80% deploy size threshold is used to warn users when their deploy size
316+
// is approaching the limit enforced by the Metadata API. This includes the number of files
317+
// being deployed as well as the byte size of the deployment. The threshold can be overridden
318+
// to be a different percentage using the SF_DEPLOY_SIZE_THRESHOLD env var. An env var value
319+
// of 100 would disable the client side warning. An env var value of 0 would always warn.
320+
private async warnIfDeployThresholdExceeded(zipSize: number, zipFileCount: number | undefined): Promise<void> {
321+
const thresholdPercentage = Math.abs(envVars.getNumber('SF_DEPLOY_SIZE_THRESHOLD', 80));
322+
if (thresholdPercentage >= 100) {
323+
this.logger.debug(
324+
`Deploy size warning is disabled since SF_DEPLOY_SIZE_THRESHOLD is overridden to: ${thresholdPercentage}`
325+
);
326+
return;
327+
}
328+
if (thresholdPercentage !== 80) {
329+
this.logger.debug(
330+
`Deploy size warning threshold has been overridden by SF_DEPLOY_SIZE_THRESHOLD to: ${thresholdPercentage}`
331+
);
332+
}
333+
// 39_000_000 is 39 MB in decimal format, which is the format used in buffer.byteLength
334+
const fileSizeThreshold = Math.round(39_000_000 * (thresholdPercentage / 100));
335+
const fileCountThreshold = Math.round(10_000 * (thresholdPercentage / 100));
336+
337+
if (zipSize > fileSizeThreshold) {
338+
await Lifecycle.getInstance().emitWarning(
339+
`Deployment zip file size is approaching the Metadata API limit (~39MB). Warning threshold is ${thresholdPercentage}% and size ${zipSize} > ${fileSizeThreshold}`
340+
);
341+
}
342+
343+
if (zipFileCount && zipFileCount > fileCountThreshold) {
344+
await Lifecycle.getInstance().emitWarning(
345+
`Deployment zip file count is approaching the Metadata API limit (10,000). Warning threshold is ${thresholdPercentage}% and count ${zipFileCount} > ${fileCountThreshold}`
346+
);
347+
}
348+
}
349+
314350
private async getZipBuffer(): Promise<{ zipBuffer: Buffer; zipFileCount?: number }> {
315351
const mdapiPath = this.options.mdapiPath;
316352

@@ -339,7 +375,7 @@ export class MetadataApiDeploy extends MetadataTransfer<
339375
}
340376
}
341377
};
342-
this.logger.debug('Zipping directory for metadata deploy:', mdapiPath);
378+
this.logger.debug(`Zipping directory for metadata deploy: ${mdapiPath}`);
343379
zipDirRecursive(mdapiPath);
344380

345381
return {

test/client/metadataApiDeploy.test.ts

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { basename, join, sep } from 'node:path';
88
import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup';
99
import chai, { assert, expect } from 'chai';
1010
import { AnyJson, ensureString, getString } from '@salesforce/ts-types';
11-
import { Lifecycle, Messages, PollingClient, StatusResult } from '@salesforce/core';
11+
import { envVars, Lifecycle, Messages, PollingClient, StatusResult } from '@salesforce/core';
1212
import { Duration } from '@salesforce/kit';
1313
import deepEqualInAnyOrder = require('deep-equal-in-any-order');
1414
import {
@@ -144,6 +144,99 @@ describe('MetadataApiDeploy', () => {
144144

145145
expect(operation.id).to.deep.equal(response.id);
146146
});
147+
148+
it('should call warnIfDeployThresholdExceeded', async () => {
149+
const component = matchingContentFile.COMPONENT;
150+
const deployedComponents = new ComponentSet([component]);
151+
const { operation, response } = await stubMetadataDeploy($$, testOrg, {
152+
components: deployedComponents,
153+
});
154+
// @ts-expect-error stubbing private method
155+
const warnStub = $$.SANDBOX.spy(operation, 'warnIfDeployThresholdExceeded');
156+
157+
await operation.start();
158+
159+
expect(operation.id).to.deep.equal(response.id);
160+
expect(warnStub.callCount).to.equal(1, 'warnIfDeployThresholdExceeded() should have been called');
161+
// 4 is the expected byte size (zipBuffer is set to '1234')
162+
// undefined is expected since we're not computing the number of files in the zip
163+
expect(warnStub.firstCall.args).to.deep.equal([4, undefined]);
164+
});
165+
});
166+
167+
describe('warnIfDeployThresholdExceeded', () => {
168+
let emitWarningStub: sinon.SinonStub;
169+
170+
beforeEach(() => {
171+
emitWarningStub = $$.SANDBOX.stub(Lifecycle.prototype, 'emitWarning').resolves();
172+
});
173+
174+
it('should emit warning with default threshold when zipSize > 80%', async () => {
175+
const loggerDebugSpy = $$.SANDBOX.spy($$.TEST_LOGGER, 'debug');
176+
const mdapThis = { logger: $$.TEST_LOGGER };
177+
// @ts-expect-error testing private method
178+
await MetadataApiDeploy.prototype.warnIfDeployThresholdExceeded.call(mdapThis, 31_200_001, 8000);
179+
expect(emitWarningStub.calledOnce, 'emitWarning for fileSize should have been called').to.be.true;
180+
const warningMsg =
181+
'Deployment zip file size is approaching the Metadata API limit (~39MB). Warning threshold is 80%';
182+
expect(emitWarningStub.firstCall.args[0]).to.include(warningMsg);
183+
expect(loggerDebugSpy.called).to.be.false;
184+
});
185+
186+
it('should emit warning with default threshold when zipFileCount > 80%', async () => {
187+
const loggerDebugSpy = $$.SANDBOX.spy($$.TEST_LOGGER, 'debug');
188+
const mdapThis = { logger: $$.TEST_LOGGER };
189+
// @ts-expect-error testing private method
190+
await MetadataApiDeploy.prototype.warnIfDeployThresholdExceeded.call(mdapThis, 31_200_000, 8001);
191+
expect(emitWarningStub.calledOnce, 'emitWarning for fileSize should have been called').to.be.true;
192+
const warningMsg =
193+
'Deployment zip file count is approaching the Metadata API limit (10,000). Warning threshold is 80%';
194+
expect(emitWarningStub.firstCall.args[0]).to.include(warningMsg);
195+
expect(loggerDebugSpy.called).to.be.false;
196+
});
197+
198+
it('should not emit warning but log debug output when threshold >= 100%', async () => {
199+
const loggerDebugSpy = $$.SANDBOX.spy($$.TEST_LOGGER, 'debug');
200+
$$.SANDBOX.stub(envVars, 'getNumber').returns(100);
201+
const mdapThis = { logger: $$.TEST_LOGGER };
202+
// @ts-expect-error testing private method
203+
await MetadataApiDeploy.prototype.warnIfDeployThresholdExceeded.call(mdapThis, 310_200_000, 12_000);
204+
expect(emitWarningStub.called).to.be.false;
205+
expect(loggerDebugSpy.calledOnce).to.be.true;
206+
const expectedMsg = 'Deploy size warning is disabled since SF_DEPLOY_SIZE_THRESHOLD is overridden to: 100';
207+
expect(loggerDebugSpy.firstCall.args[0]).to.equal(expectedMsg);
208+
});
209+
210+
it('should emit warnings and log debug output with exceeded overridden threshold', async () => {
211+
const loggerDebugSpy = $$.SANDBOX.spy($$.TEST_LOGGER, 'debug');
212+
$$.SANDBOX.stub(envVars, 'getNumber').returns(75);
213+
const mdapThis = { logger: $$.TEST_LOGGER };
214+
// @ts-expect-error testing private method
215+
await MetadataApiDeploy.prototype.warnIfDeployThresholdExceeded.call(mdapThis, 29_250_001, 7501);
216+
expect(emitWarningStub.calledTwice, 'emitWarning for fileSize and fileCount should have been called').to.be
217+
.true;
218+
const fileSizeWarningMsg =
219+
'Deployment zip file size is approaching the Metadata API limit (~39MB). Warning threshold is 75%';
220+
const fileCountWarningMsg =
221+
'Deployment zip file count is approaching the Metadata API limit (10,000). Warning threshold is 75%';
222+
expect(emitWarningStub.firstCall.args[0]).to.include(fileSizeWarningMsg);
223+
expect(emitWarningStub.secondCall.args[0]).to.include(fileCountWarningMsg);
224+
expect(loggerDebugSpy.calledOnce).to.be.true;
225+
const expectedMsg = 'Deploy size warning threshold has been overridden by SF_DEPLOY_SIZE_THRESHOLD to: 75';
226+
expect(loggerDebugSpy.firstCall.args[0]).to.equal(expectedMsg);
227+
});
228+
229+
it('should NOT emit warnings but log debug output with overridden threshold that is not exceeded', async () => {
230+
const loggerDebugSpy = $$.SANDBOX.spy($$.TEST_LOGGER, 'debug');
231+
$$.SANDBOX.stub(envVars, 'getNumber').returns(75);
232+
const mdapThis = { logger: $$.TEST_LOGGER };
233+
// @ts-expect-error testing private method
234+
await MetadataApiDeploy.prototype.warnIfDeployThresholdExceeded.call(mdapThis, 29_250_000, 7500);
235+
expect(emitWarningStub.called, 'emitWarning should not have been called').to.be.false;
236+
expect(loggerDebugSpy.calledOnce).to.be.true;
237+
const expectedMsg = 'Deploy size warning threshold has been overridden by SF_DEPLOY_SIZE_THRESHOLD to: 75';
238+
expect(loggerDebugSpy.firstCall.args[0]).to.equal(expectedMsg);
239+
});
147240
});
148241

149242
describe('pollStatus', () => {

0 commit comments

Comments
 (0)