Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"comment": "Forward the `parameterNamesToIgnore` `<project>/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"
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -409,6 +410,9 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> 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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
);
}
}
Original file line number Diff line number Diff line change
@@ -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<Operation> = 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<Operation> = 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();
});
});
Loading