-
Notifications
You must be signed in to change notification settings - Fork 708
[rush] Preserve pnpm credential environment variables on POSIX #6040
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "changes": [ | ||
| { | ||
| "packageName": "@microsoft/rush", | ||
| "comment": "Fix pnpm registry credentials being dropped by POSIX shells when provideNpmrcCredentialsViaEnvironment is enabled.", | ||
| "type": "patch" | ||
| } | ||
| ], | ||
| "packageName": "@microsoft/rush" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,12 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. | ||
| // See LICENSE in the project root for license information. | ||
|
|
||
| import * as fs from 'node:fs'; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
| import * as os from 'node:os'; | ||
|
|
||
| import { type IDisposable, Utilities } from '../Utilities'; | ||
| import { getNpmrcEnvironmentVariables, syncNpmrc } from '../npmrcUtilities'; | ||
| import { IS_WINDOWS } from '../executionUtilities'; | ||
|
|
||
| function withComSpec<T>(value: string | undefined, callback: () => T): T { | ||
| const originalValue: string | undefined = process.env.comspec; | ||
|
|
@@ -23,6 +28,152 @@ function withComSpec<T>(value: string | undefined, callback: () => T): T { | |
| } | ||
|
|
||
| describe(Utilities.name, () => { | ||
| describe('package manager credential environment', () => { | ||
| const credentialKey: string = 'npm_config_//registry.example.test/npm/:_authToken'; | ||
| const credentialValue: string = 'non-secret-test-token'; | ||
| let directory: string; | ||
| let scriptPath: string; | ||
| let environment: NodeJS.ProcessEnv; | ||
|
|
||
| beforeAll(async () => { | ||
| directory = await fs.promises.mkdtemp(`${os.tmpdir()}/rush credentials `); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use |
||
| scriptPath = `${directory}/check credentials.cjs`; | ||
| const sourceFolder: string = `${directory}/source`; | ||
| const targetFolder: string = `${directory}/target`; | ||
| await fs.promises.mkdir(sourceFolder); | ||
| await fs.promises.mkdir(targetFolder); | ||
| await fs.promises.writeFile( | ||
| `${sourceFolder}/.npmrc`, | ||
| '//registry.example.test/npm/:_authToken=${RUSH_TEST_TOKEN}\n' | ||
| ); | ||
|
Comment on lines
+43
to
+48
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const sourceEnvironment: NodeJS.ProcessEnv = { RUSH_TEST_TOKEN: credentialValue }; | ||
| syncNpmrc({ | ||
| sourceNpmrcFolder: sourceFolder, | ||
| targetNpmrcFolder: targetFolder, | ||
| supportEnvVarFallbackSyntax: true, | ||
| moveSensitiveSettingsToEnvironment: true, | ||
| env: sourceEnvironment | ||
| }); | ||
| environment = { | ||
| ...process.env, | ||
| ...getNpmrcEnvironmentVariables({ | ||
| npmrcFolder: targetFolder, | ||
| supportEnvVarFallbackSyntax: true, | ||
| env: sourceEnvironment | ||
| }) | ||
| }; | ||
| await fs.promises.writeFile( | ||
| scriptPath, | ||
| [ | ||
| `if (process.env[${JSON.stringify(credentialKey)}] !== ${JSON.stringify(credentialValue)}) {`, | ||
| ' process.exit(42);', | ||
| '}', | ||
| 'process.stdout.write(JSON.stringify(process.argv.slice(2)));' | ||
| ].join('\n') | ||
| ); | ||
| expect(await fs.promises.readFile(`${targetFolder}/.npmrc`, 'utf8')).not.toContain(credentialValue); | ||
| }); | ||
|
|
||
| afterAll(async () => { | ||
| if (directory) { | ||
| await fs.promises.rm(directory, { recursive: true, force: true }); | ||
| } | ||
| }); | ||
|
|
||
| it('preserves generated credentials through the captured subprocess path', async () => { | ||
| const output: string = await Utilities.executeCommandAndCaptureOutputAsync({ | ||
| command: process.execPath, | ||
| args: [scriptPath, 'space argument'], | ||
| workingDirectory: directory, | ||
| environment, | ||
| keepEnvironment: true, | ||
| useShell: false | ||
| }); | ||
| expect(JSON.parse(output)).toEqual(['space argument']); | ||
| }); | ||
|
|
||
| it('preserves generated credentials through the install retry path', async () => { | ||
| await Utilities.executeCommandWithRetryAsync( | ||
| { | ||
| command: process.execPath, | ||
| args: [scriptPath], | ||
| workingDirectory: directory, | ||
| environment, | ||
| keepEnvironment: true, | ||
| useShell: false, | ||
| suppressOutput: true | ||
| }, | ||
| 1 | ||
| ); | ||
| }); | ||
|
|
||
| (IS_WINDOWS ? it.skip : it)( | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ensure the cache key for this project's tests takes the OS into account. |
||
| 'passes POSIX arguments without shell expansion or pre-escaping', | ||
| async () => { | ||
| const args: string[] = [ | ||
| '', | ||
| 'two words', | ||
| '"quoted"', | ||
| "single'quote", | ||
| '$HOME', | ||
| '$(echo expanded)', | ||
| '*' | ||
| ]; | ||
| const output: string = await Utilities.executeCommandAndCaptureOutputAsync({ | ||
| command: process.execPath, | ||
| args: [scriptPath, ...args], | ||
| workingDirectory: directory, | ||
| environment, | ||
| keepEnvironment: true, | ||
| useShell: false | ||
| }); | ||
| expect(JSON.parse(output)).toEqual(args); | ||
| } | ||
| ); | ||
|
|
||
| it('retains exit-code capture for failed direct subprocesses', async () => { | ||
| const { exitCode } = await Utilities.executeCommandAsync({ | ||
| command: process.execPath, | ||
| args: [scriptPath], | ||
| workingDirectory: directory, | ||
| environment: { ...environment, [credentialKey]: 'wrong-test-token' }, | ||
| keepEnvironment: true, | ||
| useShell: false, | ||
| captureExitCodeAndSignal: true, | ||
| suppressOutput: true | ||
| }); | ||
| expect(exitCode).toBe(42); | ||
| }); | ||
|
|
||
| it('still rejects failed direct subprocesses by default', async () => { | ||
| await expect( | ||
| Utilities.executeCommandAsync({ | ||
| command: process.execPath, | ||
| args: [scriptPath], | ||
| workingDirectory: directory, | ||
| environment: { ...environment, [credentialKey]: 'wrong-test-token' }, | ||
| keepEnvironment: true, | ||
| useShell: false, | ||
| suppressOutput: true | ||
| }) | ||
| ).rejects.toThrow(); | ||
| }); | ||
|
|
||
| it('retains shell execution by default', async () => { | ||
| const output: string = await Utilities.executeCommandAndCaptureOutputAsync({ | ||
| command: 'echo', | ||
| args: ['first', '&&', 'echo', 'second'], | ||
| workingDirectory: directory | ||
| }); | ||
| expect( | ||
| output | ||
| .trim() | ||
| .split(/\r?\n/) | ||
| .map((line) => line.trim()) | ||
| ).toEqual(['first', 'second']); | ||
| }); | ||
| }); | ||
|
|
||
| describe(Utilities.usingAsync.name, () => { | ||
| let disposed: boolean; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.