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"
}
2 changes: 2 additions & 0 deletions 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 @@ -1042,6 +1043,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;
128 changes: 121 additions & 7 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 All @@ -90,6 +98,12 @@ export class RushCommandLineParser extends CommandLineParser {
private readonly _terminalProvider: ConsoleTerminalProvider;
private readonly _terminal: Terminal;
private readonly _autocreateBuildCommand: boolean;
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;

/**
* The current working directory that was used to find the Rush configuration.
Expand Down Expand Up @@ -249,12 +263,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 @@ -272,6 +304,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 @@ -312,6 +355,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 @@ -529,6 +573,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 @@ -549,6 +607,7 @@ export class RushCommandLineParser extends CommandLineParser {
console.error(`\n${error.stack}`);
}

this._emitReporterCompletion(_getNumericProcessExitCode(1));
this.flushTelemetry();

const handleExit = (): never => {
Expand Down Expand Up @@ -584,12 +643,67 @@ export class RushCommandLineParser extends CommandLineParser {
}
}

private async _closeReporterAsync(): Promise<void> {
try {
await this._rushOptions.reporterCloseAsync?.();
} catch (error) {
process.exitCode = 1;
process.stderr.write(`[reporter] Unable to finalize reporters: ${(error as Error).message}\n`);
private _closeReporterAsync(): Promise<void> {
if (!this._reporterClosePromise) {
this._reporterClosePromise = (async (): Promise<void> => {
try {
await this._rushOptions.reporterCloseAsync?.();
} catch (error) {
process.exitCode = 1;
process.stderr.write(`[reporter] Unable to finalize reporters: ${(error as Error).message}\n`);
}
})();
}
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 @@ -61,6 +61,7 @@ import { NodeDiagnosticDirPlugin } from '../../logic/operations/NodeDiagnosticDi
import { IgnoredParametersPlugin } from '../../logic/operations/IgnoredParametersPlugin';
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 @@ -668,6 +669,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