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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Emit shadow Rush lifecycle, phase-aware operation, diagnostic, telemetry, and command-result events without changing legacy terminal output.",
"type": "patch"
}
],
"packageName": "@microsoft/rush",
"email": "TheLarkInn@users.noreply.github.com"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@rushstack/rush-reporter",
"comment": "Add a stable structured diagnostic code for Rush command failures.",
"type": "patch"
}
],
"packageName": "@rushstack/rush-reporter",
"email": "TheLarkInn@users.noreply.github.com"
}
4 changes: 3 additions & 1 deletion common/reviews/api/rush-lib.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { IRushDiagnostic } from '@rushstack/rush-reporter';
import { IScopedLogger } from '@rushstack/rush-reporter';
import { IScopedMessageOptions } from '@rushstack/rush-reporter';
import { IScopedReporter } from '@rushstack/rush-reporter';
import type { ITelemetryAggregate } from '@rushstack/rush-reporter';
import { ITerminal } from '@rushstack/terminal';
import type { ITerminalChunk } from '@rushstack/terminal';
import { ITerminalProvider } from '@rushstack/terminal';
Expand Down Expand Up @@ -684,7 +685,7 @@ export interface _IOperationGraphEventSink {
onActivity?(text: string, options?: _IOperationActivityOptions): void;
onOperationChunk?(operationId: string, chunk: ITerminalChunk): void;
onOperationHeader?(operationId: string, completedOperations: number, totalOperations: number): void;
onOperationRegistered?(operationId: string, silent: boolean): void;
onOperationRegistered?(operationId: string, silent: boolean, result?: IOperationExecutionResult): void;
onOperationStatusChanged?(result: IOperationExecutionResult, previousStatus: OperationStatus): void;
onOperationStreamClosed?(operationId: string): void;
}
Expand Down Expand Up @@ -1044,6 +1045,7 @@ export interface ITelemetryData {
readonly operationResults?: Record<string, ITelemetryOperationResult>;
readonly performanceEntries?: readonly PerformanceEntry_2[];
readonly platform?: string;
readonly reporterData?: ITelemetryAggregate;
readonly result: 'Succeeded' | 'Failed';
readonly rushVersion?: string;
readonly timestampMs?: number;
Expand Down
6 changes: 6 additions & 0 deletions common/reviews/api/rush-reporter.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,12 @@ export const RUSH_DIAGNOSTIC_CODE_DEFINITIONS: readonly [{
readonly defaultSeverity: "error";
readonly summaryKey: "diagnostic.RUSH_EXTERNAL_TOOL_PROBLEM.summary";
readonly detailKey: undefined;
}, {
readonly code: "RUSH_COMMAND_FAILED";
readonly category: "operation";
readonly defaultSeverity: "error";
readonly summaryKey: "diagnostic.RUSH_COMMAND_FAILED.summary";
readonly detailKey: undefined;
}];

// @beta
Expand Down
39 changes: 20 additions & 19 deletions libraries/reporter/src/diagnostics/RushDiagnosticCodeRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,16 +111,13 @@ type AreValidRushDiagnosticCodeSegments<
? IsValidRushDiagnosticCodeSegment<TSegments>
: false;

type ValidateRushDiagnosticCode<TCode extends string> =
TCode extends `RUSH_${infer Segments}`
? AreValidRushDiagnosticCodeSegments<Segments> extends true
? TCode
: never
: never;
type ValidateRushDiagnosticCode<TCode extends string> = TCode extends `RUSH_${infer Segments}`
? AreValidRushDiagnosticCodeSegments<Segments> extends true
? TCode
: never
: never;

type ValidatedRushDiagnosticCodeDefinitions<
TDefinitions extends readonly IRushDiagnosticCodeDefinition[]
> = {
type ValidatedRushDiagnosticCodeDefinitions<TDefinitions extends readonly IRushDiagnosticCodeDefinition[]> = {
readonly [K in keyof TDefinitions]: TDefinitions[K] extends IRushDiagnosticCodeDefinition
? TDefinitions[K] & {
readonly code: ValidateRushDiagnosticCode<TDefinitions[K]['code']>;
Expand All @@ -130,9 +127,7 @@ type ValidatedRushDiagnosticCodeDefinitions<

function defineRushDiagnosticCodeDefinitions<
const TDefinitions extends readonly IRushDiagnosticCodeDefinition[]
>(
definitions: TDefinitions & ValidatedRushDiagnosticCodeDefinitions<TDefinitions>
): TDefinitions {
>(definitions: TDefinitions & ValidatedRushDiagnosticCodeDefinitions<TDefinitions>): TDefinitions {
return definitions;
}

Expand Down Expand Up @@ -233,6 +228,13 @@ export const RUSH_DIAGNOSTIC_CODE_DEFINITIONS = defineRushDiagnosticCodeDefiniti
defaultSeverity: 'error',
summaryKey: 'diagnostic.RUSH_EXTERNAL_TOOL_PROBLEM.summary',
detailKey: undefined
},
{
code: 'RUSH_COMMAND_FAILED',
category: 'operation',
defaultSeverity: 'error',
summaryKey: 'diagnostic.RUSH_COMMAND_FAILED.summary',
detailKey: undefined
}
]);

Expand All @@ -257,12 +259,11 @@ export type RushDiagnosticTemplateKey = NonNullable<
*
* @beta
*/
export const RUSH_DIAGNOSTIC_CODES: ReadonlyMap<RushDiagnosticCode, IRushDiagnosticCodeDefinition> =
new Map(
RUSH_DIAGNOSTIC_CODE_DEFINITIONS.map(
(definition: IRushDiagnosticCodeDefinition) => [definition.code, definition] as const
)
);
export const RUSH_DIAGNOSTIC_CODES: ReadonlyMap<RushDiagnosticCode, IRushDiagnosticCodeDefinition> = new Map(
RUSH_DIAGNOSTIC_CODE_DEFINITIONS.map(
(definition: IRushDiagnosticCodeDefinition) => [definition.code, definition] as const
)
);

export { isValidRushDiagnosticCode } from './RushDiagnosticCode';
export { RUSH_DIAGNOSTIC_TEMPLATES } from './templates';
export { RUSH_DIAGNOSTIC_TEMPLATES } from './templates';
3 changes: 2 additions & 1 deletion libraries/reporter/src/diagnostics/templates/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@
// eslint-disable-next-line @typescript-eslint/typedef -- literal keys are required for the Record<RushDiagnosticTemplateKey, string> aggregate check
export const OPERATION_DIAGNOSTIC_TEMPLATES = {
'diagnostic.RUSH_OPERATION_FAILED.summary': 'The operation for {projectName} failed.',
'diagnostic.RUSH_EXTERNAL_TOOL_PROBLEM.summary': '{tool} reported {code}: {message}'
'diagnostic.RUSH_EXTERNAL_TOOL_PROBLEM.summary': '{tool} reported {code}: {message}',
'diagnostic.RUSH_COMMAND_FAILED.summary': 'The Rush command {commandName} failed.'
} as const;
114 changes: 111 additions & 3 deletions libraries/rush-lib/src/cli/RushCommandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Colorize,
type ITerminal
} from '@rushstack/terminal';
import { createRushDiagnostic, type IRushDiagnostic, type LifecycleEmitter } from '@rushstack/rush-reporter';

import { RushConfiguration } from '../api/RushConfiguration';
import { RushConstants } from '../logic/RushConstants';
Expand Down Expand Up @@ -64,6 +65,13 @@ import { RushAlerts } from '../utilities/RushAlerts';
import { initializeDotEnv } from '../logic/dotenv';
import { measureAsyncFn } from '../utilities/performance';
import { EnvironmentVariableNames } from '../api/EnvironmentConfiguration';
import {
_correlateRushSessionError,
_getRushSessionDerivedExitStatus,
_getRushSessionLifecycleEmitter,
_getRushSessionReporterSourceVersion,
_isRushSessionErrorRepresented
} from '../pluginFramework/RushSession';

/**
* Options for `RushCommandLineParser`.
Expand Down Expand Up @@ -91,6 +99,11 @@ export class RushCommandLineParser extends CommandLineParser {
private readonly _terminal: Terminal;
private readonly _autocreateBuildCommand: boolean;
private _initializationFailed: boolean = false;
private _sessionLifecycleEmitter: LifecycleEmitter | undefined;
private _commandLifecycleEmitter: LifecycleEmitter | undefined;
private _sessionStartTimeMs: number | undefined;
private _commandStartTimeMs: number | undefined;
private _reporterCompletionEmitted: boolean = false;
private _reporterClosePromise: Promise<void> | undefined;

/**
Expand Down Expand Up @@ -264,12 +277,30 @@ export class RushCommandLineParser extends CommandLineParser {
this._terminalProvider.verboseEnabled = this._terminalProvider.debugEnabled =
rushArgv.includes('--debug') || rushArgv.includes('-d');

this._sessionLifecycleEmitter = _getRushSessionLifecycleEmitter(this.rushSession);
if (this._sessionLifecycleEmitter) {
this._sessionStartTimeMs = performance.now();
this._sessionLifecycleEmitter.emitSessionStarted({
rushVersion: _getRushSessionReporterSourceVersion(this.rushSession)!
});
}

try {
await measureAsyncFn('rush:initializeUnassociatedPlugins', () =>
this.pluginManager.tryInitializeUnassociatedPluginsAsync()
);

return await super.executeAsync(args);
const succeeded: boolean = await super.executeAsync(args);
if (!this._reporterCompletionEmitted) {
this._emitReporterCompletion(succeeded ? 0 : _getNumericProcessExitCode(1));
}
return succeeded;
} catch (error) {
if (!process.exitCode) {
process.exitCode = 1;
}
this._reportErrorAndSetExitCode(error as Error);
return false;
} finally {
await this._closeReporterAsync();
}
Expand All @@ -287,6 +318,17 @@ export class RushCommandLineParser extends CommandLineParser {
InternalError.breakInDebugger = true;
}

const commandName: string | undefined = this.selectedAction?.actionName;
if (commandName) {
this._commandLifecycleEmitter = _getRushSessionLifecycleEmitter(this.rushSession, {
commandName
});
if (this._commandLifecycleEmitter) {
this._commandStartTimeMs = performance.now();
this._commandLifecycleEmitter.emitCommandStarted({ commandName });
}
}

try {
await this._wrapOnExecuteAsync();

Expand Down Expand Up @@ -327,6 +369,7 @@ export class RushCommandLineParser extends CommandLineParser {

// If we make it here, everything went fine, so reset the exit code back to 0
process.exitCode = 0;
this._emitReporterCompletion(0);
} catch (error) {
this._reportErrorAndSetExitCode(error as Error);
}
Expand Down Expand Up @@ -544,6 +587,20 @@ export class RushCommandLineParser extends CommandLineParser {
}

private _reportErrorAndSetExitCode(error: Error): void {
const rushSession: RushSession | undefined = this.rushSession;
if (rushSession && !_isRushSessionErrorRepresented(rushSession, error)) {
const diagnostic: IRushDiagnostic = createRushDiagnostic('RUSH_COMMAND_FAILED', {
parameters: {
commandName: {
value: this.selectedAction?.actionName ?? 'unknown',
privacy: 'public'
}
}
});
this._commandLifecycleEmitter?.emitDiagnostic(diagnostic);
_correlateRushSessionError(rushSession, error, diagnostic.diagnosticId);
}

if (!(error instanceof AlreadyReportedError)) {
const prefix: string = 'ERROR: ';

Expand All @@ -564,15 +621,16 @@ export class RushCommandLineParser extends CommandLineParser {
console.error(`\n${error.stack}`);
}

this.flushTelemetry();

const configuredExitCode: string | number | undefined = process.exitCode;
const numericExitCode: number = Number(configuredExitCode);
const exitCode: number =
configuredExitCode !== undefined && Number.isInteger(numericExitCode) && numericExitCode !== 0
? numericExitCode
: 1;
process.exitCode = exitCode;
this._emitReporterCompletion(exitCode);
this.flushTelemetry();

const handleExit = (): never => {
// Ideally we want to eliminate all calls to process.exit() from our code, and replace them
// with normal control flow that properly cleans up its data structures.
Expand Down Expand Up @@ -620,4 +678,54 @@ export class RushCommandLineParser extends CommandLineParser {
}
return this._reporterClosePromise;
}

private _emitReporterCompletion(exitCode: number): void {
if (this._reporterCompletionEmitted) {
return;
}
this._reporterCompletionEmitted = true;

const commandName: string | undefined = this.selectedAction?.actionName;
if (commandName && this._commandLifecycleEmitter) {
const durationMs: number | undefined =
this._commandStartTimeMs === undefined ? undefined : performance.now() - this._commandStartTimeMs;
this._commandLifecycleEmitter.emitCommandResult({
commandName,
succeeded: exitCode === 0,
exitCode
});
this._commandLifecycleEmitter.emitCommandCompleted({
commandName,
exitCode,
...(durationMs === undefined ? {} : { durationMs })
});
}

if (this._sessionLifecycleEmitter) {
const durationMs: number | undefined =
this._sessionStartTimeMs === undefined ? undefined : performance.now() - this._sessionStartTimeMs;
this._sessionLifecycleEmitter.emitSessionCompleted({
exitCode,
...(durationMs === undefined ? {} : { durationMs })
});
}

// Shadow derivation is deliberately observational. process.exitCode remains authoritative.
const rushSession: RushSession | undefined = this.rushSession;
if (rushSession) {
_getRushSessionDerivedExitStatus(rushSession);
}
}
}

function _getNumericProcessExitCode(fallback: number): number {
const { exitCode } = process;
if (typeof exitCode === 'number') {
return exitCode;
}
if (typeof exitCode === 'string') {
const parsed: number = Number(exitCode);
return Number.isFinite(parsed) ? parsed : fallback;
}
return fallback;
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import { IgnoredParametersPlugin } from '../../logic/operations/IgnoredParameter
import { TrimRushEnvironmentVariablesPlugin } from '../../logic/operations/TrimRushEnvironmentVariablesPlugin';
import { DebugHashesPlugin } from '../../logic/operations/DebugHashesPlugin';
import { measureAsyncFn, measureFn } from '../../utilities/performance';
import { attachReporterOperationEventSink } from '../../logic/operations/ReporterOperationEventSink';

const PERF_PREFIX: 'rush:phasedScriptAction' = 'rush:phasedScriptAction';

Expand Down Expand Up @@ -677,6 +678,7 @@ export class PhasedScriptAction extends BaseScriptAction<IPhasedCommandConfig> i
await measureAsyncFn(`${PERF_PREFIX}:executionManager`, async () => {
await hooks.onGraphCreatedAsync.promise(graph, graphContext);
});
attachReporterOperationEventSink(graph, this.rushSession, this.actionName);

const executeOptions: IExecuteOperationsOptions = {
graph,
Expand Down
Loading