Skip to content
4 changes: 4 additions & 0 deletions apps/rush/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ _(If you don't have a GitHub account set up, you can use `rush install --bypass-

For more details and support resources, please visit: https://rushjs.io

The repository also includes an
[experimental Rush reporter opt-in and demo guide](../../docs/rush/reporter.md). Rush 5 retains legacy
terminal output unless a reporter is explicitly enabled.

## Links

- [CHANGELOG.md](
Expand Down
12 changes: 12 additions & 0 deletions apps/rush/UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ approaches:
Dynamic registry and proxy URLs must also move out of the project `.npmrc` and into trusted user,
global, CLI, or environment configuration.

### Experimental Rush reporter opt-in

Rush 5 keeps the existing terminal output by default. Maintainers can evaluate the new reporter path for one
invocation with `--reporter=<name>` or for a repository with `"useRushReporter": true` in
`common/config/rush/experiments.json`.

Use `RUSH_REPORTER=legacy` for an immediate reporter-only rollback. Environment-based automatic AI selection
and the planned Rush 6 default change are not enabled yet.

See the [experimental Rush reporter guide](../../docs/rush/reporter.md) for the control precedence, stdout and
stderr contracts, full-log and privacy behavior, cross-version prerequisite, and reproducible demo.

### Rush 5.135.0

This release of Rush deprecates the `rush-project.json`'s `operationSettings.sharding.shardOperationSettings`
Expand Down
56 changes: 50 additions & 6 deletions apps/rush/src/test/sandbox/reporter-demo/README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,53 @@
# Direct Rush reporter demo

Build the three reporter projects, then run the self-checking direct invocation demo:
For the complete control, compatibility, privacy, and troubleshooting reference, see the
[experimental Rush reporter guide](../../../../../../docs/rush/reporter.md).

From a clean checkout, install dependencies, build the reporter path, and run the self-checking direct
invocation demo:

```sh
rush build --to @microsoft/rush
node common/scripts/install-run-rush.js install
node common/scripts/install-run-rush.js 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, AI, file, and quiet modes, plus parser failure, help, and command-specific JSON cases.
It verifies payload-only machine stdout, one visible writer, ordered/lossless plaintext grouping from a
same-invocation JSON sidecar, final artifact completeness, owner-only log permissions, failure flushing,
AI parser-error context, command-JSON ownership, CI plaintext output, cache-path output, normalized
`RUSH_TEMP_FOLDER` log placement, matching purge-path selection, and the `RUSH_REPORTER=legacy` rollback
transcript. Captured stdout/stderr files are written to a temporary folder.
AI parser-error context, command-JSON ownership, exclusive sidecar destinations, and the
`RUSH_REPORTER=legacy` rollback transcript. Inherited `RUSH_REPORTER`, `RUSH_LOG_LEVEL`, and
`RUSH_QUIET_MODE` values are removed from the self-check matrix. It also verifies CI plaintext output,
cache-path output, a matching `RUSH_PREVIEW_VERSION`, normalized `RUSH_TEMP_FOLDER` log placement, and
matching purge-path selection. Captured stdout/stderr files are written to a temporary folder.

The final matrix case invokes `rush purge` with an isolated `RUSH_TEMP_FOLDER`. This also unlinks project
dependencies, so run the install command again before continuing development in the checkout.

For an individual invocation:
For individual invocations, first select the locally built engine in the same shell. The local
`apps/rush/bin/rush` still honors the version in `rush.json`; without this override, an older pinned
engine can reject the explicit reporter request. The override selects the engine, not the reporter:

```sh
export RUSH_PREVIEW_VERSION="$(node -p "require('./apps/rush/package.json').version")"

# Interactive TTY only
node apps/rush/bin/rush build --only @rushstack/rush-reporter --reporter=default

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
node apps/rush/bin/rush build --only @rushstack/rush-reporter --reporter=file
RUSH_TEMP_FOLDER=./common/temp/reporter-demo-override node apps/rush/bin/rush build --only @rushstack/rush-reporter --reporter=file
node apps/rush/bin/rush build --only @rushstack/rush-reporter --reporter=plaintext --log-level=quiet
RUSH_REPORTER=legacy node apps/rush/bin/rush build --only @rushstack/rush-reporter --reporter=json
node apps/rush/bin/rush list --json --reporter=file
```

After the individual demo commands, run `unset RUSH_PREVIEW_VERSION` to restore the repository's normal
version selection. The self-checking driver sets its own override and needs no shell export.

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.
Expand All @@ -35,3 +56,26 @@ Help stays on the legacy parser path. With repository opt-in, parameters declare
command-owned even when their values look like reporter controls (for example, `--output=json://...`
or `--log-level=debug`). Custom `--verbose` flags are preserved as well; help does not run the command
or open reporter output files.

The expected output is shape-based:

- legacy keeps the existing Rush banner, operation blocks, and final status sections;
- `default` uses a width-aware three-row live region and leaves a short final summary;
- explicit `plaintext` groups ordered output under `project (phase)` and prints the absolute full-log path;
- JSON stdout contains only NDJSON event envelopes;
- a matching preview version keeps JSON stdout parseable and writes its warning to stderr;
- AI stdout contains `ai.status` and bounded `ai.final` records;
- file mode leaves stdout empty and writes the full-log path to stderr;
- `RUSH_TEMP_FOLDER` moves the full log and the matching purge removes that override;
- quiet mode retains only the final result and full-log path.

The intentional missing-project AI failure preserves an actionable diagnostic and complete log reference.
Structured remediation is included when the producing diagnostic supplies it; this parser failure does not
currently provide a remediation action.

After the demo exits, inspect the latest invocation log with:

```sh
ls -lt common/temp/rush-logs
sed -n '1,120p' common/temp/rush-logs/latest.log
```
34 changes: 33 additions & 1 deletion apps/rush/src/test/sandbox/reporter-demo/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@ 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 rushVersion = JSON.parse(
fs.readFileSync(path.join(repoRoot, 'apps', 'rush', 'package.json'), 'utf8')
).version;
const outputFolder = fs.mkdtempSync(path.join(os.tmpdir(), 'rush-reporter-demo-'));
const commonArgs = ['build', '--only', '@rushstack/rush-reporter'];
const baseEnv = { ...process.env, RUSH_PREVIEW_VERSION: rushVersion };
delete baseEnv.RUSH_REPORTER;
delete baseEnv.RUSH_LOG_LEVEL;
delete baseEnv.RUSH_QUIET_MODE;

function run(name, args, env = {}, expectedStatus = 0) {
const result = spawnSync(process.execPath, [rushBin, ...args], {
cwd: repoRoot,
env: { ...process.env, ...env },
env: { ...baseEnv, ...env },
encoding: 'utf8'
});
fs.writeFileSync(path.join(outputFolder, `${name}.stdout`), result.stdout);
Expand All @@ -40,6 +47,9 @@ const plaintext = run('plaintext', [
`--output=json://${plaintextEventsPath}?logLevel=debug`
]).stdout;
const json = run('json', [...commonArgs, '--reporter=json', '--log-level=debug']).stdout;
const previewJson = run('preview-json', [...commonArgs, '--reporter=json'], {
RUSH_PREVIEW_VERSION: rushVersion
});
const ai = run('ai', [...commonArgs, '--reporter=ai']).stdout;
const file = run('file', [...commonArgs, '--reporter=file']);
const quiet = run('quiet', [...commonArgs, '--reporter=plaintext', '--log-level=quiet']).stdout;
Expand All @@ -62,6 +72,18 @@ const flagOffHelp = run('help-flag-off', ['--help']).stdout;
const help = run('help', ['--help', '--reporter=json'], { RUSH_REPORTER: 'legacy' }).stdout;
const commandJson = run('command-json', ['list', '--json', '--reporter=file']);
const commandJsonConflict = run('command-json-conflict', ['list', '--json', '--reporter=json'], {}, 1);
const duplicateOutputPath = path.join(outputFolder, 'duplicate-output.jsonl');
const outputConflict = run(
'output-conflict',
[
...commonArgs,
'--reporter=plaintext',
`--output=json://${duplicateOutputPath}`,
`--output=file://${duplicateOutputPath}`
],
{},
1
);
const tempOverride = path.join(outputFolder, 'rush-temp-override');
const tempOverrideFile = run('temp-override', [...commonArgs, '--reporter=file'], {
RUSH_TEMP_FOLDER: tempOverride
Expand Down Expand Up @@ -94,6 +116,7 @@ function parseNdjson(text, name) {
}

const jsonEvents = parseNdjson(json, 'json');
const previewJsonEvents = parseNdjson(previewJson.stdout, 'preview-json');
const aiRecords = parseNdjson(ai, 'ai');
const failureJsonEvents = parseNdjson(failureJson, 'failure-json');
const failureAiRecords = parseNdjson(failureAi, 'failure-ai');
Expand Down Expand Up @@ -169,6 +192,12 @@ const aiFinal = aiRecords.at(-1);
if (aiFinal?.kind !== 'ai.final' || aiFinal.log?.complete !== true) {
throw new Error('AI output did not include a complete full-log reference.');
}
if (
!previewJson.stderr.includes('RUSH_PREVIEW_VERSION') ||
!previewJsonEvents.some((event) => event.type === 'sessionCompleted')
) {
throw new Error('The matching preview version did not preserve payload-only reporter stdout.');
}
const failureAiFinal = failureAiRecords.at(-1);
if (
failureAiFinal?.result !== 'failed' ||
Expand Down Expand Up @@ -217,6 +246,9 @@ if (
) {
throw new Error('Command-specific JSON ownership arbitration failed.');
}
if (outputConflict.stdout !== '' || !outputConflict.stderr.includes('is already owned by another reporter')) {
throw new Error('Duplicate reporter output destinations were not rejected.');
}

console.log(`Reporter demo outputs: ${outputFolder}`);
console.log(`Full detail log: ${logMatch[1]}`);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Document the experimental reporter opt-in, rollback, output contracts, compatibility boundary, and reproducible demo.",
"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": "Correct local reporter demo examples to select the built Rush engine instead of an older repository-pinned version, and explain restoration of normal version selection.",
"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 the opt-in reporter guide covering built-in renderers, full logs, privacy, and rollout safety.",
"type": "patch"
}
],
"packageName": "@rushstack/rush-reporter",
"email": "TheLarkInn@users.noreply.github.com"
}
Loading