Skip to content

Commit

Permalink
Merge pull request #758 from salesforcecli/sh/nomadic-deploy
Browse files Browse the repository at this point in the history
fix: adds target-org and wait support to deploy report command
  • Loading branch information
shetzel authored Sep 27, 2023
2 parents efeaf6d + e9ca2fe commit 2c7f74e
Show file tree
Hide file tree
Showing 8 changed files with 170 additions and 61 deletions.
4 changes: 2 additions & 2 deletions command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,9 @@
{
"command": "project:deploy:report",
"plugin": "@salesforce/plugin-deploy-retrieve",
"flags": ["coverage-formatters", "job-id", "json", "junit", "results-dir", "use-most-recent"],
"flags": ["coverage-formatters", "job-id", "json", "junit", "results-dir", "target-org", "use-most-recent", "wait"],
"alias": ["deploy:metadata:report"],
"flagChars": ["i", "r"],
"flagChars": ["i", "o", "r", "w"],
"flagAliases": []
},
{
Expand Down
9 changes: 9 additions & 0 deletions messages/deploy.metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,15 @@ No local changes to deploy.

- To see conflicts and ignored files, run "%s project deploy preview" with any of the manifest, directory, or metadata flags.

# error.InvalidDeployId

Invalid deploy ID: %s for org: %s

# error.InvalidDeployId.actions

- Ensure the deploy ID is correct.
- Ensure the target-org username or alias is correct.

# flags.junit.summary

Output JUnit test results.
Expand Down
2 changes: 1 addition & 1 deletion messages/deploy.metadata.quick.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Overrides your default org.

# error.CannotQuickDeploy

Job ID can't be used for quick deployment. Possible reasons include the deployment hasn't been validated or the validation expired because you ran it more than 10 days ago.
Job ID can't be used for quick deployment. Possible reasons include the deployment hasn't been validated, has already been deployed, or the validation expired because you ran it more than 10 days ago.

# error.QuickDeployFailure

Expand Down
93 changes: 80 additions & 13 deletions src/commands/project/deploy/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { Messages, Org } from '@salesforce/core';
import { Duration } from '@salesforce/kit';
import { Messages, Org, SfProject } from '@salesforce/core';
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
import { DeployResult, MetadataApiDeployStatus } from '@salesforce/source-deploy-retrieve';
import { ComponentSet, DeployResult, MetadataApiDeploy } from '@salesforce/source-deploy-retrieve';
import { buildComponentSet } from '../../../utils/deploy';
import { DeployProgress } from '../../../utils/progressBar';
import { DeployCache } from '../../../utils/deployCache';
import { DeployReportResultFormatter } from '../../../formatters/deployReportResultFormatter';
import { DeployResultJson } from '../../../utils/types';
import { coverageFormattersFlag } from '../../../utils/flags';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'deploy.metadata.report');
const deployMessages = Messages.loadMessages('@salesforce/plugin-deploy-retrieve', 'deploy.metadata');
const testFlags = 'Test';

export default class DeployMetadataReport extends SfCommand<DeployResultJson> {
Expand All @@ -27,6 +28,11 @@ export default class DeployMetadataReport extends SfCommand<DeployResultJson> {
public static readonly deprecateAliases = true;

public static readonly flags = {
'target-org': Flags.optionalOrg({
char: 'o',
description: deployMessages.getMessage('flags.target-org.description'),
summary: deployMessages.getMessage('flags.target-org.summary'),
}),
'job-id': Flags.salesforceId({
char: 'i',
startsWith: '0Af',
Expand All @@ -51,23 +57,84 @@ export default class DeployMetadataReport extends SfCommand<DeployResultJson> {
summary: messages.getMessage('flags.results-dir.summary'),
helpGroup: testFlags,
}),
// we want to allow undefined for a simple check deploy status
// eslint-disable-next-line sf-plugin/flag-min-max-default
wait: Flags.duration({
char: 'w',
summary: deployMessages.getMessage('flags.wait.summary'),
description: deployMessages.getMessage('flags.wait.description'),
unit: 'minutes',
helpValue: '<minutes>',
min: 1,
}),
};

public async run(): Promise<DeployResultJson> {
const [{ flags }, cache] = await Promise.all([this.parse(DeployMetadataReport), DeployCache.create()]);
const jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id']);
const jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id'], false);

const deployOpts = cache.get(jobId) ?? {};
const wait = flags['wait'];
const org = flags['target-org'] ?? (await Org.create({ aliasOrUsername: deployOpts['target-org'] }));

const deployOpts = cache.get(jobId);
const org = await Org.create({ aliasOrUsername: deployOpts['target-org'] });
const [deployStatus, componentSet] = await Promise.all([
// we'll use whatever the org supports since we can't specify the 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 the cache file isn't there, use the project package directories for the CompSet
try {
this.project = await SfProject.resolve();
const sourcepath = this.project.getUniquePackageDirectories().map((pDir) => pDir.fullPath);
componentSet = await buildComponentSet({ 'source-dir': sourcepath, wait });
} catch (err) {
// ignore the error. this was just to get improved command output.
}
} else {
componentSet = await buildComponentSet({ ...deployOpts, wait });
}
}
const mdapiDeploy = new MetadataApiDeploy({
// setting an API version here won't matter since we're just checking deploy status
// eslint-disable-next-line sf-plugin/get-connection-with-version
org.getConnection().metadata.checkDeployStatus(jobId, true),
// if we're using mdapi, we won't have a component set
deployOpts.isMdapi ? undefined : buildComponentSet({ ...deployOpts, wait: Duration.minutes(deployOpts.wait) }),
]);
usernameOrConnection: org.getConnection(),
id: jobId,
components: componentSet,
apiOptions: {
rest: deployOpts.api === 'REST',
},
});

const getDeployResult = async (): Promise<DeployResult> => {
try {
const deployStatus = await mdapiDeploy.checkStatus();
return new DeployResult(deployStatus, componentSet);
} catch (error) {
if (error instanceof Error && error.name === 'sf:INVALID_CROSS_REFERENCE_KEY') {
throw deployMessages.createError('error.InvalidDeployId', [jobId, org.getUsername()]);
}
throw error;
}
};

const result = new DeployResult(deployStatus as MetadataApiDeployStatus, componentSet);
let result: DeployResult;
if (wait) {
// poll for deploy results
try {
new DeployProgress(mdapiDeploy, this.jsonEnabled()).start();
result = await mdapiDeploy.pollStatus(500, wait.seconds);
} catch (error) {
if (error instanceof Error && error.message.includes('The client has timed out')) {
this.debug('[project deploy report] polling timed out. Requesting status...');
} else {
throw error;
}
} finally {
result = await getDeployResult();
}
} else {
// check the deploy status
result = await getDeployResult();
}

const formatter = new DeployReportResultFormatter(result, {
...deployOpts,
Expand Down
12 changes: 10 additions & 2 deletions src/formatters/deployReportResultFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { ux } from '@oclif/core';
import { RequestStatus } from '@salesforce/source-deploy-retrieve';
import { StandardColors } from '@salesforce/sf-plugins-core';
import { Duration } from '@salesforce/kit';
import { tableHeader } from '../utils/output';
import { DeployResultFormatter } from './deployResultFormatter';

Expand All @@ -32,9 +33,16 @@ export class DeployReportResultFormatter extends DeployResultFormatter {
ux.table(response, { key: {}, value: {} }, { title: tableHeader('Deploy Info'), 'no-truncate': true });

const opts = Object.entries(this.flags).reduce<Array<{ key: string; value: unknown }>>((result, [key, value]) => {
if (key === 'timestamp') return result;
if (key === 'target-org')
if (key === 'timestamp') {
return result;
}
if (key === 'target-org') {
return result.concat({ key: 'target-org', value: this.flags['target-org']?.getUsername() });
}
if (key === 'wait' && this.flags['wait']) {
const wait = this.flags['wait'] instanceof Duration ? this.flags['wait'].quantity : this.flags['wait'];
return result.concat({ key: 'wait', value: `${wait} minutes` });
}
return result.concat({ key, value });
}, []);
ux.log();
Expand Down
3 changes: 2 additions & 1 deletion src/formatters/deployResultFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import * as fs from 'fs';
import { ux } from '@oclif/core';
import { DeployResult, FileResponse, FileResponseFailure, RequestStatus } from '@salesforce/source-deploy-retrieve';
import { Org, SfError, Lifecycle } from '@salesforce/core';
import { ensureArray } from '@salesforce/kit';
import { Duration, ensureArray } from '@salesforce/kit';
import {
CodeCoverageResult,
CoverageReporter,
Expand Down Expand Up @@ -46,6 +46,7 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma
junit: boolean;
'results-dir': string;
'target-org': Org;
wait: Duration | number;
}>
) {
super(result, flags);
Expand Down
43 changes: 29 additions & 14 deletions test/commands/deploy/metadata/report-mdapi.nut.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { unlinkSync, existsSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { SourceTestkit } from '@salesforce/source-testkit';
import { assert, expect } from 'chai';
import { RequestStatus } from '@salesforce/source-deploy-retrieve';
import { DeployResultJson } from '../../../../src/utils/types';

describe('deploy metadata report NUTs with source-dir', () => {
describe('[project deploy report] NUTs with metadata-dir', () => {
let testkit: SourceTestkit;
const mdSourceDir = 'mdapiOut';
const orgAlias = 'reportMdTestOrg2';

before(async () => {
testkit = await SourceTestkit.create({
repository: 'https://github.com/salesforcecli/sample-project-multiple-packages.git',
nut: __filename,
scratchOrgs: [{ duration: 1, alias: orgAlias, config: join('config', 'project-scratch-def.json') }],
});
await testkit.convert({
args: '--source-dir force-app --output-dir mdapiOut',
args: `--source-dir force-app --output-dir ${mdSourceDir}`,
json: true,
exitCode: 0,
});
Expand All @@ -31,7 +37,7 @@ describe('deploy metadata report NUTs with source-dir', () => {
describe('--use-most-recent', () => {
it('should report most recently started deployment', async () => {
await testkit.execute<DeployResultJson>('project deploy start', {
args: '--metadata-dir mdapiOut --async',
args: `--metadata-dir ${mdSourceDir} --async`,
json: true,
exitCode: 0,
});
Expand All @@ -42,40 +48,49 @@ describe('deploy metadata report NUTs with source-dir', () => {
exitCode: 0,
});
assert(deploy?.result);
expect(deploy.result.success).to.equal(true);
expect([RequestStatus.Pending, RequestStatus.Succeeded, RequestStatus.InProgress]).includes(deploy.result.status);
});
});

it.skip('should report most recently started deployment without specifying the flag', async () => {
await testkit.execute<DeployResultJson>('project deploy start', {
args: '--metadata-dir mdapiOut --async',
describe('--job-id', () => {
it('should report the provided job id', async () => {
const first = await testkit.execute<DeployResultJson>('project deploy start', {
args: `--metadata-dir ${mdSourceDir} --async`,
json: true,
exitCode: 0,
});

const deploy = await testkit.execute<DeployResultJson>('project deploy report', {
args: `--job-id ${first?.result.id}`,
json: true,
exitCode: 0,
});
assert(deploy?.result);
expect(deploy.result.success).to.equal(true);
expect([RequestStatus.Pending, RequestStatus.Succeeded, RequestStatus.InProgress]).includes(deploy.result.status);
expect(deploy.result.id).to.equal(first?.result.id);
});
});

describe('--job-id', () => {
it('should report the provided job id', async () => {
it('should report from specified target-org and job-id without deploy cache', async () => {
const first = await testkit.execute<DeployResultJson>('project deploy start', {
args: '--metadata-dir mdapiOut --async',
args: `--metadata-dir ${mdSourceDir} --async --target-org ${orgAlias}`,
json: true,
exitCode: 0,
});

// delete the cache file so we can verify that reporting just with job-id and org works
const deployCacheFilePath = resolve(testkit.projectDir, join('..', '.sf', 'deploy-cache.json'));
unlinkSync(deployCacheFilePath);
assert(!existsSync(deployCacheFilePath));

const deploy = await testkit.execute<DeployResultJson>('project deploy report', {
args: `--job-id ${first?.result.id}`,
args: `--job-id ${first?.result.id} --target-org ${orgAlias} --wait 9`,
json: true,
exitCode: 0,
});
assert(deploy?.result);
expect(deploy.result.success).to.equal(true);
expect(deploy.result.status).to.equal(RequestStatus.Succeeded);
expect(deploy.result.id).to.equal(first?.result.id);
await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']);
});
});
});
Loading

0 comments on commit 2c7f74e

Please sign in to comment.