Skip to content
Merged
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": "@microsoft/rush",
"comment": "Preserve immutable errors, diagnose pre-execution parser failures once, and register shadow operations after final watch iteration configuration.",
"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": "@microsoft/rush",
"comment": "Report early initialization failures and defer successful reporter completion until telemetry finalization preserves the command's native outcome.",
"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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@rushstack/rush-reporter",
"comment": "Correlate diagnostics using weak metadata instead of mutating potentially frozen or non-extensible errors.",
"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
10 changes: 10 additions & 0 deletions libraries/reporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ Canonical event protocol, reporter manager, and built-in reporters for Rush.

This package is released as a public beta. Exported contracts may change before the stable release.

## Shadow lifecycle compatibility

Error correlation uses external weak metadata, so frozen and non-extensible errors retain their original
identity, cause, and properties. Correlation remains visible across bridge instances without keeping errors alive.

Rush command-line parse failures emit one session-scoped `RUSH_COMMAND_FAILED` diagnostic before completion.
The original parser message is retained in the diagnostic's local-sensitive `message` parameter; native error
rendering and exit codes remain unchanged. Operation registration observes the final iteration configuration,
so unchanged watch operations do not produce visible shadow registration or status events.

## Links

- [CHANGELOG.md](https://github.com/microsoft/rushstack/blob/main/libraries/reporter/CHANGELOG.md) - Find out
Expand Down
8 changes: 4 additions & 4 deletions libraries/reporter/src/compat/LegacyErrorBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { RushError } from '../diagnostics/RushError';
*/
export const ALREADY_REPORTED_ERROR_NAME: 'AlreadyReportedError' = 'AlreadyReportedError';

const CORRELATION_KEY: unique symbol = Symbol('rush-reporter-correlated-diagnostic-id');
const correlatedDiagnosticIds: WeakMap<object, string> = new WeakMap();

/**
* The criteria that must be met before the legacy error bridge is removed.
Expand Down Expand Up @@ -94,11 +94,11 @@ export class LegacyErrorBridge {
}

/**
* Correlates a legacy sentinel error with the diagnostic id it corresponds to.
* Correlates an error with its diagnostic id without modifying the supplied object.
*/
public correlate(error: unknown, diagnosticId: string): void {
if (typeof error === 'object' && error !== null) {
(error as { [CORRELATION_KEY]?: string })[CORRELATION_KEY] = diagnosticId;
correlatedDiagnosticIds.set(error, diagnosticId);
}
}

Expand All @@ -107,7 +107,7 @@ export class LegacyErrorBridge {
*/
public getCorrelatedDiagnosticId(error: unknown): string | undefined {
if (typeof error === 'object' && error !== null) {
return (error as { [CORRELATION_KEY]?: string })[CORRELATION_KEY];
return correlatedDiagnosticIds.get(error);
}
return undefined;
}
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;
22 changes: 22 additions & 0 deletions libraries/reporter/src/test/LegacyErrorBridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,26 @@ describe('LegacyErrorBridge', () => {
bridge.recordEmittedDiagnostic('diag_2');
expect(bridge.shouldSuppressRendering(sentinel)).toBe(true);
});

it.each([Object.freeze, Object.seal, Object.preventExtensions])(
'correlates an immutable error without modifying its identity, cause, or properties (%p)',
(restrict) => {
const cause: Error = new Error('original cause');
const error: Error = new Error('original failure', { cause });
restrict(error);
const descriptors: PropertyDescriptorMap = Object.getOwnPropertyDescriptors(error);
const bridge: LegacyErrorBridge = new LegacyErrorBridge();
const otherBridge: LegacyErrorBridge = new LegacyErrorBridge();

bridge.correlate(error, 'immutable-error');

expect(Object.getOwnPropertyDescriptors(error)).toEqual(descriptors);
expect(error.cause).toBe(cause);
expect(otherBridge.getCorrelatedDiagnosticId(error)).toBe('immutable-error');
expect(otherBridge.shouldSuppressRendering(error)).toBe(false);
otherBridge.recordEmittedDiagnostic('immutable-error');
expect(otherBridge.shouldSuppressRendering(error)).toBe(true);
expect(otherBridge.shouldSuppressRendering(new Error(error.message, { cause }))).toBe(false);
}
);
});
Loading