Skip to content

Commit

Permalink
fix: handle 15-char not in cache but with org
Browse files Browse the repository at this point in the history
  • Loading branch information
mshanemc committed Jan 29, 2024
1 parent 9faee74 commit 81151b5
Show file tree
Hide file tree
Showing 4 changed files with 188 additions and 59 deletions.
6 changes: 3 additions & 3 deletions src/commands/project/deploy/quick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ export default class DeployMetadataQuick extends SfCommand<DeployResultJson> {
/** 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
Expand All @@ -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<Org> => {
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) {
Expand Down
14 changes: 7 additions & 7 deletions src/commands/project/deploy/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -75,13 +73,15 @@ export default class DeployMetadataReport extends SfCommand<DeployResultJson> {
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 {
Expand All @@ -102,7 +102,7 @@ export default class DeployMetadataReport extends SfCommand<DeployResultJson> {
id: jobId,
components: componentSet,
apiOptions: {
rest: deployOpts.api === API['REST'],
rest: deployOpts?.api === API['REST'],
},
});

Expand Down
39 changes: 32 additions & 7 deletions src/utils/deployCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,24 @@ export class DeployCache extends TTLConfig<TTLConfig.Options, CachedOptions> {
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;
Expand All @@ -78,7 +83,27 @@ export class DeployCache extends TTLConfig<TTLConfig.Options, CachedOptions> {
}
}

/**
*
* @deprecated. Use maybeGet because the typings are wrong in sfdx-core
*/
public get(jobId: string): TTLConfig.Entry<CachedOptions> {
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<CachedOptions> | 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]);
}
}
188 changes: 146 additions & 42 deletions test/commands/deploy/metadata/quick.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('deploy:metadata', {
args: '--source-dir force-app',
json: true,
exitCode: 0,
});
assert(first);
const deploy = await testkit.execute<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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<DeployResultJson>('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');
});
});
});

0 comments on commit 81151b5

Please sign in to comment.