Skip to content

Commit b1fe55d

Browse files
Copiloticlanton
andauthored
Add provideNpmrcCredentialsViaEnvironment experiment to work around pnpm 10.34.2+ ignoring ${VAR} in project .npmrc credentials
Co-authored-by: iclanton <5010588+iclanton@users.noreply.github.com>
1 parent 6d4b487 commit b1fe55d

12 files changed

Lines changed: 639 additions & 48 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@microsoft/rush",
5+
"comment": "Add a new `provideNpmrcCredentialsViaEnvironment` experiment. PNPM 10.34.2 and newer ignore `${VAR}` tokens that appear in credentials and registry URLs in a project `.npmrc` file, which broke the practice of supplying registry credentials via environment variables in CI. When this experiment is enabled, Rush expands those tokens itself, passing credentials to PNPM using `npm_config_*` environment variables instead of writing them to the generated `.npmrc` file.",
6+
"type": "minor"
7+
}
8+
]
9+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ export interface IExperimentsJson {
489489
omitAppleDoubleFilesFromBuildCache?: boolean;
490490
omitImportersFromPreventManualShrinkwrapChanges?: boolean;
491491
printEventHooksOutputToConsole?: boolean;
492+
provideNpmrcCredentialsViaEnvironment?: boolean;
492493
rushAlerts?: boolean;
493494
strictChangefileValidation?: boolean;
494495
useDirectFileTransfersForBuildCache?: boolean;

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,5 +141,16 @@
141141
* must implement the optional file-based methods for this to take effect; otherwise it falls back to the
142142
* buffer-based approach.
143143
*/
144-
/*[LINE "HYPOTHETICAL"]*/ "useDirectFileTransfersForBuildCache": true
144+
/*[LINE "HYPOTHETICAL"]*/ "useDirectFileTransfersForBuildCache": true,
145+
146+
/**
147+
* PNPM 10.34.2 and newer ignore "${VAR}" tokens that appear in credentials and registry URLs in a
148+
* project or workspace .npmrc file, because such files are normally committed to Git. Rush generates
149+
* "common/temp/.npmrc", which PNPM classifies as a project file even though it is not committed, so
150+
* PNPM discards those settings and prints a warning. If true, when using PNPM, Rush expands those
151+
* tokens itself: credentials are passed to PNPM using "npm_config_*" environment variables instead of
152+
* being written to the generated .npmrc file, and non-secret settings such as registry URLs are
153+
* written to the generated file with their values already expanded.
154+
*/
155+
/*[LINE "HYPOTHETICAL"]*/ "provideNpmrcCredentialsViaEnvironment": true
145156
}

libraries/rush-lib/src/api/ExperimentsConfiguration.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,20 @@ export interface IExperimentsJson {
153153
* effect; otherwise it falls back to the buffer-based approach.
154154
*/
155155
useDirectFileTransfersForBuildCache?: boolean;
156+
157+
/**
158+
* If true, when using PNPM, Rush resolves the `${VAR}` tokens that appear in credentials and
159+
* registry URLs in the `.npmrc` file, instead of relying on PNPM to expand them. Credentials are
160+
* passed to PNPM using `npm_config_*` environment variables and are not written to the generated
161+
* `.npmrc` file.
162+
*
163+
* @remarks
164+
* PNPM 10.34.2 and newer ignore `${VAR}` tokens in credentials and registry URLs that come from a
165+
* project or workspace `.npmrc` file, because such files are normally committed to Git. Rush
166+
* generates `common/temp/.npmrc`, which PNPM classifies as a project file even though it is not
167+
* committed, so without this experiment PNPM discards those settings and prints a warning.
168+
*/
169+
provideNpmrcCredentialsViaEnvironment?: boolean;
156170
}
157171

158172
const _EXPERIMENTS_JSON_SCHEMA: JsonSchema = JsonSchema.fromLoadedObject(schemaJson);

libraries/rush-lib/src/cli/RushPnpmCommandLineParser.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import type { IBuiltInPluginConfiguration } from '../pluginFramework/PluginLoade
3030
import type { BaseInstallManager } from '../logic/base/BaseInstallManager';
3131
import type { IInstallManagerOptions } from '../logic/base/BaseInstallManagerTypes';
3232
import { Utilities } from '../utilities/Utilities';
33+
import { getNpmrcEnvironmentVariables } from '../utilities/npmrcUtilities';
34+
import { InstallHelpers } from '../logic/installManager/InstallHelpers';
3335
import type { Subspace } from '../api/Subspace';
3436
import type { PnpmOptionsConfiguration } from '../logic/pnpm/PnpmOptionsConfiguration';
3537
import { PnpmWorkspaceFile } from '../logic/pnpm/PnpmWorkspaceFile';
@@ -476,6 +478,18 @@ export class RushPnpmCommandLineParser {
476478
}
477479
}
478480

481+
// Provide any credentials that "rush install" moved out of the generated .npmrc file.
482+
// See the "provideNpmrcCredentialsViaEnvironment" experiment.
483+
if (InstallHelpers.shouldProvideNpmrcCredentialsViaEnvironment(rushConfiguration)) {
484+
const npmrcEnvironmentVariables: Record<string, string> | undefined = getNpmrcEnvironmentVariables({
485+
npmrcFolder: workspaceFolder,
486+
supportEnvVarFallbackSyntax: rushConfiguration.isPnpm
487+
});
488+
for (const [envKey, envValue] of Object.entries(npmrcEnvironmentVariables ?? {})) {
489+
pnpmEnvironmentMap.set(envKey, envValue);
490+
}
491+
}
492+
479493
let onStdoutStreamChunk: ((chunk: string) => string | void) | undefined;
480494
switch (this._commandName) {
481495
case 'patch': {

libraries/rush-lib/src/logic/Autoinstaller.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { Colorize } from '@rushstack/terminal';
1616

1717
import { AsyncRecycler } from '../utilities/AsyncRecycler';
1818
import { Utilities } from '../utilities/Utilities';
19+
import { getNpmrcEnvironmentVariables } from '../utilities/npmrcUtilities';
1920
import type { RushConfiguration } from '../api/RushConfiguration';
2021
import { PackageJsonEditor } from '../api/PackageJsonEditor';
2122
import { InstallHelpers } from './installManager/InstallHelpers';
@@ -143,7 +144,10 @@ export class Autoinstaller {
143144
Utilities.syncNpmrc({
144145
sourceNpmrcFolder: this._rushConfiguration.commonRushConfigFolder,
145146
targetNpmrcFolder: autoinstallerFullPath,
146-
supportEnvVarFallbackSyntax: this._rushConfiguration.isPnpm
147+
supportEnvVarFallbackSyntax: this._rushConfiguration.isPnpm,
148+
moveSensitiveSettingsToEnvironment: InstallHelpers.shouldProvideNpmrcCredentialsViaEnvironment(
149+
this._rushConfiguration
150+
)
147151
});
148152

149153
this._logIfConsoleOutputIsNotRestricted(
@@ -154,6 +158,7 @@ export class Autoinstaller {
154158
command: this._rushConfiguration.packageManagerToolFilename,
155159
args: ['install', '--frozen-lockfile'],
156160
workingDirectory: autoinstallerFullPath,
161+
environment: this._getPackageManagerEnvironment(autoinstallerFullPath),
157162
keepEnvironment: true
158163
});
159164

@@ -229,13 +234,17 @@ export class Autoinstaller {
229234
Utilities.syncNpmrc({
230235
sourceNpmrcFolder: this._rushConfiguration.commonRushConfigFolder,
231236
targetNpmrcFolder: this.folderFullPath,
232-
supportEnvVarFallbackSyntax: this._rushConfiguration.isPnpm
237+
supportEnvVarFallbackSyntax: this._rushConfiguration.isPnpm,
238+
moveSensitiveSettingsToEnvironment: InstallHelpers.shouldProvideNpmrcCredentialsViaEnvironment(
239+
this._rushConfiguration
240+
)
233241
});
234242

235243
await Utilities.executeCommandAsync({
236244
command: this._rushConfiguration.packageManagerToolFilename,
237245
args: ['install'],
238246
workingDirectory: this.folderFullPath,
247+
environment: this._getPackageManagerEnvironment(this.folderFullPath),
239248
keepEnvironment: true
240249
});
241250

@@ -278,4 +287,21 @@ export class Autoinstaller {
278287
console.log(message ?? '');
279288
}
280289
}
290+
291+
/**
292+
* Returns the environment to invoke the package manager with, or `undefined` to inherit this
293+
* process's environment. See the `provideNpmrcCredentialsViaEnvironment` experiment.
294+
*/
295+
private _getPackageManagerEnvironment(npmrcFolder: string): NodeJS.ProcessEnv | undefined {
296+
if (!InstallHelpers.shouldProvideNpmrcCredentialsViaEnvironment(this._rushConfiguration)) {
297+
return undefined;
298+
}
299+
300+
const npmrcEnvironmentVariables: Record<string, string> | undefined = getNpmrcEnvironmentVariables({
301+
npmrcFolder,
302+
supportEnvVarFallbackSyntax: this._rushConfiguration.isPnpm
303+
});
304+
305+
return npmrcEnvironmentVariables && { ...process.env, ...npmrcEnvironmentVariables };
306+
}
281307
}

libraries/rush-lib/src/logic/base/BaseInstallManager.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,10 @@ export abstract class BaseInstallManager {
559559
targetNpmrcFolder: subspace.getSubspaceTempFolderPath(),
560560
linesToPrepend: extraNpmrcLines,
561561
createIfMissing: this.rushConfiguration.subspacesFeatureEnabled,
562-
supportEnvVarFallbackSyntax: this.rushConfiguration.isPnpm
562+
supportEnvVarFallbackSyntax: this.rushConfiguration.isPnpm,
563+
moveSensitiveSettingsToEnvironment: InstallHelpers.shouldProvideNpmrcCredentialsViaEnvironment(
564+
this.rushConfiguration
565+
)
563566
});
564567
this._syncNpmrcAlreadyCalled = true;
565568

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

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type { IConfigurationEnvironment } from '../base/BasePackageManagerOption
2121
import type { PnpmOptionsConfiguration } from '../pnpm/PnpmOptionsConfiguration';
2222
import { PnpmWorkspaceFile } from '../pnpm/PnpmWorkspaceFile';
2323
import { merge } from '../../utilities/objectUtilities';
24+
import { getNpmrcEnvironmentVariables } from '../../utilities/npmrcUtilities';
2425
import type { Subspace } from '../../api/Subspace';
2526
import { RushConstants } from '../RushConstants';
2627

@@ -377,10 +378,29 @@ export class InstallHelpers {
377378
};
378379
}
379380

381+
/**
382+
* Returns true if Rush (rather than PNPM) should expand the `${VAR}` tokens that appear in
383+
* credentials and registry URLs in the `.npmrc` file. See the
384+
* `provideNpmrcCredentialsViaEnvironment` experiment.
385+
*/
386+
public static shouldProvideNpmrcCredentialsViaEnvironment(rushConfiguration: RushConfiguration): boolean {
387+
// Only PNPM refuses to expand these tokens, and only PNPM's "npm_config_*" environment variable
388+
// name normalization has been validated here.
389+
return (
390+
rushConfiguration.isPnpm &&
391+
!!rushConfiguration.experimentsConfiguration.configuration.provideNpmrcCredentialsViaEnvironment
392+
);
393+
}
394+
395+
/**
396+
* Returns the environment that the package manager should be invoked with, including any
397+
* credentials that were moved out of the generated `.npmrc` file in `npmrcFolder`.
398+
*/
380399
public static getPackageManagerEnvironment(
381400
rushConfiguration: RushConfiguration,
382401
options: {
383402
debug?: boolean;
403+
npmrcFolder?: string;
384404
} = {}
385405
): NodeJS.ProcessEnv {
386406
let configurationEnvironment: IConfigurationEnvironment | undefined = undefined;
@@ -393,7 +413,26 @@ export class InstallHelpers {
393413
configurationEnvironment = rushConfiguration.yarnOptions?.environmentVariables;
394414
}
395415

396-
return _mergeEnvironmentVariables(process.env, configurationEnvironment, options);
416+
const packageManagerEnvironment: NodeJS.ProcessEnv = _mergeEnvironmentVariables(
417+
process.env,
418+
configurationEnvironment,
419+
options
420+
);
421+
422+
const { npmrcFolder } = options;
423+
const shouldProvideCredentials: boolean =
424+
InstallHelpers.shouldProvideNpmrcCredentialsViaEnvironment(rushConfiguration);
425+
if (npmrcFolder !== undefined && shouldProvideCredentials) {
426+
Object.assign(
427+
packageManagerEnvironment,
428+
getNpmrcEnvironmentVariables({
429+
npmrcFolder,
430+
supportEnvVarFallbackSyntax: rushConfiguration.isPnpm
431+
})
432+
);
433+
}
434+
435+
return packageManagerEnvironment;
397436
}
398437

399438
/**

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ export class WorkspaceInstallManager extends BaseInstallManager {
494494

495495
const packageManagerEnv: NodeJS.ProcessEnv = InstallHelpers.getPackageManagerEnvironment(
496496
this.rushConfiguration,
497-
this.options
497+
{ ...this.options, npmrcFolder: subspace.getSubspaceTempFolderPath() }
498498
);
499499
if (ConsoleTerminalProvider.supportsColor) {
500500
packageManagerEnv.FORCE_COLOR = '1';

libraries/rush-lib/src/schemas/experiments.schema.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@
9393
"useDirectFileTransfersForBuildCache": {
9494
"description": "If true, the build cache will use file-based APIs to transfer cache entries to and from cloud storage. This avoids loading the entire cache entry into memory, which can prevent out-of-memory errors for large build outputs and allow cache entries to exceed the limit of a single Buffer. The cloud cache provider plugin must implement the optional file-based methods for this to take effect; otherwise it falls back to the buffer-based approach.",
9595
"type": "boolean"
96+
},
97+
98+
"provideNpmrcCredentialsViaEnvironment": {
99+
"description": "If true, when using PNPM, Rush resolves the \"${VAR}\" tokens that appear in credentials and registry URLs in the .npmrc file, instead of relying on PNPM to expand them. Credentials are passed to PNPM using \"npm_config_*\" environment variables and are not written to the generated .npmrc file. PNPM 10.34.2 and newer ignore such tokens in a project .npmrc file, which otherwise breaks the recommended practice of supplying registry credentials via environment variables in CI.",
100+
"type": "boolean"
96101
}
97102
},
98103
"additionalProperties": false

0 commit comments

Comments
 (0)