Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions messages/package_version_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ Could not find a package in sfdx-project.json file using "path" %s. Add it to th

Couldn't find a package directory for package using %s %s. Add it to the packageDirectories section and add the alias to packageAliases with its 0Ho ID.

# missingPackagePropertyForDirectory

Can't convert package. Your project configuration file (sfdx-project.json) specifies unpackagedMetadata or apexTestAccess, but the attribute can't be resolved because the packageDirectories entry doesn't specify the package parameter. Specify a valid 0Ho package ID or a package alias.

# unresolvedPackageAliasForDirectory

Can't convert package. Your project configuration file (sfdx-project.json) specifies unpackagedMetadata or apexTestAccess, but the attribute can't be resolved because the package specified as "%s" isn't a valid package ID or package alias. Specify a valid 0Ho package ID or a package alias that resolves to a valid 0Ho package ID.

# noSourceInRootDirectory

No matching source was found within the package root directory: %s
Expand Down
21 changes: 20 additions & 1 deletion src/package/packageConvert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ export async function createPackageVersionCreateRequest(
);

if (context.codecoverage) {
assertPackagePropertiesAreResolvable(project);

const unpackagedMetadataPath = packageDescriptorJson.unpackagedMetadata?.path;
const hasUnpackaged = await new MetadataResolver().resolveMetadata(
unpackagedMetadataPath,
Expand Down Expand Up @@ -328,6 +330,24 @@ function buildPackageDescriptorJson(args: {
return descriptor;
}

// These properties only resolve when their packageDirectories entry has a `package` that maps to a
// packageAliases key; otherwise they're silently dropped, so fail fast here instead.
function assertPackagePropertiesAreResolvable(project?: SfProject): void {
const packageScopedProperties = ['unpackagedMetadata', 'apexTestAccess'] as const;
for (const dir of project?.getPackageDirectories() ?? []) {
const declared = packageScopedProperties.find((property) => property in dir);
if (!declared) continue;
if (!isPackagingDirectory(dir)) {
throw messages.createError('missingPackagePropertyForDirectory');
}
// Package directory entries support either an alias or a literal 0Ho ID for backward compatibility.
const resolvedPackageId = project?.getPackageIdFromAlias(dir.package) ?? dir.package;
if (!pkgUtils.validateIdNoThrow(pkgUtils.BY_LABEL.PACKAGE_ID, resolvedPackageId)) {
throw messages.createError('unresolvedPackageAliasForDirectory', [dir.package]);
}
}
}

async function createRequestObject(
packageId: string,
options: { installationkey?: string; buildinstance?: string; codecoverage?: boolean },
Expand Down Expand Up @@ -403,7 +423,6 @@ async function pollForStatusWithInterval(
// for multiple errors, display one per line prefixed with (x)
if (results[0].Error.length > 1) {
results[0].Error.forEach((error) => {

errors.push(`(${errors.length + 1}) ${error}`);
});
errors.unshift(messages.getMessage('versionCreateFailedWithMultipleErrors'));
Expand Down
137 changes: 131 additions & 6 deletions test/package/packageConvert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ describe('packageConvert', () => {
// the most we can assert about VersionInfo because it is a zip file string representation, which changes with time
expect(typeof request.VersionInfo).to.equal('string');


const seedMD = hasSeedMdSpy.firstCall.args[0];
expect(seedMD).to.equal('seed');
});
Expand Down Expand Up @@ -215,6 +214,133 @@ describe('packageConvert', () => {
expect(unpackagedCall).to.be.undefined;
});

it('should fail fast when unpackagedMetadata is declared without a package property and codecoverage is enabled', async () => {
$$.inProject(true);
const project = SfProject.getInstance();

await fs.promises.mkdir(path.join(project.getPath(), 'force-app'), { recursive: true });

// packageDirectories entry declares unpackagedMetadata but omits `package`
project.getSfProjectJson().set('packageDirectories', [
{
path: 'force-app',
unpackagedMetadata: { path: 'unpackaged-md' },
},
]);
await project.getSfProjectJson().write();

try {
await createPackageVersionCreateRequest({ codecoverage: true }, '0Ho3i000000Gmj6CAC', '60.0', project);
expect.fail('expected createPackageVersionCreateRequest to throw');
} catch (e) {
const error = e as Error & { name: string };
expect(error.name).to.equal('MissingPackagePropertyForDirectoryError');
expect(error.message).to.include('unpackagedMetadata');
}
});

it('should fail fast when apexTestAccess is declared without a package property and codecoverage is enabled', async () => {
$$.inProject(true);
const project = SfProject.getInstance();

await fs.promises.mkdir(path.join(project.getPath(), 'force-app'), { recursive: true });

// packageDirectories entry declares apexTestAccess but omits `package`
project.getSfProjectJson().set('packageDirectories', [
{
path: 'force-app',
apexTestAccess: {
permissionSets: ['Test_Permission_Set'],
permissionSetLicenses: ['TestPsl'],
},
},
]);
await project.getSfProjectJson().write();

try {
await createPackageVersionCreateRequest({ codecoverage: true }, '0Ho3i000000Gmj6CAC', '60.0', project);
expect.fail('expected createPackageVersionCreateRequest to throw');
} catch (e) {
const error = e as Error & { name: string };
expect(error.name).to.equal('MissingPackagePropertyForDirectoryError');
expect(error.message).to.include('apexTestAccess');
}
});

it('should fail fast when the package property has no matching packageAliases entry and codecoverage is enabled', async () => {
$$.inProject(true);
const project = SfProject.getInstance();

await fs.promises.mkdir(path.join(project.getPath(), 'force-app'), { recursive: true });

// package is declared but its alias is absent from packageAliases, so it can't be resolved
project.getSfProjectJson().set('packageDirectories', [
{
path: 'force-app',
package: 'UnknownPackageAlias',
unpackagedMetadata: { path: 'unpackaged-md' },
},
]);
await project.getSfProjectJson().write();

try {
await createPackageVersionCreateRequest({ codecoverage: true }, '0Ho3i000000Gmj6CAC', '60.0', project);
expect.fail('expected createPackageVersionCreateRequest to throw');
} catch (e) {
const error = e as Error & { name: string };
expect(error.name).to.equal('UnresolvedPackageAliasForDirectoryError');
expect(error.message).to.include('UnknownPackageAlias');
expect(error.message).to.include('unpackagedMetadata');
}
});

it('should NOT fail fast when a property is declared without a package property but codecoverage is disabled', async () => {
$$.inProject(true);
const project = SfProject.getInstance();

await fs.promises.mkdir(path.join(project.getPath(), 'force-app'), { recursive: true });

project.getSfProjectJson().set('packageDirectories', [
{
path: 'force-app',
unpackagedMetadata: { path: 'unpackaged-md' },
},
]);
await project.getSfProjectJson().write();

// codecoverage disabled: these properties are never consumed, so no error is raised
const request = await createPackageVersionCreateRequest(
{ codecoverage: false },
'0Ho3i000000Gmj6CAC',
'60.0',
project
);
expect(request.Package2Id).to.equal('0Ho3i000000Gmj6CAC');
});

it('should NOT fail fast for a plain source directory without these properties or a package property', async () => {
$$.inProject(true);
const project = SfProject.getInstance();

await fs.promises.mkdir(path.join(project.getPath(), 'force-app'), { recursive: true });

// Ordinary source directory: no package, no unpackagedMetadata/apexTestAccess — must not trip the guard
project.getSfProjectJson().set('packageDirectories', [
{
path: 'force-app',
},
]);
await project.getSfProjectJson().write();

const request = await createPackageVersionCreateRequest(
{ codecoverage: true },
'0Ho3i000000Gmj6CAC',
'60.0',
project
);
expect(request.Package2Id).to.equal('0Ho3i000000Gmj6CAC');
});

it('should set apexTestAccess permissions in package2descriptor.json when codecoverage is enabled', async () => {
$$.inProject(true);
const project = SfProject.getInstance();
Expand Down Expand Up @@ -332,7 +458,7 @@ describe('packageConvert', () => {
);

// Verify resolveMetadata was called with the project config path

const seedMD = hasSeedMdSpy.firstCall.args[0];
expect(seedMD).to.equal('seed');

Expand All @@ -355,7 +481,7 @@ describe('packageConvert', () => {
return typeof filePath === 'string' && filePath.includes('package2-descriptor.json');
});
expect(descriptorWriteCall).to.not.be.undefined;

const descriptorContent = descriptorWriteCall?.args[1];
expect(descriptorContent).to.not.have.string('seedMetadata');
});
Expand Down Expand Up @@ -412,7 +538,7 @@ describe('packageConvert', () => {
);

// Verify resolveMetadata was called with the CLI path, not the project config path

const seedMD = hasSeedMdSpy.firstCall.args[0];
expect(seedMD).to.equal('seed-cli');
expect(seedMD).to.not.equal('seed');
Expand All @@ -436,7 +562,7 @@ describe('packageConvert', () => {
return typeof filePath === 'string' && filePath.includes('package2-descriptor.json');
});
expect(descriptorWriteCall).to.not.be.undefined;

const descriptorContent = descriptorWriteCall?.args[1];
expect(descriptorContent).to.not.have.string('seedMetadata');
});
Expand Down Expand Up @@ -570,7 +696,6 @@ describe('packageConvert', () => {
};

Lifecycle.getInstance().on(PackageEvents.convert.progress, async (data) => {

// @ts-ignore
expect(data).to.deep.equal({
id: '0Ho3i000000Gmj6YYY',
Expand Down
Loading