From 262e8a48b74a66b6b4c98e3e0800ab7808657611 Mon Sep 17 00:00:00 2001 From: haulakh Date: Thu, 23 Apr 2026 13:03:29 +0530 Subject: [PATCH 1/2] feat: accept 15-char package version IDs in bundle definition file --- src/package/packageBundleVersionCreate.ts | 8 ++- src/utils/packageUtils.ts | 27 +++++++++ test/package/bundleVersionCreate.test.ts | 69 +++++++++++++++++++++++ test/utils/packageUtils.test.ts | 26 +++++++++ 4 files changed, 129 insertions(+), 1 deletion(-) diff --git a/src/package/packageBundleVersionCreate.ts b/src/package/packageBundleVersionCreate.ts index 3c32d6626..484e95557 100644 --- a/src/package/packageBundleVersionCreate.ts +++ b/src/package/packageBundleVersionCreate.ts @@ -17,6 +17,7 @@ import * as fs from 'node:fs'; import { Connection, Messages, SfError, SfProject } from '@salesforce/core'; import { BundleSObjects, BundleVersionCreateOptions } from '../interfaces'; import { massageErrorMessage } from '../utils/bundleUtils'; +import { convertTo18CharId } from '../utils/packageUtils'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/packaging', 'bundle_version_create'); @@ -174,11 +175,16 @@ export class PackageBundleVersionCreate { return bundleVersionComponents.map((component) => { const packageVersion = component.packageVersion; - // Check if it's already an ID (04t followed by 15 characters) + // Check if it's already an 18-char ID (04t followed by 15 characters) if (/^04t[a-zA-Z0-9]{15}$/.test(packageVersion)) { return packageVersion; } + // Check if it's a 15-char ID (04t followed by 12 characters) and convert to 18-char + if (/^04t[a-zA-Z0-9]{12}$/.test(packageVersion)) { + return convertTo18CharId(packageVersion); + } + // Otherwise, treat it as an alias and resolve it from sfdx-project.json const packageVersionId = project.getPackageIdFromAlias(packageVersion); if (!packageVersionId) { diff --git a/src/utils/packageUtils.ts b/src/utils/packageUtils.ts index 212a150c5..4e24cd55a 100644 --- a/src/utils/packageUtils.ts +++ b/src/utils/packageUtils.ts @@ -88,6 +88,33 @@ export function uniqid(options?: { template?: string; length?: number }): string : `${options.template}${uniqueString}`; } +/** + * Converts a 15-character Salesforce ID to its 18-character case-insensitive equivalent. + * Returns the ID unchanged if it is not exactly 15 characters. + * + * @param id - The Salesforce ID to convert + * @returns The 18-character Salesforce ID + */ +export function convertTo18CharId(id: string): string { + if (!id || id.length !== 15) { + return id; + } + + const suffix: string[] = []; + for (let i = 0; i < 3; i++) { + let flags = 0; + for (let j = 0; j < 5; j++) { + const char = id.charAt(i * 5 + j); + if (char >= 'A' && char <= 'Z') { + flags += 1 << j; + } + } + suffix.push('ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'.charAt(flags)); + } + + return id + suffix.join(''); +} + export function validateId(idObj: Many, value: string | undefined): void { if (!value || !validateIdNoThrow(idObj, value)) { throw messages.createError('invalidIdOrAlias', [ diff --git a/test/package/bundleVersionCreate.test.ts b/test/package/bundleVersionCreate.test.ts index af7ddf260..802b58f8b 100644 --- a/test/package/bundleVersionCreate.test.ts +++ b/test/package/bundleVersionCreate.test.ts @@ -585,6 +585,75 @@ describe('PackageBundleVersion.create', () => { fs.unlinkSync(componentsPath); }); + it('should accept 15-char package version IDs and convert to 18-char', async () => { + const componentsPath = path.join(project.getPath(), 'bundle-components.json'); + const components = [ + { packageVersion: '04t5f000000WM9y' }, // 15-char ID + { packageVersion: '04t000000000000003' }, // 18-char ID (should stay unchanged) + ]; + fs.writeFileSync(componentsPath, JSON.stringify(components)); + + let capturedRequest: { BundleVersionComponents: string } | undefined; + Object.assign(connection.tooling, { + sobject: () => ({ + create: (req: { BundleVersionComponents: string }) => { + capturedRequest = req; + return Promise.resolve({ + success: true, + id: '0Ho000000000000', + }); + }, + }), + query: () => + Promise.resolve({ + records: [{ BundleName: 'testBundle' }], + }), + }); + + Object.assign(connection, { + autoFetchQuery: () => + Promise.resolve({ + records: [ + { + Id: '0Ho000000000000', + RequestStatus: BundleSObjects.PkgBundleVersionCreateReqStatus.success, + PackageBundle: { Id: '0Ho123456789012' }, + PackageBundleVersion: { Id: '1Q8000000000001' }, + VersionName: 'ver 1.0', + MajorVersion: '1', + MinorVersion: '0', + Ancestor: null, + BundleVersionComponents: JSON.stringify(components), + CreatedDate: '2025-01-01T00:00:00.000Z', + CreatedById: '005000000000000', + ValidationError: '', + }, + ], + }), + }); + + const options: BundleVersionCreateOptions = { + connection, + project, + PackageBundle: 'testBundle', + MajorVersion: '1', + MinorVersion: '0', + Ancestor: null, + BundleVersionComponentsPath: componentsPath, + }; + + const result = await PackageBundleVersion.create(options); + expect(result).to.have.property('RequestStatus', BundleSObjects.PkgBundleVersionCreateReqStatus.success); + + // Verify the 15-char ID was converted to 18-char in the API request + expect(capturedRequest).to.not.be.undefined; + const sentComponents = JSON.parse(capturedRequest!.BundleVersionComponents) as string[]; + expect(sentComponents[0]).to.equal('04t5f000000WM9yAAG'); // converted from 15 to 18 + expect(sentComponents[1]).to.equal('04t000000000000003'); // unchanged 18-char + + fs.unlinkSync(componentsPath); + }); + it('should handle invalid bundle components format', async () => { const componentsPath = path.join(project.getPath(), 'bundle-components.json'); diff --git a/test/utils/packageUtils.test.ts b/test/utils/packageUtils.test.ts index 2e9e3b6f0..a7f4dff4c 100644 --- a/test/utils/packageUtils.test.ts +++ b/test/utils/packageUtils.test.ts @@ -26,6 +26,7 @@ import JSZIP from 'jszip'; import { applyErrorAction, combineSaveErrors, + convertTo18CharId, findPackageDirectory, resolveBuildUserPermissions, getPackageVersionNumber, @@ -49,6 +50,31 @@ describe('packageUtils', () => { restoreContext($$); }); + describe('convertTo18CharId', () => { + it('should convert a 15-char ID to 18-char', () => { + expect(convertTo18CharId('04t5f000000WM9y')).to.equal('04t5f000000WM9yAAG'); + }); + + it('should return an 18-char ID unchanged', () => { + expect(convertTo18CharId('04t5f000000WM9yAAG')).to.equal('04t5f000000WM9yAAG'); + }); + + it('should return empty string unchanged', () => { + expect(convertTo18CharId('')).to.equal(''); + }); + + it('should handle all-lowercase 15-char ID', () => { + const result = convertTo18CharId('04t5f000000wm9y'); + expect(result).to.have.lengthOf(18); + expect(result).to.equal('04t5f000000wm9yAAA'); + }); + + it('should handle mixed-case 15-char ID', () => { + const result = convertTo18CharId('001A000001ABCDE'); + expect(result).to.have.lengthOf(18); + }); + }); + describe('getPackage2VersionNumber', () => { it('should return the correct version number', () => { const version = { From a0054b96a6ba535c2b7bc90c024d005aa402e6e1 Mon Sep 17 00:00:00 2001 From: haulakh Date: Thu, 23 Apr 2026 14:27:10 +0530 Subject: [PATCH 2/2] fix: remove conversion logic, accept 15-char IDs as-is --- src/package/packageBundleVersionCreate.ts | 10 ++------- src/utils/packageUtils.ts | 27 ----------------------- test/package/bundleVersionCreate.test.ts | 10 ++++----- test/utils/packageUtils.test.ts | 26 ---------------------- 4 files changed, 7 insertions(+), 66 deletions(-) diff --git a/src/package/packageBundleVersionCreate.ts b/src/package/packageBundleVersionCreate.ts index 484e95557..215027fcc 100644 --- a/src/package/packageBundleVersionCreate.ts +++ b/src/package/packageBundleVersionCreate.ts @@ -17,7 +17,6 @@ import * as fs from 'node:fs'; import { Connection, Messages, SfError, SfProject } from '@salesforce/core'; import { BundleSObjects, BundleVersionCreateOptions } from '../interfaces'; import { massageErrorMessage } from '../utils/bundleUtils'; -import { convertTo18CharId } from '../utils/packageUtils'; Messages.importMessagesDirectory(__dirname); const messages = Messages.loadMessages('@salesforce/packaging', 'bundle_version_create'); @@ -175,16 +174,11 @@ export class PackageBundleVersionCreate { return bundleVersionComponents.map((component) => { const packageVersion = component.packageVersion; - // Check if it's already an 18-char ID (04t followed by 15 characters) - if (/^04t[a-zA-Z0-9]{15}$/.test(packageVersion)) { + // Check if it's a package version ID (04t prefix, 15 or 18 characters) + if (/^04t[a-zA-Z0-9]{12,15}$/.test(packageVersion)) { return packageVersion; } - // Check if it's a 15-char ID (04t followed by 12 characters) and convert to 18-char - if (/^04t[a-zA-Z0-9]{12}$/.test(packageVersion)) { - return convertTo18CharId(packageVersion); - } - // Otherwise, treat it as an alias and resolve it from sfdx-project.json const packageVersionId = project.getPackageIdFromAlias(packageVersion); if (!packageVersionId) { diff --git a/src/utils/packageUtils.ts b/src/utils/packageUtils.ts index 4e24cd55a..212a150c5 100644 --- a/src/utils/packageUtils.ts +++ b/src/utils/packageUtils.ts @@ -88,33 +88,6 @@ export function uniqid(options?: { template?: string; length?: number }): string : `${options.template}${uniqueString}`; } -/** - * Converts a 15-character Salesforce ID to its 18-character case-insensitive equivalent. - * Returns the ID unchanged if it is not exactly 15 characters. - * - * @param id - The Salesforce ID to convert - * @returns The 18-character Salesforce ID - */ -export function convertTo18CharId(id: string): string { - if (!id || id.length !== 15) { - return id; - } - - const suffix: string[] = []; - for (let i = 0; i < 3; i++) { - let flags = 0; - for (let j = 0; j < 5; j++) { - const char = id.charAt(i * 5 + j); - if (char >= 'A' && char <= 'Z') { - flags += 1 << j; - } - } - suffix.push('ABCDEFGHIJKLMNOPQRSTUVWXYZ012345'.charAt(flags)); - } - - return id + suffix.join(''); -} - export function validateId(idObj: Many, value: string | undefined): void { if (!value || !validateIdNoThrow(idObj, value)) { throw messages.createError('invalidIdOrAlias', [ diff --git a/test/package/bundleVersionCreate.test.ts b/test/package/bundleVersionCreate.test.ts index 802b58f8b..d8f980e3a 100644 --- a/test/package/bundleVersionCreate.test.ts +++ b/test/package/bundleVersionCreate.test.ts @@ -585,11 +585,11 @@ describe('PackageBundleVersion.create', () => { fs.unlinkSync(componentsPath); }); - it('should accept 15-char package version IDs and convert to 18-char', async () => { + it('should accept 15-char package version IDs', async () => { const componentsPath = path.join(project.getPath(), 'bundle-components.json'); const components = [ { packageVersion: '04t5f000000WM9y' }, // 15-char ID - { packageVersion: '04t000000000000003' }, // 18-char ID (should stay unchanged) + { packageVersion: '04t000000000000003' }, // 18-char ID ]; fs.writeFileSync(componentsPath, JSON.stringify(components)); @@ -645,11 +645,11 @@ describe('PackageBundleVersion.create', () => { const result = await PackageBundleVersion.create(options); expect(result).to.have.property('RequestStatus', BundleSObjects.PkgBundleVersionCreateReqStatus.success); - // Verify the 15-char ID was converted to 18-char in the API request + // Verify both 15-char and 18-char IDs are passed through to the API expect(capturedRequest).to.not.be.undefined; const sentComponents = JSON.parse(capturedRequest!.BundleVersionComponents) as string[]; - expect(sentComponents[0]).to.equal('04t5f000000WM9yAAG'); // converted from 15 to 18 - expect(sentComponents[1]).to.equal('04t000000000000003'); // unchanged 18-char + expect(sentComponents[0]).to.equal('04t5f000000WM9y'); // 15-char passed through + expect(sentComponents[1]).to.equal('04t000000000000003'); // 18-char passed through fs.unlinkSync(componentsPath); }); diff --git a/test/utils/packageUtils.test.ts b/test/utils/packageUtils.test.ts index a7f4dff4c..2e9e3b6f0 100644 --- a/test/utils/packageUtils.test.ts +++ b/test/utils/packageUtils.test.ts @@ -26,7 +26,6 @@ import JSZIP from 'jszip'; import { applyErrorAction, combineSaveErrors, - convertTo18CharId, findPackageDirectory, resolveBuildUserPermissions, getPackageVersionNumber, @@ -50,31 +49,6 @@ describe('packageUtils', () => { restoreContext($$); }); - describe('convertTo18CharId', () => { - it('should convert a 15-char ID to 18-char', () => { - expect(convertTo18CharId('04t5f000000WM9y')).to.equal('04t5f000000WM9yAAG'); - }); - - it('should return an 18-char ID unchanged', () => { - expect(convertTo18CharId('04t5f000000WM9yAAG')).to.equal('04t5f000000WM9yAAG'); - }); - - it('should return empty string unchanged', () => { - expect(convertTo18CharId('')).to.equal(''); - }); - - it('should handle all-lowercase 15-char ID', () => { - const result = convertTo18CharId('04t5f000000wm9y'); - expect(result).to.have.lengthOf(18); - expect(result).to.equal('04t5f000000wm9yAAA'); - }); - - it('should handle mixed-case 15-char ID', () => { - const result = convertTo18CharId('001A000001ABCDE'); - expect(result).to.have.lengthOf(18); - }); - }); - describe('getPackage2VersionNumber', () => { it('should return the correct version number', () => { const version = {