Skip to content

Commit 100d8b8

Browse files
TheLarkInnCopilot
andcommitted
Add scoped session reporting and plugin API compatibility
Expose the producer-facing reporting surface for @rushstack/reporter (#5858). - Add createScopedReporter and RushSessionReporting so RushSession can create scoped reporters and loggers bound to a command, operation, project, and phase - Add IScopedLogger, a presentation-free logger with no terminal handle - Pass the sink to actions through IReporterExecutionContext while exposing only emit methods, so plugins cannot inspect modes, destinations, or thresholds - Add the Rush plugin API version, manifest field, a support check, and a structured migration diagnostic for incompatible plugins - Cover scoped emission, the privacy floor, the emit-only surface, logging, the session facade, and plugin compatibility 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 6a36aae commit 100d8b8

10 files changed

Lines changed: 658 additions & 1 deletion

File tree

common/reviews/api/reporter.api.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,18 @@ export function computeEnvelopePrivacyFloor(classifications: Iterable<ReporterPr
4141
// @beta
4242
export function createEngineSink(providedSink?: IReporterEventSink): IEngineSinkResolution;
4343

44+
// @beta
45+
export function createPluginApiIncompatibleDiagnostic(manifest: IRushPluginManifest): IRushDiagnostic;
46+
4447
// @beta
4548
export function createRushDiagnostic(code: string, options?: ICreateRushDiagnosticOptions): IRushDiagnostic;
4649

50+
// @beta
51+
export function createScopedLogger(reporter: IScopedReporter): IScopedLogger;
52+
53+
// @beta
54+
export function createScopedReporter(options: ICreateScopedReporterOptions): IScopedReporter;
55+
4756
// @beta
4857
export const DEFAULT_FLUSH_TIMEOUT_MS: number;
4958

@@ -123,6 +132,15 @@ export interface ICreateRushDiagnosticOptions {
123132
readonly source?: IRushDiagnosticSource;
124133
}
125134

135+
// @beta
136+
export interface ICreateScopedReporterOptions {
137+
readonly protocolVersion?: IReporterProtocolVersion;
138+
readonly scope?: IReporterEventScope;
139+
readonly sessionId: string;
140+
readonly sink: IReporterEventSink;
141+
readonly source: IReporterEventSource;
142+
}
143+
126144
// @beta
127145
export interface IEarlyReporterControls {
128146
readonly logLevel?: string;
@@ -220,6 +238,12 @@ export interface IReporterEventSource {
220238
readonly packageVersion: string;
221239
}
222240

241+
// @beta
242+
export interface IReporterExecutionContext {
243+
readonly reporter: IScopedReporter;
244+
readonly sink: IReporterEventSink;
245+
}
246+
223247
// @beta
224248
export interface IReporterFrontendDescriptor {
225249
readonly hasManager: boolean;
@@ -327,6 +351,12 @@ export interface IRushDiagnosticSource {
327351
readonly toolName?: string;
328352
}
329353

354+
// @beta
355+
export interface IRushPluginManifest {
356+
readonly pluginApiVersion: string;
357+
readonly pluginName: string;
358+
}
359+
330360
// @beta
331361
export interface IRushRemediationAction {
332362
readonly automatedExecutionSafety: RushRemediationSafety;
@@ -335,9 +365,25 @@ export interface IRushRemediationAction {
335365
readonly documentationUrl?: string;
336366
}
337367

368+
// @beta
369+
export interface IRushSessionReportingOptions {
370+
readonly protocolVersion?: IReporterProtocolVersion;
371+
readonly sessionId: string;
372+
readonly sink: IReporterEventSink;
373+
readonly source: IReporterEventSource;
374+
}
375+
338376
// @beta
339377
export function isBootstrapHandoffFileName(fileName: string): boolean;
340378

379+
// @beta
380+
export interface IScopedLogger {
381+
writeDebugLine(text: string): string;
382+
writeErrorLine(text: string): string;
383+
writeLine(text: string): string;
384+
writeWarningLine(text: string): string;
385+
}
386+
341387
// @beta
342388
export interface IScopedMessageOptions {
343389
readonly privacy?: ReporterPrivacyClassification;
@@ -352,6 +398,9 @@ export interface IScopedReporter {
352398
emitMessage(options: IScopedMessageOptions): string;
353399
}
354400

401+
// @beta
402+
export function isPluginApiVersionSupported(declaredApiVersion: string, supportedApiVersion?: string): boolean;
403+
355404
// @beta
356405
export function isReporterExtensionEventName(name: string): boolean;
357406

@@ -488,6 +537,9 @@ export const RUSH_DIAGNOSTIC_TEMPLATES: {
488537
// @beta
489538
export const RUSH_INTERNAL_ERROR_CODE: 'RUSH_INTERNAL_UNEXPECTED';
490539

540+
// @beta
541+
export const RUSH_PLUGIN_API_VERSION: '1.0.0';
542+
491543
// @beta
492544
export const RUSH_REPORTER_BOOTSTRAP_HANDOFF_ENV_VAR: '_RUSH_REPORTER_BOOTSTRAP_HANDOFF';
493545

@@ -507,6 +559,15 @@ export class RushError extends Error {
507559
// @beta
508560
export type RushRemediationSafety = 'safe' | 'requires-confirmation' | 'unsafe';
509561

562+
// @beta
563+
export class RushSessionReporting {
564+
constructor(options: IRushSessionReportingOptions);
565+
createExecutionContext(scope?: IReporterEventScope): IReporterExecutionContext;
566+
createScopedLogger(scope?: IReporterEventScope): IScopedLogger;
567+
createScopedReporter(scope?: IReporterEventScope): IScopedReporter;
568+
getSink(): IReporterEventSink;
569+
}
570+
510571
// @beta
511572
export function writeBootstrapHandoffFileAsync(buffer: BootstrapEventBuffer, options?: IWriteBootstrapHandoffOptions): Promise<string>;
512573

libraries/reporter/src/diagnostics/RushDiagnosticCodeRegistry.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ export const RUSH_DIAGNOSTIC_CODE_DEFINITIONS: readonly IRushDiagnosticCodeDefin
7878
defaultSeverity: 'error',
7979
summaryKey: 'diagnostic.RUSH_INPUT_UNKNOWN_PROJECT.summary'
8080
},
81+
{
82+
code: 'RUSH_PLUGIN_API_INCOMPATIBLE',
83+
category: 'configuration',
84+
defaultSeverity: 'error',
85+
summaryKey: 'diagnostic.RUSH_PLUGIN_API_INCOMPATIBLE.summary',
86+
detailKey: 'diagnostic.RUSH_PLUGIN_API_INCOMPATIBLE.detail'
87+
},
8188
{
8289
code: 'RUSH_DEPENDENCY_TOOL_FAILED',
8390
category: 'dependency-tool',
@@ -143,6 +150,10 @@ export const RUSH_DIAGNOSTIC_CODES: ReadonlyMap<string, IRushDiagnosticCodeDefin
143150
export const RUSH_DIAGNOSTIC_TEMPLATES: { readonly [resourceKey: string]: string } = {
144151
'diagnostic.RUSH_CONFIG_INVALID_JSON.summary': 'The configuration file {file} contains invalid JSON.',
145152
'diagnostic.RUSH_INPUT_UNKNOWN_PROJECT.summary': 'The project {projectName} was not found in rush.json.',
153+
'diagnostic.RUSH_PLUGIN_API_INCOMPATIBLE.summary':
154+
'The plugin {pluginName} declares an unsupported Rush plugin API version {declaredApiVersion}.',
155+
'diagnostic.RUSH_PLUGIN_API_INCOMPATIBLE.detail':
156+
'This Rush supports plugin API version {supportedApiVersion}. Update the plugin or Rush so the major versions match.',
146157
'diagnostic.RUSH_DEPENDENCY_TOOL_FAILED.summary': 'The package manager exited with code {exitCode}.',
147158
'diagnostic.RUSH_DEPENDENCY_TOOL_FAILED.detail':
148159
'The command {command} failed. See {logPath} for the full output.',

libraries/reporter/src/index.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,19 @@ export { LegacyFallbackSink, createEngineSink } from './compat/LegacyFallbackSin
114114
export type { IOldEngineOutputAdapterOptions } from './compat/OldEngineOutputAdapter';
115115
export { OldEngineOutputAdapter } from './compat/OldEngineOutputAdapter';
116116

117+
export type { ICreateScopedReporterOptions } from './session/ScopedReporterFactory';
118+
export { createScopedReporter } from './session/ScopedReporterFactory';
119+
export type { IScopedLogger } from './session/ScopedLogger';
120+
export { createScopedLogger } from './session/ScopedLogger';
121+
export type { IRushSessionReportingOptions, IReporterExecutionContext } from './session/RushSessionReporting';
122+
export { RushSessionReporting } from './session/RushSessionReporting';
123+
export type { IRushPluginManifest } from './session/PluginApi';
124+
export {
125+
RUSH_PLUGIN_API_VERSION,
126+
isPluginApiVersionSupported,
127+
createPluginApiIncompatibleDiagnostic
128+
} from './session/PluginApi';
129+
117130
export type { IReporterEmitEventInput, IReporterEventSink } from './producers/IReporterEventSink';
118131
export type {
119132
ReporterMessageSeverity,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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 { IRushDiagnostic } from '../diagnostics/IRushDiagnostic';
5+
import { createRushDiagnostic } from '../diagnostics/createRushDiagnostic';
6+
7+
/**
8+
* The Rush plugin API version this package implements.
9+
*
10+
* @remarks
11+
* A plugin manifest declares the plugin API version it targets. Compatibility is
12+
* gated on the major version.
13+
*
14+
* @beta
15+
*/
16+
export const RUSH_PLUGIN_API_VERSION: '1.0.0' = '1.0.0';
17+
18+
/**
19+
* The reporting-relevant fields of a Rush plugin manifest.
20+
*
21+
* @beta
22+
*/
23+
export interface IRushPluginManifest {
24+
/**
25+
* The plugin's name.
26+
*/
27+
readonly pluginName: string;
28+
29+
/**
30+
* The Rush plugin API version the plugin targets, for example `1.0.0`.
31+
*/
32+
readonly pluginApiVersion: string;
33+
}
34+
35+
function majorOf(version: string): number {
36+
return Number.parseInt(version.split('.')[0], 10);
37+
}
38+
39+
/**
40+
* Returns `true` if a plugin's declared API version is supported.
41+
*
42+
* @remarks
43+
* Compatibility requires an equal major version.
44+
*
45+
* @param declaredApiVersion - the version declared by the plugin manifest
46+
* @param supportedApiVersion - the version supported by Rush; defaults to
47+
* {@link RUSH_PLUGIN_API_VERSION}
48+
*
49+
* @beta
50+
*/
51+
export function isPluginApiVersionSupported(
52+
declaredApiVersion: string,
53+
supportedApiVersion: string = RUSH_PLUGIN_API_VERSION
54+
): boolean {
55+
const declaredMajor: number = majorOf(declaredApiVersion);
56+
const supportedMajor: number = majorOf(supportedApiVersion);
57+
return Number.isFinite(declaredMajor) && declaredMajor === supportedMajor;
58+
}
59+
60+
/**
61+
* Creates the structured migration diagnostic for an incompatible plugin.
62+
*
63+
* @remarks
64+
* An incompatible plugin fails before its `apply()` runs. This diagnostic is
65+
* emitted at that boundary.
66+
*
67+
* @param manifest - the incompatible plugin's manifest
68+
*
69+
* @beta
70+
*/
71+
export function createPluginApiIncompatibleDiagnostic(manifest: IRushPluginManifest): IRushDiagnostic {
72+
return createRushDiagnostic('RUSH_PLUGIN_API_INCOMPATIBLE', {
73+
parameters: {
74+
pluginName: { value: manifest.pluginName, privacy: 'public' },
75+
declaredApiVersion: { value: manifest.pluginApiVersion, privacy: 'public' },
76+
supportedApiVersion: { value: RUSH_PLUGIN_API_VERSION, privacy: 'public' }
77+
}
78+
});
79+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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 { IScopedReporter } from '../producers/IScopedReporter';
8+
import { createScopedReporter } from './ScopedReporterFactory';
9+
import { createScopedLogger, type IScopedLogger } from './ScopedLogger';
10+
11+
/**
12+
* Options for constructing a {@link RushSessionReporting}.
13+
*
14+
* @beta
15+
*/
16+
export interface IRushSessionReportingOptions {
17+
/**
18+
* The sink events are emitted into.
19+
*/
20+
readonly sink: IReporterEventSink;
21+
22+
/**
23+
* The session id stamped onto emitted events.
24+
*/
25+
readonly sessionId: string;
26+
27+
/**
28+
* The producer identity stamped onto emitted events.
29+
*/
30+
readonly source: IReporterEventSource;
31+
32+
/**
33+
* The protocol version stamped onto emitted events.
34+
*/
35+
readonly protocolVersion?: IReporterProtocolVersion;
36+
}
37+
38+
/**
39+
* The reporting context handed to an action.
40+
*
41+
* @remarks
42+
* Actions receive both the raw sink and a scoped reporter through their
43+
* execution context. Neither exposes reporter instances, destinations, modes, or
44+
* thresholds.
45+
*
46+
* @beta
47+
*/
48+
export interface IReporterExecutionContext {
49+
/**
50+
* The sink the action may emit into directly.
51+
*/
52+
readonly sink: IReporterEventSink;
53+
54+
/**
55+
* A scoped reporter bound to the action's scope.
56+
*/
57+
readonly reporter: IScopedReporter;
58+
}
59+
60+
/**
61+
* The reporting surface exposed by `RushSession` to actions and plugins.
62+
*
63+
* @remarks
64+
* `RushSession` composes this to create scoped reporters and loggers. Plugins
65+
* receive scoped reporters and loggers only, so they cannot inspect active
66+
* modes, destinations, or thresholds. Actions additionally receive the sink
67+
* through an execution context.
68+
*
69+
* @beta
70+
*/
71+
export class RushSessionReporting {
72+
private readonly _sink: IReporterEventSink;
73+
private readonly _sessionId: string;
74+
private readonly _source: IReporterEventSource;
75+
private readonly _protocolVersion: IReporterProtocolVersion | undefined;
76+
77+
public constructor(options: IRushSessionReportingOptions) {
78+
this._sink = options.sink;
79+
this._sessionId = options.sessionId;
80+
this._source = options.source;
81+
this._protocolVersion = options.protocolVersion;
82+
}
83+
84+
/**
85+
* Creates a scoped reporter bound to the given scope.
86+
*/
87+
public createScopedReporter(scope?: IReporterEventScope): IScopedReporter {
88+
return createScopedReporter({
89+
sink: this._sink,
90+
sessionId: this._sessionId,
91+
source: this._source,
92+
scope,
93+
protocolVersion: this._protocolVersion
94+
});
95+
}
96+
97+
/**
98+
* Creates a scoped logger bound to the given scope.
99+
*/
100+
public createScopedLogger(scope?: IReporterEventScope): IScopedLogger {
101+
return createScopedLogger(this.createScopedReporter(scope));
102+
}
103+
104+
/**
105+
* Returns the raw sink handed to actions through the execution context.
106+
*/
107+
public getSink(): IReporterEventSink {
108+
return this._sink;
109+
}
110+
111+
/**
112+
* Creates the execution context an action receives.
113+
*/
114+
public createExecutionContext(scope?: IReporterEventScope): IReporterExecutionContext {
115+
return {
116+
sink: this._sink,
117+
reporter: this.createScopedReporter(scope)
118+
};
119+
}
120+
}

0 commit comments

Comments
 (0)