From 3ebdc078704ccacd67de180862f0e9e05bfd62b2 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Tue, 8 Oct 2024 15:20:09 -0600 Subject: [PATCH] feat: add deploy size and count warnings (#1435) --- src/client/metadataApiDeploy.ts | 40 ++++++++++- test/client/metadataApiDeploy.test.ts | 95 ++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 3 deletions(-) diff --git a/src/client/metadataApiDeploy.ts b/src/client/metadataApiDeploy.ts index 284a65f88..5b8086481 100644 --- a/src/client/metadataApiDeploy.ts +++ b/src/client/metadataApiDeploy.ts @@ -9,7 +9,7 @@ import { format } from 'node:util'; import { isString } from '@salesforce/ts-types'; import JSZip from 'jszip'; import fs from 'graceful-fs'; -import { Lifecycle, Messages, SfError } from '@salesforce/core'; +import { Lifecycle, Messages, SfError, envVars } from '@salesforce/core'; import { ensureArray } from '@salesforce/kit'; import { RegistryAccess } from '../registry/registryAccess'; import { ReplacementEvent } from '../convert/types'; @@ -234,6 +234,7 @@ export class MetadataApiDeploy extends MetadataTransfer< this.logger.debug(zipMessage); await LifecycleInstance.emit('apiVersionDeploy', { webService, manifestVersion, apiVersion }); await LifecycleInstance.emit('deployZipData', { zipSize: this.zipSize, zipFileCount }); + await this.warnIfDeployThresholdExceeded(this.zipSize, zipFileCount); return this.isRestDeploy ? connection.metadata.deployRest(zipBuffer, optionsWithoutRest) @@ -311,6 +312,41 @@ export class MetadataApiDeploy extends MetadataTransfer< return deployResult; } + // By default, an 80% deploy size threshold is used to warn users when their deploy size + // is approaching the limit enforced by the Metadata API. This includes the number of files + // being deployed as well as the byte size of the deployment. The threshold can be overridden + // to be a different percentage using the SF_DEPLOY_SIZE_THRESHOLD env var. An env var value + // of 100 would disable the client side warning. An env var value of 0 would always warn. + private async warnIfDeployThresholdExceeded(zipSize: number, zipFileCount: number | undefined): Promise { + const thresholdPercentage = Math.abs(envVars.getNumber('SF_DEPLOY_SIZE_THRESHOLD', 80)); + if (thresholdPercentage >= 100) { + this.logger.debug( + `Deploy size warning is disabled since SF_DEPLOY_SIZE_THRESHOLD is overridden to: ${thresholdPercentage}` + ); + return; + } + if (thresholdPercentage !== 80) { + this.logger.debug( + `Deploy size warning threshold has been overridden by SF_DEPLOY_SIZE_THRESHOLD to: ${thresholdPercentage}` + ); + } + // 39_000_000 is 39 MB in decimal format, which is the format used in buffer.byteLength + const fileSizeThreshold = Math.round(39_000_000 * (thresholdPercentage / 100)); + const fileCountThreshold = Math.round(10_000 * (thresholdPercentage / 100)); + + if (zipSize > fileSizeThreshold) { + await Lifecycle.getInstance().emitWarning( + `Deployment zip file size is approaching the Metadata API limit (~39MB). Warning threshold is ${thresholdPercentage}% and size ${zipSize} > ${fileSizeThreshold}` + ); + } + + if (zipFileCount && zipFileCount > fileCountThreshold) { + await Lifecycle.getInstance().emitWarning( + `Deployment zip file count is approaching the Metadata API limit (10,000). Warning threshold is ${thresholdPercentage}% and count ${zipFileCount} > ${fileCountThreshold}` + ); + } + } + private async getZipBuffer(): Promise<{ zipBuffer: Buffer; zipFileCount?: number }> { const mdapiPath = this.options.mdapiPath; @@ -339,7 +375,7 @@ export class MetadataApiDeploy extends MetadataTransfer< } } }; - this.logger.debug('Zipping directory for metadata deploy:', mdapiPath); + this.logger.debug(`Zipping directory for metadata deploy: ${mdapiPath}`); zipDirRecursive(mdapiPath); return { diff --git a/test/client/metadataApiDeploy.test.ts b/test/client/metadataApiDeploy.test.ts index 48809027f..cb2279066 100644 --- a/test/client/metadataApiDeploy.test.ts +++ b/test/client/metadataApiDeploy.test.ts @@ -8,7 +8,7 @@ import { basename, join, sep } from 'node:path'; import { MockTestOrgData, TestContext } from '@salesforce/core/testSetup'; import chai, { assert, expect } from 'chai'; import { AnyJson, ensureString, getString } from '@salesforce/ts-types'; -import { Lifecycle, Messages, PollingClient, StatusResult } from '@salesforce/core'; +import { envVars, Lifecycle, Messages, PollingClient, StatusResult } from '@salesforce/core'; import { Duration } from '@salesforce/kit'; import deepEqualInAnyOrder = require('deep-equal-in-any-order'); import { @@ -144,6 +144,99 @@ describe('MetadataApiDeploy', () => { expect(operation.id).to.deep.equal(response.id); }); + + it('should call warnIfDeployThresholdExceeded', async () => { + const component = matchingContentFile.COMPONENT; + const deployedComponents = new ComponentSet([component]); + const { operation, response } = await stubMetadataDeploy($$, testOrg, { + components: deployedComponents, + }); + // @ts-expect-error stubbing private method + const warnStub = $$.SANDBOX.spy(operation, 'warnIfDeployThresholdExceeded'); + + await operation.start(); + + expect(operation.id).to.deep.equal(response.id); + expect(warnStub.callCount).to.equal(1, 'warnIfDeployThresholdExceeded() should have been called'); + // 4 is the expected byte size (zipBuffer is set to '1234') + // undefined is expected since we're not computing the number of files in the zip + expect(warnStub.firstCall.args).to.deep.equal([4, undefined]); + }); + }); + + describe('warnIfDeployThresholdExceeded', () => { + let emitWarningStub: sinon.SinonStub; + + beforeEach(() => { + emitWarningStub = $$.SANDBOX.stub(Lifecycle.prototype, 'emitWarning').resolves(); + }); + + it('should emit warning with default threshold when zipSize > 80%', async () => { + const loggerDebugSpy = $$.SANDBOX.spy($$.TEST_LOGGER, 'debug'); + const mdapThis = { logger: $$.TEST_LOGGER }; + // @ts-expect-error testing private method + await MetadataApiDeploy.prototype.warnIfDeployThresholdExceeded.call(mdapThis, 31_200_001, 8000); + expect(emitWarningStub.calledOnce, 'emitWarning for fileSize should have been called').to.be.true; + const warningMsg = + 'Deployment zip file size is approaching the Metadata API limit (~39MB). Warning threshold is 80%'; + expect(emitWarningStub.firstCall.args[0]).to.include(warningMsg); + expect(loggerDebugSpy.called).to.be.false; + }); + + it('should emit warning with default threshold when zipFileCount > 80%', async () => { + const loggerDebugSpy = $$.SANDBOX.spy($$.TEST_LOGGER, 'debug'); + const mdapThis = { logger: $$.TEST_LOGGER }; + // @ts-expect-error testing private method + await MetadataApiDeploy.prototype.warnIfDeployThresholdExceeded.call(mdapThis, 31_200_000, 8001); + expect(emitWarningStub.calledOnce, 'emitWarning for fileSize should have been called').to.be.true; + const warningMsg = + 'Deployment zip file count is approaching the Metadata API limit (10,000). Warning threshold is 80%'; + expect(emitWarningStub.firstCall.args[0]).to.include(warningMsg); + expect(loggerDebugSpy.called).to.be.false; + }); + + it('should not emit warning but log debug output when threshold >= 100%', async () => { + const loggerDebugSpy = $$.SANDBOX.spy($$.TEST_LOGGER, 'debug'); + $$.SANDBOX.stub(envVars, 'getNumber').returns(100); + const mdapThis = { logger: $$.TEST_LOGGER }; + // @ts-expect-error testing private method + await MetadataApiDeploy.prototype.warnIfDeployThresholdExceeded.call(mdapThis, 310_200_000, 12_000); + expect(emitWarningStub.called).to.be.false; + expect(loggerDebugSpy.calledOnce).to.be.true; + const expectedMsg = 'Deploy size warning is disabled since SF_DEPLOY_SIZE_THRESHOLD is overridden to: 100'; + expect(loggerDebugSpy.firstCall.args[0]).to.equal(expectedMsg); + }); + + it('should emit warnings and log debug output with exceeded overridden threshold', async () => { + const loggerDebugSpy = $$.SANDBOX.spy($$.TEST_LOGGER, 'debug'); + $$.SANDBOX.stub(envVars, 'getNumber').returns(75); + const mdapThis = { logger: $$.TEST_LOGGER }; + // @ts-expect-error testing private method + await MetadataApiDeploy.prototype.warnIfDeployThresholdExceeded.call(mdapThis, 29_250_001, 7501); + expect(emitWarningStub.calledTwice, 'emitWarning for fileSize and fileCount should have been called').to.be + .true; + const fileSizeWarningMsg = + 'Deployment zip file size is approaching the Metadata API limit (~39MB). Warning threshold is 75%'; + const fileCountWarningMsg = + 'Deployment zip file count is approaching the Metadata API limit (10,000). Warning threshold is 75%'; + expect(emitWarningStub.firstCall.args[0]).to.include(fileSizeWarningMsg); + expect(emitWarningStub.secondCall.args[0]).to.include(fileCountWarningMsg); + expect(loggerDebugSpy.calledOnce).to.be.true; + const expectedMsg = 'Deploy size warning threshold has been overridden by SF_DEPLOY_SIZE_THRESHOLD to: 75'; + expect(loggerDebugSpy.firstCall.args[0]).to.equal(expectedMsg); + }); + + it('should NOT emit warnings but log debug output with overridden threshold that is not exceeded', async () => { + const loggerDebugSpy = $$.SANDBOX.spy($$.TEST_LOGGER, 'debug'); + $$.SANDBOX.stub(envVars, 'getNumber').returns(75); + const mdapThis = { logger: $$.TEST_LOGGER }; + // @ts-expect-error testing private method + await MetadataApiDeploy.prototype.warnIfDeployThresholdExceeded.call(mdapThis, 29_250_000, 7500); + expect(emitWarningStub.called, 'emitWarning should not have been called').to.be.false; + expect(loggerDebugSpy.calledOnce).to.be.true; + const expectedMsg = 'Deploy size warning threshold has been overridden by SF_DEPLOY_SIZE_THRESHOLD to: 75'; + expect(loggerDebugSpy.firstCall.args[0]).to.equal(expectedMsg); + }); }); describe('pollStatus', () => {