From 6eccefe2147e99edccdc7df05ba705c34ad4a72a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 19:50:59 +0000 Subject: [PATCH 1/5] Initial plan From 3b427fe1ee718c791d3dd9ba3bb80bd5c83e9be1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 19:55:50 +0000 Subject: [PATCH 2/5] Add approve-builds command support to rush-pnpm Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com> --- .../src/cli/RushPnpmCommandLineParser.ts | 63 +++++++++++++++++++ .../installManager/WorkspaceInstallManager.ts | 25 +++++++- .../logic/pnpm/PnpmOptionsConfiguration.ts | 10 +++ 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index 7c7787a4362..e748443d1c5 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -359,6 +359,36 @@ export class RushPnpmCommandLineParser { } break; } + case 'approve-builds': { + const semver: typeof import('semver') = await import('semver'); + /** + * The "approve-builds" command was introduced in pnpm version 10.1.0 + * to approve packages for running build scripts when onlyBuiltDependencies is used + */ + if (semver.lt(this._rushConfiguration.packageManagerToolVersion, '10.1.0')) { + this._terminal.writeErrorLine( + PrintUtilities.wrapWords( + `Error: The "pnpm approve-builds" command is added after pnpm@10.1.0.` + + ` Please update "pnpmVersion" >= 10.1.0 in ${RushConstants.rushJsonFilename} file and run "rush update" to use this command.` + ) + '\n' + ); + throw new AlreadyReportedError(); + } + const pnpmOptionsJsonFilename: string = path.join( + this._rushConfiguration.commonRushConfigFolder, + RushConstants.pnpmConfigFilename + ); + if (this._rushConfiguration.rushConfigurationJson.pnpmOptions) { + this._terminal.writeErrorLine( + PrintUtilities.wrapWords( + `Error: The "pnpm approve-builds" command is incompatible with specifying "pnpmOptions" in ${RushConstants.rushJsonFilename} file.` + + ` Please move the content of "pnpmOptions" in ${RushConstants.rushJsonFilename} file to ${pnpmOptionsJsonFilename}` + ) + '\n' + ); + throw new AlreadyReportedError(); + } + break; + } // Known safe case 'audit': @@ -532,6 +562,39 @@ export class RushPnpmCommandLineParser { } break; } + case 'approve-builds': { + if (this._subspace.getPnpmOptions() === undefined) { + const subspaceConfigFolder: string = this._subspace.getSubspaceConfigFolderPath(); + this._terminal.writeErrorLine( + `The "rush-pnpm approve-builds" command cannot proceed without a pnpm-config.json file.` + + ` Create one in this folder: ${subspaceConfigFolder}` + ); + break; + } + + // Example: "C:\MyRepo\common\temp\package.json" + const commonPackageJsonFilename: string = `${subspaceTempFolder}/${FileConstants.PackageJson}`; + const commonPackageJson: JsonObject = JsonFile.load(commonPackageJsonFilename); + const newGlobalOnlyBuiltDependencies: string[] | undefined = + commonPackageJson?.pnpm?.onlyBuiltDependencies; + const pnpmOptions: PnpmOptionsConfiguration | undefined = this._subspace.getPnpmOptions(); + const currentGlobalOnlyBuiltDependencies: string[] | undefined = + pnpmOptions?.globalOnlyBuiltDependencies; + + if (!Objects.areDeepEqual(currentGlobalOnlyBuiltDependencies, newGlobalOnlyBuiltDependencies)) { + // Update onlyBuiltDependencies to pnpm configuration file + pnpmOptions?.updateGlobalOnlyBuiltDependencies(newGlobalOnlyBuiltDependencies); + + // Rerun installation to update + await this._doRushUpdateAsync(); + + this._terminal.writeWarningLine( + `Rush refreshed the ${RushConstants.pnpmConfigFilename} and shrinkwrap file.\n` + + ' Please commit this change to Git.' + ); + } + break; + } } } diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 0c603aef59b..6e48810fc48 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -600,17 +600,36 @@ export class WorkspaceInstallManager extends BaseInstallManager { } } - const onPnpmStdoutChunk: ((chunk: string) => void) | undefined = + const onPnpmStdoutChunk: ((chunk: string) => string | void) | undefined = pnpmTips.length > 0 - ? (chunk: string): void => { + ? (chunk: string): string | void => { // Iterate over the supported custom tip metadata and try to match the chunk. for (const { isMatch, tipId } of pnpmTips) { if (isMatch?.(chunk)) { tipIDsToBePrinted.add(tipId); } } + + // Replace `pnpm approve-builds` with `rush-pnpm approve-builds` when running + // `rush install` or `rush update` to instruct users to use the correct command + const modifiedChunk = chunk.replace( + /pnpm approve-builds/g, + `rush-pnpm --subspace ${subspace.subspaceName} approve-builds` + ); + + // Return modified chunk if it was changed, otherwise return void to keep original + return modifiedChunk !== chunk ? modifiedChunk : undefined; } - : undefined; + : (chunk: string): string | void => { + // Even when no tips are registered, we still need to rewrite the approve-builds command + const modifiedChunk = chunk.replace( + /pnpm approve-builds/g, + `rush-pnpm --subspace ${subspace.subspaceName} approve-builds` + ); + + // Return modified chunk if it was changed, otherwise return void to keep original + return modifiedChunk !== chunk ? modifiedChunk : undefined; + }; try { await Utilities.executeCommandWithRetryAsync( { diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 7e59c637b6b..026ecd29339 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -539,4 +539,14 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true }); } } + + /** + * Updates globalOnlyBuiltDependencies field of the PNPM options in the common/config/rush/pnpm-config.json file. + */ + public updateGlobalOnlyBuiltDependencies(onlyBuiltDependencies: string[] | undefined): void { + this._json.globalOnlyBuiltDependencies = onlyBuiltDependencies; + if (this.jsonFilename) { + JsonFile.save(this._json, this.jsonFilename, { updateExistingFile: true }); + } + } } From 30fc4f1638444ae78cd2ed0bd22addf70bbd0d79 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 20:03:04 +0000 Subject: [PATCH 3/5] Fix linter warnings and update API file Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com> --- common/reviews/api/rush-lib.api.md | 1 + .../logic/installManager/WorkspaceInstallManager.ts | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index e0f49f952c7..22ee65f07da 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -1176,6 +1176,7 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly resolutionMode: PnpmResolutionMode | undefined; readonly strictPeerDependencies: boolean; readonly unsupportedPackageJsonSettings: unknown | undefined; + updateGlobalOnlyBuiltDependencies(onlyBuiltDependencies: string[] | undefined): void; updateGlobalPatchedDependencies(patchedDependencies: Record | undefined): void; readonly useWorkspaces: boolean; } diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 6e48810fc48..ec579ad186a 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -609,24 +609,24 @@ export class WorkspaceInstallManager extends BaseInstallManager { tipIDsToBePrinted.add(tipId); } } - + // Replace `pnpm approve-builds` with `rush-pnpm approve-builds` when running // `rush install` or `rush update` to instruct users to use the correct command - const modifiedChunk = chunk.replace( + const modifiedChunk: string = chunk.replace( /pnpm approve-builds/g, `rush-pnpm --subspace ${subspace.subspaceName} approve-builds` ); - + // Return modified chunk if it was changed, otherwise return void to keep original return modifiedChunk !== chunk ? modifiedChunk : undefined; } : (chunk: string): string | void => { // Even when no tips are registered, we still need to rewrite the approve-builds command - const modifiedChunk = chunk.replace( + const modifiedChunk: string = chunk.replace( /pnpm approve-builds/g, `rush-pnpm --subspace ${subspace.subspaceName} approve-builds` ); - + // Return modified chunk if it was changed, otherwise return void to keep original return modifiedChunk !== chunk ? modifiedChunk : undefined; }; From 106860a88340e25f6ae9f49a13334a3361c71635 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:58:09 +0000 Subject: [PATCH 4/5] Add changelog entry for approve-builds command Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com> --- .../rush/approve-builds_2026-01-26-21-57.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@microsoft/rush/approve-builds_2026-01-26-21-57.json diff --git a/common/changes/@microsoft/rush/approve-builds_2026-01-26-21-57.json b/common/changes/@microsoft/rush/approve-builds_2026-01-26-21-57.json new file mode 100644 index 00000000000..0a04580e4be --- /dev/null +++ b/common/changes/@microsoft/rush/approve-builds_2026-01-26-21-57.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Add support for `rush-pnpm approve-builds` command to persist `globalOnlyBuiltDependencies` in pnpm-config.json", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} From 4ceb9b336900b8d3851ea6479281dce3dc520e19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 26 Jan 2026 23:44:01 +0000 Subject: [PATCH 5/5] DRY up duplicate code and use async JSON loading Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com> --- .../src/cli/RushPnpmCommandLineParser.ts | 2 +- .../installManager/WorkspaceInstallManager.ts | 48 ++++++++----------- 2 files changed, 21 insertions(+), 29 deletions(-) diff --git a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts index e748443d1c5..1980c57f085 100644 --- a/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts +++ b/libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts @@ -574,7 +574,7 @@ export class RushPnpmCommandLineParser { // Example: "C:\MyRepo\common\temp\package.json" const commonPackageJsonFilename: string = `${subspaceTempFolder}/${FileConstants.PackageJson}`; - const commonPackageJson: JsonObject = JsonFile.load(commonPackageJsonFilename); + const commonPackageJson: JsonObject = await JsonFile.loadAsync(commonPackageJsonFilename); const newGlobalOnlyBuiltDependencies: string[] | undefined = commonPackageJson?.pnpm?.onlyBuiltDependencies; const pnpmOptions: PnpmOptionsConfiguration | undefined = this._subspace.getPnpmOptions(); diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index ec579ad186a..4be7bb4a913 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -600,36 +600,28 @@ export class WorkspaceInstallManager extends BaseInstallManager { } } - const onPnpmStdoutChunk: ((chunk: string) => string | void) | undefined = - pnpmTips.length > 0 - ? (chunk: string): string | void => { - // Iterate over the supported custom tip metadata and try to match the chunk. - for (const { isMatch, tipId } of pnpmTips) { - if (isMatch?.(chunk)) { - tipIDsToBePrinted.add(tipId); - } - } + const onPnpmStdoutChunk: ((chunk: string) => string | void) | undefined = ( + chunk: string + ): string | void => { + // Iterate over the supported custom tip metadata and try to match the chunk. + if (pnpmTips.length > 0) { + for (const { isMatch, tipId } of pnpmTips) { + if (isMatch?.(chunk)) { + tipIDsToBePrinted.add(tipId); + } + } + } - // Replace `pnpm approve-builds` with `rush-pnpm approve-builds` when running - // `rush install` or `rush update` to instruct users to use the correct command - const modifiedChunk: string = chunk.replace( - /pnpm approve-builds/g, - `rush-pnpm --subspace ${subspace.subspaceName} approve-builds` - ); + // Replace `pnpm approve-builds` with `rush-pnpm approve-builds` when running + // `rush install` or `rush update` to instruct users to use the correct command + const modifiedChunk: string = chunk.replace( + /pnpm approve-builds/g, + `rush-pnpm --subspace ${subspace.subspaceName} approve-builds` + ); - // Return modified chunk if it was changed, otherwise return void to keep original - return modifiedChunk !== chunk ? modifiedChunk : undefined; - } - : (chunk: string): string | void => { - // Even when no tips are registered, we still need to rewrite the approve-builds command - const modifiedChunk: string = chunk.replace( - /pnpm approve-builds/g, - `rush-pnpm --subspace ${subspace.subspaceName} approve-builds` - ); - - // Return modified chunk if it was changed, otherwise return void to keep original - return modifiedChunk !== chunk ? modifiedChunk : undefined; - }; + // Return modified chunk if it was changed, otherwise return void to keep original + return modifiedChunk !== chunk ? modifiedChunk : undefined; + }; try { await Utilities.executeCommandWithRetryAsync( {