From 9c80df2cb210b65230e60502026c608d97786eb6 Mon Sep 17 00:00:00 2001 From: TheLarkInn Date: Fri, 28 Aug 2026 07:52:04 +0000 Subject: [PATCH 1/5] Add direct Rush reporter demo path Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d6318e80-5da9-4858-a147-817e8692f10e --- apps/rush/src/MinimalRushConfiguration.ts | 42 ++++- apps/rush/src/RushFrontend.ts | 25 ++- apps/rush/src/RushReporterHost.ts | 168 +++++++++++------ .../src/test/MinimalRushConfiguration.test.ts | 1 + apps/rush/src/test/RushReporterHost.test.ts | 76 +++++++- .../src/test/sandbox/reporter-demo/README.md | 25 +++ .../src/test/sandbox/reporter-demo/run.mjs | 51 ++++++ ...r-r5b-demo-reporters_2026-08-28-07-10.json | 11 ++ ...r-r5b-demo-reporters_2026-08-28-07-10.json | 11 ++ common/reviews/api/rush-lib.api.md | 2 + common/reviews/api/rush-reporter.api.md | 1 + .../reporter/src/reporters/AiReporter.ts | 25 ++- .../reporters/DefaultInteractiveReporter.ts | 79 +++++--- .../reporter/src/reporters/FileReporter.ts | 170 +++++++++++++++++- .../reporter/src/reporters/LegacyReporter.ts | 4 +- .../src/reporters/PlaintextReporter.ts | 71 +++++++- .../test/DefaultInteractiveReporter.test.ts | 32 +++- .../reporter/src/test/FileReporter.test.ts | 64 ++++++- .../reporter/src/test/JsonAiReporter.test.ts | 26 +++ .../src/test/OperationStreamEmitter.test.ts | 3 + .../src/test/PlaintextReporter.test.ts | 35 ++++ libraries/rush-lib/src/api/Rush.ts | 2 +- .../rush-lib/src/cli/RushCommandLineParser.ts | 144 +++++++++++---- .../src/cli/actions/BaseRushAction.ts | 11 +- .../cli/scriptActions/PhasedScriptAction.ts | 38 ++-- .../cli/test/RushCommandLineParser.test.ts | 30 ++++ .../rush-lib/src/logic/ProjectWatcher.ts | 14 +- .../operations/ReporterOperationEventSink.ts | 46 ++++- .../test/OperationGraphEventSink.test.ts | 97 ++++++++++ .../src/pluginFramework/RushSession.ts | 25 +++ 30 files changed, 1176 insertions(+), 153 deletions(-) create mode 100644 apps/rush/src/test/sandbox/reporter-demo/README.md create mode 100644 apps/rush/src/test/sandbox/reporter-demo/run.mjs create mode 100644 common/changes/@microsoft/rush/copilot-reporter-r5b-demo-reporters_2026-08-28-07-10.json create mode 100644 common/changes/@rushstack/rush-reporter/copilot-reporter-r5b-demo-reporters_2026-08-28-07-10.json diff --git a/apps/rush/src/MinimalRushConfiguration.ts b/apps/rush/src/MinimalRushConfiguration.ts index 62aef01d11d..8a7ffa0d3b7 100644 --- a/apps/rush/src/MinimalRushConfiguration.ts +++ b/apps/rush/src/MinimalRushConfiguration.ts @@ -52,14 +52,29 @@ export class MinimalRushConfiguration { } public static loadFromDefaultLocation(): MinimalRushConfiguration | undefined { + const showVerbose: boolean = !RushCommandLineParser.shouldRestrictConsoleOutput(); const rushJsonLocation: string | undefined = RushConfiguration.tryFindRushJsonLocation({ - showVerbose: !RushCommandLineParser.shouldRestrictConsoleOutput() + showVerbose: false }); if (rushJsonLocation) { const minimalRushConfigurationJson: IMinimalRushConfigurationJson | undefined = _loadConfigurationJson(rushJsonLocation); if (minimalRushConfigurationJson) { - return new MinimalRushConfiguration(minimalRushConfigurationJson, rushJsonLocation); + const configuration: MinimalRushConfiguration = new MinimalRushConfiguration( + minimalRushConfigurationJson, + rushJsonLocation + ); + if ( + showVerbose && + !configuration.useRushReporter && + !_hasExplicitNonLegacyReporter(process.argv.slice(2)) && + path.dirname(rushJsonLocation) !== process.cwd() + ) { + // Preserve the legacy discovery message exactly when the reporter path is not taking ownership. + console.log('Found configuration in ' + rushJsonLocation); + console.log(''); + } + return configuration; } return undefined; } else { @@ -94,6 +109,29 @@ export class MinimalRushConfiguration { public get useRushReporter(): boolean { return this._useRushReporter; } + + /** + * The repository's common temp folder, used for invocation-scoped reporter logs. + */ + public get commonTempFolder(): string { + return path.resolve(this._commonRushConfigFolder, '..', '..', 'temp'); + } +} + +function _hasExplicitNonLegacyReporter(argv: readonly string[]): boolean { + for (let index: number = 0; index < argv.length; index++) { + const argument: string = argv[index]; + let value: string | undefined; + if (argument === '--reporter') { + value = argv[index + 1]; + } else if (argument.startsWith('--reporter=')) { + value = argument.slice('--reporter='.length); + } + if (value !== undefined) { + return value.trim().toLowerCase() !== 'legacy'; + } + } + return false; } function _loadConfigurationJson(rushJsonFilename: string): IMinimalRushConfigurationJson | undefined { diff --git a/apps/rush/src/RushFrontend.ts b/apps/rush/src/RushFrontend.ts index 00caadebf4b..c6088eb195d 100644 --- a/apps/rush/src/RushFrontend.ts +++ b/apps/rush/src/RushFrontend.ts @@ -4,7 +4,10 @@ import { randomUUID } from 'node:crypto'; import type { ILaunchOptions } from '@microsoft/rush-lib'; -import { DEFAULT_SIGNAL_FLUSH_TIMEOUT_MS } from '@rushstack/rush-reporter'; +import { + DEFAULT_SIGNAL_FLUSH_TIMEOUT_MS, + REPORTER_PROTOCOL_VERSION +} from '@rushstack/rush-reporter'; import { initializeRushReporterHostAsync, @@ -139,10 +142,13 @@ export async function launchRushFrontendAsync(options: IRushFrontendOptions): Pr processLifecycle = createProcessLifecycle() } = options; + const engineArgv: string[] = stripReporterValueControls(process.argv.slice(2)); const reporterHost: IInitializedRushReporterHost = await initializeReporterHostAsync({ repositoryOptIn: configuration?.useRushReporter, forceLegacy: rushVersionToLoad !== undefined && rushVersionToLoad !== currentPackageVersion, - selectedRushVersion: rushVersionToLoad + selectedRushVersion: rushVersionToLoad, + commonTempFolder: configuration?.commonTempFolder, + actionName: engineArgv.find((argument: string) => !argument.startsWith('-')) }); const reporterLifecycle: RushFrontendReporterLifecycle | undefined = reporterHost.selection.enabled ? new RushFrontendReporterLifecycle(reporterHost, processLifecycle) @@ -157,6 +163,21 @@ export async function launchRushFrontendAsync(options: IRushFrontendOptions): Pr const reporterCloseAsync: () => Promise = () => reporterLifecycle?.closeAsync() ?? reporterHost.closeAsync(); const sessionId: string = createSessionId(); + if (reporterHost.selection.enabled && reporterHost.logArtifact?.path) { + reporterHost.sink.emit({ + protocolVersion: REPORTER_PROTOCOL_VERSION, + sessionId, + source: { packageName: '@microsoft/rush', packageVersion: currentPackageVersion }, + privacy: 'local-sensitive', + type: 'artifactAvailable', + payload: { + role: 'log', + path: reporterHost.logArtifact.path, + format: 'plaintext', + complete: false + } + }); + } const reporterLaunchOptions: IRushFrontendLaunchOptions = { ...launchOptions, reporter: { diff --git a/apps/rush/src/RushReporterHost.ts b/apps/rush/src/RushReporterHost.ts index 82dc2d4f902..fcfc53232cf 100644 --- a/apps/rush/src/RushReporterHost.ts +++ b/apps/rush/src/RushReporterHost.ts @@ -20,8 +20,10 @@ import { shouldRenderAtLogLevel, type IReporter, type IReporterContext, + type IReporterEmitEventInput, type IReporterEventEnvelope, type IReporterEventSink, + type IFileReporterArtifact, type IReporterOutputTarget, type ReporterEventType, type ReporterLogLevel, @@ -39,6 +41,9 @@ export interface IRushReporterHostOptions { readonly env?: Record; readonly cwd?: string; readonly stdout?: IRushReporterOutputStream; + readonly stderr?: IRushReporterOutputStream; + readonly commonTempFolder?: string; + readonly actionName?: string; readonly includeDefaultFileReporter?: boolean; readonly commandName?: 'rush' | 'rush-pnpm' | 'rushx'; readonly repositoryOptIn?: boolean; @@ -65,13 +70,14 @@ export interface IInitializedRushReporterHost { readonly host: ReporterHost; readonly sink: IReporterEventSink; readonly selection: IRushReporterSelection; + readonly logArtifact: IFileReporterArtifact | undefined; closeAsync(timeoutMs?: number): Promise; } const REPORTER_VALUE_FLAGS: ReadonlySet = new Set(['--reporter', '--output', '--log-level']); const ALL_REPORTER_VALUE_FLAGS: readonly string[] = ['--reporter', '--output', '--log-level']; const REPORTER_SELECTION_FLAG: readonly string[] = ['--reporter']; -const DEFERRED_OPERATION_EVENT_TYPES: ReadonlySet = new Set([ +const GROUPED_OPERATION_EVENT_TYPES: ReadonlySet = new Set([ 'operationRegistered', 'operationStatusChanged', 'operationStreamClosed', @@ -93,10 +99,16 @@ class LogLevelReporter implements IReporter { private readonly _reporter: IReporter; private readonly _logLevel: ReporterLogLevel; + private readonly _preserveOperationStream: boolean; - public constructor(reporter: IReporter, logLevel: ReporterLogLevel) { + public constructor( + reporter: IReporter, + logLevel: ReporterLogLevel, + preserveOperationStream: boolean = false + ) { this._reporter = reporter; this._logLevel = logLevel; + this._preserveOperationStream = preserveOperationStream; this.name = reporter.name; } @@ -105,39 +117,18 @@ class LogLevelReporter implements IReporter { } public report(event: IReporterEventEnvelope): void { - if (shouldRenderAtLogLevel(this._logLevel, event)) { - this._reporter.report(event); - } - } - - public flushAsync(): Promise { - return this._reporter.flushAsync(); - } - - public closeAsync(): Promise { - return this._reporter.closeAsync(); - } -} - -/** - * Keeps operation presentation on the legacy collator until R5B transfers terminal ownership. - */ -class DeferredOperationPresentationReporter implements IReporter { - public readonly name: string; - - private readonly _reporter: IReporter; - - public constructor(reporter: IReporter) { - this._reporter = reporter; - this.name = reporter.name; - } - - public initializeAsync(context: IReporterContext): Promise { - return this._reporter.initializeAsync(context); - } - - public report(event: IReporterEventEnvelope): void { - if (!DEFERRED_OPERATION_EVENT_TYPES.has(event.type)) { + const verboseTerminalMessage: boolean = + this._logLevel === 'verbose' && + event.type === 'messageEmitted' && + (event.payload as { severity?: string }).severity === 'debug'; + if ( + shouldRenderAtLogLevel(this._logLevel, event) || + verboseTerminalMessage || + event.type === 'artifactAvailable' || + (this._preserveOperationStream && + GROUPED_OPERATION_EVENT_TYPES.has(event.type) && + !(this._logLevel === 'quiet' && event.type === 'externalOutput')) + ) { this._reporter.report(event); } } @@ -202,6 +193,43 @@ class ExplicitOutputReporter implements IReporter { } } +class FilePathReporter implements IReporter { + public readonly name: string = 'file-path'; + + private readonly _write: (text: string) => unknown; + private _path: string | undefined; + + public constructor(write: (text: string) => unknown) { + this._write = write; + } + + public async initializeAsync(): Promise { + /* no-op */ + } + + public report(event: IReporterEventEnvelope): void { + if (event.type === 'artifactAvailable') { + const payload: { role?: string; path?: string } = event.payload as { + role?: string; + path?: string; + }; + if (payload.role === 'log') { + this._path = payload.path; + } + } else if (event.type === 'commandResult' && this._path) { + this._write(`Rush full log: ${this._path}\n`); + } + } + + public async flushAsync(): Promise { + /* no-op */ + } + + public async closeAsync(): Promise { + /* no-op */ + } +} + function readValue( argv: readonly string[], index: number, @@ -397,6 +425,19 @@ function resolveLogLevel( } const environmentLogLevel: string | undefined = includeEnvironment ? env.RUSH_LOG_LEVEL : undefined; + const environmentQuiet: boolean = + includeEnvironment && (env.RUSH_QUIET_MODE === '1' || env.RUSH_QUIET_MODE?.toLowerCase() === 'true'); + if (environmentQuiet && environmentLogLevel) { + const normalizedLogLevel: string = environmentLogLevel.trim().toLowerCase(); + if (normalizedLogLevel !== 'quiet') { + throw new Error( + 'RUSH_QUIET_MODE contradicts RUSH_LOG_LEVEL. Remove one of these environment controls.' + ); + } + } + if (environmentQuiet) { + return 'quiet'; + } if (environmentLogLevel) { const normalizedLogLevel: string = environmentLogLevel.trim().toLowerCase(); if (!isSupportedLogLevel(normalizedLogLevel)) { @@ -527,6 +568,19 @@ export function resolveRushReporterSelection(options: IRushReporterHostOptions = }; } + if (argv.includes('--help') || argv.includes('-h')) { + return { + reporter: 'legacy', + logLevel: 'normal', + outputs: [], + commandJson, + enabled: false, + reporterControlsOwnedByFrontend: requestedReporter !== undefined, + reporterValueFlagsToStrip: requestedReporter === undefined ? [] : REPORTER_SELECTION_FLAG, + reason: requestedReporter === undefined ? 'pre-major legacy default' : 'explicit --reporter' + }; + } + function getCommandName(): 'rush' | 'rush-pnpm' | 'rushx' { const executableName: string = path.basename(process.argv[1] ?? '').toLowerCase(); if (executableName === 'rush-pnpm') { @@ -629,11 +683,12 @@ function createPrimaryReporter( case 'plaintext': return new PlaintextReporter({ write: (text: string) => stdout.write(text), - variant: isCiDetected(env) ? 'detailed' : 'concise', - color: false + variant: selection.reason === 'explicit --reporter' || isCiDetected(env) ? 'detailed' : 'concise', + color: false, + logLevel: selection.logLevel }); case 'file': - return new FileReporter(); + return undefined; case 'legacy': return undefined; } @@ -644,30 +699,36 @@ export async function initializeRushReporterHostAsync( ): Promise { const env: Record = options.env ?? process.env; const stdout: IRushReporterOutputStream = options.stdout ?? process.stdout; + const stderr: IRushReporterOutputStream = options.stderr ?? process.stderr; const selection: IRushReporterSelection = resolveRushReporterSelection({ ...options, env, stdout }); const host: ReporterHost = new ReporterHost({ env }); + let fullDetailReporter: FileReporter | undefined; if (selection.enabled) { const primaryReporter: IReporter | undefined = createPrimaryReporter(selection, stdout, env); if (primaryReporter) { - const presentationReporter: IReporter = - selection.reporter === 'file' - ? primaryReporter - : new DeferredOperationPresentationReporter(primaryReporter); - host.manager.addReporter(new LogLevelReporter(presentationReporter, selection.logLevel), { - destination: selection.reporter === 'file' ? 'file:auto' : 'stdout' + host.manager.addReporter( + new LogLevelReporter(primaryReporter, selection.logLevel, selection.reporter === 'plaintext'), + { + destination: 'stdout' + } + ); + } + + if (options.includeDefaultFileReporter !== false || selection.reporter === 'file') { + fullDetailReporter = new FileReporter({ + commonTempFolder: options.commonTempFolder, + actionName: options.actionName + }); + host.manager.addReporter(fullDetailReporter, { + destination: 'file:auto' }); } - const hasExplicitFileOutput: boolean = selection.outputs.some( - (output: IReporterOutputTarget) => output.reporter === 'file' - ); - if ( - options.includeDefaultFileReporter !== false && - selection.reporter !== 'file' && - !hasExplicitFileOutput - ) { - host.manager.addReporter(new FileReporter(), { destination: 'file:auto' }); + if (selection.reporter === 'file') { + host.manager.addReporter(new FilePathReporter((text: string) => stderr.write(text)), { + destination: 'stderr' + }); } for (const output of selection.outputs) { @@ -689,6 +750,7 @@ export async function initializeRushReporterHostAsync( host, sink: host.getSink(), selection, + logArtifact: fullDetailReporter?.getArtifact(), closeAsync: (timeoutMs?: number) => { closePromise ??= host.manager.closeAsync(timeoutMs); return closePromise; diff --git a/apps/rush/src/test/MinimalRushConfiguration.test.ts b/apps/rush/src/test/MinimalRushConfiguration.test.ts index 80b95dbd6aa..3f01ee36a9b 100644 --- a/apps/rush/src/test/MinimalRushConfiguration.test.ts +++ b/apps/rush/src/test/MinimalRushConfiguration.test.ts @@ -33,6 +33,7 @@ describe(MinimalRushConfiguration.name, () => { MinimalRushConfiguration.loadFromDefaultLocation() as MinimalRushConfiguration; expect(config.rushVersion).toEqual('4.0.0'); expect(config.useRushReporter).toBe(true); + expect(config.commonTempFolder).toBe(path.resolve(__dirname, 'sandbox', 'repo', 'common', 'temp')); }); }); }); diff --git a/apps/rush/src/test/RushReporterHost.test.ts b/apps/rush/src/test/RushReporterHost.test.ts index 7846cb673c6..abd6884bbc2 100644 --- a/apps/rush/src/test/RushReporterHost.test.ts +++ b/apps/rush/src/test/RushReporterHost.test.ts @@ -234,6 +234,14 @@ describe(resolveRushReporterSelection.name, () => { ).toEqual(['node', 'rush', 'custom', '--output', 'custom.zip', '--log-level', 'custom', '--verbose']); }); + it('keeps help on the legacy parser-only path', () => { + expect(resolve(['build', '--help', '--reporter=json'], {}, false)).toMatchObject({ + reporter: 'legacy', + enabled: false, + reporterControlsOwnedByFrontend: true + }); + }); + it('removes reporter-only value controls before invoking a legacy engine', () => { expect( stripReporterValueControls([ @@ -325,6 +333,13 @@ describe(resolveRushReporterSelection.name, () => { ); }); + it('preserves RUSH_QUIET_MODE as a quiet reporter alias', () => { + expect(resolve(['build', '--reporter=plaintext'], { RUSH_QUIET_MODE: 'true' }).logLevel).toBe('quiet'); + expect(() => + resolve(['build', '--reporter=plaintext'], { RUSH_QUIET_MODE: '1', RUSH_LOG_LEVEL: 'debug' }) + ).toThrow(/contradicts RUSH_LOG_LEVEL/); + }); + it('preserves legacy verbosity combinations when the reporter path is disabled', () => { expect(resolve(['build', '--quiet', '--debug'])).toMatchObject({ reporter: 'legacy', @@ -466,9 +481,61 @@ describe(initializeRushReporterHostAsync.name, () => { await initialized.closeAsync(); expect(initialized.selection.enabled).toBe(false); + expect(initialized.logArtifact).toBeUndefined(); expect(output).toBe(''); }); + it('always creates a repository full-detail log on the enabled path', async () => { + const directory: string = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rush-full-log-')); + try { + const initialized = await initializeRushReporterHostAsync({ + argv: ['build', '--reporter=plaintext'], + env: {}, + commonTempFolder: directory, + actionName: 'build', + stdout: { isTTY: false, write: () => undefined } + }); + + expect(initialized.logArtifact).toMatchObject({ available: true }); + expect(initialized.logArtifact?.path).toMatch( + new RegExp(`^${directory.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`) + ); + await initialized.closeAsync(); + } finally { + await fs.promises.rm(directory, { recursive: true, force: true }); + } + }); + + it('does not render operation output at quiet plaintext log level', async () => { + let output: string = ''; + const quietHost = await initializeRushReporterHostAsync({ + argv: ['build', '--reporter=plaintext', '--log-level=quiet'], + env: {}, + stdout: { + isTTY: false, + write: (text: string) => { + output += text; + } + }, + includeDefaultFileReporter: false + }); + + emitCommandStarted(quietHost.sink); + emitOperationEvents(quietHost.sink); + quietHost.sink.emit({ + protocolVersion: { major: 1, minor: 1 }, + sessionId: 'session', + source: { packageName: '@microsoft/rush-lib', packageVersion: '5.178.1' }, + privacy: 'public', + type: 'commandResult', + payload: { commandName: 'build', succeeded: true, exitCode: 0 } + }); + await quietHost.closeAsync(); + + expect(output).not.toContain('raw operation output'); + expect(output).toContain('rush build succeeded'); + }); + it('initializes the explicitly selected reporter and output destinations', async () => { const directory: string = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'rush-frontend-')); const outputPath: string = path.join(directory, 'events.jsonl'); @@ -505,7 +572,14 @@ describe(initializeRushReporterHostAsync.name, () => { .trim() .split('\n') .map((line: string) => JSON.parse(line) as Record); - expect(stdoutEvents.map(({ type }) => type)).toEqual(['commandStarted']); + expect(stdoutEvents.map(({ type }) => type)).toEqual([ + 'commandStarted', + 'operationRegistered', + 'operationStatusChanged', + 'externalOutput', + 'operationStreamClosed', + 'operationCompleted' + ]); expect(fileEvents.map(({ type }) => type)).toEqual([ 'commandStarted', 'operationRegistered', diff --git a/apps/rush/src/test/sandbox/reporter-demo/README.md b/apps/rush/src/test/sandbox/reporter-demo/README.md new file mode 100644 index 00000000000..39eeccdd4fb --- /dev/null +++ b/apps/rush/src/test/sandbox/reporter-demo/README.md @@ -0,0 +1,25 @@ +# Direct Rush reporter demo + +Build the three reporter projects, then run the self-checking direct invocation demo: + +```sh +rush build --to @microsoft/rush +node apps/rush/src/test/sandbox/reporter-demo/run.mjs +``` + +The script runs the same `rush build --only @rushstack/rush-reporter` operation stream through legacy, +plaintext, JSON, and AI modes. It verifies JSON/AI payload-only stdout, confirms the plaintext result +contains an existing absolute full-log path, and writes captured stdout/stderr files to a temporary folder. + +For an individual invocation: + +```sh +node apps/rush/bin/rush build --only @rushstack/rush-reporter --reporter=plaintext +node apps/rush/bin/rush build --only @rushstack/rush-reporter --reporter=json --log-level=debug +node apps/rush/bin/rush build --only @rushstack/rush-reporter --reporter=ai +RUSH_REPORTER=legacy node apps/rush/bin/rush build --only @rushstack/rush-reporter --reporter=json +``` + +Repositories can opt in without a command-line flag by setting `"useRushReporter": true` in +`common/config/rush/experiments.json`. Remove that setting or use `RUSH_REPORTER=legacy` for immediate +rollback. diff --git a/apps/rush/src/test/sandbox/reporter-demo/run.mjs b/apps/rush/src/test/sandbox/reporter-demo/run.mjs new file mode 100644 index 00000000000..0cf345a6314 --- /dev/null +++ b/apps/rush/src/test/sandbox/reporter-demo/run.mjs @@ -0,0 +1,51 @@ +import { spawnSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const scriptFolder = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(scriptFolder, '..', '..', '..', '..', '..', '..'); +const rushBin = path.join(repoRoot, 'apps', 'rush', 'bin', 'rush'); +const outputFolder = fs.mkdtempSync(path.join(os.tmpdir(), 'rush-reporter-demo-')); +const commonArgs = ['build', '--only', '@rushstack/rush-reporter']; + +function run(name, args, env = {}) { + const result = spawnSync(process.execPath, [rushBin, ...args], { + cwd: repoRoot, + env: { ...process.env, ...env }, + encoding: 'utf8' + }); + fs.writeFileSync(path.join(outputFolder, `${name}.stdout`), result.stdout); + fs.writeFileSync(path.join(outputFolder, `${name}.stderr`), result.stderr); + if (result.status !== 0) { + throw new Error(`${name} failed with exit code ${result.status}\n${result.stderr}`); + } + return result.stdout; +} + +run('warmup', commonArgs); +const legacy = run('legacy', commonArgs); +const rollback = run('rollback', [...commonArgs, '--reporter=json'], { RUSH_REPORTER: 'legacy' }); +const normalizeDurations = (text) => text.replace(/\d+\.\d+ seconds/g, '