diff --git a/common/changes/@microsoft/rush/feature-add-trust-policy-support_2026-04-07-00-00.json b/common/changes/@microsoft/rush/feature-add-trust-policy-support_2026-04-07-00-00.json new file mode 100644 index 00000000000..e2ac3443af1 --- /dev/null +++ b/common/changes/@microsoft/rush/feature-add-trust-policy-support_2026-04-07-00-00.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Add support for pnpm trustPolicy, trustPolicyExclude, and trustPolicyIgnoreAfter settings in pnpm-config.json", + "type": "minor", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "fpapado@users.noreply.github.com" +} diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index dbbb440e40b..2a96f43bdc8 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -767,6 +767,9 @@ export interface _IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { preventManualShrinkwrapChanges?: boolean; resolutionMode?: PnpmResolutionMode; strictPeerDependencies?: boolean; + trustPolicy?: PnpmTrustPolicy; + trustPolicyExclude?: string[]; + trustPolicyIgnoreAfter?: number; unsupportedPackageJsonSettings?: unknown; useWorkspaces?: boolean; } @@ -1194,6 +1197,9 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration readonly preventManualShrinkwrapChanges: boolean; readonly resolutionMode: PnpmResolutionMode | undefined; readonly strictPeerDependencies: boolean; + readonly trustPolicy: PnpmTrustPolicy | undefined; + readonly trustPolicyExclude: string[] | undefined; + readonly trustPolicyIgnoreAfter: number | undefined; readonly unsupportedPackageJsonSettings: unknown | undefined; updateGlobalOnlyBuiltDependencies(onlyBuiltDependencies: string[] | undefined): void; updateGlobalPatchedDependencies(patchedDependencies: Record | undefined): void; @@ -1209,6 +1215,9 @@ export type PnpmStoreLocation = 'local' | 'global'; // @public @deprecated (undocumented) export type PnpmStoreOptions = PnpmStoreLocation; +// @public +export type PnpmTrustPolicy = 'no-downgrade' | 'off'; + // @beta (undocumented) export class ProjectChangeAnalyzer { constructor(rushConfiguration: RushConfiguration); diff --git a/libraries/rush-lib/assets/rush-init/common/config/rush/pnpm-config.json b/libraries/rush-lib/assets/rush-init/common/config/rush/pnpm-config.json index 5aeba49a31c..c1c6b5a497f 100644 --- a/libraries/rush-lib/assets/rush-init/common/config/rush/pnpm-config.json +++ b/libraries/rush-lib/assets/rush-init/common/config/rush/pnpm-config.json @@ -99,6 +99,58 @@ */ /*[LINE "HYPOTHETICAL"]*/ "minimumReleaseAgeExclude": ["@myorg/*"], + /** + * The trust policy controls whether pnpm should block installation of package versions where + * the trust level has decreased (e.g., a package previously published with provenance is now + * published without it). Setting this to `"no-downgrade"` enables the protection. + * + * (SUPPORTED ONLY IN PNPM 10.21.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#trustpolicy + * + * Possible values are: `off` and `no-downgrade`. + * The default is `off`. + */ + /*[LINE "HYPOTHETICAL"]*/ "trustPolicy": "no-downgrade", + + /** + * An array of package names or patterns to exclude from the trust policy check. + * These packages will be allowed to install even if their trust level has decreased. + * Patterns are supported using glob syntax (e.g., "@myorg/*" to exclude all packages + * from an organization). + * + * For example: + * + * "trustPolicyExclude": ["@babel/core@7.28.5", "chokidar@4.0.3", "@myorg/*"] + * + * (SUPPORTED ONLY IN PNPM 10.22.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#trustpolicyexclude + * + * The default value is []. + */ + /*[LINE "HYPOTHETICAL"]*/ "trustPolicyExclude": ["@myorg/*"], + + /** + * The number of minutes after which pnpm will ignore trust level downgrades. Packages + * published longer ago than this threshold will not be blocked even if their trust level + * has decreased. This is useful when enabling strict trust policies, as it allows older versions + * of packages (which may lack a process for publishing with signatures or provenance) to be + * installed without manual exclusion, assuming they are safe due to their age. + * + * For example, the following setting ignores trust level changes for packages published + * more than 14 days ago: + * + * "trustPolicyIgnoreAfter": 20160 + * + * (SUPPORTED ONLY IN PNPM 10.27.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#trustpolicyignoreafter + * + * The default value is undefined (no exclusion). + */ + /*[LINE "HYPOTHETICAL"]*/ "trustPolicyIgnoreAfter": 20160, + /** * If true, then Rush will add the `--strict-peer-dependencies` command-line parameter when * invoking PNPM. This causes `rush update` to fail if there are unsatisfied peer dependencies, diff --git a/libraries/rush-lib/src/index.ts b/libraries/rush-lib/src/index.ts index a91cea23a98..a333473716d 100644 --- a/libraries/rush-lib/src/index.ts +++ b/libraries/rush-lib/src/index.ts @@ -48,7 +48,8 @@ export { type IPnpmPeerDependenciesMeta, type PnpmStoreOptions, PnpmOptionsConfiguration, - type PnpmResolutionMode + type PnpmResolutionMode, + type PnpmTrustPolicy } from './logic/pnpm/PnpmOptionsConfiguration'; export { BuildCacheConfiguration } from './api/BuildCacheConfiguration'; diff --git a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts index 515f9e5bec3..c65e3fc197a 100644 --- a/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts +++ b/libraries/rush-lib/src/logic/installManager/InstallHelpers.ts @@ -37,6 +37,9 @@ interface ICommonPackageJson extends IPackageJson { patchedDependencies?: typeof PnpmOptionsConfiguration.prototype.globalPatchedDependencies; minimumReleaseAge?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAge; minimumReleaseAgeExclude?: typeof PnpmOptionsConfiguration.prototype.minimumReleaseAgeExclude; + trustPolicy?: typeof PnpmOptionsConfiguration.prototype.trustPolicy; + trustPolicyExclude?: typeof PnpmOptionsConfiguration.prototype.trustPolicyExclude; + trustPolicyIgnoreAfter?: typeof PnpmOptionsConfiguration.prototype.trustPolicyIgnoreAfter; }; } @@ -145,6 +148,60 @@ export class InstallHelpers { } } + if (pnpmOptions.trustPolicy !== undefined) { + if ( + rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '10.21.0') + ) { + terminal.writeWarningLine( + Colorize.yellow( + `Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + + `doesn't support the "trustPolicy" field in ` + + `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove this field or upgrade to pnpm 10.21.0 or newer.' + ) + ); + } + + commonPackageJson.pnpm.trustPolicy = pnpmOptions.trustPolicy; + } + + if (pnpmOptions.trustPolicyExclude) { + if ( + rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '10.22.0') + ) { + terminal.writeWarningLine( + Colorize.yellow( + `Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + + `doesn't support the "trustPolicyExclude" field in ` + + `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove this field or upgrade to pnpm 10.22.0 or newer.' + ) + ); + } + + commonPackageJson.pnpm.trustPolicyExclude = pnpmOptions.trustPolicyExclude; + } + + if (pnpmOptions.trustPolicyIgnoreAfter !== undefined) { + if ( + rushConfiguration.rushConfigurationJson.pnpmVersion !== undefined && + semver.lt(rushConfiguration.rushConfigurationJson.pnpmVersion, '10.27.0') + ) { + terminal.writeWarningLine( + Colorize.yellow( + `Your version of pnpm (${rushConfiguration.rushConfigurationJson.pnpmVersion}) ` + + `doesn't support the "trustPolicyIgnoreAfter" field in ` + + `${rushConfiguration.commonRushConfigFolder}/${RushConstants.pnpmConfigFilename}. ` + + 'Remove this field or upgrade to pnpm 10.27.0 or newer.' + ) + ); + } + + commonPackageJson.pnpm.trustPolicyIgnoreAfter = pnpmOptions.trustPolicyIgnoreAfter; + } + if (pnpmOptions.unsupportedPackageJsonSettings) { merge(commonPackageJson, pnpmOptions.unsupportedPackageJsonSettings); } diff --git a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts index 026ecd29339..b6dba629d75 100644 --- a/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts +++ b/libraries/rush-lib/src/logic/pnpm/PnpmOptionsConfiguration.ts @@ -34,6 +34,16 @@ export type PnpmStoreOptions = PnpmStoreLocation; */ export type PnpmResolutionMode = 'highest' | 'time-based' | 'lowest-direct'; +/** + * Possible values for the `trustPolicy` setting in Rush's pnpm-config.json file. + * @remarks + * These values correspond to PNPM's `trust-policy` setting, which is documented here: + * {@link https://pnpm.io/settings#trustpolicy} + * + * @public + */ +export type PnpmTrustPolicy = 'no-downgrade' | 'off'; + /** * Possible values for the `pnpmLockfilePolicies` setting in Rush's pnpm-config.json file. * @public @@ -150,6 +160,18 @@ export interface IPnpmOptionsJson extends IPackageManagerOptionsJsonBase { * {@inheritDoc PnpmOptionsConfiguration.minimumReleaseAgeExclude} */ minimumReleaseAgeExclude?: string[]; + /** + * {@inheritDoc PnpmOptionsConfiguration.trustPolicy} + */ + trustPolicy?: PnpmTrustPolicy; + /** + * {@inheritDoc PnpmOptionsConfiguration.trustPolicyExclude} + */ + trustPolicyExclude?: string[]; + /** + * {@inheritDoc PnpmOptionsConfiguration.trustPolicyIgnoreAfter} + */ + trustPolicyIgnoreAfter?: number; /** * {@inheritDoc PnpmOptionsConfiguration.alwaysInjectDependenciesFromOtherSubspaces} */ @@ -301,6 +323,42 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration */ public readonly minimumReleaseAgeExclude: string[] | undefined; + /** + * The trust policy controls whether pnpm should block installation of package versions where the + * trust level has decreased (e.g., a package previously published with provenance is now published + * without it). Setting this to `"no-downgrade"` enables the protection. + * + * @remarks + * (SUPPORTED ONLY IN PNPM 10.21.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#trustpolicy + */ + public readonly trustPolicy: PnpmTrustPolicy | undefined; + + /** + * List of package names or patterns that are excluded from the trust policy check. + * These packages will be allowed to install even if their trust level has decreased. + * + * @remarks + * (SUPPORTED ONLY IN PNPM 10.22.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#trustpolicyexclude + * + * Example: ["webpack", "react", "\@myorg/*"] + */ + public readonly trustPolicyExclude: string[] | undefined; + + /** + * The number of minutes after which pnpm will ignore trust level downgrades. Packages published + * longer ago than this threshold will not be blocked even if their trust level has decreased. + * + * @remarks + * (SUPPORTED ONLY IN PNPM 10.27.0 AND NEWER) + * + * PNPM documentation: https://pnpm.io/settings#trustpolicyignoreafter + */ + public readonly trustPolicyIgnoreAfter: number | undefined; + /** * If true, then `rush update` add injected install options for all cross-subspace * workspace dependencies, to avoid subspace doppelganger issue. @@ -495,6 +553,9 @@ export class PnpmOptionsConfiguration extends PackageManagerOptionsConfiguration this.autoInstallPeers = json.autoInstallPeers; this.minimumReleaseAge = json.minimumReleaseAge; this.minimumReleaseAgeExclude = json.minimumReleaseAgeExclude; + this.trustPolicy = json.trustPolicy; + this.trustPolicyExclude = json.trustPolicyExclude; + this.trustPolicyIgnoreAfter = json.trustPolicyIgnoreAfter; this.alwaysInjectDependenciesFromOtherSubspaces = json.alwaysInjectDependenciesFromOtherSubspaces; this.alwaysFullInstall = json.alwaysFullInstall; this.pnpmLockfilePolicies = json.pnpmLockfilePolicies; diff --git a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts index 369054709ad..efb5a530340 100644 --- a/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts +++ b/libraries/rush-lib/src/logic/pnpm/test/PnpmOptionsConfiguration.test.ts @@ -100,6 +100,22 @@ describe(PnpmOptionsConfiguration.name, () => { ]); }); + it('loads trustPolicy', () => { + const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( + `${__dirname}/jsonFiles/pnpm-config-trustPolicy.json`, + fakeCommonTempFolder + ); + + expect(pnpmConfiguration.trustPolicy).toEqual('no-downgrade'); + expect(TestUtilities.stripAnnotations(pnpmConfiguration.trustPolicyExclude)).toEqual([ + '@myorg/*', + 'chokidar@4.0.3', + 'webpack@4.47.0 || 5.102.1', + '@babel/core@7.28.5' + ]); + expect(pnpmConfiguration.trustPolicyIgnoreAfter).toEqual(20160); + }); + it('loads catalog and catalogs', () => { const pnpmConfiguration: PnpmOptionsConfiguration = PnpmOptionsConfiguration.loadFromJsonFileOrThrow( `${__dirname}/jsonFiles/pnpm-config-catalog.json`, diff --git a/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-trustPolicy.json b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-trustPolicy.json new file mode 100644 index 00000000000..28d3e69f476 --- /dev/null +++ b/libraries/rush-lib/src/logic/pnpm/test/jsonFiles/pnpm-config-trustPolicy.json @@ -0,0 +1,5 @@ +{ + "trustPolicy": "no-downgrade", + "trustPolicyExclude": ["@myorg/*", "chokidar@4.0.3", "webpack@4.47.0 || 5.102.1", "@babel/core@7.28.5"], + "trustPolicyIgnoreAfter": 20160 +} diff --git a/libraries/rush-lib/src/schemas/pnpm-config.schema.json b/libraries/rush-lib/src/schemas/pnpm-config.schema.json index 4646fe2398e..5c364744fce 100644 --- a/libraries/rush-lib/src/schemas/pnpm-config.schema.json +++ b/libraries/rush-lib/src/schemas/pnpm-config.schema.json @@ -214,6 +214,26 @@ } }, + "trustPolicy": { + "description": "The trust policy controls whether pnpm should block installation of package versions where the trust level has decreased (e.g., a package previously published with provenance is now published without it). Setting this to \"no-downgrade\" enables the protection.\n\n(SUPPORTED ONLY IN PNPM 10.21.0 AND NEWER)\n\nPNPM documentation: https://pnpm.io/settings#trustpolicy", + "type": "string", + "enum": ["no-downgrade", "off"] + }, + + "trustPolicyExclude": { + "description": "List of package names or patterns that are excluded from the trust policy check. These packages will be allowed to install even if their trust level has decreased. Supports glob patterns (e.g., \"@myorg/*\").\n\n(SUPPORTED ONLY IN PNPM 10.22.0 AND NEWER)\n\nPNPM documentation: https://pnpm.io/settings#trustpolicyexclude\n\nExample: [\"webpack\", \"react\", \"@myorg/*\"]", + "type": "array", + "items": { + "description": "Package name or pattern", + "type": "string" + } + }, + + "trustPolicyIgnoreAfter": { + "description": "The number of minutes after which pnpm will ignore trust level downgrades. Packages published longer ago than this threshold will not be blocked even if their trust level has decreased.\n\n(SUPPORTED ONLY IN PNPM 10.27.0 AND NEWER)\n\nPNPM documentation: https://pnpm.io/settings#trustpolicyignoreafter", + "type": "number" + }, + "alwaysFullInstall": { "description": "(EXPERIMENTAL) If 'true', then filtered installs ('rush install --to my-project') * will be disregarded, instead always performing a full installation of the lockfile.", "type": "boolean"