Skip to content
Open
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
4 changes: 4 additions & 0 deletions messages/package_create.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# unableToFindPackageWithId

Unable to find Package with Id: "%s"

# createPackagingNotEnabledOnOrg

Can't create package. The Dev Hub you specified doesn't have the Second-Generation Managed Packages setting enabled. Enable this setting on your Dev Hub, and try again.
4 changes: 4 additions & 0 deletions messages/package_version_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ No subscriber package was found for seed id: %s
Only one package in a Dev Hub is allowed per converted from first-generation package, but the following were found:
%s

# convertPackagingNotEnabledOnOrg

Can't convert package. The Dev Hub you specified doesn't have the Second-Generation Managed Packages setting enabled. Enable this setting on your Dev Hub, and try again.

# errorMissingPackageIdOrPath

You must specify either a package ID or a package path to create a new package version.
Expand Down
35 changes: 34 additions & 1 deletion src/package/packageConvert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,15 @@ export async function findOrCreatePackage2(
project?: SfProject
): Promise<string> {
const query = `SELECT Id, Name FROM Package2 WHERE ConvertedFromPackageId = '${seedPackage}'`;
const queryResult = (await connection.tooling.query<PackagingSObjects.Package2>(query)).records;
let queryResult;
try {
queryResult = (await connection.tooling.query<PackagingSObjects.Package2>(query)).records;
} catch (e) {
if (isPackage2NotSupportedError(e)) {
throw messages.createError('convertPackagingNotEnabledOnOrg');
}
throw e;
}
if (queryResult?.length > 1) {
const ids = queryResult.map((r) => r.Id);
throw messages.createError('errorMoreThanOnePackage2WithSeed', [ids.join(', ')]);
Expand Down Expand Up @@ -100,6 +108,9 @@ export async function findOrCreatePackage2(

const createResult = await connection.tooling.create('Package2', request);
if (!createResult.success) {
if (createResult.errors?.some((error) => isPackage2NotSupportedError(error))) {
throw messages.createError('convertPackagingNotEnabledOnOrg');
}
throw pkgUtils.combineSaveErrors('Package2', 'create', createResult.errors);
}

Expand Down Expand Up @@ -475,3 +486,25 @@ const isStatusEqualTo = (
results: PackageVersionCreateRequestResult[],
statuses: Package2VersionStatus[] = []
): boolean => (!results?.length ? false : statuses.some((status) => results[0].Status === status));

/**
* Detects the tooling-API error thrown when a Dev Hub does not have second-generation
* packaging enabled and the Package2 entity is therefore not accessible. The full server
* message may append custom-object WSDL boilerplate, so match on a substring (consistent
* with the handling in packageVersionRetrieve.ts).
*
* @param err the error thrown by a Package2 tooling call
* @returns true if the error indicates Package2 is not supported on the org
*/
const isPackage2NotSupportedError = (err: unknown): boolean => {
let msg: string;
if (err instanceof Error) {
msg = err.message;
} else if (typeof err === 'object' && err !== null && 'message' in err) {
// jsforce SaveError objects are plain objects that carry a `message` field.
msg = String(err.message);
} else {
msg = String(err);
}
return msg.includes("sObject type 'Package2' is not supported.");
};
40 changes: 39 additions & 1 deletion src/package/packageCreate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
* limitations under the License.
*/

import { Connection, SfError, SfProject } from '@salesforce/core';
import { Connection, Messages, SfError, SfProject } from '@salesforce/core';
import { env } from '@salesforce/kit';
import { PackagePackageDir, PackageDir } from '@salesforce/schemas';
import { isPackagingDirectory } from '@salesforce/core/project';
import * as pkgUtils from '../utils/packageUtils';
import { applyErrorAction, massageErrorMessage } from '../utils/packageUtils';
import { PackageCreateOptions, PackagingSObjects } from '../interfaces';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/packaging', 'package_create');

type Package2Request = Pick<
PackagingSObjects.Package2,
'Name' | 'Description' | 'NamespacePrefix' | 'ContainerOptions' | 'IsOrgDependent' | 'PackageErrorUsername'
Expand Down Expand Up @@ -75,11 +78,17 @@ export async function createPackage(
.sobject('Package2')
.create(request)
.catch((err) => {
if (isPackagingNotEnabledError(err)) {
throw messages.createError('createPackagingNotEnabledOnOrg');
}
const error = err instanceof Error ? err : new Error(typeof err === 'string' ? err : 'Unknown error');
throw SfError.wrap(applyErrorAction(massageErrorMessage(error)));
});

if (!createResult.success) {
if (createResult.errors?.some((error) => isPackagingNotEnabledError(error))) {
throw messages.createError('createPackagingNotEnabledOnOrg');
}
throw pkgUtils.combineSaveErrors('Package2', 'create', createResult.errors);
}

Expand All @@ -98,3 +107,32 @@ const sanitizePackageCreateOptions = (options: PackageCreateOptions): PackageCre
...options,
path: options.path.replace(/\/$/, ''),
});

/**
* Detects the tooling-API error thrown when a Dev Hub does not have second-generation
* packaging enabled and the Package2 entity is therefore not accessible. The Package2
* create REST endpoint (connection.tooling.sobject('Package2').create) 404s with a
* NOT_FOUND / "The requested resource does not exist" error. (This differs from the SOQL
* path used by package convert, which instead reports "sObject type 'Package2' is not
* supported" -- but package create never issues a SOQL query, so that form cannot occur
* here.)
*
* @param err the error (or jsforce SaveError) thrown by a Package2 tooling call
* @returns true if the error indicates Package2 is not supported on the org
*/
const isPackagingNotEnabledError = (err: unknown): boolean => {
let name = '';
let msg: string;
if (err instanceof Error) {
name = err.name;
msg = err.message;
} else if (typeof err === 'object' && err !== null) {
// jsforce SaveError objects are plain objects that carry `errorCode`/`statusCode` and `message`.
const e = err as { errorCode?: string; statusCode?: string; message?: string };
name = String(e.errorCode ?? e.statusCode ?? '');
msg = typeof e.message === 'string' ? e.message : '';
} else {
msg = String(err);
}
return name === 'NOT_FOUND' && msg.includes('The requested resource does not exist');
};
81 changes: 81 additions & 0 deletions test/package/packageConvert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,87 @@ describe('packageConvert', () => {
expect((e as Error).message).to.include('No subscriber package was found for seed id: 0Ho3i000000Gmj6CAC');
}
});

it('will throw an actionable error when the Dev Hub does not have 2GP enabled', async () => {
const notSupported = new Error(
"sObject type 'Package2' is not supported. If you are attempting to use a custom object, be sure to append the '__c' after the entity name. Please reference your WSDL or the describe call for the appropriate names."
);
notSupported.name = 'INVALID_TYPE';
const conn = {
tooling: {
query: () => {
throw notSupported;
},
},
} as unknown as Connection;

try {
await findOrCreatePackage2('0Ho3i000000Gmj6CAC', conn);
expect.fail('should have thrown');
} catch (e) {
expect((e as Error).message).to.equal(
"Can't convert package. The Dev Hub you specified doesn't have the Second-Generation Managed Packages setting enabled. Enable this setting on your Dev Hub, and try again."
);
// the raw INVALID_TYPE text must not leak to the user
expect((e as Error).message).to.not.include('is not supported');
expect((e as Error).name).to.not.equal('INVALID_TYPE');
}
});

it('will throw an actionable error when the Package2 create reports 2GP not supported', async () => {
const conn = await testOrg.getConnection();

$$.SANDBOX.stub(conn.tooling, 'query')
.onFirstCall()
// @ts-ignore
.resolves({ records: [] })
.onSecondCall()
// @ts-ignore
.resolves({ records: [{ Name: 'pkg', Description: 'desc', NamespacePrefix: 'ns' }] });
$$.SANDBOX.stub(conn.tooling, 'create').resolves({
errors: [{ errorCode: 'INVALID_TYPE', message: "sObject type 'Package2' is not supported." }],
success: false,
id: undefined,
});

try {
await findOrCreatePackage2('0Ho3i000000Gmj6CAC', conn);
expect.fail('should have thrown');
} catch (e) {
expect((e as Error).message).to.equal(
"Can't convert package. The Dev Hub you specified doesn't have the Second-Generation Managed Packages setting enabled. Enable this setting on your Dev Hub, and try again."
);
expect((e as Error).message).to.not.include('is not supported');
}
});
});

it('convertPackage surfaces the actionable 2GP-not-enabled error', async () => {
const notSupported = new Error("sObject type 'Package2' is not supported.");
notSupported.name = 'INVALID_TYPE';
const conn = {
tooling: {
query: () => {
throw notSupported;
},
},
} as unknown as Connection;

try {
await convertPackage('0Ho3i000000Gmj6CAC', conn, {
buildInstance: '',
installationKey: '',
definitionfile: '',
installationKeyBypass: true,
wait: Duration.minutes(1),
});
expect.fail('should have thrown');
} catch (e) {
expect((e as Error).message).to.equal(
"Can't convert package. The Dev Hub you specified doesn't have the Second-Generation Managed Packages setting enabled. Enable this setting on your Dev Hub, and try again."
);
expect((e as Error).message).to.not.include('is not supported');
}
});
it('will throw correct error when create call fails', async () => {
const conn = await testOrg.getConnection();
Expand Down
97 changes: 95 additions & 2 deletions test/package/packageCreate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,22 @@ import path from 'node:path';
import fs from 'node:fs';
import { expect } from 'chai';
import { instantiateContext, restoreContext, stubContext } from '@salesforce/core/testSetup';
import { SfProject } from '@salesforce/core';
import { createPackageRequestFromContext, createPackageDirEntry } from '../../src/package/packageCreate';
import { Connection, SfProject } from '@salesforce/core';
import { env } from '@salesforce/kit';
import { createPackage, createPackageRequestFromContext, createPackageDirEntry } from '../../src/package/packageCreate';

const PACKAGING_NOT_ENABLED_MSG =
"Can't create package. The Dev Hub you specified doesn't have the Second-Generation Managed Packages setting enabled. Enable this setting on your Dev Hub, and try again.";

const createOptions = {
name: 'TestPkg',
description: 'desc',
path: 'force-app',
packageType: 'Managed' as const,
orgDependent: false,
errorNotificationUsername: 'foo@bar.org',
noNamespace: false,
};

async function setupProject(setup: (project: SfProject) => void = () => {}) {
const project = await SfProject.resolve();
Expand Down Expand Up @@ -198,4 +212,83 @@ describe('packageCreate', () => {
});
});
});

describe('createPackage 2GP-not-enabled handling', () => {
// don't let a successful stub try to write to sfdx-project.json
beforeEach(() => {
env.setBoolean('SF_PROJECT_AUTOUPDATE_DISABLE_FOR_PACKAGE_CREATE', true);
});
afterEach(() => {
env.unset('SF_PROJECT_AUTOUPDATE_DISABLE_FOR_PACKAGE_CREATE');
});

it('throws an actionable error when the Package2 create endpoint 404s (NOT_FOUND)', async () => {
$$.inProject(true);
const project = await setupProject();
const notFound = new Error('The requested resource does not exist');
notFound.name = 'NOT_FOUND';
const conn = {
tooling: {
sobject: () => ({
create: () => Promise.reject(notFound),
}),
},
} as unknown as Connection;

try {
await createPackage(conn, project, createOptions);
expect.fail('should have thrown');
} catch (e) {
expect((e as Error).message).to.equal(PACKAGING_NOT_ENABLED_MSG);
// the raw NOT_FOUND text must not leak to the user
expect((e as Error).message).to.not.include('requested resource');
}
});

it('throws an actionable error when the create result reports NOT_FOUND', async () => {
$$.inProject(true);
const project = await setupProject();
const conn = {
tooling: {
sobject: () => ({
create: () =>
Promise.resolve({
success: false,
id: undefined,
errors: [{ errorCode: 'NOT_FOUND', message: 'The requested resource does not exist' }],
}),
}),
},
} as unknown as Connection;

try {
await createPackage(conn, project, createOptions);
expect.fail('should have thrown');
} catch (e) {
expect((e as Error).message).to.equal(PACKAGING_NOT_ENABLED_MSG);
}
});

it('rethrows unrelated create errors unchanged', async () => {
$$.inProject(true);
const project = await setupProject();
const other = new Error('some other failure');
other.name = 'INVALID_FIELD';
const conn = {
tooling: {
sobject: () => ({
create: () => Promise.reject(other),
}),
},
} as unknown as Connection;

try {
await createPackage(conn, project, createOptions);
expect.fail('should have thrown');
} catch (e) {
expect((e as Error).message).to.not.equal(PACKAGING_NOT_ENABLED_MSG);
expect((e as Error).message).to.include('some other failure');
}
});
});
});
Loading