diff --git a/common/changes/@microsoft/rush/copilot-forward-parameter-names-ignore_2025-12-10-21-30.json b/common/changes/@microsoft/rush/copilot-forward-parameter-names-ignore_2025-12-10-21-30.json new file mode 100644 index 00000000000..5879bd3e5f7 --- /dev/null +++ b/common/changes/@microsoft/rush/copilot-forward-parameter-names-ignore_2025-12-10-21-30.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "comment": "Forward the `parameterNamesToIgnore` `/config/rush-project.json` property to child processes via a `RUSHSTACK_CLI_IGNORED_PARAMETER_NAMES` environment variable", + "type": "none", + "packageName": "@microsoft/rush" + } + ], + "packageName": "@microsoft/rush", + "email": "198982749+Copilot@users.noreply.github.com" +} diff --git a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts index b7d7ef8a692..6bf22cd9c18 100644 --- a/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts +++ b/libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts @@ -59,6 +59,7 @@ import { WeightedOperationPlugin } from '../../logic/operations/WeightedOperatio import { getVariantAsync, VARIANT_PARAMETER } from '../../api/Variants'; import { Selection } from '../../logic/Selection'; import { NodeDiagnosticDirPlugin } from '../../logic/operations/NodeDiagnosticDirPlugin'; +import { IgnoredParametersPlugin } from '../../logic/operations/IgnoredParametersPlugin'; import { DebugHashesPlugin } from '../../logic/operations/DebugHashesPlugin'; import { measureAsyncFn, measureFn } from '../../utilities/performance'; @@ -409,6 +410,9 @@ export class PhasedScriptAction extends BaseScriptAction i new WeightedOperationPlugin().apply(hooks); new ValidateOperationsPlugin(terminal).apply(hooks); + // Forward ignored parameters to child processes as an environment variable + new IgnoredParametersPlugin().apply(hooks); + const showTimeline: boolean = this._timelineParameter?.value ?? false; if (showTimeline) { const { ConsoleTimelinePlugin } = await import( diff --git a/libraries/rush-lib/src/logic/operations/IgnoredParametersPlugin.ts b/libraries/rush-lib/src/logic/operations/IgnoredParametersPlugin.ts new file mode 100644 index 00000000000..0de6aa422f6 --- /dev/null +++ b/libraries/rush-lib/src/logic/operations/IgnoredParametersPlugin.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { IPhasedCommandPlugin, PhasedCommandHooks } from '../../pluginFramework/PhasedCommandHooks'; +import type { IEnvironment } from '../../utilities/Utilities'; +import type { IOperationExecutionResult } from './IOperationExecutionResult'; + +const PLUGIN_NAME: 'IgnoredParametersPlugin' = 'IgnoredParametersPlugin'; + +/** + * Environment variable name for forwarding ignored parameters to child processes + * @public + */ +export const RUSHSTACK_CLI_IGNORED_PARAMETER_NAMES_ENV_VAR: 'RUSHSTACK_CLI_IGNORED_PARAMETER_NAMES' = + 'RUSHSTACK_CLI_IGNORED_PARAMETER_NAMES'; + +/** + * Phased command plugin that forwards the value of the `parameterNamesToIgnore` operation setting + * to child processes as the RUSHSTACK_CLI_IGNORED_PARAMETER_NAMES environment variable. + */ +export class IgnoredParametersPlugin implements IPhasedCommandPlugin { + public apply(hooks: PhasedCommandHooks): void { + hooks.createEnvironmentForOperation.tap( + PLUGIN_NAME, + (env: IEnvironment, record: IOperationExecutionResult) => { + const { settings } = record.operation; + + // If there are parameter names to ignore, set the environment variable + if (settings?.parameterNamesToIgnore && settings.parameterNamesToIgnore.length > 0) { + env[RUSHSTACK_CLI_IGNORED_PARAMETER_NAMES_ENV_VAR] = JSON.stringify( + settings.parameterNamesToIgnore + ); + } + + return env; + } + ); + } +} diff --git a/libraries/rush-lib/src/logic/operations/test/IgnoredParametersPlugin.test.ts b/libraries/rush-lib/src/logic/operations/test/IgnoredParametersPlugin.test.ts new file mode 100644 index 00000000000..ee6b3fe6d3b --- /dev/null +++ b/libraries/rush-lib/src/logic/operations/test/IgnoredParametersPlugin.test.ts @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import path from 'node:path'; +import { JsonFile } from '@rushstack/node-core-library'; +import { ConsoleTerminalProvider, Terminal } from '@rushstack/terminal'; + +import { RushConfiguration } from '../../../api/RushConfiguration'; +import { CommandLineConfiguration, type IPhasedCommandConfig } from '../../../api/CommandLineConfiguration'; +import type { Operation } from '../Operation'; +import type { ICommandLineJson } from '../../../api/CommandLineJson'; +import { PhasedOperationPlugin } from '../PhasedOperationPlugin'; +import { ShellOperationRunnerPlugin } from '../ShellOperationRunnerPlugin'; +import { + IgnoredParametersPlugin, + RUSHSTACK_CLI_IGNORED_PARAMETER_NAMES_ENV_VAR +} from '../IgnoredParametersPlugin'; +import { + type ICreateOperationsContext, + PhasedCommandHooks +} from '../../../pluginFramework/PhasedCommandHooks'; +import { RushProjectConfiguration } from '../../../api/RushProjectConfiguration'; +import type { IEnvironment } from '../../../utilities/Utilities'; +import type { IOperationRunnerContext } from '../IOperationRunner'; +import type { IOperationExecutionResult } from '../IOperationExecutionResult'; + +/** + * Helper function to create a minimal mock record for testing the createEnvironmentForOperation hook + */ +function createMockRecord(operation: Operation): IOperationRunnerContext & IOperationExecutionResult { + return { + operation, + environment: undefined + } as IOperationRunnerContext & IOperationExecutionResult; +} + +describe(IgnoredParametersPlugin.name, () => { + it('should set RUSHSTACK_OPERATION_IGNORED_PARAMETERS environment variable', async () => { + const rushJsonFile: string = path.resolve(__dirname, `../../test/parameterIgnoringRepo/rush.json`); + const commandLineJsonFile: string = path.resolve( + __dirname, + `../../test/parameterIgnoringRepo/common/config/rush/command-line.json` + ); + + const rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFile); + const commandLineJson: ICommandLineJson = JsonFile.load(commandLineJsonFile); + + const commandLineConfiguration = new CommandLineConfiguration(commandLineJson); + const buildCommand: IPhasedCommandConfig = commandLineConfiguration.commands.get( + 'build' + )! as IPhasedCommandConfig; + + // Load project configurations + const terminalProvider: ConsoleTerminalProvider = new ConsoleTerminalProvider(); + const terminal: Terminal = new Terminal(terminalProvider); + + const projectConfigurations = await RushProjectConfiguration.tryLoadForProjectsAsync( + rushConfiguration.projects, + terminal + ); + + const fakeCreateOperationsContext: Pick< + ICreateOperationsContext, + | 'phaseOriginal' + | 'phaseSelection' + | 'projectSelection' + | 'projectsInUnknownState' + | 'projectConfigurations' + | 'rushConfiguration' + > = { + phaseOriginal: buildCommand.phases, + phaseSelection: buildCommand.phases, + projectSelection: new Set(rushConfiguration.projects), + projectsInUnknownState: new Set(rushConfiguration.projects), + projectConfigurations, + rushConfiguration + }; + + const hooks: PhasedCommandHooks = new PhasedCommandHooks(); + + // Apply plugins + new PhasedOperationPlugin().apply(hooks); + new ShellOperationRunnerPlugin().apply(hooks); + new IgnoredParametersPlugin().apply(hooks); + + const operations: Set = await hooks.createOperations.promise( + new Set(), + fakeCreateOperationsContext as ICreateOperationsContext + ); + + // Test project 'a' which has parameterNamesToIgnore: ["--production"] + const operationA = Array.from(operations).find((op) => op.name === 'a'); + expect(operationA).toBeDefined(); + + // Create a mock operation execution result with required fields + const mockRecordA = createMockRecord(operationA!); + + // Call the hook to get the environment + const envA: IEnvironment = hooks.createEnvironmentForOperation.call({ ...process.env }, mockRecordA); + + // Verify the environment variable is set correctly for project 'a' + expect(envA[RUSHSTACK_CLI_IGNORED_PARAMETER_NAMES_ENV_VAR]).toBe('["--production"]'); + + // Test project 'b' which has parameterNamesToIgnore: ["--verbose", "--config", "--mode", "--tags"] + const operationB = Array.from(operations).find((op) => op.name === 'b'); + expect(operationB).toBeDefined(); + + const mockRecordB = createMockRecord(operationB!); + + const envB: IEnvironment = hooks.createEnvironmentForOperation.call({ ...process.env }, mockRecordB); + + // Verify the environment variable is set correctly for project 'b' + expect(envB[RUSHSTACK_CLI_IGNORED_PARAMETER_NAMES_ENV_VAR]).toBe( + '["--verbose","--config","--mode","--tags"]' + ); + }); + + it('should not set environment variable when parameterNamesToIgnore is not specified', async () => { + const rushJsonFile: string = path.resolve(__dirname, `../../test/customShellCommandinBulkRepo/rush.json`); + const commandLineJsonFile: string = path.resolve( + __dirname, + `../../test/customShellCommandinBulkRepo/common/config/rush/command-line.json` + ); + + const rushConfiguration = RushConfiguration.loadFromConfigurationFile(rushJsonFile); + const commandLineJson: ICommandLineJson = JsonFile.load(commandLineJsonFile); + + const commandLineConfiguration = new CommandLineConfiguration(commandLineJson); + const echoCommand: IPhasedCommandConfig = commandLineConfiguration.commands.get( + 'echo' + )! as IPhasedCommandConfig; + + const fakeCreateOperationsContext: Pick< + ICreateOperationsContext, + | 'phaseOriginal' + | 'phaseSelection' + | 'projectSelection' + | 'projectsInUnknownState' + | 'projectConfigurations' + | 'rushConfiguration' + > = { + phaseOriginal: echoCommand.phases, + phaseSelection: echoCommand.phases, + projectSelection: new Set(rushConfiguration.projects), + projectsInUnknownState: new Set(rushConfiguration.projects), + projectConfigurations: new Map(), + rushConfiguration + }; + + const hooks: PhasedCommandHooks = new PhasedCommandHooks(); + + // Apply plugins + new PhasedOperationPlugin().apply(hooks); + new ShellOperationRunnerPlugin().apply(hooks); + new IgnoredParametersPlugin().apply(hooks); + + const operations: Set = await hooks.createOperations.promise( + new Set(), + fakeCreateOperationsContext as ICreateOperationsContext + ); + + // Get any operation + const operation = Array.from(operations)[0]; + expect(operation).toBeDefined(); + + const mockRecord = createMockRecord(operation); + + const env: IEnvironment = hooks.createEnvironmentForOperation.call({ ...process.env }, mockRecord); + + // Verify the environment variable is not set + expect(env[RUSHSTACK_CLI_IGNORED_PARAMETER_NAMES_ENV_VAR]).toBeUndefined(); + }); +});