From 94d3ce3893ec7bcd6e2f2a743c841cf33971c88d Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Fri, 19 Jan 2024 17:25:15 -0700 Subject: [PATCH 1/6] fix: don't throw when 15 char job ID not in cache --- messages/cache.md | 4 ++++ messages/deploy.metadata.quick.md | 4 ++++ src/commands/project/deploy/quick.ts | 23 +++++++++++++++++++---- src/utils/deployCache.ts | 2 +- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/messages/cache.md b/messages/cache.md index 69dd16b8..bd891752 100644 --- a/messages/cache.md +++ b/messages/cache.md @@ -5,3 +5,7 @@ No job found for ID: %s. # error.NoRecentJobId There are no recent job IDs available. + +# error.NoMatchingJobId + +No matching job found in cache for ID: %s. diff --git a/messages/deploy.metadata.quick.md b/messages/deploy.metadata.quick.md index 8767e89c..c818c3ea 100644 --- a/messages/deploy.metadata.quick.md +++ b/messages/deploy.metadata.quick.md @@ -84,6 +84,10 @@ Job ID can't be used for quick deployment. Possible reasons include the deployme Deployment %s exited with status code: %s. +# error.NoTargetOrg + +No target org found in cache, from a flag, or in the environment. + # info.QuickDeploySuccess Successfully deployed (%s). diff --git a/src/commands/project/deploy/quick.ts b/src/commands/project/deploy/quick.ts index 6877259b..024e1d63 100644 --- a/src/commands/project/deploy/quick.ts +++ b/src/commands/project/deploy/quick.ts @@ -6,11 +6,11 @@ */ import chalk from 'chalk'; -import { Messages, Org } from '@salesforce/core'; +import { Messages, Org, TTLConfig } from '@salesforce/core'; import { SfCommand, toHelpSection, Flags } from '@salesforce/sf-plugins-core'; import { MetadataApiDeploy, RequestStatus } from '@salesforce/source-deploy-retrieve'; import { Duration } from '@salesforce/kit'; -import { DeployOptions, determineExitCode, resolveApi } from '../../../utils/deploy.js'; +import { CachedOptions, DeployOptions, determineExitCode, resolveApi } from '../../../utils/deploy.js'; import { DeployCache } from '../../../utils/deployCache.js'; import { DEPLOY_STATUS_CODES_DESCRIPTIONS } from '../../../utils/errorCodes.js'; import { AsyncDeployResultFormatter } from '../../../formatters/asyncDeployResultFormatter.js'; @@ -84,9 +84,24 @@ export default class DeployMetadataQuick extends SfCommand { const [{ flags }, cache] = await Promise.all([this.parse(DeployMetadataQuick), DeployCache.create()]); // This is the ID of the validation request - const jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id'], false); + let jobId: string; + let deployOpts: DeployOptions | TTLConfig.Entry = {} as DeployOptions; + try { + jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id'], false); + deployOpts = cache.get(jobId) ?? ({} as DeployOptions); + } catch (e: unknown) { + const errName = e instanceof Error ? e.name : 'unknown'; + if (errName === 'NoMatchingJobIdError' && flags['job-id']) { + // In this case there must be a target-org. If not, throw. + if (!flags['target-org']) { + throw messages.createError('error.NoTargetOrg'); + } + jobId = flags['job-id']; + } else { + throw e; + } + } - const deployOpts = cache.get(jobId) ?? ({} as DeployOptions); const org = flags['target-org'] ?? (await Org.create({ aliasOrUsername: deployOpts['target-org'] })); const api = await resolveApi(this.configAggregator); const connection = org.getConnection(flags['api-version']); diff --git a/src/utils/deployCache.ts b/src/utils/deployCache.ts index d7446abd..c74863c9 100644 --- a/src/utils/deployCache.ts +++ b/src/utils/deployCache.ts @@ -72,7 +72,7 @@ export class DeployCache extends TTLConfig { if (match) { return match; } - throw cacheMessages.createError('error.InvalidJobId', [jobId]); + throw cacheMessages.createError('error.NoMatchingJobId', [jobId]); } else { throw cacheMessages.createError('error.InvalidJobId', [jobId]); } From 94bf5023f701c00c21ed12b8c243fea2b8172a70 Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Tue, 23 Jan 2024 16:23:35 -0700 Subject: [PATCH 2/6] fix: refactor ID and org resolution --- src/commands/project/deploy/quick.ts | 54 ++++++++++++++++------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/src/commands/project/deploy/quick.ts b/src/commands/project/deploy/quick.ts index 024e1d63..f2786575 100644 --- a/src/commands/project/deploy/quick.ts +++ b/src/commands/project/deploy/quick.ts @@ -6,11 +6,11 @@ */ import chalk from 'chalk'; -import { Messages, Org, TTLConfig } from '@salesforce/core'; +import { Messages, Org } from '@salesforce/core'; import { SfCommand, toHelpSection, Flags } from '@salesforce/sf-plugins-core'; import { MetadataApiDeploy, RequestStatus } from '@salesforce/source-deploy-retrieve'; import { Duration } from '@salesforce/kit'; -import { CachedOptions, DeployOptions, determineExitCode, resolveApi } from '../../../utils/deploy.js'; +import { determineExitCode, resolveApi } from '../../../utils/deploy.js'; import { DeployCache } from '../../../utils/deployCache.js'; import { DEPLOY_STATUS_CODES_DESCRIPTIONS } from '../../../utils/errorCodes.js'; import { AsyncDeployResultFormatter } from '../../../formatters/asyncDeployResultFormatter.js'; @@ -84,27 +84,10 @@ export default class DeployMetadataQuick extends SfCommand { const [{ flags }, cache] = await Promise.all([this.parse(DeployMetadataQuick), DeployCache.create()]); // This is the ID of the validation request - let jobId: string; - let deployOpts: DeployOptions | TTLConfig.Entry = {} as DeployOptions; - try { - jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id'], false); - deployOpts = cache.get(jobId) ?? ({} as DeployOptions); - } catch (e: unknown) { - const errName = e instanceof Error ? e.name : 'unknown'; - if (errName === 'NoMatchingJobIdError' && flags['job-id']) { - // In this case there must be a target-org. If not, throw. - if (!flags['target-org']) { - throw messages.createError('error.NoTargetOrg'); - } - jobId = flags['job-id']; - } else { - throw e; - } - } - - const org = flags['target-org'] ?? (await Org.create({ aliasOrUsername: deployOpts['target-org'] })); + const jobId = resolveJobId(cache, flags['use-most-recent'], flags['job-id']); + const targetOrg = await resolveTargetOrg(cache, jobId, flags['target-org']); const api = await resolveApi(this.configAggregator); - const connection = org.getConnection(flags['api-version']); + const connection = targetOrg.getConnection(flags['api-version']); // This is the ID of the deploy (of the validated metadata) const deployId = await connection.metadata.deployRecentValidation({ @@ -160,3 +143,30 @@ export default class DeployMetadataQuick extends SfCommand { return super.catch(error); } } + +// Resolve a job ID for a validated deploy using cache, most recent, or a job ID flag. +const resolveJobId = (cache: DeployCache, useMostRecentFlag: boolean, jobIdFlag?: string): string => { + try { + return cache.resolveLatest(useMostRecentFlag, jobIdFlag, false); + } catch (e: unknown) { + const errName = e instanceof Error ? e.name : 'unknown'; + if (errName === 'NoMatchingJobIdError' && jobIdFlag) { + return jobIdFlag; // Use the specified 15 char job ID + } else { + throw e; + } + } +}; + +// Resolve a target org using job ID in cache, or a target org flag. +const resolveTargetOrg = async (cache: DeployCache, jobId: string, targetOrgFlag: Org): Promise => { + const aliasOrUsername = cache.get(jobId)?.['target-org']; + const targetOrg = aliasOrUsername ? await Org.create({ aliasOrUsername }) : targetOrgFlag; + + // If we don't have a target org at this point, throw. + if (!targetOrg) { + throw messages.createError('error.NoTargetOrg'); + } + + return targetOrg; +}; From 5becc76fd26e79928925e0be1a6d70375c65ebc4 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 23 Jan 2024 18:12:46 -0600 Subject: [PATCH 3/6] chore: lockfile --- yarn.lock | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/yarn.lock b/yarn.lock index b4f98de3..c50b2273 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1787,7 +1787,7 @@ "@salesforce/ts-types" "^2.0.9" chalk "^5.3.0" -"@salesforce/source-deploy-retrieve@^10.0.0", "@salesforce/source-deploy-retrieve@^10.2.10", "@salesforce/source-deploy-retrieve@^10.2.11", "@salesforce/source-deploy-retrieve@^10.2.12": +"@salesforce/source-deploy-retrieve@^10.0.0", "@salesforce/source-deploy-retrieve@^10.2.10", "@salesforce/source-deploy-retrieve@^10.2.12": version "10.2.12" resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-10.2.12.tgz#8311593f3be004530896d9264466b59ec64b0a76" integrity sha512-i12707TLzzJTdt7/lXLFatOwSKW1VK/ns13A8/6/6QHElT6nEvnoxP/bah3BAl9x/mKh7Sr8hD6+3ObnYqha4A== @@ -1822,22 +1822,7 @@ shelljs "^0.8.4" sinon "^10.0.0" -"@salesforce/source-tracking@^5.0.0": - version "5.1.5" - resolved "https://registry.yarnpkg.com/@salesforce/source-tracking/-/source-tracking-5.1.5.tgz#a8ff65d738ed12ce585540d3efe4a8a4a65afafc" - integrity sha512-4uQGz6Vg213CLDCGVI8VIxsFVguShEMDV535YHV3YPK/Q3ls4z4eOBk8vOrFpItlHuKpfYYmAKKy16AFJMgk2w== - dependencies: - "@oclif/core" "^3.18.1" - "@salesforce/core" "^6.4.7" - "@salesforce/kit" "^3.0.15" - "@salesforce/source-deploy-retrieve" "^10.2.11" - "@salesforce/ts-types" "^2.0.9" - fast-xml-parser "^4.2.5" - graceful-fs "^4.2.11" - isomorphic-git "1.23.0" - ts-retry-promise "^0.7.0" - -"@salesforce/source-tracking@^5.1.7": +"@salesforce/source-tracking@^5.0.0", "@salesforce/source-tracking@^5.1.7": version "5.1.7" resolved "https://registry.yarnpkg.com/@salesforce/source-tracking/-/source-tracking-5.1.7.tgz#2bfc3e6c9bf0aabd7ba5644a69977dfd1fef84c3" integrity sha512-kkXWt4X+wxmYsLqG1OIWKgo3aFEg1f+1X6MBIHIrmjEmfMXIRiGX0dRyY+IjFl54w+CnOffNDQNsGSmnPImEYg== @@ -9059,7 +9044,7 @@ ts-node@^10.8.1, ts-node@^10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -ts-retry-promise@^0.7.0, ts-retry-promise@^0.7.1: +ts-retry-promise@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ts-retry-promise/-/ts-retry-promise-0.7.1.tgz#176d6eee6415f07b6c7c286d3657355e284a6906" integrity sha512-NhHOCZ2AQORvH42hOPO5UZxShlcuiRtm7P2jIq2L2RY3PBxw2mLnUsEdHrIslVBFya1v5aZmrR55lWkzo13LrQ== From 9faee74b2e838ced62995e90b726f73ee44b6bf3 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Tue, 23 Jan 2024 18:13:04 -0600 Subject: [PATCH 4/6] style: code review --- src/commands/project/deploy/quick.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/commands/project/deploy/quick.ts b/src/commands/project/deploy/quick.ts index f2786575..79198bab 100644 --- a/src/commands/project/deploy/quick.ts +++ b/src/commands/project/deploy/quick.ts @@ -144,21 +144,19 @@ export default class DeployMetadataQuick extends SfCommand { } } -// Resolve a job ID for a validated deploy using cache, most recent, or a job ID flag. +/** Resolve a job ID for a validated deploy using cache, most recent, or a job ID flag. */ const resolveJobId = (cache: DeployCache, useMostRecentFlag: boolean, jobIdFlag?: string): string => { try { return cache.resolveLatest(useMostRecentFlag, jobIdFlag, false); - } catch (e: unknown) { - const errName = e instanceof Error ? e.name : 'unknown'; - if (errName === 'NoMatchingJobIdError' && jobIdFlag) { + } catch (e) { + if (e instanceof Error && e.name === 'NoMatchingJobIdError' && jobIdFlag) { return jobIdFlag; // Use the specified 15 char job ID - } else { - throw e; } + throw e; } }; -// Resolve a target org using job ID in cache, or a target org flag. +/** Resolve a target org using job ID in cache, or a target org flag. */ const resolveTargetOrg = async (cache: DeployCache, jobId: string, targetOrgFlag: Org): Promise => { const aliasOrUsername = cache.get(jobId)?.['target-org']; const targetOrg = aliasOrUsername ? await Org.create({ aliasOrUsername }) : targetOrgFlag; From 81151b5636044123e2079cc906a7a57251d7d182 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Mon, 29 Jan 2024 12:53:16 -0600 Subject: [PATCH 5/6] fix: handle 15-char not in cache but with org --- src/commands/project/deploy/quick.ts | 6 +- src/commands/project/deploy/report.ts | 14 +- src/utils/deployCache.ts | 39 ++++- test/commands/deploy/metadata/quick.nut.ts | 188 ++++++++++++++++----- 4 files changed, 188 insertions(+), 59 deletions(-) diff --git a/src/commands/project/deploy/quick.ts b/src/commands/project/deploy/quick.ts index 79198bab..9656f61f 100644 --- a/src/commands/project/deploy/quick.ts +++ b/src/commands/project/deploy/quick.ts @@ -147,7 +147,7 @@ export default class DeployMetadataQuick extends SfCommand { /** Resolve a job ID for a validated deploy using cache, most recent, or a job ID flag. */ const resolveJobId = (cache: DeployCache, useMostRecentFlag: boolean, jobIdFlag?: string): string => { try { - return cache.resolveLatest(useMostRecentFlag, jobIdFlag, false); + return cache.resolveLatest(useMostRecentFlag, jobIdFlag, true); } catch (e) { if (e instanceof Error && e.name === 'NoMatchingJobIdError' && jobIdFlag) { return jobIdFlag; // Use the specified 15 char job ID @@ -158,8 +158,8 @@ const resolveJobId = (cache: DeployCache, useMostRecentFlag: boolean, jobIdFlag? /** Resolve a target org using job ID in cache, or a target org flag. */ const resolveTargetOrg = async (cache: DeployCache, jobId: string, targetOrgFlag: Org): Promise => { - const aliasOrUsername = cache.get(jobId)?.['target-org']; - const targetOrg = aliasOrUsername ? await Org.create({ aliasOrUsername }) : targetOrgFlag; + const orgFromCache = cache.maybeGet(jobId)?.['target-org']; + const targetOrg = orgFromCache ? await Org.create({ aliasOrUsername: orgFromCache }) : targetOrgFlag; // If we don't have a target org at this point, throw. if (!targetOrg) { diff --git a/src/commands/project/deploy/report.ts b/src/commands/project/deploy/report.ts index d0689611..07dfc809 100644 --- a/src/commands/project/deploy/report.ts +++ b/src/commands/project/deploy/report.ts @@ -5,8 +5,6 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ - - import { Messages, Org, SfProject } from '@salesforce/core'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { ComponentSet, DeployResult, MetadataApiDeploy } from '@salesforce/source-deploy-retrieve'; @@ -17,7 +15,7 @@ import { DeployReportResultFormatter } from '../../../formatters/deployReportRes import { API, DeployResultJson } from '../../../utils/types.js'; import { coverageFormattersFlag } from '../../../utils/flags.js'; -Messages.importMessagesDirectoryFromMetaUrl(import.meta.url) +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'deploy.metadata.report'); const deployMessages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'deploy.metadata'); const testFlags = 'Test'; @@ -75,13 +73,15 @@ export default class DeployMetadataReport extends SfCommand { const [{ flags }, cache] = await Promise.all([this.parse(DeployMetadataReport), DeployCache.create()]); const jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id'], false); - const deployOpts = cache.get(jobId) ?? {}; + const deployOpts = cache.maybeGet(jobId); const wait = flags['wait']; - const org = flags['target-org'] ?? (await Org.create({ aliasOrUsername: deployOpts['target-org'] })); + const org = deployOpts?.['target-org'] + ? await Org.create({ aliasOrUsername: deployOpts['target-org'] }) + : flags['target-org']; // if we're using mdapi we won't have a component set let componentSet = new ComponentSet(); - if (!deployOpts.isMdapi) { + if (!deployOpts?.isMdapi) { if (!cache.get(jobId)) { // If the cache file isn't there, use the project package directories for the CompSet try { @@ -102,7 +102,7 @@ export default class DeployMetadataReport extends SfCommand { id: jobId, components: componentSet, apiOptions: { - rest: deployOpts.api === API['REST'], + rest: deployOpts?.api === API['REST'], }, }); diff --git a/src/utils/deployCache.ts b/src/utils/deployCache.ts index c74863c9..db7dea0b 100644 --- a/src/utils/deployCache.ts +++ b/src/utils/deployCache.ts @@ -51,19 +51,24 @@ export class DeployCache extends TTLConfig { await cache.write(); } - public resolveLatest(useMostRecent: boolean, key: string | undefined, throwOnNotFound = true): string { - const keyFromLatest = useMostRecent ? this.getLatestKey() : key; - if (!keyFromLatest) throw cacheMessages.createError('error.NoRecentJobId'); + public resolveLatest(useMostRecent: boolean, key: string | undefined, throwOnNotFound?: boolean): string { + const resolvedKey = useMostRecent ? this.getLatestKey() : key; + if (!resolvedKey) throw cacheMessages.createError('error.NoRecentJobId'); - const jobId = this.resolveLongId(keyFromLatest); + const match = this.maybeGet(resolvedKey); - if (throwOnNotFound && !this.has(jobId)) { - throw cacheMessages.createError('error.InvalidJobId', [jobId]); + if (throwOnNotFound === true && !match) { + throw cacheMessages.createError('error.NoMatchingJobId', [resolvedKey]); } - return jobId; + return resolvedKey; } + /** + * @deprecated. Use maybeGet to handle both 15 and 18 char IDs + * returns 18-char ID unmodified, regardless of whether it's in cache or not + * returns 15-char ID if it matches a key in the cache, otherwise throws + */ public resolveLongId(jobId: string): string { if (jobId.length === 18) { return jobId; @@ -78,7 +83,27 @@ export class DeployCache extends TTLConfig { } } + /** + * + * @deprecated. Use maybeGet because the typings are wrong in sfdx-core + */ public get(jobId: string): TTLConfig.Entry { return super.get(this.resolveLongId(jobId)); } + + /** + * works with 18 and 15-character IDs. + * Prefer 18 as that's how the cache is keyed. + * Returns undefined if no match is found. + */ + public maybeGet(jobId: string): TTLConfig.Entry | undefined { + if (jobId.length === 18) { + return super.get(jobId); + } + if (jobId.length === 15) { + const match = this.keys().find((k) => k.startsWith(jobId)); + return match ? super.get(match) : undefined; + } + throw cacheMessages.createError('error.InvalidJobId', [jobId]); + } } diff --git a/test/commands/deploy/metadata/quick.nut.ts b/test/commands/deploy/metadata/quick.nut.ts index 56d2b97f..aa748f04 100644 --- a/test/commands/deploy/metadata/quick.nut.ts +++ b/test/commands/deploy/metadata/quick.nut.ts @@ -6,10 +6,13 @@ */ import { fileURLToPath } from 'node:url'; +import fs from 'node:fs'; +import path from 'node:path'; import { SourceTestkit } from '@salesforce/source-testkit'; import { assert, config } from 'chai'; import { execCmd } from '@salesforce/cli-plugins-testkit'; import { DeployResultJson } from '../../../../src/utils/types.js'; + config.truncateThreshold = 0; describe('deploy metadata quick NUTs', () => { @@ -81,59 +84,160 @@ describe('deploy metadata quick NUTs', () => { }); }); - describe('--job-id', () => { - it('should deploy previously validated deployment (async)', async () => { - const validation = await testkit.execute('project:deploy:validate', { - args: '--source-dir force-app', - json: true, - exitCode: 0, + describe('using cache', () => { + describe('--job-id 18', () => { + it('should deploy previously validated deployment (async)', async () => { + const validation = await testkit.execute('project:deploy:validate', { + args: '--source-dir force-app', + json: true, + exitCode: 0, + }); + assert(validation); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); + + const deploy = await testkit.execute('project:deploy:quick', { + args: `--job-id ${validation.result.id}`, + json: true, + exitCode: 0, + }); + assert(deploy); + assert(deploy.result.id !== validation.result.id, 'deploy result ID should not be the validation ID'); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); }); - assert(validation); - await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); - const deploy = await testkit.execute('project:deploy:quick', { - args: `--job-id ${validation.result.id}`, - json: true, - exitCode: 0, + it('should deploy previously validated deployment (poll)', async () => { + const validation = await testkit.execute('project:deploy:validate', { + args: '--source-dir force-app', + json: true, + exitCode: 0, + }); + assert(validation); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); + + const deploy = await testkit.execute('project:deploy:quick', { + args: `--job-id ${validation.result.id} --wait 20`, + json: true, + exitCode: 0, + }); + assert(deploy); + assert(deploy.result.id !== validation.result.id, 'deploy result ID should not be the validation ID'); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); }); - assert(deploy); - assert(deploy.result.id !== validation.result.id, 'deploy result ID should not be the validation ID'); - await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); - }); - it('should deploy previously validated deployment (poll)', async () => { - const validation = await testkit.execute('project:deploy:validate', { - args: '--source-dir force-app', - json: true, - exitCode: 0, + it('should fail to deploy previously deployed deployment', async () => { + const first = await testkit.execute('deploy:metadata', { + args: '--source-dir force-app', + json: true, + exitCode: 0, + }); + assert(first); + const deploy = await testkit.execute('project:deploy:quick', { + args: `--job-id ${first.result.id}`, + json: true, + exitCode: 1, + }); + assert(deploy); + testkit.expect.errorToHaveName(deploy, 'CannotQuickDeployError'); }); - assert(validation); - await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); + }); - const deploy = await testkit.execute('project:deploy:quick', { - args: `--job-id ${validation.result.id} --wait 20`, - json: true, - exitCode: 0, + describe('--job-id 15', () => { + it('should deploy previously validated deployment (async)', async () => { + const validation = await testkit.execute('project:deploy:validate', { + args: '--source-dir force-app', + json: true, + exitCode: 0, + }); + assert(validation); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); + + const deploy = await testkit.execute('project:deploy:quick', { + args: `--job-id ${validation.result.id?.slice(0, 15)}`, + json: true, + exitCode: 0, + }); + assert(deploy); + assert(deploy.result.id !== validation.result.id, 'deploy result ID should not be the validation ID'); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); }); - assert(deploy); - assert(deploy.result.id !== validation.result.id, 'deploy result ID should not be the validation ID'); - await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); }); + }); - it('should fail to deploy previously deployed deployment', async () => { - const first = await testkit.execute('deploy:metadata', { - args: '--source-dir force-app', - json: true, - exitCode: 0, + describe('no cache using default org', () => { + describe('--job-id 18', () => { + it('should deploy previously validated deployment (async)', async () => { + const validation = await testkit.execute('project:deploy:validate', { + args: '--source-dir force-app', + json: true, + exitCode: 0, + }); + assert(validation); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); + + await fs.promises.rm(path.join(testkit.projectDir, '..', '.sf', 'deploy-cache.json'), { + recursive: true, + force: true, + }); + + const deploy = await testkit.execute('project:deploy:quick', { + args: `--job-id ${validation.result.id}`, + json: true, + exitCode: 0, + }); + assert(deploy); + assert(deploy.result.id !== validation.result.id, 'deploy result ID should not be the validation ID'); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); }); - assert(first); - const deploy = await testkit.execute('project:deploy:quick', { - args: `--job-id ${first.result.id}`, - json: true, - exitCode: 1, + + it('should deploy previously validated deployment (poll)', async () => { + const validation = await testkit.execute('project:deploy:validate', { + args: '--source-dir force-app', + json: true, + exitCode: 0, + }); + assert(validation); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); + + await fs.promises.rm(path.join(testkit.projectDir, '..', '.sf', 'deploy-cache.json'), { + recursive: true, + force: true, + }); + + const deploy = await testkit.execute('project:deploy:quick', { + args: `--job-id ${validation.result.id} --wait 20`, + json: true, + exitCode: 0, + }); + assert(deploy); + assert(deploy.result.id !== validation.result.id, 'deploy result ID should not be the validation ID'); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); + }); + }); + + describe('--job-id 15', () => { + it('should deploy previously validated deployment (async)', async () => { + const validation = await testkit.execute('project:deploy:validate', { + args: '--source-dir force-app', + json: true, + exitCode: 0, + }); + assert(validation); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); + + await fs.promises.rm(path.join(testkit.projectDir, '..', '.sf', 'deploy-cache.json'), { + recursive: true, + force: true, + }); + + const deploy = await testkit.execute('project:deploy:quick', { + args: `--job-id ${validation.result.id?.slice(0, 15)}`, + json: true, + exitCode: 0, + }); + assert(deploy); + assert(deploy.result.id !== validation.result.id, 'deploy result ID should not be the validation ID'); + await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']); }); - assert(deploy); - testkit.expect.errorToHaveName(deploy, 'CannotQuickDeployError'); }); }); }); From 380830e1a10a03a0e5c98372d939172010741d64 Mon Sep 17 00:00:00 2001 From: mshanemc Date: Mon, 29 Jan 2024 23:40:53 -0600 Subject: [PATCH 6/6] fix: another 15/18 edge case (yay, nuts!) --- src/commands/project/deploy/cancel.ts | 4 +-- src/commands/project/deploy/report.ts | 2 +- src/commands/project/deploy/resume.ts | 11 ++++--- src/utils/deployCache.ts | 41 ++++++++++++++++++--------- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/commands/project/deploy/cancel.ts b/src/commands/project/deploy/cancel.ts index 20af243f..0dfc39af 100644 --- a/src/commands/project/deploy/cancel.ts +++ b/src/commands/project/deploy/cancel.ts @@ -63,7 +63,7 @@ export default class DeployMetadataCancel extends SfCommand { const jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id']); // cancel don't care about your tracking conflicts - const deployOpts = { ...cache.get(jobId), 'ignore-conflicts': true }; + const deployOpts = { ...cache.maybeGet(jobId), 'ignore-conflicts': true }; // we may already know the job finished if ( deployOpts.status && @@ -80,7 +80,7 @@ export default class DeployMetadataCancel extends SfCommand { if (!this.jsonEnabled()) formatter.display(); return formatter.getJson(); } else { - const wait = flags.wait ?? Duration.minutes(deployOpts.wait); + const wait = flags.wait ?? Duration.minutes(deployOpts.wait ?? 33); const result = await cancelDeploy({ ...deployOpts, wait }, jobId); const formatter = new DeployCancelResultFormatter(result); if (!this.jsonEnabled()) formatter.display(); diff --git a/src/commands/project/deploy/report.ts b/src/commands/project/deploy/report.ts index 07dfc809..cde43b68 100644 --- a/src/commands/project/deploy/report.ts +++ b/src/commands/project/deploy/report.ts @@ -82,7 +82,7 @@ export default class DeployMetadataReport extends SfCommand { // if we're using mdapi we won't have a component set let componentSet = new ComponentSet(); if (!deployOpts?.isMdapi) { - if (!cache.get(jobId)) { + if (!cache.maybeGet(jobId)) { // If the cache file isn't there, use the project package directories for the CompSet try { this.project = await SfProject.resolve(); diff --git a/src/commands/project/deploy/resume.ts b/src/commands/project/deploy/resume.ts index d4c1c147..d8557af1 100644 --- a/src/commands/project/deploy/resume.ts +++ b/src/commands/project/deploy/resume.ts @@ -5,8 +5,6 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ - - import chalk from 'chalk'; import { EnvironmentVariable, Messages, Org, SfError } from '@salesforce/core'; import { SfCommand, toHelpSection, Flags } from '@salesforce/sf-plugins-core'; @@ -20,7 +18,7 @@ import { DeployCache } from '../../../utils/deployCache.js'; import { DEPLOY_STATUS_CODES_DESCRIPTIONS } from '../../../utils/errorCodes.js'; import { coverageFormattersFlag } from '../../../utils/flags.js'; -Messages.importMessagesDirectoryFromMetaUrl(import.meta.url) +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'deploy.metadata.resume'); const testFlags = 'Test'; @@ -83,10 +81,10 @@ export default class DeployMetadataResume extends SfCommand { public async run(): Promise { const [{ flags }, cache] = await Promise.all([this.parse(DeployMetadataResume), DeployCache.create()]); - const jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id']); + const jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id'], true); // if it was async before, then it should not be async now. - const deployOpts = { ...cache.get(jobId), async: false }; + const deployOpts = { ...cache.maybeGet(jobId), async: false }; let result: DeployResult; @@ -108,7 +106,7 @@ export default class DeployMetadataResume extends SfCommand { const deployStatus = await mdapiDeploy.checkStatus(); result = new DeployResult(deployStatus, componentSet); } else { - const wait = flags.wait ?? Duration.minutes(deployOpts.wait); + const wait = flags.wait ?? Duration.minutes(deployOpts.wait ?? 33); const { deploy } = await executeDeploy( // there will always be conflicts on a resume if anything deployed--the changes on the server are not synced to local { @@ -134,6 +132,7 @@ export default class DeployMetadataResume extends SfCommand { if (!deploy.id) { throw new SfError('The deploy id is not available.'); } + cache.update(deploy.id, { status: result.response.status }); await cache.write(); } diff --git a/src/utils/deployCache.ts b/src/utils/deployCache.ts index db7dea0b..b6f149cd 100644 --- a/src/utils/deployCache.ts +++ b/src/utils/deployCache.ts @@ -41,16 +41,21 @@ export class DeployCache extends TTLConfig { public static async unset(key: string): Promise { const cache = await DeployCache.create(); - cache.unset(key); + cache.unset(ensure18(key, cache)); await Promise.all([cache.write(), maybeDestroyManifest(key)]); } public static async update(key: string, obj: JsonMap): Promise { const cache = await DeployCache.create(); - cache.update(key, obj); + cache.update(ensure18(key, cache), obj); await cache.write(); } + public update(key: string, obj: JsonMap): void { + super.update(ensure18(key, this), obj); + } + + /** will return an 18 character ID if throwOnNotFound is true (because the cache can be used to shift 15 to 18) */ public resolveLatest(useMostRecent: boolean, key: string | undefined, throwOnNotFound?: boolean): string { const resolvedKey = useMostRecent ? this.getLatestKey() : key; if (!resolvedKey) throw cacheMessages.createError('error.NoRecentJobId'); @@ -61,7 +66,7 @@ export class DeployCache extends TTLConfig { throw cacheMessages.createError('error.NoMatchingJobId', [resolvedKey]); } - return resolvedKey; + return throwOnNotFound ? ensure18(resolvedKey, this) : resolvedKey; } /** @@ -70,17 +75,7 @@ export class DeployCache extends TTLConfig { * returns 15-char ID if it matches a key in the cache, otherwise throws */ public resolveLongId(jobId: string): string { - if (jobId.length === 18) { - return jobId; - } else if (jobId.length === 15) { - const match = this.keys().find((k) => k.startsWith(jobId)); - if (match) { - return match; - } - throw cacheMessages.createError('error.NoMatchingJobId', [jobId]); - } else { - throw cacheMessages.createError('error.InvalidJobId', [jobId]); - } + return ensure18(jobId, this); } /** @@ -107,3 +102,21 @@ export class DeployCache extends TTLConfig { throw cacheMessages.createError('error.InvalidJobId', [jobId]); } } + +/** + * if the jobId is 15 characters, use the cache to convert to 18 + * will throw if the value is not in the cache + */ +const ensure18 = (jobId: string, cache: DeployCache): string => { + if (jobId.length === 18) { + return jobId; + } else if (jobId.length === 15) { + const match = cache.keys().find((k) => k.startsWith(jobId)); + if (match) { + return match; + } + throw cacheMessages.createError('error.NoMatchingJobId', [jobId]); + } else { + throw cacheMessages.createError('error.InvalidJobId', [jobId]); + } +};