Skip to content

Commit 31fdbeb

Browse files
[rush] Fix minimumReleaseAge and minimumReleaseAgeExclude for PNPM by moving to pnpm-workspace.yaml (#5859)
* Fix minimumReleaseAge and minimumReleaseAgeExclude for PNPM by moving to pnpm-workspace.yaml * Update common/changes/@microsoft/rush/aaron_fix_pnpm_minimum_release_age_2026-07-02-19-35-11.json Co-authored-by: Ian Clanton-Thuon <iclanton@users.noreply.github.com> * Removing unnecessary guards for undefined values --------- Co-authored-by: Aaron Levy <aaronmaxlevy@users.noreply.github.com> Co-authored-by: Ian Clanton-Thuon <iclanton@users.noreply.github.com>
1 parent aceb1fe commit 31fdbeb

6 files changed

Lines changed: 188 additions & 28 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"changes": [
3+
{
4+
"comment": "Fix `minimumReleaseAge` and `minimumReleaseAgeExclude` in `pnpm-config.json` being silently ignored because they were written to package.json instead of `pnpm-workspace.yaml`",
5+
"type": "none",
6+
"packageName": "@microsoft/rush"
7+
}
8+
],
9+
"packageName": "@microsoft/rush",
10+
"email": "aaronmaxlevy@users.noreply.github.com"
11+
}

libraries/rush-lib/src/logic/installManager/InstallHelpers.ts

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ interface ICommonPackageJson extends IPackageJson {
3535
ignoredOptionalDependencies?: typeof PnpmOptionsConfiguration.prototype.globalIgnoredOptionalDependencies;
3636
allowedDeprecatedVersions?: typeof PnpmOptionsConfiguration.prototype.globalAllowedDeprecatedVersions;
3737
patchedDependencies?: typeof PnpmOptionsConfiguration.prototype.globalPatchedDependencies;
38-
minimumReleaseAge?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAgeMinutes;
39-
minimumReleaseAgeExclude?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAgeExclude;
4038
trustPolicy?: typeof PnpmOptionsConfiguration.prototype.trustPolicy;
4139
trustPolicyExclude?: typeof PnpmOptionsConfiguration.prototype.trustPolicyExclude;
4240
trustPolicyIgnoreAfter?: typeof PnpmOptionsConfiguration.prototype.trustPolicyIgnoreAfterMinutes;
@@ -142,28 +140,6 @@ export class InstallHelpers {
142140
commonPackageJson.pnpm.patchedDependencies = pnpmOptions.globalPatchedDependencies;
143141
}
144142

145-
if (pnpmOptions.minimumReleaseAgeMinutes !== undefined || pnpmOptions.minimumReleaseAgeExclude) {
146-
if (semver.lt(pnpmVersion, '10.16.0')) {
147-
terminal.writeWarningLine(
148-
Colorize.yellow(
149-
`Your version of PNPM (${pnpmVersion}) ` +
150-
`doesn't support the "minimumReleaseAgeMinutes" or "minimumReleaseAgeExclude" fields in ` +
151-
`${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
152-
'Remove these fields or upgrade to PNPM 10.16.0 or newer.'
153-
)
154-
);
155-
}
156-
157-
if (pnpmOptions.minimumReleaseAgeMinutes !== undefined) {
158-
// NOTE: the pnpm setting is `minimumReleaseAge`, but the Rush setting is `minimumReleaseAgeMinutes`
159-
commonPackageJson.pnpm.minimumReleaseAge = pnpmOptions.minimumReleaseAgeMinutes;
160-
}
161-
162-
if (pnpmOptions.minimumReleaseAgeExclude) {
163-
commonPackageJson.pnpm.minimumReleaseAgeExclude = pnpmOptions.minimumReleaseAgeExclude;
164-
}
165-
}
166-
167143
if (pnpmOptions.trustPolicy !== undefined) {
168144
if (semver.lt(pnpmVersion, '10.21.0')) {
169145
terminal.writeWarningLine(

libraries/rush-lib/src/logic/installManager/WorkspaceInstallManager.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -476,10 +476,7 @@ export class WorkspaceInstallManager extends BaseInstallManager {
476476
) {
477477
if (pnpmOptions.globalAllowBuilds) {
478478
workspaceFile.setAllowBuilds(pnpmOptions.globalAllowBuilds);
479-
} else if (
480-
pnpmOptions.globalOnlyBuiltDependencies ||
481-
pnpmOptions.globalNeverBuiltDependencies
482-
) {
479+
} else if (pnpmOptions.globalOnlyBuiltDependencies || pnpmOptions.globalNeverBuiltDependencies) {
483480
// Backward compatibility: convert globalOnlyBuiltDependencies/globalNeverBuiltDependencies
484481
// to allowBuilds format for pnpm 11+
485482
const allowBuilds: Record<string, boolean> = {};
@@ -509,6 +506,33 @@ export class WorkspaceInstallManager extends BaseInstallManager {
509506
);
510507
}
511508

509+
// Set minimumReleaseAge/minimumReleaseAgeExclude in the workspace file.
510+
// pnpm does not read these fields from package.json, only from pnpm-workspace.yaml or .npmrc.
511+
if (pnpmOptions.minimumReleaseAgeMinutes !== undefined || pnpmOptions.minimumReleaseAgeExclude) {
512+
if (
513+
this.rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
514+
semver.lt(this.rushConfiguration.rushConfigurationJson.pnpmVersion, '10.16.0')
515+
) {
516+
this._terminal.writeWarningLine(
517+
Colorize.yellow(
518+
`Your version of pnpm (${this.rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
519+
`doesn't support the "minimumReleaseAgeMinutes" or "minimumReleaseAgeExclude" fields in ` +
520+
`${this.rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
521+
'Remove these fields or upgrade to pnpm 10.16.0 or newer.'
522+
)
523+
);
524+
}
525+
526+
if (pnpmOptions.minimumReleaseAgeMinutes !== undefined) {
527+
// NOTE: the pnpm setting is `minimumReleaseAge`, but the Rush setting is `minimumReleaseAgeMinutes`
528+
workspaceFile.setMinimumReleaseAge(pnpmOptions.minimumReleaseAgeMinutes);
529+
}
530+
531+
if (pnpmOptions.minimumReleaseAgeExclude) {
532+
workspaceFile.setMinimumReleaseAgeExclude(pnpmOptions.minimumReleaseAgeExclude);
533+
}
534+
}
535+
512536
// Save the generated workspace file. Don't update the file timestamp unless the content has changed,
513537
// since "rush install" will consider this timestamp
514538
workspaceFile.save(workspaceFile.workspaceFilename, { onlyIfChanged: true });

libraries/rush-lib/src/logic/pnpm/PnpmWorkspaceFile.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ interface IPnpmWorkspaceYaml {
4242
* (SUPPORTED ONLY IN PNPM 11.0.0 AND NEWER)
4343
*/
4444
allowBuilds?: Record<string, boolean>;
45+
/**
46+
* The minimum number of minutes that must pass after a version is published before pnpm will install it.
47+
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
48+
*/
49+
minimumReleaseAge?: number;
50+
/**
51+
* List of package names or patterns that are excluded from the minimumReleaseAge check.
52+
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
53+
*/
54+
minimumReleaseAgeExclude?: string[];
4555
}
4656

4757
export class PnpmWorkspaceFile extends BaseWorkspaceFile {
@@ -53,6 +63,8 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
5363
private _workspacePackages: Set<string>;
5464
private _catalogs: Record<string, Record<string, string>> | undefined;
5565
private _allowBuilds: Record<string, boolean> | undefined;
66+
private _minimumReleaseAge: number | undefined;
67+
private _minimumReleaseAgeExclude: string[] | undefined;
5668

5769
/**
5870
* 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 {
6779
this._workspacePackages = new Set<string>();
6880
this._catalogs = undefined;
6981
this._allowBuilds = undefined;
82+
this._minimumReleaseAge = undefined;
83+
this._minimumReleaseAgeExclude = undefined;
7084
}
7185

7286
/**
@@ -86,6 +100,24 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
86100
this._allowBuilds = allowBuilds;
87101
}
88102

103+
/**
104+
* Sets the minimumReleaseAge setting for the workspace.
105+
* The minimum number of minutes that must pass after a version is published before pnpm will install it.
106+
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
107+
*/
108+
public setMinimumReleaseAge(minimumReleaseAge: number | undefined): void {
109+
this._minimumReleaseAge = minimumReleaseAge;
110+
}
111+
112+
/**
113+
* Sets the minimumReleaseAgeExclude setting for the workspace.
114+
* List of package names or patterns that are excluded from the minimumReleaseAge check.
115+
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
116+
*/
117+
public setMinimumReleaseAgeExclude(minimumReleaseAgeExclude: string[] | undefined): void {
118+
this._minimumReleaseAgeExclude = minimumReleaseAgeExclude;
119+
}
120+
89121
/** @override */
90122
public addPackage(packagePath: string): void {
91123
// Ensure the path is relative to the pnpm-workspace.yaml file
@@ -115,6 +147,10 @@ export class PnpmWorkspaceFile extends BaseWorkspaceFile {
115147
workspaceYaml.allowBuilds = this._allowBuilds;
116148
}
117149

150+
// js-yaml omits mapping entries whose value is `undefined`, so no guard is needed here.
151+
workspaceYaml.minimumReleaseAge = this._minimumReleaseAge;
152+
workspaceYaml.minimumReleaseAgeExclude = this._minimumReleaseAgeExclude;
153+
118154
return yamlModule.dump(workspaceYaml, PNPM_SHRINKWRAP_YAML_FORMAT);
119155
}
120156
}

libraries/rush-lib/src/logic/pnpm/test/PnpmWorkspaceFile.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,4 +242,92 @@ describe(PnpmWorkspaceFile.name, () => {
242242
expect(content).not.toContain('allowBuilds');
243243
});
244244
});
245+
246+
describe('minimumReleaseAge functionality', () => {
247+
it('generates workspace file with minimumReleaseAge', () => {
248+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
249+
workspaceFile.addPackage(path.join(projectsDir, 'app1'));
250+
251+
workspaceFile.setMinimumReleaseAge(20160);
252+
253+
workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });
254+
255+
const content: string = FileSystem.readFile(workspaceFilePath);
256+
expect(content).toMatchSnapshot();
257+
});
258+
259+
it('generates workspace file with minimumReleaseAge and minimumReleaseAgeExclude', () => {
260+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
261+
workspaceFile.addPackage(path.join(projectsDir, 'app1'));
262+
263+
workspaceFile.setMinimumReleaseAge(1440);
264+
workspaceFile.setMinimumReleaseAgeExclude(['webpack', '@myorg/*']);
265+
266+
workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });
267+
268+
const content: string = FileSystem.readFile(workspaceFilePath);
269+
expect(content).toMatchSnapshot();
270+
});
271+
272+
it('generates workspace file with minimumReleaseAgeExclude only', () => {
273+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
274+
workspaceFile.addPackage(path.join(projectsDir, 'app1'));
275+
276+
workspaceFile.setMinimumReleaseAgeExclude(['webpack']);
277+
278+
workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });
279+
280+
const content: string = FileSystem.readFile(workspaceFilePath);
281+
expect(content).toMatchSnapshot();
282+
});
283+
284+
it('handles zero value for minimumReleaseAge', () => {
285+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
286+
workspaceFile.addPackage(path.join(projectsDir, 'app1'));
287+
288+
workspaceFile.setMinimumReleaseAge(0);
289+
290+
workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });
291+
292+
const content: string = FileSystem.readFile(workspaceFilePath);
293+
expect(content).toContain('minimumReleaseAge: 0');
294+
});
295+
296+
it('handles undefined minimumReleaseAge', () => {
297+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
298+
workspaceFile.addPackage(path.join(projectsDir, 'app1'));
299+
300+
workspaceFile.setMinimumReleaseAge(undefined);
301+
workspaceFile.setMinimumReleaseAgeExclude(undefined);
302+
303+
workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });
304+
305+
const content: string = FileSystem.readFile(workspaceFilePath);
306+
expect(content).not.toContain('minimumReleaseAge');
307+
});
308+
309+
it('passes through an explicitly-set empty minimumReleaseAgeExclude', () => {
310+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
311+
workspaceFile.addPackage(path.join(projectsDir, 'app1'));
312+
313+
workspaceFile.setMinimumReleaseAgeExclude([]);
314+
315+
workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });
316+
317+
const content: string = FileSystem.readFile(workspaceFilePath);
318+
expect(content).toContain('minimumReleaseAgeExclude: []');
319+
});
320+
321+
it('omits an undefined minimumReleaseAgeExclude', () => {
322+
const workspaceFile: PnpmWorkspaceFile = new PnpmWorkspaceFile(workspaceFilePath);
323+
workspaceFile.addPackage(path.join(projectsDir, 'app1'));
324+
325+
workspaceFile.setMinimumReleaseAgeExclude(undefined);
326+
327+
workspaceFile.save(workspaceFilePath, { onlyIfChanged: true });
328+
329+
const content: string = FileSystem.readFile(workspaceFilePath);
330+
expect(content).not.toContain('minimumReleaseAgeExclude');
331+
});
332+
});
245333
});

libraries/rush-lib/src/logic/pnpm/test/__snapshots__/PnpmWorkspaceFile.test.ts.snap

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,28 @@ exports[`PnpmWorkspaceFile catalog functionality handles undefined catalog 1`] =
8686
- projects/app1
8787
"
8888
`;
89+
90+
exports[`PnpmWorkspaceFile minimumReleaseAge functionality generates workspace file with minimumReleaseAge 1`] = `
91+
"minimumReleaseAge: 20160
92+
packages:
93+
- projects/app1
94+
"
95+
`;
96+
97+
exports[`PnpmWorkspaceFile minimumReleaseAge functionality generates workspace file with minimumReleaseAge and minimumReleaseAgeExclude 1`] = `
98+
"minimumReleaseAge: 1440
99+
minimumReleaseAgeExclude:
100+
- webpack
101+
- '@myorg/*'
102+
packages:
103+
- projects/app1
104+
"
105+
`;
106+
107+
exports[`PnpmWorkspaceFile minimumReleaseAge functionality generates workspace file with minimumReleaseAgeExclude only 1`] = `
108+
"minimumReleaseAgeExclude:
109+
- webpack
110+
packages:
111+
- projects/app1
112+
"
113+
`;

0 commit comments

Comments
 (0)