Skip to content

Commit e9b18ee

Browse files
TheLarkInnCopilot
andcommitted
Add reporter-independent exit-code semantics
Preserve command success and exit-code semantics for @rushstack/reporter (#5858). - Add resolveExitStatus and resolveExitStatusFromEvents, which return 0 for success including warning-only success, 1 for failure and logical cancellation, and the conventional signal-derived status for OS signals - Take only failure, cancellation, and signal state as inputs, so the reporter mode and diagnostic categories can never select the exit code - Add separateJsonControls so the command-specific --json flag stays distinct from the json reporter and keeps its own schema - Cover success, failure, cancellation, signal precedence, category independence, and JSON-control separation 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 86c51f1 commit e9b18ee

7 files changed

Lines changed: 424 additions & 1 deletion

File tree

common/reviews/api/reporter.api.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,18 @@ export function deriveExitCodeFromEvents(events: readonly IReporterEventEnvelope
7777
// @beta
7878
export function encodeNdjsonRecord(value: unknown, options?: INdjsonOptions): string;
7979

80+
// @beta
81+
export const EXIT_CODE_FAILURE: 1;
82+
83+
// @beta
84+
export const EXIT_CODE_SUCCESS: 0;
85+
8086
// @beta
8187
export function getPrivacyClassificationRank(classification: ReporterPrivacyClassification): number;
8288

89+
// @beta
90+
export function getSignalExitCode(signal: NodeJS.Signals): number;
91+
8392
// @beta
8493
export interface IBootstrapEventBufferOptions {
8594
readonly maxBytes?: number;
@@ -185,6 +194,12 @@ export interface IEngineSinkResolution {
185194
readonly sink: IReporterEventSink;
186195
}
187196

197+
// @beta
198+
export interface IJsonControls {
199+
readonly commandJson: boolean;
200+
readonly reporterJson: boolean;
201+
}
202+
188203
// @beta
189204
export interface ILifecycleEmitterOptions {
190205
readonly protocolVersion?: IReporterProtocolVersion;
@@ -370,6 +385,19 @@ export interface IReporterRegistrationOptions {
370385
readonly required?: boolean;
371386
}
372387

388+
// @beta
389+
export interface IResolveExitStatusFromEventsOptions {
390+
readonly cancelled?: boolean;
391+
readonly signal?: NodeJS.Signals;
392+
}
393+
394+
// @beta
395+
export interface IResolveExitStatusOptions {
396+
readonly cancelled?: boolean;
397+
readonly hasFailures?: boolean;
398+
readonly signal?: NodeJS.Signals;
399+
}
400+
373401
// @beta
374402
export interface IRushDiagnostic {
375403
readonly category: RushDiagnosticCategory;
@@ -405,6 +433,13 @@ export interface IRushDiagnosticSource {
405433
readonly toolName?: string;
406434
}
407435

436+
// @beta
437+
export interface IRushExitStatus {
438+
readonly exitCode: number;
439+
readonly outcome: RushCommandOutcome;
440+
readonly signal?: NodeJS.Signals;
441+
}
442+
408443
// @beta
409444
export interface IRushPluginManifest {
410445
readonly pluginApiVersion: string;
@@ -648,6 +683,12 @@ export class ReporterMultiplexer implements IReporter {
648683
// @beta
649684
export type ReporterPrivacyClassification = 'public' | 'local-sensitive' | 'secret';
650685

686+
// @beta
687+
export function resolveExitStatus(options: IResolveExitStatusOptions): IRushExitStatus;
688+
689+
// @beta
690+
export function resolveExitStatusFromEvents(events: readonly IReporterEventEnvelope<unknown>[], options?: IResolveExitStatusFromEventsOptions): IRushExitStatus;
691+
651692
// @beta
652693
export function resolveReporterCompatibility(frontend: IReporterFrontendDescriptor, engine: IReporterEngineDescriptor): IReporterCompatibilityDecision;
653694

@@ -671,6 +712,9 @@ export const RUSH_PLUGIN_API_VERSION: '1.0.0';
671712
// @beta
672713
export const RUSH_REPORTER_BOOTSTRAP_HANDOFF_ENV_VAR: '_RUSH_REPORTER_BOOTSTRAP_HANDOFF';
673714

715+
// @beta
716+
export type RushCommandOutcome = 'succeeded' | 'failed' | 'cancelled' | 'signal';
717+
674718
// @beta
675719
export type RushDiagnosticCategory = 'configuration' | 'input' | 'dependency-tool' | 'environment' | 'network-auth' | 'operation' | 'internal';
676720

@@ -696,6 +740,9 @@ export class RushSessionReporting {
696740
getSink(): IReporterEventSink;
697741
}
698742

743+
// @beta
744+
export function separateJsonControls(argv: readonly string[]): IJsonControls;
745+
699746
// @beta
700747
export function summarizeShadowResult(events: readonly IReporterEventEnvelope<unknown>[]): IShadowResultSummary;
701748

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
/**
5+
* The independently-resolved JSON controls on a command line.
6+
*
7+
* @beta
8+
*/
9+
export interface IJsonControls {
10+
/**
11+
* Whether the command-specific `--json` flag was requested. This selects the
12+
* command's own JSON schema and is unchanged by the reporter system.
13+
*/
14+
readonly commandJson: boolean;
15+
16+
/**
17+
* Whether the `json` reporter was requested through `--reporter`.
18+
*/
19+
readonly reporterJson: boolean;
20+
}
21+
22+
/**
23+
* Separates the command-specific `--json` flag from the `json` reporter selection.
24+
*
25+
* @remarks
26+
* The command-specific `--json` behavior is preserved and is never an alias for
27+
* `--reporter=json`. Both may be requested independently, and each keeps its own
28+
* output schema.
29+
*
30+
* @param argv - the command-line arguments, excluding the node executable and script
31+
*
32+
* @beta
33+
*/
34+
export function separateJsonControls(argv: readonly string[]): IJsonControls {
35+
let commandJson: boolean = false;
36+
let reporterJson: boolean = false;
37+
38+
for (let index: number = 0; index < argv.length; index++) {
39+
const arg: string = argv[index];
40+
if (arg === '--json') {
41+
commandJson = true;
42+
} else if (arg === '--reporter=json') {
43+
reporterJson = true;
44+
} else if (arg === '--reporter' && argv[index + 1] === 'json') {
45+
reporterJson = true;
46+
}
47+
}
48+
49+
return { commandJson, reporterJson };
50+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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 * as os from 'node:os';
5+
6+
import type { IReporterEventEnvelope } from '../events/IReporterEventEnvelope';
7+
8+
/**
9+
* The exit code returned for success, including warning-only success.
10+
*
11+
* @beta
12+
*/
13+
export const EXIT_CODE_SUCCESS: 0 = 0;
14+
15+
/**
16+
* The exit code returned for a Rush or operation failure and for logical cancellation.
17+
*
18+
* @beta
19+
*/
20+
export const EXIT_CODE_FAILURE: 1 = 1;
21+
22+
/**
23+
* The logical outcome of a command, independent of presentation.
24+
*
25+
* @beta
26+
*/
27+
export type RushCommandOutcome = 'succeeded' | 'failed' | 'cancelled' | 'signal';
28+
29+
/**
30+
* The resolved exit status of a command.
31+
*
32+
* @beta
33+
*/
34+
export interface IRushExitStatus {
35+
/**
36+
* The process exit code.
37+
*/
38+
readonly exitCode: number;
39+
40+
/**
41+
* The logical outcome.
42+
*/
43+
readonly outcome: RushCommandOutcome;
44+
45+
/**
46+
* The terminating OS signal, when the outcome is `signal`.
47+
*/
48+
readonly signal?: NodeJS.Signals;
49+
}
50+
51+
/**
52+
* Returns the conventional exit code for an OS signal, `128 + signalNumber`.
53+
*
54+
* @param signal - the terminating signal
55+
*
56+
* @beta
57+
*/
58+
export function getSignalExitCode(signal: NodeJS.Signals): number {
59+
const signalNumber: number | undefined = (os.constants.signals as Record<string, number>)[signal];
60+
return 128 + (signalNumber ?? 0);
61+
}
62+
63+
/**
64+
* Options for {@link resolveExitStatus}.
65+
*
66+
* @beta
67+
*/
68+
export interface IResolveExitStatusOptions {
69+
/**
70+
* Whether the command had a Rush or operation failure. Warnings alone are not failures.
71+
*/
72+
readonly hasFailures?: boolean;
73+
74+
/**
75+
* Whether the command was logically cancelled or aborted.
76+
*/
77+
readonly cancelled?: boolean;
78+
79+
/**
80+
* The terminating OS signal, if any.
81+
*/
82+
readonly signal?: NodeJS.Signals;
83+
}
84+
85+
/**
86+
* Resolves a command's exit status from its outcome.
87+
*
88+
* @remarks
89+
* A signal termination yields the conventional signal-derived status.
90+
* Cancellation and failure yield `1`. Everything else, including warning-only
91+
* success, yields `0`. The reporter mode and diagnostic categories are never
92+
* inputs, so they can never select the exit code.
93+
*
94+
* @param options - the command outcome
95+
*
96+
* @beta
97+
*/
98+
export function resolveExitStatus(options: IResolveExitStatusOptions): IRushExitStatus {
99+
if (options.signal) {
100+
return { exitCode: getSignalExitCode(options.signal), outcome: 'signal', signal: options.signal };
101+
}
102+
if (options.cancelled) {
103+
return { exitCode: EXIT_CODE_FAILURE, outcome: 'cancelled' };
104+
}
105+
if (options.hasFailures) {
106+
return { exitCode: EXIT_CODE_FAILURE, outcome: 'failed' };
107+
}
108+
return { exitCode: EXIT_CODE_SUCCESS, outcome: 'succeeded' };
109+
}
110+
111+
/**
112+
* Options for {@link resolveExitStatusFromEvents}.
113+
*
114+
* @beta
115+
*/
116+
export interface IResolveExitStatusFromEventsOptions {
117+
/**
118+
* Whether the command was logically cancelled or aborted.
119+
*/
120+
readonly cancelled?: boolean;
121+
122+
/**
123+
* The terminating OS signal, if any.
124+
*/
125+
readonly signal?: NodeJS.Signals;
126+
}
127+
128+
/**
129+
* Resolves a command's exit status from its structured event stream.
130+
*
131+
* @remarks
132+
* A failure is any failed command result, any error-severity diagnostic, or any
133+
* failed operation. Diagnostic categories and the selected reporter are never
134+
* consulted, so they cannot influence the exit code. Warning-severity
135+
* diagnostics never cause failure.
136+
*
137+
* @param events - the structured events emitted during the command
138+
* @param options - cancellation and signal state
139+
*
140+
* @beta
141+
*/
142+
export function resolveExitStatusFromEvents(
143+
events: readonly IReporterEventEnvelope<unknown>[],
144+
options: IResolveExitStatusFromEventsOptions = {}
145+
): IRushExitStatus {
146+
let hasFailures: boolean = false;
147+
for (const event of events) {
148+
if (event.type === 'commandResult') {
149+
if ((event.payload as { succeeded: boolean }).succeeded === false) {
150+
hasFailures = true;
151+
}
152+
} else if (event.type === 'diagnosticEmitted') {
153+
if ((event.payload as { severity?: string }).severity === 'error') {
154+
hasFailures = true;
155+
}
156+
} else if (event.type === 'operationStatusChanged') {
157+
if ((event.payload as { status?: string }).status === 'failure') {
158+
hasFailures = true;
159+
}
160+
}
161+
}
162+
163+
return resolveExitStatus({ hasFailures, cancelled: options.cancelled, signal: options.signal });
164+
}

libraries/reporter/src/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,22 @@ export { TelemetrySubscriber, createTelemetryReporter } from './telemetry/Teleme
149149
export type { LegacyBeforeLogHook } from './telemetry/BeforeLogAdapter';
150150
export { createBeforeLogAdapter } from './telemetry/BeforeLogAdapter';
151151

152+
export type {
153+
RushCommandOutcome,
154+
IRushExitStatus,
155+
IResolveExitStatusOptions,
156+
IResolveExitStatusFromEventsOptions
157+
} from './exit/ExitStatus';
158+
export {
159+
EXIT_CODE_SUCCESS,
160+
EXIT_CODE_FAILURE,
161+
getSignalExitCode,
162+
resolveExitStatus,
163+
resolveExitStatusFromEvents
164+
} from './exit/ExitStatus';
165+
export type { IJsonControls } from './exit/CommandJson';
166+
export { separateJsonControls } from './exit/CommandJson';
167+
152168
export type { IReporterEmitEventInput, IReporterEventSink } from './producers/IReporterEventSink';
153169
export type {
154170
ReporterMessageSeverity,

0 commit comments

Comments
 (0)