diff --git a/messages/deploy.metadata.md b/messages/deploy.metadata.md index 8a15113f..40506839 100644 --- a/messages/deploy.metadata.md +++ b/messages/deploy.metadata.md @@ -78,7 +78,7 @@ Overrides your default org. # flags.metadata.summary -Metadata component names to deploy. Wildcards ( * ) supported as long as you use quotes, such as 'ApexClass:MyClass*' +Metadata component names to deploy. Wildcards ( _ ) supported as long as you use quotes, such as 'ApexClass:MyClass_' # flags.test-level.summary @@ -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. diff --git a/src/commands/project/deploy/report.ts b/src/commands/project/deploy/report.ts index 13e94079..043a5fda 100644 --- a/src/commands/project/deploy/report.ts +++ b/src/commands/project/deploy/report.ts @@ -8,7 +8,7 @@ import { Messages, Org, SfProject } from '@salesforce/core'; import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; import { ComponentSet, DeployResult, MetadataApiDeploy } from '@salesforce/source-deploy-retrieve'; -import { buildComponentSet, DeployOptions } from '../../../utils/deploy'; +import { buildComponentSet } from '../../../utils/deploy'; import { DeployProgress } from '../../../utils/progressBar'; import { DeployCache } from '../../../utils/deployCache'; import { DeployReportResultFormatter } from '../../../formatters/deployReportResultFormatter'; @@ -73,7 +73,7 @@ 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) ?? ({} as DeployOptions & { isMdapi: boolean }); + const deployOpts = cache.get(jobId) ?? {}; const waitDuration = flags['wait']; const org = flags['target-org'] ?? (await Org.create({ aliasOrUsername: deployOpts['target-org'] })); @@ -105,8 +105,15 @@ export default class DeployMetadataReport extends SfCommand { }); const getDeployResult = async (): Promise => { - const deployStatus = await mdapiDeploy.checkStatus(); - return new DeployResult(deployStatus, componentSet); + 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; + } }; let result: DeployResult; diff --git a/src/formatters/deployReportResultFormatter.ts b/src/formatters/deployReportResultFormatter.ts index 421db1d0..2d338923 100644 --- a/src/formatters/deployReportResultFormatter.ts +++ b/src/formatters/deployReportResultFormatter.ts @@ -32,9 +32,15 @@ export class DeployReportResultFormatter extends DeployResultFormatter { ux.table(response, { key: {}, value: {} }, { title: tableHeader('Deploy Info'), 'no-truncate': true }); const opts = Object.entries(this.flags).reduce>((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') { + return result.concat({ key: 'wait', value: `${this.flags['wait']?.quantity} minutes` }); + } return result.concat({ key, value }); }, []); ux.log(); diff --git a/src/formatters/deployResultFormatter.ts b/src/formatters/deployResultFormatter.ts index 7abe6a3a..4393544f 100644 --- a/src/formatters/deployResultFormatter.ts +++ b/src/formatters/deployResultFormatter.ts @@ -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, @@ -45,6 +45,7 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma junit: boolean; 'results-dir': string; 'target-org': Org; + wait: Duration; }> ) { super(result, flags); diff --git a/test/commands/deploy/metadata/report-mdapi.nut.ts b/test/commands/deploy/metadata/report-mdapi.nut.ts index e47af141..8c0cbfff 100644 --- a/test/commands/deploy/metadata/report-mdapi.nut.ts +++ b/test/commands/deploy/metadata/report-mdapi.nut.ts @@ -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, }); @@ -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('project deploy start', { - args: '--metadata-dir mdapiOut --async', + args: `--metadata-dir ${mdSourceDir} --async`, json: true, exitCode: 0, }); @@ -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('project deploy start', { - args: '--metadata-dir mdapiOut --async', + describe('--job-id', () => { + it('should report the provided job id', async () => { + const first = await testkit.execute('project deploy start', { + args: `--metadata-dir ${mdSourceDir} --async`, json: true, exitCode: 0, }); - const deploy = await testkit.execute('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('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('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/**/*']); }); }); }); diff --git a/test/commands/deploy/metadata/report.nut.ts b/test/commands/deploy/metadata/report.nut.ts index 0072c006..f21f11a0 100644 --- a/test/commands/deploy/metadata/report.nut.ts +++ b/test/commands/deploy/metadata/report.nut.ts @@ -5,8 +5,8 @@ * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import * as fs from 'fs'; -import * as path from 'path'; +import { unlinkSync, existsSync } from 'node:fs'; +import { join, resolve } from 'node:path'; import { SourceTestkit } from '@salesforce/source-testkit'; import { assert, isObject } from '@salesforce/ts-types'; import { expect } from 'chai'; @@ -21,7 +21,7 @@ describe('[project deploy report] NUTs with source-dir', () => { testkit = await SourceTestkit.create({ repository: 'https://github.com/salesforcecli/sample-project-multiple-packages.git', nut: __filename, - scratchOrgs: [{ duration: 1, alias: orgAlias, config: path.join('config', 'project-scratch-def.json') }], + scratchOrgs: [{ duration: 1, alias: orgAlias, config: join('config', 'project-scratch-def.json') }], }); }); @@ -71,9 +71,9 @@ describe('[project deploy report] NUTs with source-dir', () => { }); // delete the cache file so we can verify that reporting just with job-id and org works - const deployCacheFilePath = path.resolve(testkit.projectDir, path.join('..', '.sf', 'deploy-cache.json')); - fs.unlinkSync(deployCacheFilePath); - assert(!fs.existsSync(deployCacheFilePath)); + const deployCacheFilePath = resolve(testkit.projectDir, join('..', '.sf', 'deploy-cache.json')); + unlinkSync(deployCacheFilePath); + assert(!existsSync(deployCacheFilePath)); const deploy = await testkit.execute('project deploy report', { args: `--job-id ${first?.result.id} --target-org ${orgAlias} --wait 9`, @@ -97,13 +97,13 @@ describe('[project deploy report] NUTs with source-dir', () => { json: true, exitCode: 0, }); - expect(fs.existsSync(path.join(testkit.projectDir, 'test-output-override'))).to.be.true; - expect(fs.existsSync(path.join(testkit.projectDir, 'test-output-override', 'coverage'))).to.be.true; - expect(fs.existsSync(path.join(testkit.projectDir, 'test-output-override', 'coverage', 'html'))).to.be.true; - expect(fs.existsSync(path.join(testkit.projectDir, 'test-output-override', 'coverage', 'text.txt'))).to.be.true; - expect(fs.existsSync(path.join(testkit.projectDir, 'test-output-override', 'junit'))).to.be.true; - expect(fs.existsSync(path.join(testkit.projectDir, 'test-output-override', 'junit', 'junit.xml'))).to.be.true; - expect(fs.existsSync(path.join(testkit.projectDir, 'test-output'))).to.be.false; + expect(existsSync(join(testkit.projectDir, 'test-output-override'))).to.be.true; + expect(existsSync(join(testkit.projectDir, 'test-output-override', 'coverage'))).to.be.true; + expect(existsSync(join(testkit.projectDir, 'test-output-override', 'coverage', 'html'))).to.be.true; + expect(existsSync(join(testkit.projectDir, 'test-output-override', 'coverage', 'text.txt'))).to.be.true; + expect(existsSync(join(testkit.projectDir, 'test-output-override', 'junit'))).to.be.true; + expect(existsSync(join(testkit.projectDir, 'test-output-override', 'junit', 'junit.xml'))).to.be.true; + expect(existsSync(join(testkit.projectDir, 'test-output'))).to.be.false; assert(isObject(deploy)); await testkit.expect.filesToBeDeployedViaResult(['force-app/**/*'], ['force-app/test/**/*'], deploy.result.files); });