From 4d64d64d59e6b2fcb2d7fcda0d3f2c5aeae38608 Mon Sep 17 00:00:00 2001 From: Aaron Levy Date: Thu, 2 Jul 2026 12:34:14 -0700 Subject: [PATCH] Fix minimumReleaseAge and minimumReleaseAgeExclude for PNPM by moving to pnpm-workspace.yaml --- ...nimum_release_age_2026-07-02-19-35-11.json | 11 +++ .../logic/installManager/InstallHelpers.ts | 24 ------ .../installManager/WorkspaceInstallManager.ts | 32 +++++++- .../src/logic/pnpm/PnpmWorkspaceFile.ts | 40 ++++++++++ .../logic/pnpm/test/PnpmWorkspaceFile.test.ts | 76 +++++++++++++++++++ .../PnpmWorkspaceFile.test.ts.snap | 25 ++++++ 6 files changed, 180 insertions(+), 28 deletions(-) create mode 100644 common/changes/@microsoft/rush/aaron_fix_pnpm_minimum_release_age_2026-07-02-19-35-11.json diff --git a/common/changes/@microsoft/rush/aaron_fix_pnpm_minimum_release_age_2026-07-02-19-35-11.json b/common/changes/@microsoft/rush/aaron_fix_pnpm_minimum_release_age_2026-07-02-19-35-11.json new file mode 100644 index 0000000000..2ccb1ec1a9 --- /dev/null +++ b/common/changes/@microsoft/rush/aaron_fix_pnpm_minimum_release_age_2026-07-02-19-35-11.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Fix minimumReleaseAge and minimumReleaseAgeExclude in pnpm-config.json being silently ignored because they were written to package.json instead of pnpm-workspace.yaml", + "type": "none", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "aaronmaxlevy@users.noreply.github.com" +} \ No newline at end of file diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 1296125a98..9dfa031b72 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -35,8 +35,6 @@ interface ICommonPackageJson extends IPackageJson { ignoredOptionalDependencies?: typeof PnpmOptionsConfiguration.prototype.globalIgnoredOptionalDependencies; allowedDeprecatedVersions?: typeof PnpmOptionsConfiguration.prototype.globalAllowedDeprecatedVersions; patchedDependencies?: typeof PnpmOptionsConfiguration.prototype.globalPatchedDependencies; - minimumReleaseAge?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAgeMinutes; - minimumReleaseAgeExclude?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAgeExclude; trustPolicy?: typeof PnpmOptionsConfiguration.prototype.trustPolicy; trustPolicyExclude?: typeof PnpmOptionsConfiguration.prototype.trustPolicyExclude; trustPolicyIgnoreAfter?: typeof PnpmOptionsConfiguration.prototype.trustPolicyIgnoreAfterMinutes; @@ -142,28 +140,6 @@ export class InstallHelpers { commonPackageJson.pnpm.patchedDependencies = pnpmOptions.globalPatchedDependencies; } - if (pnpmOptions.minimumReleaseAgeMinutes !== undefined || pnpmOptions.minimumReleaseAgeExclude) { - if (semver.lt(pnpmVersion, '10.16.0')) { - terminal.writeWarningLine( - Colorize.yellow( - `Your version of PNPM (${pnpmVersion}) ` + - `doesn't support the "minimumReleaseAgeMinutes" or "minimumReleaseAgeExclude" fields in ` + - `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + - 'Remove these fields or upgrade to PNPM 10.16.0 or newer.' - ) - ); - } - - if (pnpmOptions.minimumReleaseAgeMinutes !== undefined) { - // NOTE: the pnpm setting is `minimumReleaseAge`, but the Rush setting is `minimumReleaseAgeMinutes` - commonPackageJson.pnpm.minimumReleaseAge = pnpmOptions.minimumReleaseAgeMinutes; - } - - if (pnpmOptions.minimumReleaseAgeExclude) { - commonPackageJson.pnpm.minimumReleaseAgeExclude = pnpmOptions.minimumReleaseAgeExclude; - } - } - if (pnpmOptions.trustPolicy !== undefined) { if (semver.lt(pnpmVersion, '10.21.0')) { terminal.writeWarningLine( diff --git a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts index 222dd45141..24029bc41c 100644 --- a/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts +++ b/libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts @@ -476,10 +476,7 @@ export class WorkspaceInstallManager extends BaseInstallManager { ) { if (pnpmOptions.globalAllowBuilds) { workspaceFile.setAllowBuilds(pnpmOptions.globalAllowBuilds); - } else if ( - pnpmOptions.globalOnlyBuiltDependencies || - pnpmOptions.globalNeverBuiltDependencies - ) { + } else if (pnpmOptions.globalOnlyBuiltDependencies || pnpmOptions.globalNeverBuiltDependencies) { // Backward compatibility: convert globalOnlyBuiltDependencies/globalNeverBuiltDependencies // to allowBuilds format for pnpm 11+ const allowBuilds: Record = {}; @@ -509,6 +506,33 @@ export class WorkspaceInstallManager extends BaseInstallManager { ); } + // Set minimumReleaseAge/minimumReleaseAgeExclude in the workspace file. + // pnpm does not read these fields from package.json, only from pnpm-workspace.yaml or .npmrc. + if (pnpmOptions.minimumReleaseAgeMinutes !== undefined || pnpmOptions.minimumReleaseAgeExclude) { + if ( + this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '10.16.0') + ) { + this._terminal.writeWarningLine( + Colorize.yellow( + `Your version of pnpm (${this.rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + + `doesn't support the "minimumReleaseAgeMinutes" or "minimumReleaseAgeExclude" fields in ` + + `${this.rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove these fields or upgrade to pnpm 10.16.0 or newer.' + ) + ); + } + + if (pnpmOptions.minimumReleaseAgeMinutes !== undefined) { + // NOTE: the pnpm setting is `minimumReleaseAge`, but the Rush setting is `minimumReleaseAgeMinutes` + workspaceFile.setMinimumReleaseAge(pnpmOptions.minimumReleaseAgeMinutes); + } + + if (pnpmOptions.minimumReleaseAgeExclude) { + workspaceFile.setMinimumReleaseAgeExclude(pnpmOptions.minimumReleaseAgeExclude); + } + } + // Save the generated workspace file. Don't update the file timestamp unless the content has changed, // since "rush install" will consider this timestamp workspaceFile.save(workspaceFile.workspaceFilename, { onlyIfChanged: true }); diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts index 850aaf1687..df3e42abaf 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts @@ -42,6 +42,16 @@ interface IPnpmWorkspaceYaml { * (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER) */ allowBuilds?: Record; + /** + * The minimum number of minutes that must pass after a version is published before pnpm will install it. + * (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER) + */ + minimumReleaseAge?: number; + /** + * List of package names or patterns that are excluded from the minimumReleaseAge check. + * (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER) + */ + minimumReleaseAgeExclude?: string[]; } export class PnpmWorkspaceFile extends BaseWorkspaceFile { @@ -53,6 +63,8 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { private _workspacePackages: Set; private _catalogs: Record> | undefined; private _allowBuilds: Record | undefined; + private _minimumReleaseAge: number | undefined; + private _minimumReleaseAgeExclude: string[] | undefined; /** * The PNPM workspace file is used to specify the location of workspaces relative to the root @@ -67,6 +79,8 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { this._workspacePackages = new Set(); this._catalogs = undefined; this._allowBuilds = undefined; + this._minimumReleaseAge = undefined; + this._minimumReleaseAgeExclude = undefined; } /** @@ -86,6 +100,24 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { this._allowBuilds = allowBuilds; } + /** + * Sets the minimumReleaseAge setting for the workspace. + * The minimum number of minutes that must pass after a version is published before pnpm will install it. + * (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER) + */ + public setMinimumReleaseAge(minimumReleaseAge: number | undefined): void { + this._minimumReleaseAge = minimumReleaseAge; + } + + /** + * Sets the minimumReleaseAgeExclude setting for the workspace. + * List of package names or patterns that are excluded from the minimumReleaseAge check. + * (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER) + */ + public setMinimumReleaseAgeExclude(minimumReleaseAgeExclude: string[] | undefined): void { + this._minimumReleaseAgeExclude = minimumReleaseAgeExclude; + } + /** @override */ public addPackage(packagePath: string): void { // Ensure the path is relative to the pnpm-workspace.yaml file @@ -115,6 +147,14 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile { workspaceYaml.allowBuilds = this._allowBuilds; } + if (this._minimumReleaseAge !== undefined) { + workspaceYaml.minimumReleaseAge = this._minimumReleaseAge; + } + + if (this._minimumReleaseAgeExclude && this._minimumReleaseAgeExclude.length > 0) { + workspaceYaml.minimumReleaseAgeExclude = this._minimumReleaseAgeExclude; + } + return yamlModule.dump(workspaceYaml, PNPM_SHRINKWRAP_YAML_FORMAT); } } diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts index a113bcc0b8..4f856860f2 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts @@ -242,4 +242,80 @@ describe(PnpmWorkspaceFile.name, () => { expect(content).not.toContain('allowBuilds'); }); }); + + describe('minimumReleaseAge functionality', () => { + it('generates workspace file with minimumReleaseAge', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setMinimumReleaseAge(20160); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + + it('generates workspace file with minimumReleaseAge and minimumReleaseAgeExclude', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setMinimumReleaseAge(1440); + workspaceFile.setMinimumReleaseAgeExclude(['webpack', '@myorg/*']); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + + it('generates workspace file with minimumReleaseAgeExclude only', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setMinimumReleaseAgeExclude(['webpack']); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toMatchSnapshot(); + }); + + it('handles zero value for minimumReleaseAge', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setMinimumReleaseAge(0); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).toContain('minimumReleaseAge: 0'); + }); + + it('handles undefined minimumReleaseAge', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setMinimumReleaseAge(undefined); + workspaceFile.setMinimumReleaseAgeExclude(undefined); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('minimumReleaseAge'); + }); + + it('handles empty minimumReleaseAgeExclude', () => { + const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath); + workspaceFile.addPackage(path.join(projectsDir, 'app1')); + + workspaceFile.setMinimumReleaseAgeExclude([]); + + workspaceFile.save(workspaceFilePath, { onlyIfChanged: true }); + + const content: string = FileSystem.readFile(workspaceFilePath); + expect(content).not.toContain('minimumReleaseAgeExclude'); + }); + }); }); diff --git a/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap b/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap index 18f15e7456..7cf5594bd9 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap +++ b/libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap @@ -86,3 +86,28 @@ exports[`PnpmWorkspaceFile catalog functionality handles undefined catalog 1`] = - projects/app1 " `; + +exports[`PnpmWorkspaceFile minimumReleaseAge functionality generates workspace file with minimumReleaseAge 1`] = ` +"minimumReleaseAge: 20160 +packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile minimumReleaseAge functionality generates workspace file with minimumReleaseAge and minimumReleaseAgeExclude 1`] = ` +"minimumReleaseAge: 1440 +minimumReleaseAgeExclude: + - webpack + - '@myorg/*' +packages: + - projects/app1 +" +`; + +exports[`PnpmWorkspaceFile minimumReleaseAge functionality generates workspace file with minimumReleaseAgeExclude only 1`] = ` +"minimumReleaseAgeExclude: + - webpack +packages: + - projects/app1 +" +`;