From 34d8ea3f8f4429a12ea535f790f8e05893436927 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 23 Jun 2026 14:32:04 -0600 Subject: [PATCH 1/4] fix: scopeProfiles excludes profiles correctly with ./-prefixed paths @W-22149938@ Two bugs caused package version create to include phantom profile names in package.xml when scopeProfiles: true was set: 1. findAllProfiles built glob ignore patterns that couldn't match ./-prefixed directory paths from sfdx-project.json (e.g. `**/./src-access-management/**` never matches). Normalize the path before globbing. 2. excludedProfiles from generateProfiles() were full path stems but the manifest filter compared them against profile member names. Use path.basename() to extract the name for comparison. --- src/package/packageProfileApi.ts | 5 ++++- src/package/packageVersionCreate.ts | 3 ++- test/package/packageVersionCreate.test.ts | 21 +++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/package/packageProfileApi.ts b/src/package/packageProfileApi.ts index 443696a43..5d0a2cf2b 100644 --- a/src/package/packageProfileApi.ts +++ b/src/package/packageProfileApi.ts @@ -123,7 +123,10 @@ export class PackageProfileApi extends AsyncCreatable { // Look for profiles in all package directories private findAllProfiles(excludedDirectories: string[] = []): string[] { - const ignore = excludedDirectories.map((dir) => `**/${dir.split(path.sep).join(path.posix.sep)}/**`); + const ignore = excludedDirectories.map((dir) => { + const normalized = path.normalize(dir).split(path.sep).filter(Boolean).join(path.posix.sep); + return `**/${normalized}/**`; + }); const patterns = this.project .getUniquePackageDirectories() .map((pDir) => pDir.fullPath) diff --git a/src/package/packageVersionCreate.ts b/src/package/packageVersionCreate.ts index bb29cc64e..a13a5d98b 100644 --- a/src/package/packageVersionCreate.ts +++ b/src/package/packageVersionCreate.ts @@ -587,9 +587,10 @@ export class PackageVersionCreate { profileExcludeDirs ); + const excludedProfileNames = excludedProfiles?.map((p) => path.basename(p)); packageXmlAsJson.types = typesArr.map((type) => { if (type.name !== 'Profile') return type; - return { ...type, members: type.members.filter((m) => !excludedProfiles?.includes(m)) }; + return { ...type, members: type.members.filter((m) => !excludedProfileNames?.includes(m)) }; }); const xml = packageXmlJsonToXmlString(packageXmlAsJson); diff --git a/test/package/packageVersionCreate.test.ts b/test/package/packageVersionCreate.test.ts index 6e221bd9e..b9966d2db 100644 --- a/test/package/packageVersionCreate.test.ts +++ b/test/package/packageVersionCreate.test.ts @@ -836,6 +836,27 @@ describe('Package Version Create', () => { expect(pkgTypeMembers[1].members).to.deep.equal(['Test Profile']); }); + it('should exclude profiles from ./-prefixed directories when scopeProfiles is true', async () => { + const pkgProfileApi = await PackageProfileApi.create({ project, includeUserLicenses: false }); + const types = [ + { name: 'Layout', members: ['Test Layout'] }, + { name: 'Profile', members: ['Excluded Profile', 'Included Profile'] }, + ]; + // create directories simulating ./-prefixed paths from sfdx-project.json + const excludedDir = path.join(project.getPath(), 'force-app', 'src-access-management'); + const includedDir = path.join(project.getPath(), 'force-app', 'main'); + await fs.promises.mkdir(excludedDir, { recursive: true }); + await fs.promises.mkdir(includedDir, { recursive: true }); + const fileContents = ''; + await fs.promises.writeFile(path.join(excludedDir, 'Excluded Profile.profile-meta.xml'), fileContents); + await fs.promises.writeFile(path.join(includedDir, 'Included Profile.profile-meta.xml'), fileContents); + + // Simulate the ./-prefixed path that comes from sfdx-project.json + const excludedDirectories = ['./src-access-management']; + const pkgTypeMembers = pkgProfileApi.filterAndGenerateProfilesForManifest(types, excludedDirectories); + expect(pkgTypeMembers.find((t) => t.name === 'Profile')?.members).to.deep.equal(['Included Profile']); + }); + describe('validateAncestorId', () => { it('should throw if the explicitUseNoAncestor is true and highestReleasedVersion is not undefined', () => { const ancestorId = 'ancestorId'; From 62280df971c0506c59bc94c3a1826dd846e897b5 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 23 Jun 2026 14:44:34 -0600 Subject: [PATCH 2/4] test: use realistic multi-directory project fixture for scopeProfiles test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register the sibling directory as a packageDirectory so the test exercises the actual globbing path (getUniquePackageDirectories → glob with ignore patterns) rather than relying on pattern matching against subdirectories. --- test/package/packageVersionCreate.test.ts | 33 ++++++++++++++--------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/test/package/packageVersionCreate.test.ts b/test/package/packageVersionCreate.test.ts index b9966d2db..492e1d5e7 100644 --- a/test/package/packageVersionCreate.test.ts +++ b/test/package/packageVersionCreate.test.ts @@ -836,25 +836,34 @@ describe('Package Version Create', () => { expect(pkgTypeMembers[1].members).to.deep.equal(['Test Profile']); }); - it('should exclude profiles from ./-prefixed directories when scopeProfiles is true', async () => { + it('should exclude profiles from ./-prefixed sibling package directories when scopeProfiles is true', async () => { + // Configure a multi-directory project where sibling uses ./ prefix in path + project.getSfProjectJson().set('packageDirectories', [ + { path: './src-access-management', package: 'ACCESS', versionName: 'ver 0.1', versionNumber: '0.1.0.NEXT' }, + { path: 'force-app', package: 'TEST', versionName: 'ver 0.1', versionNumber: '0.1.0.NEXT', default: true }, + ]); + await project.getSfProjectJson().write(); + + const siblingDir = path.join(project.getPath(), 'src-access-management'); + await fs.promises.mkdir(siblingDir, { recursive: true }); + const fileContents = ''; + await fs.promises.writeFile(path.join(siblingDir, 'Sibling Profile.profile-meta.xml'), fileContents); + await fs.promises.writeFile( + path.join(project.getPath(), 'force-app', 'Target Profile.profile-meta.xml'), + fileContents + ); + const pkgProfileApi = await PackageProfileApi.create({ project, includeUserLicenses: false }); const types = [ { name: 'Layout', members: ['Test Layout'] }, - { name: 'Profile', members: ['Excluded Profile', 'Included Profile'] }, + { name: 'Profile', members: ['Sibling Profile', 'Target Profile'] }, ]; - // create directories simulating ./-prefixed paths from sfdx-project.json - const excludedDir = path.join(project.getPath(), 'force-app', 'src-access-management'); - const includedDir = path.join(project.getPath(), 'force-app', 'main'); - await fs.promises.mkdir(excludedDir, { recursive: true }); - await fs.promises.mkdir(includedDir, { recursive: true }); - const fileContents = ''; - await fs.promises.writeFile(path.join(excludedDir, 'Excluded Profile.profile-meta.xml'), fileContents); - await fs.promises.writeFile(path.join(includedDir, 'Included Profile.profile-meta.xml'), fileContents); - // Simulate the ./-prefixed path that comes from sfdx-project.json + // When scopeProfiles is true, packageVersionCreate passes sibling package dir paths + // as excludedDirectories. These retain the ./ prefix from sfdx-project.json. const excludedDirectories = ['./src-access-management']; const pkgTypeMembers = pkgProfileApi.filterAndGenerateProfilesForManifest(types, excludedDirectories); - expect(pkgTypeMembers.find((t) => t.name === 'Profile')?.members).to.deep.equal(['Included Profile']); + expect(pkgTypeMembers.find((t) => t.name === 'Profile')?.members).to.deep.equal(['Target Profile']); }); describe('validateAncestorId', () => { From 7c4a826f9b210dbcec369b478fde2d99b3bebd0e Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 23 Jun 2026 14:46:58 -0600 Subject: [PATCH 3/4] test: add scopeProfiles=false test confirming all profiles are included --- test/package/packageVersionCreate.test.ts | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/package/packageVersionCreate.test.ts b/test/package/packageVersionCreate.test.ts index 492e1d5e7..ca7008d61 100644 --- a/test/package/packageVersionCreate.test.ts +++ b/test/package/packageVersionCreate.test.ts @@ -836,6 +836,36 @@ describe('Package Version Create', () => { expect(pkgTypeMembers[1].members).to.deep.equal(['Test Profile']); }); + it('should include profiles from all package directories when scopeProfiles is false', async () => { + project.getSfProjectJson().set('packageDirectories', [ + { path: './src-access-management', package: 'ACCESS', versionName: 'ver 0.1', versionNumber: '0.1.0.NEXT' }, + { path: 'force-app', package: 'TEST', versionName: 'ver 0.1', versionNumber: '0.1.0.NEXT', default: true }, + ]); + await project.getSfProjectJson().write(); + + const siblingDir = path.join(project.getPath(), 'src-access-management'); + await fs.promises.mkdir(siblingDir, { recursive: true }); + const fileContents = ''; + await fs.promises.writeFile(path.join(siblingDir, 'Sibling Profile.profile-meta.xml'), fileContents); + await fs.promises.writeFile( + path.join(project.getPath(), 'force-app', 'Target Profile.profile-meta.xml'), + fileContents + ); + + const pkgProfileApi = await PackageProfileApi.create({ project, includeUserLicenses: false }); + const types = [ + { name: 'Layout', members: ['Test Layout'] }, + { name: 'Profile', members: ['Sibling Profile', 'Target Profile'] }, + ]; + + // With no excludedDirectories (scopeProfiles=false), all profiles from all dirs are included + const pkgTypeMembers = pkgProfileApi.filterAndGenerateProfilesForManifest(types); + expect(pkgTypeMembers.find((t) => t.name === 'Profile')?.members).to.deep.equal([ + 'Sibling Profile', + 'Target Profile', + ]); + }); + it('should exclude profiles from ./-prefixed sibling package directories when scopeProfiles is true', async () => { // Configure a multi-directory project where sibling uses ./ prefix in path project.getSfProjectJson().set('packageDirectories', [ From 43e9b55ae10c856d4c14a4cd03d2b1db5a1a9809 Mon Sep 17 00:00:00 2001 From: Willie Ruemmele Date: Tue, 23 Jun 2026 14:52:20 -0600 Subject: [PATCH 4/4] chore: bump core --- package.json | 2 +- yarn.lock | 27 ++++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b524b112d..556b3a0dd 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ ], "dependencies": { "@jsforce/jsforce-node": "^3.10.16", - "@salesforce/core": "^8.31.1", + "@salesforce/core": "^8.31.2", "@salesforce/kit": "^3.2.6", "@salesforce/schemas": "^1.10.3", "@salesforce/source-deploy-retrieve": "^12.36.2", diff --git a/yarn.lock b/yarn.lock index 0452a707b..c55cf3bf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -551,7 +551,7 @@ strip-ansi "6.0.1" ts-retry-promise "^0.8.1" -"@salesforce/core@^8.23.1", "@salesforce/core@^8.30.3", "@salesforce/core@^8.31.0", "@salesforce/core@^8.31.1": +"@salesforce/core@^8.23.1", "@salesforce/core@^8.30.3", "@salesforce/core@^8.31.0": version "8.31.1" resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.31.1.tgz#a0057e46568b5aeb6d838c461d7c98105ec11dc2" integrity sha512-dnBfLI0v/Ucsh/QrpYPGeo39qsvvglWMRSifx1lmAwLc9QAnL3Hhp9zUxJyX5icD9jj1uMftsAtIOGyjC2+KXA== @@ -576,6 +576,31 @@ ts-retry-promise "^0.8.1" zod "^4.1.12" +"@salesforce/core@^8.31.2": + version "8.31.2" + resolved "https://registry.yarnpkg.com/@salesforce/core/-/core-8.31.2.tgz#968448f423b553f726f42c27da6b55c4ec7eb93a" + integrity sha512-naqnq7Z+gbl1LdnyNvrGrNUoeMUQtCOsnrS6DfqeuLMJTFqcL9Dq0/od+xcuqi0+l7HTyH0/gU1BQitWpd1rag== + dependencies: + "@jsforce/jsforce-node" "^3.10.13" + "@salesforce/kit" "^3.2.4" + "@salesforce/ts-types" "^2.0.12" + ajv "^8.18.0" + change-case "^4.1.2" + fast-levenshtein "^3.0.0" + faye "^1.4.1" + form-data "^4.0.5" + js2xmlparser "^4.0.1" + jsonwebtoken "9.0.3" + jszip "3.10.1" + memfs "4.38.1" + pino "^9.7.0" + pino-abstract-transport "^1.2.0" + pino-pretty "^11.3.0" + proper-lockfile "^4.1.2" + semver "^7.8.0" + ts-retry-promise "^0.8.1" + zod "^4.1.12" + "@salesforce/dev-config@^4.3.1": version "4.3.2" resolved "https://registry.yarnpkg.com/@salesforce/dev-config/-/dev-config-4.3.2.tgz#10047e2b8d289c93f157ab4243a1b1de57f2d6a2"