Skip to content

Commit

Permalink
Merge pull request #884 from salesforcecli/sh/support-15-char-quick-d…
Browse files Browse the repository at this point in the history
…eploy

fix: don't throw when 15 char job ID not in cache
  • Loading branch information
shetzel authored Jan 30, 2024
2 parents a44b3a7 + 380830e commit 2570ec4
Show file tree
Hide file tree
Showing 9 changed files with 259 additions and 117 deletions.
4 changes: 4 additions & 0 deletions messages/cache.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions messages/deploy.metadata.quick.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
4 changes: 2 additions & 2 deletions src/commands/project/deploy/cancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default class DeployMetadataCancel extends SfCommand<DeployResultJson> {
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 &&
Expand All @@ -80,7 +80,7 @@ export default class DeployMetadataCancel extends SfCommand<DeployResultJson> {
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();
Expand Down
35 changes: 29 additions & 6 deletions src/commands/project/deploy/quick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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 { 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';
Expand Down Expand Up @@ -84,12 +84,10 @@ export default class DeployMetadataQuick extends SfCommand<DeployResultJson> {
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);

const deployOpts = cache.get(jobId) ?? ({} as DeployOptions);
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({
Expand Down Expand Up @@ -145,3 +143,28 @@ export default class DeployMetadataQuick extends SfCommand<DeployResultJson> {
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, true);
} catch (e) {
if (e instanceof Error && e.name === 'NoMatchingJobIdError' && jobIdFlag) {
return jobIdFlag; // Use the specified 15 char job ID
}
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<Org> => {
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) {
throw messages.createError('error.NoTargetOrg');
}

return targetOrg;
};
16 changes: 8 additions & 8 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,14 +73,16 @@ 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 (!cache.get(jobId)) {
if (!deployOpts?.isMdapi) {
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();
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
11 changes: 5 additions & 6 deletions src/commands/project/deploy/resume.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 chalk from 'chalk';
import { EnvironmentVariable, Messages, Org, SfError } from '@salesforce/core';
import { SfCommand, toHelpSection, Flags } from '@salesforce/sf-plugins-core';
Expand All @@ -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';
Expand Down Expand Up @@ -83,10 +81,10 @@ export default class DeployMetadataResume extends SfCommand<DeployResultJson> {

public async run(): Promise<DeployResultJson> {
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;

Expand All @@ -108,7 +106,7 @@ export default class DeployMetadataResume extends SfCommand<DeployResultJson> {
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
{
Expand All @@ -134,6 +132,7 @@ export default class DeployMetadataResume extends SfCommand<DeployResultJson> {
if (!deploy.id) {
throw new SfError('The deploy id is not available.');
}

cache.update(deploy.id, { status: result.response.status });
await cache.write();
}
Expand Down
78 changes: 58 additions & 20 deletions src/utils/deployCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,44 +41,82 @@ export class DeployCache extends TTLConfig<TTLConfig.Options, CachedOptions> {

public static async unset(key: string): Promise<void> {
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<void> {
const cache = await DeployCache.create();
cache.update(key, obj);
cache.update(ensure18(key, cache), obj);
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 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');

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 throwOnNotFound ? ensure18(resolvedKey, this) : 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;
} else if (jobId.length === 15) {
const match = this.keys().find((k) => k.startsWith(jobId));
if (match) {
return match;
}
throw cacheMessages.createError('error.InvalidJobId', [jobId]);
} else {
throw cacheMessages.createError('error.InvalidJobId', [jobId]);
}
return ensure18(jobId, this);
}

/**
*
* @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]);
}
}

/**
* 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]);
}
};
Loading

0 comments on commit 2570ec4

Please sign in to comment.