Skip to content
Open
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
10 changes: 10 additions & 0 deletions common/changes/@microsoft/rush/status-emojis_2025-07-30-23-06.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Add emojis for operation statuses. Replace the \"x of y\" operations indicator with a breakdown by status using the emojis. Add a legend at the start of the build log.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
14 changes: 10 additions & 4 deletions libraries/rush-lib/src/cli/scriptActions/PhasedScriptAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { PhasedOperationPlugin } from '../../logic/operations/PhasedOperationPlu
import { ShellOperationRunnerPlugin } from '../../logic/operations/ShellOperationRunnerPlugin';
import { Event } from '../../api/EventHooks';
import { ProjectChangeAnalyzer } from '../../logic/ProjectChangeAnalyzer';
import { OperationStatus } from '../../logic/operations/OperationStatus';
import { OperationStatus, STATUS_EMOJIS } from '../../logic/operations/OperationStatus';
import type {
IExecutionResult,
IOperationExecutionResult
Expand Down Expand Up @@ -955,7 +955,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {

stopwatch.stop();

const message: string = `rush ${this.actionName} (${stopwatch.toString()})`;
const message: string = `${STATUS_EMOJIS[result.status]} rush ${this.actionName} (${stopwatch.toString()})`;
if (result.status === OperationStatus.Success) {
terminal.writeLine(Colorize.green(message));
} else {
Expand All @@ -966,7 +966,9 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
stopwatch.stop();

if (error instanceof AlreadyReportedError) {
terminal.writeLine(`rush ${this.actionName} (${stopwatch.toString()})`);
terminal.writeLine(
`${STATUS_EMOJIS[OperationStatus.Failure]} rush ${this.actionName} (${stopwatch.toString()})`
);
} else {
if (error && (error as Error).message) {
if (this.parser.isDebug) {
Expand All @@ -976,7 +978,11 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> {
}
}

terminal.writeErrorLine(Colorize.red(`rush ${this.actionName} - Errors! (${stopwatch.toString()})`));
terminal.writeErrorLine(
Colorize.red(
`${STATUS_EMOJIS[OperationStatus.Failure]} rush ${this.actionName} - Errors! (${stopwatch.toString()})`
)
);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { NewlineKind, Async, InternalError, AlreadyReportedError } from '@rushst

import { AsyncOperationQueue, type IOperationSortFunction } from './AsyncOperationQueue';
import type { Operation } from './Operation';
import { OperationStatus } from './OperationStatus';
import { OperationStatus, STATUS_BY_EMOJI, STATUS_EMOJIS } from './OperationStatus';
import { type IOperationExecutionRecordContext, OperationExecutionRecord } from './OperationExecutionRecord';
import type { IExecutionResult } from './IOperationExecutionResult';
import type { IEnvironment } from '../../utilities/Utilities';
Expand Down Expand Up @@ -81,16 +81,19 @@ export class OperationExecutionManager {
operation: OperationExecutionRecord
) => Promise<OperationStatus | undefined>;
private readonly _afterExecuteOperation?: (operation: OperationExecutionRecord) => Promise<void>;
private readonly _onOperationStatusChanged?: (record: OperationExecutionRecord) => void;
private readonly _onOperationStatusChanged?: (
record: OperationExecutionRecord,
oldStatus: OperationStatus
) => void;
private readonly _beforeExecuteOperations?: (
records: Map<Operation, OperationExecutionRecord>
) => Promise<void>;
private readonly _createEnvironmentForOperation?: (operation: OperationExecutionRecord) => IEnvironment;

// Variables for current status
private readonly _operationCountByStatusEmoji: Record<string, number>;
private _hasAnyFailures: boolean;
private _hasAnyNonAllowedWarnings: boolean;
private _completedOperations: number;
private _executionQueue: AsyncOperationQueue;

public constructor(operations: Set<Operation>, options: IOperationExecutionManagerOptions) {
Expand All @@ -105,7 +108,6 @@ export class OperationExecutionManager {
beforeExecuteOperationsAsync: beforeExecuteOperations,
createEnvironmentForOperation
} = options;
this._completedOperations = 0;
this._quietMode = quietMode;
this._hasAnyFailures = false;
this._hasAnyNonAllowedWarnings = false;
Expand All @@ -115,10 +117,23 @@ export class OperationExecutionManager {
this._afterExecuteOperation = afterExecuteOperation;
this._beforeExecuteOperations = beforeExecuteOperations;
this._createEnvironmentForOperation = createEnvironmentForOperation;
this._onOperationStatusChanged = (record: OperationExecutionRecord) => {

const operationCountByStatusEmoji: Record<string, number> = {};
for (const statusEmoji of Object.values(STATUS_EMOJIS)) {
operationCountByStatusEmoji[statusEmoji] = 0;
}
this._operationCountByStatusEmoji = operationCountByStatusEmoji;

this._onOperationStatusChanged = (record: OperationExecutionRecord, oldStatus: OperationStatus) => {
if (record.status === OperationStatus.Ready) {
this._executionQueue.assignOperations();
}

if (!record.silent) {
operationCountByStatusEmoji[STATUS_EMOJIS[oldStatus]]--;
operationCountByStatusEmoji[STATUS_EMOJIS[record.status]]++;
}

onOperationStatusChanged?.(record);
};

Expand Down Expand Up @@ -192,24 +207,41 @@ export class OperationExecutionManager {
prioritySort
);
this._executionQueue = executionQueue;
for (const operation of executionRecords.values()) {
if (operation.silent) {
continue;
}
// Initialize the status counts
operationCountByStatusEmoji[STATUS_EMOJIS[operation.status]]++;
}
}

private _getStatusBar(): string {
const statusBarParts: string[] = [];
for (const emoji of STATUS_BY_EMOJI.keys()) {
const count: number = this._operationCountByStatusEmoji[emoji];
if (count > 0) {
statusBarParts.push(`${emoji} ${count}`);
}
}
return statusBarParts.join(' ');
}

private _streamCollator_onWriterActive = (writer: CollatedWriter | undefined): void => {
if (writer) {
this._completedOperations++;

// Format a header like this
//
// ==[ @rushstack/the-long-thing ]=================[ 1 of 1000 ]==
// ==[ @rushstack/the-long-thing ]=================[❌3 ✅20 📦245]==

// leftPart: "==[ @rushstack/the-long-thing "
const leftPart: string = Colorize.gray('==[') + ' ' + Colorize.cyan(writer.taskName) + ' ';
const leftPartLength: number = 4 + writer.taskName.length + 1;

// rightPart: " 1 of 1000 ]=="
const completedOfTotal: string = `${this._completedOperations} of ${this._totalOperations}`;
const rightPart: string = ' ' + Colorize.white(completedOfTotal) + ' ' + Colorize.gray(']==');
const rightPartLength: number = 1 + completedOfTotal.length + 4;
const statusBar: string = this._getStatusBar();

// rightPart: "❌3 ✅20 📦245]=="
const rightPart: string = Colorize.white(statusBar) + Colorize.gray(']==');
const rightPartLength: number = statusBar.length + 3;

// middlePart: "]=================["
const twoBracketsLength: number = 2;
Expand All @@ -233,7 +265,6 @@ export class OperationExecutionManager {
* operations are completed successfully, or rejects when any operation fails.
*/
public async executeAsync(): Promise<IExecutionResult> {
this._completedOperations = 0;
const totalOperations: number = this._totalOperations;

if (!this._quietMode) {
Expand All @@ -254,6 +285,11 @@ export class OperationExecutionManager {

this._terminal.writeStdoutLine(`Executing a maximum of ${this._parallelism} simultaneous processes...`);

this._terminal.writeStdoutLine(`Legend:`);
for (const [emoji, statuses] of STATUS_BY_EMOJI) {
this._terminal.writeStdoutLine(`${emoji} ${statuses.join(' / ')}`);
}

const maxParallelism: number = Math.min(totalOperations, this._parallelism);

await this._beforeExecuteOperations?.(this._executionRecords);
Expand Down Expand Up @@ -352,7 +388,9 @@ export class OperationExecutionManager {

// This creates the writer, so don't do this globally
const { terminal } = record.collatedWriter;
terminal.writeStderrLine(Colorize.red(`"${name}" failed to build.`));
terminal.writeStderrLine(
`${STATUS_EMOJIS[OperationStatus.Failure]} ${Colorize.red(`"${name}" failed to build.`)}`
);
const blockedQueue: Set<OperationExecutionRecord> = new Set(record.consumers);

for (const blockedRecord of blockedQueue) {
Expand All @@ -361,16 +399,13 @@ export class OperationExecutionManager {
// {blockedRecord.runner} with a no-op that sets status to Blocked and logs the blocking
// operations. However, the existing behavior is a bit simpler, so keeping that for now.
if (!blockedRecord.silent) {
terminal.writeStdoutLine(`"${blockedRecord.name}" is blocked by "${name}".`);
terminal.writeStdoutLine(
`${STATUS_EMOJIS[OperationStatus.Blocked]} "${blockedRecord.name}" is blocked by "${name}".`
);
}
blockedRecord.status = OperationStatus.Blocked;

this._executionQueue.complete(blockedRecord);
if (!blockedRecord.silent) {
// Only increment the count if the operation is not silent to avoid confusing the user.
// The displayed total is the count of non-silent operations.
this._completedOperations++;
}

for (const dependent of blockedRecord.consumers) {
blockedQueue.add(dependent);
Expand All @@ -392,7 +427,7 @@ export class OperationExecutionManager {
case OperationStatus.FromCache: {
if (!silent) {
record.collatedWriter.terminal.writeStdoutLine(
Colorize.green(`"${name}" was restored from the build cache.`)
`${STATUS_EMOJIS[OperationStatus.FromCache]} ${Colorize.green(`"${name}" was restored from the build cache.`)}`
);
}
break;
Expand All @@ -403,7 +438,9 @@ export class OperationExecutionManager {
*/
case OperationStatus.Skipped: {
if (!silent) {
record.collatedWriter.terminal.writeStdoutLine(Colorize.green(`"${name}" was skipped.`));
record.collatedWriter.terminal.writeStdoutLine(
`${STATUS_EMOJIS[OperationStatus.Skipped]} ${Colorize.green(`"${name}" was skipped.`)}`
);
}
break;
}
Expand All @@ -413,15 +450,17 @@ export class OperationExecutionManager {
*/
case OperationStatus.NoOp: {
if (!silent) {
record.collatedWriter.terminal.writeStdoutLine(Colorize.gray(`"${name}" did not define any work.`));
record.collatedWriter.terminal.writeStdoutLine(
`${STATUS_EMOJIS[OperationStatus.NoOp]} ${Colorize.gray(`"${name}" did not define any work.`)}`
);
}
break;
}

case OperationStatus.Success: {
if (!silent) {
record.collatedWriter.terminal.writeStdoutLine(
Colorize.green(`"${name}" completed successfully in ${stopwatch.toString()}.`)
`${STATUS_EMOJIS[OperationStatus.Success]} ${Colorize.green(`"${name}" completed successfully in ${stopwatch.toString()}.`)}`
);
}
break;
Expand All @@ -430,7 +469,7 @@ export class OperationExecutionManager {
case OperationStatus.SuccessWithWarning: {
if (!silent) {
record.collatedWriter.terminal.writeStderrLine(
Colorize.yellow(`"${name}" completed with warnings in ${stopwatch.toString()}.`)
`${STATUS_EMOJIS[OperationStatus.SuccessWithWarning]} ${Colorize.yellow(`"${name}" completed with warnings in ${stopwatch.toString()}.`)}`
);
}
this._hasAnyNonAllowedWarnings = this._hasAnyNonAllowedWarnings || !runner.warningsAreAllowed;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
*/
export interface IOperationExecutionRecordContext {
streamCollator: StreamCollator;
onOperationStatusChanged?: (record: OperationExecutionRecord) => void;
onOperationStatusChanged?: (record: OperationExecutionRecord, oldStatus: OperationStatus) => void;
createEnvironment?: (record: OperationExecutionRecord) => IEnvironment;
inputsSnapshot: IInputsSnapshot | undefined;

Expand Down Expand Up @@ -207,11 +207,12 @@ export class OperationExecutionRecord implements IOperationRunnerContext, IOpera
return this._status;
}
public set status(newStatus: OperationStatus) {
if (newStatus === this._status) {
const oldStatus: OperationStatus = this._status;
if (newStatus === oldStatus) {
return;
}
this._status = newStatus;
this._context.onOperationStatusChanged?.(this);
this._context.onOperationStatusChanged?.(this, oldStatus);
}

public get silent(): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
} from '../../pluginFramework/PhasedCommandHooks';
import type { IExecutionResult, IOperationExecutionResult } from './IOperationExecutionResult';
import type { Operation } from './Operation';
import { OperationStatus } from './OperationStatus';
import { OperationStatus, STATUS_EMOJIS } from './OperationStatus';
import type { OperationExecutionRecord } from './OperationExecutionRecord';
import type { IStopwatchResult } from '../../utilities/Stopwatch';

Expand Down Expand Up @@ -257,7 +257,7 @@ function writeDetailedSummary(
);

terminal.writeLine(
`${Colorize.gray('--[')} ${headingColor(subheadingText)} ${Colorize.gray(
`${Colorize.gray('--[')}${STATUS_EMOJIS[operationResult.status]} ${headingColor(subheadingText)} ${Colorize.gray(
`]${'-'.repeat(middlePartLengthMinusTwoBrackets)}[`
)} ${Colorize.white(time)} ${Colorize.gray(']--')}\n`
);
Expand Down Expand Up @@ -289,14 +289,14 @@ function writeSummaryHeader(
const headingText: string = `${status}: ${projectsText}`;

// leftPart: "==[ FAILED: 2 operations "
const leftPartLength: number = 3 + 1 + headingText.length + 1;
const leftPartLength: number = 3 + 2 + headingText.length + 1;

const rightPartLengthMinusBracket: number = Math.max(ASCII_HEADER_WIDTH - (leftPartLength + 1), 0);

// rightPart: "]======================"

terminal.writeLine(
`${Colorize.gray('==[')} ${headingColor(headingText)} ${Colorize.gray(
`${Colorize.gray('==[')}${STATUS_EMOJIS[status]} ${headingColor(headingText)} ${Colorize.gray(
`]${'='.repeat(rightPartLengthMinusBracket)}`
)}\n`
);
Expand Down
39 changes: 39 additions & 0 deletions libraries/rush-lib/src/logic/operations/OperationStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,45 @@ export enum OperationStatus {
NoOp = 'NO OP'
}

/**
* Mapping from {@link OperationStatus} to an emoji that can be used in the terminal.
* @alpha
*/
export const STATUS_EMOJIS: Record<OperationStatus, string> = {
[OperationStatus.Failure]: '❌\ufe0f',
[OperationStatus.SuccessWithWarning]: '⚡\ufe0f',

[OperationStatus.Executing]: '🔄',

[OperationStatus.Waiting]: '⏳\ufe0f',
[OperationStatus.Queued]: '⏳\ufe0f',
[OperationStatus.Ready]: '⏳\ufe0f',

[OperationStatus.Blocked]: '🚧',

[OperationStatus.Success]: '✅\ufe0f',

[OperationStatus.FromCache]: '📦',
[OperationStatus.Skipped]: '📦',
[OperationStatus.NoOp]: ''
};

/**
* The set of unique status emojis used in the `STATUS_EMOJIS` mapping.
* @alpha
*/
export const STATUS_BY_EMOJI: Map<string, OperationStatus[]> = new Map();
for (const [status, emoji] of Object.entries(STATUS_EMOJIS)) {
if (emoji) {
let existingStatuses: OperationStatus[] | undefined = STATUS_BY_EMOJI.get(emoji);
if (!existingStatuses) {
existingStatuses = [];
STATUS_BY_EMOJI.set(emoji, existingStatuses);
}
existingStatuses.push(status as OperationStatus);
}
}

/**
* The set of statuses that are considered terminal.
* @alpha
Expand Down
Loading