Skip to content

Commit 2c7f74e

Browse files
authored
Merge pull request #758 from salesforcecli/sh/nomadic-deploy
fix: adds target-org and wait support to deploy report command
2 parents efeaf6d + e9ca2fe commit 2c7f74e

File tree

8 files changed

+170
-61
lines changed

8 files changed

+170
-61
lines changed

command-snapshot.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,9 @@
101101
{
102102
"command": "project:deploy:report",
103103
"plugin": "@salesforce/plugin-deploy-retrieve",
104-
"flags": ["coverage-formatters", "job-id", "json", "junit", "results-dir", "use-most-recent"],
104+
"flags": ["coverage-formatters", "job-id", "json", "junit", "results-dir", "target-org", "use-most-recent", "wait"],
105105
"alias": ["deploy:metadata:report"],
106-
"flagChars": ["i", "r"],
106+
"flagChars": ["i", "o", "r", "w"],
107107
"flagAliases": []
108108
},
109109
{

messages/deploy.metadata.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,15 @@ No local changes to deploy.
219219

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

222+
# error.InvalidDeployId
223+
224+
Invalid deploy ID: %s for org: %s
225+
226+
# error.InvalidDeployId.actions
227+
228+
- Ensure the deploy ID is correct.
229+
- Ensure the target-org username or alias is correct.
230+
222231
# flags.junit.summary
223232

224233
Output JUnit test results.

messages/deploy.metadata.quick.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ Overrides your default org.
7878

7979
# error.CannotQuickDeploy
8080

81-
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.
81+
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.
8282

8383
# error.QuickDeployFailure
8484

src/commands/project/deploy/report.ts

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import { Messages, Org } from '@salesforce/core';
9-
import { Duration } from '@salesforce/kit';
8+
import { Messages, Org, SfProject } from '@salesforce/core';
109
import { SfCommand, Flags } from '@salesforce/sf-plugins-core';
11-
import { DeployResult, MetadataApiDeployStatus } from '@salesforce/source-deploy-retrieve';
10+
import { ComponentSet, DeployResult, MetadataApiDeploy } from '@salesforce/source-deploy-retrieve';
1211
import { buildComponentSet } from '../../../utils/deploy';
12+
import { DeployProgress } from '../../../utils/progressBar';
1313
import { DeployCache } from '../../../utils/deployCache';
1414
import { DeployReportResultFormatter } from '../../../formatters/deployReportResultFormatter';
1515
import { DeployResultJson } from '../../../utils/types';
1616
import { coverageFormattersFlag } from '../../../utils/flags';
1717

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

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

2930
public static readonly flags = {
31+
'target-org': Flags.optionalOrg({
32+
char: 'o',
33+
description: deployMessages.getMessage('flags.target-org.description'),
34+
summary: deployMessages.getMessage('flags.target-org.summary'),
35+
}),
3036
'job-id': Flags.salesforceId({
3137
char: 'i',
3238
startsWith: '0Af',
@@ -51,23 +57,84 @@ export default class DeployMetadataReport extends SfCommand<DeployResultJson> {
5157
summary: messages.getMessage('flags.results-dir.summary'),
5258
helpGroup: testFlags,
5359
}),
60+
// we want to allow undefined for a simple check deploy status
61+
// eslint-disable-next-line sf-plugin/flag-min-max-default
62+
wait: Flags.duration({
63+
char: 'w',
64+
summary: deployMessages.getMessage('flags.wait.summary'),
65+
description: deployMessages.getMessage('flags.wait.description'),
66+
unit: 'minutes',
67+
helpValue: '<minutes>',
68+
min: 1,
69+
}),
5470
};
5571

5672
public async run(): Promise<DeployResultJson> {
5773
const [{ flags }, cache] = await Promise.all([this.parse(DeployMetadataReport), DeployCache.create()]);
58-
const jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id']);
74+
const jobId = cache.resolveLatest(flags['use-most-recent'], flags['job-id'], false);
75+
76+
const deployOpts = cache.get(jobId) ?? {};
77+
const wait = flags['wait'];
78+
const org = flags['target-org'] ?? (await Org.create({ aliasOrUsername: deployOpts['target-org'] }));
5979

60-
const deployOpts = cache.get(jobId);
61-
const org = await Org.create({ aliasOrUsername: deployOpts['target-org'] });
62-
const [deployStatus, componentSet] = await Promise.all([
63-
// we'll use whatever the org supports since we can't specify the org
80+
// if we're using mdapi we won't have a component set
81+
let componentSet = new ComponentSet();
82+
if (!deployOpts.isMdapi) {
83+
if (!cache.get(jobId)) {
84+
// If the cache file isn't there, use the project package directories for the CompSet
85+
try {
86+
this.project = await SfProject.resolve();
87+
const sourcepath = this.project.getUniquePackageDirectories().map((pDir) => pDir.fullPath);
88+
componentSet = await buildComponentSet({ 'source-dir': sourcepath, wait });
89+
} catch (err) {
90+
// ignore the error. this was just to get improved command output.
91+
}
92+
} else {
93+
componentSet = await buildComponentSet({ ...deployOpts, wait });
94+
}
95+
}
96+
const mdapiDeploy = new MetadataApiDeploy({
97+
// setting an API version here won't matter since we're just checking deploy status
6498
// eslint-disable-next-line sf-plugin/get-connection-with-version
65-
org.getConnection().metadata.checkDeployStatus(jobId, true),
66-
// if we're using mdapi, we won't have a component set
67-
deployOpts.isMdapi ? undefined : buildComponentSet({ ...deployOpts, wait: Duration.minutes(deployOpts.wait) }),
68-
]);
99+
usernameOrConnection: org.getConnection(),
100+
id: jobId,
101+
components: componentSet,
102+
apiOptions: {
103+
rest: deployOpts.api === 'REST',
104+
},
105+
});
106+
107+
const getDeployResult = async (): Promise<DeployResult> => {
108+
try {
109+
const deployStatus = await mdapiDeploy.checkStatus();
110+
return new DeployResult(deployStatus, componentSet);
111+
} catch (error) {
112+
if (error instanceof Error && error.name === 'sf:INVALID_CROSS_REFERENCE_KEY') {
113+
throw deployMessages.createError('error.InvalidDeployId', [jobId, org.getUsername()]);
114+
}
115+
throw error;
116+
}
117+
};
69118

70-
const result = new DeployResult(deployStatus as MetadataApiDeployStatus, componentSet);
119+
let result: DeployResult;
120+
if (wait) {
121+
// poll for deploy results
122+
try {
123+
new DeployProgress(mdapiDeploy, this.jsonEnabled()).start();
124+
result = await mdapiDeploy.pollStatus(500, wait.seconds);
125+
} catch (error) {
126+
if (error instanceof Error && error.message.includes('The client has timed out')) {
127+
this.debug('[project deploy report] polling timed out. Requesting status...');
128+
} else {
129+
throw error;
130+
}
131+
} finally {
132+
result = await getDeployResult();
133+
}
134+
} else {
135+
// check the deploy status
136+
result = await getDeployResult();
137+
}
71138

72139
const formatter = new DeployReportResultFormatter(result, {
73140
...deployOpts,

src/formatters/deployReportResultFormatter.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { ux } from '@oclif/core';
88
import { RequestStatus } from '@salesforce/source-deploy-retrieve';
99
import { StandardColors } from '@salesforce/sf-plugins-core';
10+
import { Duration } from '@salesforce/kit';
1011
import { tableHeader } from '../utils/output';
1112
import { DeployResultFormatter } from './deployResultFormatter';
1213

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

3435
const opts = Object.entries(this.flags).reduce<Array<{ key: string; value: unknown }>>((result, [key, value]) => {
35-
if (key === 'timestamp') return result;
36-
if (key === 'target-org')
36+
if (key === 'timestamp') {
37+
return result;
38+
}
39+
if (key === 'target-org') {
3740
return result.concat({ key: 'target-org', value: this.flags['target-org']?.getUsername() });
41+
}
42+
if (key === 'wait' && this.flags['wait']) {
43+
const wait = this.flags['wait'] instanceof Duration ? this.flags['wait'].quantity : this.flags['wait'];
44+
return result.concat({ key: 'wait', value: `${wait} minutes` });
45+
}
3846
return result.concat({ key, value });
3947
}, []);
4048
ux.log();

src/formatters/deployResultFormatter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import * as fs from 'fs';
1010
import { ux } from '@oclif/core';
1111
import { DeployResult, FileResponse, FileResponseFailure, RequestStatus } from '@salesforce/source-deploy-retrieve';
1212
import { Org, SfError, Lifecycle } from '@salesforce/core';
13-
import { ensureArray } from '@salesforce/kit';
13+
import { Duration, ensureArray } from '@salesforce/kit';
1414
import {
1515
CodeCoverageResult,
1616
CoverageReporter,
@@ -46,6 +46,7 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma
4646
junit: boolean;
4747
'results-dir': string;
4848
'target-org': Org;
49+
wait: Duration | number;
4950
}>
5051
) {
5152
super(result, flags);

test/commands/deploy/metadata/report-mdapi.nut.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,26 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

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

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

1520
before(async () => {
1621
testkit = await SourceTestkit.create({
1722
repository: 'https://github.com/salesforcecli/sample-project-multiple-packages.git',
1823
nut: __filename,
24+
scratchOrgs: [{ duration: 1, alias: orgAlias, config: join('config', 'project-scratch-def.json') }],
1925
});
2026
await testkit.convert({
21-
args: '--source-dir force-app --output-dir mdapiOut',
27+
args: `--source-dir force-app --output-dir ${mdSourceDir}`,
2228
json: true,
2329
exitCode: 0,
2430
});
@@ -31,7 +37,7 @@ describe('deploy metadata report NUTs with source-dir', () => {
3137
describe('--use-most-recent', () => {
3238
it('should report most recently started deployment', async () => {
3339
await testkit.execute<DeployResultJson>('project deploy start', {
34-
args: '--metadata-dir mdapiOut --async',
40+
args: `--metadata-dir ${mdSourceDir} --async`,
3541
json: true,
3642
exitCode: 0,
3743
});
@@ -42,40 +48,49 @@ describe('deploy metadata report NUTs with source-dir', () => {
4248
exitCode: 0,
4349
});
4450
assert(deploy?.result);
45-
expect(deploy.result.success).to.equal(true);
51+
expect([RequestStatus.Pending, RequestStatus.Succeeded, RequestStatus.InProgress]).includes(deploy.result.status);
4652
});
53+
});
4754

48-
it.skip('should report most recently started deployment without specifying the flag', async () => {
49-
await testkit.execute<DeployResultJson>('project deploy start', {
50-
args: '--metadata-dir mdapiOut --async',
55+
describe('--job-id', () => {
56+
it('should report the provided job id', async () => {
57+
const first = await testkit.execute<DeployResultJson>('project deploy start', {
58+
args: `--metadata-dir ${mdSourceDir} --async`,
5159
json: true,
5260
exitCode: 0,
5361
});
54-
5562
const deploy = await testkit.execute<DeployResultJson>('project deploy report', {
63+
args: `--job-id ${first?.result.id}`,
5664
json: true,
5765
exitCode: 0,
5866
});
5967
assert(deploy?.result);
60-
expect(deploy.result.success).to.equal(true);
68+
expect([RequestStatus.Pending, RequestStatus.Succeeded, RequestStatus.InProgress]).includes(deploy.result.status);
69+
expect(deploy.result.id).to.equal(first?.result.id);
6170
});
62-
});
6371

64-
describe('--job-id', () => {
65-
it('should report the provided job id', async () => {
72+
it('should report from specified target-org and job-id without deploy cache', async () => {
6673
const first = await testkit.execute<DeployResultJson>('project deploy start', {
67-
args: '--metadata-dir mdapiOut --async',
74+
args: `--metadata-dir ${mdSourceDir} --async --target-org ${orgAlias}`,
6875
json: true,
6976
exitCode: 0,
7077
});
78+
79+
// delete the cache file so we can verify that reporting just with job-id and org works
80+
const deployCacheFilePath = resolve(testkit.projectDir, join('..', '.sf', 'deploy-cache.json'));
81+
unlinkSync(deployCacheFilePath);
82+
assert(!existsSync(deployCacheFilePath));
83+
7184
const deploy = await testkit.execute<DeployResultJson>('project deploy report', {
72-
args: `--job-id ${first?.result.id}`,
85+
args: `--job-id ${first?.result.id} --target-org ${orgAlias} --wait 9`,
7386
json: true,
7487
exitCode: 0,
7588
});
7689
assert(deploy?.result);
7790
expect(deploy.result.success).to.equal(true);
91+
expect(deploy.result.status).to.equal(RequestStatus.Succeeded);
7892
expect(deploy.result.id).to.equal(first?.result.id);
93+
await testkit.expect.filesToBeDeployed(['force-app/**/*'], ['force-app/test/**/*']);
7994
});
8095
});
8196
});

0 commit comments

Comments
 (0)