Skip to content

Commit 569e2f4

Browse files
authored
[rush-lib] Add support for PNPM's minimumReleaseAge setting (#5405)
* [rush-lib] Add support for PNPM's minimumReleaseAge setting This change adds support for PNPM's minimumReleaseAge setting in Rush's pnpm-config.json file to help mitigate supply chain attacks by requiring a minimum age (in minutes) for package versions before installation. Fixes #5372 * Revert manual API changes - let API Extractor regenerate * Revert "Revert manual API changes - let API Extractor regenerate" This reverts commit 6def020. * Update API documentation with minimumReleaseAge property * Remove rush-lib change file as requested by reviewer * Add minimumReleaseAgeExclude support and refactor to use package.json - Add minimumReleaseAgeExclude property to PnpmOptionsConfiguration - Update pnpm-config.schema.json with new property definition - Add documentation to template pnpm-config.json - Write minimumReleaseAge and minimumReleaseAgeExclude to package.json instead of passing as CLI args - Add PNPM version check (10.16.0+) in InstallHelpers - Remove command-line argument passing from BaseInstallManager - Update tests to verify minimumReleaseAgeExclude functionality - Fix TSDoc warning for @ character escaping Addresses code review feedback from PR #5405
1 parent 11550cd commit 569e2f4

File tree

8 files changed

+140
-0
lines changed

8 files changed

+140
-0
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "Add support for PNPM's minimumReleaseAge setting to help mitigate supply chain attacks",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@microsoft/rush"
10+
}

common/reviews/api/rush-lib.api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,8 @@ export interface _IPnpmOptionsJson extends IPackageManagerOptionsJsonBase {
773773
globalPackageExtensions?: Record<string, IPnpmPackageExtension>;
774774
globalPatchedDependencies?: Record<string, string>;
775775
globalPeerDependencyRules?: IPnpmPeerDependencyRules;
776+
minimumReleaseAge?: number;
777+
minimumReleaseAgeExclude?: string[];
776778
pnpmLockfilePolicies?: IPnpmLockfilePolicies;
777779
pnpmStore?: PnpmStoreLocation;
778780
preventManualShrinkwrapChanges?: boolean;
@@ -1188,6 +1190,8 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
11881190
static loadFromJsonFileOrThrow(jsonFilePath: string, commonTempFolder: string): PnpmOptionsConfiguration;
11891191
// @internal (undocumented)
11901192
static loadFromJsonObject(json: _IPnpmOptionsJson, commonTempFolder: string): PnpmOptionsConfiguration;
1193+
readonly minimumReleaseAge: number | undefined;
1194+
readonly minimumReleaseAgeExclude: string[] | undefined;
11911195
readonly pnpmLockfilePolicies: IPnpmLockfilePolicies | undefined;
11921196
readonly pnpmStore: PnpmStoreLocation;
11931197
readonly pnpmStorePath: string;

libraries/rush-lib/assets/rush-init/common/config/rush/pnpm-config.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,38 @@
6767
*/
6868
/*[LINE "DEMO"]*/ "autoInstallPeers": false,
6969

70+
/**
71+
* The minimum number of minutes that must pass after a version is published before pnpm will install it.
72+
* This setting helps reduce the risk of installing compromised packages, as malicious releases are typically
73+
* discovered and removed within a short time frame.
74+
*
75+
* For example, the following setting ensures that only packages released at least one day ago can be installed:
76+
*
77+
* "minimumReleaseAge": 1440
78+
*
79+
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
80+
*
81+
* PNPM documentation: https://pnpm.io/settings#minimumreleaseage
82+
*
83+
* The default value is 0 (disabled).
84+
*/
85+
/*[LINE "HYPOTHETICAL"]*/ "minimumReleaseAge": 1440,
86+
87+
/**
88+
* An array of package names or patterns to exclude from the minimumReleaseAge check.
89+
* This allows certain trusted packages to be installed immediately after publication.
90+
* Patterns are supported using glob syntax (e.g., "@myorg/*" to exclude all packages from an organization).
91+
*
92+
* For example:
93+
*
94+
* "minimumReleaseAgeExclude": ["webpack", "react", "@myorg/*"]
95+
*
96+
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
97+
*
98+
* PNPM documentation: https://pnpm.io/settings#minimumreleaseageexclude
99+
*/
100+
/*[LINE "HYPOTHETICAL"]*/ "minimumReleaseAgeExclude": ["@myorg/*"],
101+
70102
/**
71103
* If true, then Rush will add the `--strict-peer-dependencies` command-line parameter when
72104
* invoking PNPM. This causes `rush update` to fail if there are unsatisfied peer dependencies,

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ interface ICommonPackageJson extends IPackageJson {
3434
ignoredOptionalDependencies?: typeof PnpmOptionsConfiguration.prototype.globalIgnoredOptionalDependencies;
3535
allowedDeprecatedVersions?: typeof PnpmOptionsConfiguration.prototype.globalAllowedDeprecatedVersions;
3636
patchedDependencies?: typeof PnpmOptionsConfiguration.prototype.globalPatchedDependencies;
37+
minimumReleaseAge?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAge;
38+
minimumReleaseAgeExclude?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAgeExclude;
3739
};
3840
}
3941

@@ -100,6 +102,30 @@ export class InstallHelpers {
100102
commonPackageJson.pnpm.patchedDependencies = pnpmOptions.globalPatchedDependencies;
101103
}
102104

105+
if (pnpmOptions.minimumReleaseAge !== undefined || pnpmOptions.minimumReleaseAgeExclude) {
106+
if (
107+
rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined &&
108+
semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '10.16.0')
109+
) {
110+
terminal.writeWarningLine(
111+
Colorize.yellow(
112+
`Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` +
113+
`doesn't support the "minimumReleaseAge" or "minimumReleaseAgeExclude" fields in ` +
114+
`${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` +
115+
'Remove these fields or upgrade to pnpm 10.16.0 or newer.'
116+
)
117+
);
118+
}
119+
120+
if (pnpmOptions.minimumReleaseAge !== undefined) {
121+
commonPackageJson.pnpm.minimumReleaseAge = pnpmOptions.minimumReleaseAge;
122+
}
123+
124+
if (pnpmOptions.minimumReleaseAgeExclude) {
125+
commonPackageJson.pnpm.minimumReleaseAgeExclude = pnpmOptions.minimumReleaseAgeExclude;
126+
}
127+
}
128+
103129
if (pnpmOptions.unsupportedPackageJsonSettings) {
104130
merge(commonPackageJson, pnpmOptions.unsupportedPackageJsonSettings);
105131
}

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,14 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase {
138138
* {@inheritDoc PnpmOptionsConfiguration.autoInstallPeers}
139139
*/
140140
autoInstallPeers?: boolean;
141+
/**
142+
* {@inheritDoc PnpmOptionsConfiguration.minimumReleaseAge}
143+
*/
144+
minimumReleaseAge?: number;
145+
/**
146+
* {@inheritDoc PnpmOptionsConfiguration.minimumReleaseAgeExclude}
147+
*/
148+
minimumReleaseAgeExclude?: string[];
141149
/**
142150
* {@inheritDoc PnpmOptionsConfiguration.alwaysInjectDependenciesFromOtherSubspaces}
143151
*/
@@ -258,6 +266,33 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
258266
*/
259267
public readonly autoInstallPeers: boolean | undefined;
260268

269+
/**
270+
* The minimum number of minutes that must pass after a version is published before pnpm will install it.
271+
* This setting helps reduce the risk of installing compromised packages, as malicious releases are typically
272+
* discovered and removed within a short time frame.
273+
*
274+
* @remarks
275+
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
276+
*
277+
* PNPM documentation: https://pnpm.io/settings#minimumreleaseage
278+
*
279+
* The default value is 0 (disabled).
280+
*/
281+
public readonly minimumReleaseAge: number | undefined;
282+
283+
/**
284+
* List of package names or patterns that are excluded from the minimumReleaseAge check.
285+
* These packages will always install the newest version immediately, even if minimumReleaseAge is set.
286+
*
287+
* @remarks
288+
* (SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)
289+
*
290+
* PNPM documentation: https://pnpm.io/settings#minimumreleaseageexclude
291+
*
292+
* Example: ["webpack", "react", "\@myorg/*"]
293+
*/
294+
public readonly minimumReleaseAgeExclude: string[] | undefined;
295+
261296
/**
262297
* If true, then `rush update` add injected install options for all cross-subspace
263298
* workspace dependencies, to avoid subspace doppelganger issue.
@@ -425,6 +460,8 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration
425460
this._globalPatchedDependencies = json.globalPatchedDependencies;
426461
this.resolutionMode = json.resolutionMode;
427462
this.autoInstallPeers = json.autoInstallPeers;
463+
this.minimumReleaseAge = json.minimumReleaseAge;
464+
this.minimumReleaseAgeExclude = json.minimumReleaseAgeExclude;
428465
this.alwaysInjectDependenciesFromOtherSubspaces = json.alwaysInjectDependenciesFromOtherSubspaces;
429466
this.alwaysFullInstall = json.alwaysFullInstall;
430467
this.pnpmLockfilePolicies = json.pnpmLockfilePolicies;

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,17 @@ describe(PnpmOptionsConfiguration.name, () => {
7373
'level'
7474
]);
7575
});
76+
77+
it('loads minimumReleaseAge', () => {
78+
const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow(
79+
`${__dirname}/jsonFiles/pnpm-config-minimumReleaseAge.json`,
80+
fakeCommonTempFolder
81+
);
82+
83+
expect(pnpmConfiguration.minimumReleaseAge).toEqual(1440);
84+
expect(TestUtilities.stripAnnotations(pnpmConfiguration.minimumReleaseAgeExclude)).toEqual([
85+
'webpack',
86+
'@myorg/*'
87+
]);
88+
});
7689
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"minimumReleaseAge": 1440,
3+
"minimumReleaseAgeExclude": ["webpack", "@myorg/*"]
4+
}

libraries/rush-lib/src/schemas/pnpm-config.schema.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,20 @@
191191
"type": "boolean"
192192
},
193193

194+
"minimumReleaseAge": {
195+
"description": "The minimum number of minutes that must pass after a version is published before pnpm will install it. This setting helps reduce the risk of installing compromised packages, as malicious releases are typically discovered and removed within a short time frame.\n\n(SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)\n\nPNPM documentation: https://pnpm.io/settings#minimumreleaseage\n\nThe default value is 0 (disabled).",
196+
"type": "number"
197+
},
198+
199+
"minimumReleaseAgeExclude": {
200+
"description": "List of package names or patterns that are excluded from the minimumReleaseAge check. These packages will always install the newest version immediately, even if minimumReleaseAge is set. Supports glob patterns (e.g., \"@myorg/*\").\n\n(SUPPORTED ONLY IN PNPM 10.16.0 AND NEWER)\n\nPNPM documentation: https://pnpm.io/settings#minimumreleaseageexclude\n\nExample: [\"webpack\", \"react\", \"@myorg/*\"]",
201+
"type": "array",
202+
"items": {
203+
"description": "Package name or pattern",
204+
"type": "string"
205+
}
206+
},
207+
194208
"alwaysFullInstall": {
195209
"description": "(EXPERIMENTAL) If 'true', then filtered installs ('rush install --to my-project') * will be disregarded, instead always performing a full installation of the lockfile.",
196210
"type": "boolean"

0 commit comments

Comments
 (0)