From dc885ee9c16072480dad34d064f4885ee7d806f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 7 Jun 2026 00:04:23 +0000 Subject: [PATCH 1/2] Initial plan From 462881f820dc1eb62dd1e493702391c0c224ba19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" Date: Sun, 7 Jun 2026 00:22:12 +0000 Subject: [PATCH 2/2] Fix rush change to enforce git email policy --- .../rush-lib/src/cli/actions/ChangeAction.ts | 33 ++++--- .../src/cli/actions/test/ChangeAction.test.ts | 88 +++++++++++++++++++ .../actions/test/changeRepo/a/package.json | 4 + .../src/cli/actions/test/changeRepo/rush.json | 16 ++++ 4 files changed, 123 insertions(+), 18 deletions(-) create mode 100644 libraries/rush-lib/src/cli/actions/test/ChangeAction.test.ts create mode 100644 libraries/rush-lib/src/cli/actions/test/changeRepo/a/package.json create mode 100644 libraries/rush-lib/src/cli/actions/test/changeRepo/rush.json diff --git a/libraries/rush-lib/src/cli/actions/ChangeAction.ts b/libraries/rush-lib/src/cli/actions/ChangeAction.ts index fed5bb1f8f5..c302a9a3e46 100644 --- a/libraries/rush-lib/src/cli/actions/ChangeAction.ts +++ b/libraries/rush-lib/src/cli/actions/ChangeAction.ts @@ -4,7 +4,6 @@ import * as path from 'node:path'; import * as child_process from 'node:child_process'; - import type { CommandLineFlagParameter, CommandLineStringParameter, @@ -30,6 +29,7 @@ import { import { ProjectChangeAnalyzer } from '../../logic/ProjectChangeAnalyzer'; import { Git } from '../../logic/Git'; import { RushConstants } from '../../logic/RushConstants'; +import * as PolicyValidator from '../../logic/policy/PolicyValidator'; const BULK_LONG_NAME: string = '--bulk'; const BULK_MESSAGE_LONG_NAME: string = '--message'; @@ -171,6 +171,15 @@ export class ChangeAction extends BaseRushAction { } public async runAsync(): Promise { + await PolicyValidator.validatePolicyAsync( + this.rushConfiguration, + this.rushConfiguration.defaultSubspace, + undefined, + { + allowShrinkwrapUpdates: true + } + ); + if (this._verifyAllParameter.value) { const incompatibleParameters: ( | CommandLineFlagParameter @@ -319,10 +328,7 @@ export class ChangeAction extends BaseRushAction { this.terminal, await this._getChangeFilesSinceBaseBranchAsync() ); - changeFileData = await this._promptForChangeFileDataAsync( - sortedProjectList, - existingChangeComments - ); + changeFileData = await this._promptForChangeFileDataAsync(sortedProjectList, existingChangeComments); if (this._isEmailRequired(changeFileData)) { const email: string = this._changeEmailParameter.value @@ -592,9 +598,7 @@ export class ChangeAction extends BaseRushAction { } } - private async _promptForCommentsAsync( - packageName: string - ): Promise { + private async _promptForCommentsAsync(packageName: string): Promise { const bumpOptions: { [type: string]: string } = this._getBumpOptions(packageName); const { default: input } = await import('@inquirer/input'); const comment: string = await input({ message: `Describe changes, or ENTER if no changes:` }); @@ -680,10 +684,7 @@ export class ChangeAction extends BaseRushAction { * or will ask for it if it is not found or the Git config is wrong. */ private async _detectOrAskForEmailAsync(): Promise { - return ( - (await this._detectAndConfirmEmailAsync()) || - (await this._promptForEmailAsync()) - ); + return (await this._detectAndConfirmEmailAsync()) || (await this._promptForEmailAsync()); } private _detectEmail(): string | undefined { @@ -780,9 +781,7 @@ export class ChangeAction extends BaseRushAction { const fileExists: boolean = FileSystem.exists(filePath); const shouldWrite: boolean = - !fileExists || - overwrite || - (interactiveMode ? await this._promptForOverwriteAsync(filePath) : false); + !fileExists || overwrite || (interactiveMode ? await this._promptForOverwriteAsync(filePath) : false); if (!interactiveMode && fileExists && !overwrite) { throw new Error(`Changefile ${filePath} already exists`); @@ -794,9 +793,7 @@ export class ChangeAction extends BaseRushAction { } } - private async _promptForOverwriteAsync( - filePath: string - ): Promise { + private async _promptForOverwriteAsync(filePath: string): Promise { const { default: confirm } = await import('@inquirer/confirm'); const overwrite: boolean = await confirm({ message: `Overwrite ${filePath}?` diff --git a/libraries/rush-lib/src/cli/actions/test/ChangeAction.test.ts b/libraries/rush-lib/src/cli/actions/test/ChangeAction.test.ts new file mode 100644 index 00000000000..cbb1c9b2219 --- /dev/null +++ b/libraries/rush-lib/src/cli/actions/test/ChangeAction.test.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import '../../test/mockRushCommandLineParser'; + +import { AlreadyReportedError, LockFile } from '@rushstack/node-core-library'; + +import { EnvironmentConfiguration } from '../../../api/EnvironmentConfiguration'; +import * as PolicyValidator from '../../../logic/policy/PolicyValidator'; +import { RushCommandLineParser } from '../../RushCommandLineParser'; +import { ChangeAction } from '../ChangeAction'; + +describe(ChangeAction.name, () => { + let oldExitCode: number | string | undefined; + let oldArgs: string[]; + + beforeEach(() => { + jest.spyOn(process, 'exit').mockImplementation(); + + // Suppress "Another Rush command is already running" error + jest.spyOn(LockFile, 'tryAcquire').mockImplementation(() => ({}) as LockFile); + + oldExitCode = process.exitCode; + oldArgs = process.argv; + }); + + afterEach(() => { + jest.clearAllMocks(); + process.exitCode = oldExitCode; + process.argv = oldArgs; + EnvironmentConfiguration.reset(); + }); + + it('runs policy validation before verifying change files', async () => { + const startPath: string = `${__dirname}/changeRepo`; + const parser: RushCommandLineParser = new RushCommandLineParser({ cwd: startPath }); + + const validatePolicySpy: jest.SpyInstance = jest + .spyOn(PolicyValidator, 'validatePolicyAsync') + .mockResolvedValue(); + const verifySpy: jest.SpyInstance = jest + .spyOn(ChangeAction.prototype as unknown as { _verifyAsync: () => Promise }, '_verifyAsync') + .mockResolvedValue(); + + process.argv = [ + 'pretend-this-is-node.exe', + 'pretend-this-is-rush', + 'change', + '--verify', + '--target-branch', + 'origin/main' + ]; + + await expect(parser.executeAsync()).resolves.toEqual(true); + expect(validatePolicySpy).toHaveBeenCalledTimes(1); + expect(validatePolicySpy).toHaveBeenCalledWith( + parser.rushConfiguration, + parser.rushConfiguration.defaultSubspace, + undefined, + { + allowShrinkwrapUpdates: true + } + ); + expect(verifySpy).toHaveBeenCalledTimes(1); + }); + + it('aborts rush change when policy validation fails', async () => { + const startPath: string = `${__dirname}/changeRepo`; + const parser: RushCommandLineParser = new RushCommandLineParser({ cwd: startPath }); + + const verifySpy: jest.SpyInstance = jest + .spyOn(ChangeAction.prototype as unknown as { _verifyAsync: () => Promise }, '_verifyAsync') + .mockResolvedValue(); + jest.spyOn(PolicyValidator, 'validatePolicyAsync').mockRejectedValue(new AlreadyReportedError()); + + process.argv = [ + 'pretend-this-is-node.exe', + 'pretend-this-is-rush', + 'change', + '--verify', + '--target-branch', + 'origin/main' + ]; + + await expect(parser.executeAsync()).resolves.toEqual(false); + expect(verifySpy).not.toHaveBeenCalled(); + }); +}); diff --git a/libraries/rush-lib/src/cli/actions/test/changeRepo/a/package.json b/libraries/rush-lib/src/cli/actions/test/changeRepo/a/package.json new file mode 100644 index 00000000000..9113c2528ed --- /dev/null +++ b/libraries/rush-lib/src/cli/actions/test/changeRepo/a/package.json @@ -0,0 +1,4 @@ +{ + "name": "a", + "version": "1.0.0" +} diff --git a/libraries/rush-lib/src/cli/actions/test/changeRepo/rush.json b/libraries/rush-lib/src/cli/actions/test/changeRepo/rush.json new file mode 100644 index 00000000000..33ab285f0b1 --- /dev/null +++ b/libraries/rush-lib/src/cli/actions/test/changeRepo/rush.json @@ -0,0 +1,16 @@ +{ + "npmVersion": "6.4.1", + "rushVersion": "5.5.2", + "projectFolderMinDepth": 1, + "projectFolderMaxDepth": 99, + "gitPolicy": { + "allowedEmailRegExps": ["[^@]+@users\\.noreply\\.github\\.com"], + "sampleEmail": "example@users.noreply.github.com" + }, + "projects": [ + { + "packageName": "a", + "projectFolder": "a" + } + ] +}