Skip to content

Commit dad6c0c

Browse files
TheLarkInnCopilot
andcommitted
Add shadow lifecycle emission and parity helpers
Add first-party lifecycle emission for @rushstack/reporter (#5858). - Add typed lifecycle payloads and a LifecycleEmitter that actions, the scheduler, and plugins use to publish session, command, and operation events plus structured diagnostics - Mark every lifecycle, result, and diagnostic event required, and keep the emitter output-neutral so legacy rendering stays the sole visible output - Add deriveExitCodeFromEvents and summarizeShadowResult to validate exit-code and result parity against the frozen baselines - Cover emission, scope merging, output neutrality, and parity with tests Assistant-model: GitHub Copilot Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 897dcf7e-e6e8-4a84-85ca-34b93fa29be3
1 parent f992184 commit dad6c0c

8 files changed

Lines changed: 749 additions & 1 deletion

File tree

common/reviews/api/reporter.api.md

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ export const DEFAULT_SIGNAL_FLUSH_TIMEOUT_MS: number;
6565
// @beta
6666
export function deleteBootstrapHandoffFileAsync(filePath: string): Promise<void>;
6767

68+
// @beta
69+
export function deriveExitCodeFromEvents(events: readonly IReporterEventEnvelope<unknown>[]): number;
70+
6871
// @beta
6972
export function encodeNdjsonRecord(value: unknown, options?: INdjsonOptions): string;
7073

@@ -118,6 +121,29 @@ export interface IClassifiedDiagnosticValue {
118121
readonly value: ReporterJsonValue;
119122
}
120123

124+
// @beta
125+
export interface ICommandCompletedPayload {
126+
readonly commandName: string;
127+
readonly durationMs?: number;
128+
readonly exitCode: number;
129+
}
130+
131+
// @beta
132+
export interface ICommandResultPayload {
133+
readonly commandName: string;
134+
readonly exitCode: number;
135+
readonly operationCounts?: {
136+
readonly [status: string]: number;
137+
};
138+
readonly succeeded: boolean;
139+
}
140+
141+
// @beta
142+
export interface ICommandStartedPayload {
143+
readonly argv?: readonly string[];
144+
readonly commandName: string;
145+
}
146+
121147
// @beta
122148
export interface ICreateRushDiagnosticOptions {
123149
readonly causeDiagnosticIds?: readonly string[];
@@ -153,6 +179,15 @@ export interface IEngineSinkResolution {
153179
readonly sink: IReporterEventSink;
154180
}
155181

182+
// @beta
183+
export interface ILifecycleEmitterOptions {
184+
readonly protocolVersion?: IReporterProtocolVersion;
185+
readonly scope?: IReporterEventScope;
186+
readonly sessionId: string;
187+
readonly sink: IReporterEventSink;
188+
readonly source: IReporterEventSource;
189+
}
190+
156191
// @beta
157192
export interface INdjsonOptions {
158193
readonly maxRecordBytes?: number;
@@ -167,6 +202,19 @@ export interface IOldEngineOutputAdapterOptions {
167202
readonly source: IReporterEventSource;
168203
}
169204

205+
// @beta
206+
export interface IOperationRegisteredPayload {
207+
readonly operationId: string;
208+
readonly phaseName?: string;
209+
readonly projectName?: string;
210+
}
211+
212+
// @beta
213+
export interface IOperationStatusChangedPayload {
214+
readonly operationId: string;
215+
readonly status: OperationStatus;
216+
}
217+
170218
// @beta
171219
export interface IReporter {
172220
closeAsync(): Promise<void>;
@@ -398,6 +446,28 @@ export interface IScopedReporter {
398446
emitMessage(options: IScopedMessageOptions): string;
399447
}
400448

449+
// @beta
450+
export interface ISessionCompletedPayload {
451+
readonly durationMs?: number;
452+
readonly exitCode: number;
453+
}
454+
455+
// @beta
456+
export interface ISessionStartedPayload {
457+
readonly cwd?: string;
458+
readonly rushVersion: string;
459+
}
460+
461+
// @beta
462+
export interface IShadowResultSummary {
463+
readonly commandName?: string;
464+
readonly exitCode: number;
465+
readonly operationCounts: {
466+
readonly [status: string]: number;
467+
};
468+
readonly succeeded: boolean;
469+
}
470+
401471
// @beta
402472
export function isPluginApiVersionSupported(declaredApiVersion: string, supportedApiVersion?: string): boolean;
403473

@@ -410,6 +480,12 @@ export function isReporterProtocolCompatible(consumer: IReporterProtocolVersion,
410480
// @beta
411481
export function isValidRushDiagnosticCode(code: string): boolean;
412482

483+
// @beta
484+
export interface IWatchCycleCompletedPayload {
485+
readonly changedProjects?: readonly string[];
486+
readonly succeeded: boolean;
487+
}
488+
413489
// @beta
414490
export interface IWriteBootstrapHandoffOptions {
415491
readonly directory?: string;
@@ -422,6 +498,28 @@ export class LegacyFallbackSink implements IReporterEventSink {
422498
emit(): string;
423499
}
424500

501+
// @beta
502+
export class LifecycleEmitter {
503+
constructor(options: ILifecycleEmitterOptions);
504+
// (undocumented)
505+
emitCommandCompleted(payload: ICommandCompletedPayload): string;
506+
// (undocumented)
507+
emitCommandResult(payload: ICommandResultPayload): string;
508+
// (undocumented)
509+
emitCommandStarted(payload: ICommandStartedPayload): string;
510+
emitDiagnostic(diagnostic: IRushDiagnostic): string;
511+
// (undocumented)
512+
emitOperationRegistered(payload: IOperationRegisteredPayload): string;
513+
// (undocumented)
514+
emitOperationStatusChanged(payload: IOperationStatusChangedPayload): string;
515+
// (undocumented)
516+
emitSessionCompleted(payload: ISessionCompletedPayload): string;
517+
// (undocumented)
518+
emitSessionStarted(payload: ISessionStartedPayload): string;
519+
// (undocumented)
520+
emitWatchCycleCompleted(payload: IWatchCycleCompletedPayload): string;
521+
}
522+
425523
// @beta
426524
export class NdjsonDecoder {
427525
constructor(options?: INdjsonOptions);
@@ -444,6 +542,9 @@ export class OldEngineOutputAdapter {
444542
capture(stream: 'stdout' | 'stderr', text: string): string[];
445543
}
446544

545+
// @beta
546+
export type OperationStatus = 'ready' | 'executing' | 'success' | 'successWithWarnings' | 'failure' | 'blocked' | 'skipped' | 'fromCache' | 'noOp';
547+
447548
// @beta
448549
export function parseEarlyReporterControls(argv: readonly string[], env: Record<string, string | undefined>): IEarlyReporterControls;
449550

@@ -568,6 +669,9 @@ export class RushSessionReporting {
568669
getSink(): IReporterEventSink;
569670
}
570671

672+
// @beta
673+
export function summarizeShadowResult(events: readonly IReporterEventEnvelope<unknown>[]): IShadowResultSummary;
674+
571675
// @beta
572676
export function writeBootstrapHandoffFileAsync(buffer: BootstrapEventBuffer, options?: IWriteBootstrapHandoffOptions): Promise<string>;
573677

libraries/reporter/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@ export {
127127
createPluginApiIncompatibleDiagnostic
128128
} from './session/PluginApi';
129129

130+
export type {
131+
OperationStatus,
132+
ISessionStartedPayload,
133+
ISessionCompletedPayload,
134+
ICommandStartedPayload,
135+
ICommandCompletedPayload,
136+
IOperationRegisteredPayload,
137+
IOperationStatusChangedPayload,
138+
ICommandResultPayload,
139+
IWatchCycleCompletedPayload
140+
} from './lifecycle/LifecycleEvents';
141+
export type { ILifecycleEmitterOptions } from './lifecycle/LifecycleEmitter';
142+
export { LifecycleEmitter } from './lifecycle/LifecycleEmitter';
143+
export type { IShadowResultSummary } from './lifecycle/ShadowParity';
144+
export { deriveExitCodeFromEvents, summarizeShadowResult } from './lifecycle/ShadowParity';
145+
130146
export type { IReporterEmitEventInput, IReporterEventSink } from './producers/IReporterEventSink';
131147
export type {
132148
ReporterMessageSeverity,
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
2+
// See LICENSE in the project root for license information.
3+
4+
import type { IReporterProtocolVersion } from '../events/ReporterProtocolVersion';
5+
import type { IReporterEventScope, IReporterEventSource } from '../events/IReporterEventEnvelope';
6+
import type { IReporterEventSink } from '../producers/IReporterEventSink';
7+
import type { IRushDiagnostic } from '../diagnostics/IRushDiagnostic';
8+
import { computeEnvelopePrivacyFloor } from '../diagnostics/DiagnosticPrivacy';
9+
import { REPORTER_PROTOCOL_VERSION } from '../protocol/ReporterProtocol';
10+
import type {
11+
ISessionStartedPayload,
12+
ISessionCompletedPayload,
13+
ICommandStartedPayload,
14+
ICommandCompletedPayload,
15+
IOperationRegisteredPayload,
16+
IOperationStatusChangedPayload,
17+
ICommandResultPayload,
18+
IWatchCycleCompletedPayload
19+
} from './LifecycleEvents';
20+
21+
/**
22+
* Options for constructing a {@link LifecycleEmitter}.
23+
*
24+
* @beta
25+
*/
26+
export interface ILifecycleEmitterOptions {
27+
/**
28+
* The sink events are emitted into.
29+
*/
30+
readonly sink: IReporterEventSink;
31+
32+
/**
33+
* The session id stamped onto emitted events.
34+
*/
35+
readonly sessionId: string;
36+
37+
/**
38+
* The producer identity stamped onto emitted events.
39+
*/
40+
readonly source: IReporterEventSource;
41+
42+
/**
43+
* The base scope merged into every emitted event.
44+
*/
45+
readonly scope?: IReporterEventScope;
46+
47+
/**
48+
* The protocol version stamped onto emitted events. Defaults to
49+
* {@link REPORTER_PROTOCOL_VERSION}.
50+
*/
51+
readonly protocolVersion?: IReporterProtocolVersion;
52+
}
53+
54+
/**
55+
* Emits the canonical first-party lifecycle and diagnostic events.
56+
*
57+
* @remarks
58+
* Actions, the operation scheduler, and plugins use this to publish structured
59+
* events. During the shadow phase these events flow to subscribers while legacy
60+
* rendering remains the sole visible output; the emitter itself writes nothing
61+
* to stdout or stderr. Every lifecycle, result, and diagnostic event is marked
62+
* required so the manager never drops it.
63+
*
64+
* @beta
65+
*/
66+
export class LifecycleEmitter {
67+
private readonly _sink: IReporterEventSink;
68+
private readonly _sessionId: string;
69+
private readonly _source: IReporterEventSource;
70+
private readonly _scope: IReporterEventScope | undefined;
71+
private readonly _protocolVersion: IReporterProtocolVersion;
72+
73+
public constructor(options: ILifecycleEmitterOptions) {
74+
this._sink = options.sink;
75+
this._sessionId = options.sessionId;
76+
this._source = options.source;
77+
this._scope = options.scope;
78+
this._protocolVersion = options.protocolVersion ?? REPORTER_PROTOCOL_VERSION;
79+
}
80+
81+
public emitSessionStarted(payload: ISessionStartedPayload): string {
82+
return this._emit('sessionStarted', payload, 'public');
83+
}
84+
85+
public emitSessionCompleted(payload: ISessionCompletedPayload): string {
86+
return this._emit('sessionCompleted', payload, 'public');
87+
}
88+
89+
public emitCommandStarted(payload: ICommandStartedPayload): string {
90+
return this._emit('commandStarted', payload, 'public', { commandName: payload.commandName });
91+
}
92+
93+
public emitCommandCompleted(payload: ICommandCompletedPayload): string {
94+
return this._emit('commandCompleted', payload, 'public', { commandName: payload.commandName });
95+
}
96+
97+
public emitOperationRegistered(payload: IOperationRegisteredPayload): string {
98+
return this._emit('operationRegistered', payload, 'public', {
99+
operationId: payload.operationId,
100+
projectName: payload.projectName,
101+
phaseName: payload.phaseName
102+
});
103+
}
104+
105+
public emitOperationStatusChanged(payload: IOperationStatusChangedPayload): string {
106+
return this._emit('operationStatusChanged', payload, 'public', {
107+
operationId: payload.operationId
108+
});
109+
}
110+
111+
public emitCommandResult(payload: ICommandResultPayload): string {
112+
return this._emit('commandResult', payload, 'public', { commandName: payload.commandName });
113+
}
114+
115+
public emitWatchCycleCompleted(payload: IWatchCycleCompletedPayload): string {
116+
return this._emit('watchCycleCompleted', payload, 'public');
117+
}
118+
119+
/**
120+
* Emits a structured diagnostic alongside the existing legacy rendering.
121+
*/
122+
public emitDiagnostic(diagnostic: IRushDiagnostic): string {
123+
const classifications: ReadonlyArray<'public' | 'local-sensitive' | 'secret'> = diagnostic.parameters
124+
? Object.values(diagnostic.parameters).map((value) => value.privacy)
125+
: [];
126+
return this._emit('diagnosticEmitted', diagnostic, computeEnvelopePrivacyFloor(classifications));
127+
}
128+
129+
private _emit(
130+
type:
131+
| 'sessionStarted'
132+
| 'sessionCompleted'
133+
| 'commandStarted'
134+
| 'commandCompleted'
135+
| 'operationRegistered'
136+
| 'operationStatusChanged'
137+
| 'commandResult'
138+
| 'watchCycleCompleted'
139+
| 'diagnosticEmitted',
140+
payload: unknown,
141+
privacy: 'public' | 'local-sensitive' | 'secret',
142+
scopeOverride?: IReporterEventScope
143+
): string {
144+
const scope: IReporterEventScope | undefined =
145+
this._scope || scopeOverride ? { ...this._scope, ...scopeOverride } : undefined;
146+
return this._sink.emit({
147+
protocolVersion: this._protocolVersion,
148+
sessionId: this._sessionId,
149+
source: this._source,
150+
scope,
151+
privacy,
152+
required: true,
153+
type,
154+
payload
155+
});
156+
}
157+
}

0 commit comments

Comments
 (0)