Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 15 additions & 18 deletions libraries/rush-lib/src/cli/actions/ChangeAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import * as path from 'node:path';
import * as child_process from 'node:child_process';


import type {
CommandLineFlagParameter,
CommandLineStringParameter,
Expand All @@ -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';
Expand Down Expand Up @@ -171,6 +171,15 @@ export class ChangeAction extends BaseRushAction {
}

public async runAsync(): Promise<void> {
await PolicyValidator.validatePolicyAsync(
this.rushConfiguration,
this.rushConfiguration.defaultSubspace,
undefined,
{
allowShrinkwrapUpdates: true
}
);

if (this._verifyAllParameter.value) {
const incompatibleParameters: (
| CommandLineFlagParameter
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -592,9 +598,7 @@ export class ChangeAction extends BaseRushAction {
}
}

private async _promptForCommentsAsync(
packageName: string
): Promise<IChangeInfo | undefined> {
private async _promptForCommentsAsync(packageName: string): Promise<IChangeInfo | undefined> {
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:` });
Expand Down Expand Up @@ -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<string> {
return (
(await this._detectAndConfirmEmailAsync()) ||
(await this._promptForEmailAsync())
);
return (await this._detectAndConfirmEmailAsync()) || (await this._promptForEmailAsync());
}

private _detectEmail(): string | undefined {
Expand Down Expand Up @@ -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`);
Expand All @@ -794,9 +793,7 @@ export class ChangeAction extends BaseRushAction {
}
}

private async _promptForOverwriteAsync(
filePath: string
): Promise<boolean> {
private async _promptForOverwriteAsync(filePath: string): Promise<boolean> {
const { default: confirm } = await import('@inquirer/confirm');
const overwrite: boolean = await confirm({
message: `Overwrite ${filePath}?`
Expand Down
88 changes: 88 additions & 0 deletions libraries/rush-lib/src/cli/actions/test/ChangeAction.test.ts
Original file line number Diff line number Diff line change
@@ -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<void> }, '_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<void> }, '_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();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "a",
"version": "1.0.0"
}
16 changes: 16 additions & 0 deletions libraries/rush-lib/src/cli/actions/test/changeRepo/rush.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}