From 98f218232870230ef34ff8d4dd54b6ec2005cf7d Mon Sep 17 00:00:00 2001 From: TheLarkInn Date: Fri, 28 Aug 2026 02:13:24 +0000 Subject: [PATCH 1/2] Generate reporter bootstrap protocol Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d6318e80-5da9-4858-a147-817e8692f10e --- ...bootstrap-generation_2026-08-28-01-58.json | 11 ++ ...bootstrap-generation_2026-08-28-01-58.json | 11 ++ libraries/reporter/config/heft.json | 24 ++++ libraries/reporter/package.json | 2 + .../scripts/generateBootstrapProtocol.js | 104 ++++++++++++++++++ .../src/bootstrap/BootstrapEventBuffer.ts | 16 +-- .../src/bootstrap/BootstrapProtocol.ts | 64 ++++++++++- libraries/reporter/src/test/Bootstrap.test.ts | 22 ++++ .../scripts/generated/BootstrapProtocol.ts | 72 ++++++++++++ .../rush-lib/src/scripts/install-run-rush.ts | 12 +- 10 files changed, 324 insertions(+), 14 deletions(-) create mode 100644 common/changes/@microsoft/rush/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json create mode 100644 common/changes/@rushstack/rush-reporter/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json create mode 100644 libraries/reporter/config/heft.json create mode 100644 libraries/reporter/scripts/generateBootstrapProtocol.js create mode 100644 libraries/rush-lib/src/scripts/generated/BootstrapProtocol.ts diff --git a/common/changes/@microsoft/rush/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json b/common/changes/@microsoft/rush/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json new file mode 100644 index 00000000000..fb9e4abe7ba --- /dev/null +++ b/common/changes/@microsoft/rush/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Embed the generated zero-dependency Rush reporter bootstrap protocol in the install-run-rush bundle.", + "type": "patch" + } + ], + "packageName": "@microsoft/rush", + "email": "223556219+Copilot@users.noreply.github.com" +} diff --git a/common/changes/@rushstack/rush-reporter/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json b/common/changes/@rushstack/rush-reporter/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json new file mode 100644 index 00000000000..c1b94f1b98d --- /dev/null +++ b/common/changes/@rushstack/rush-reporter/copilot-reporter-bootstrap-generation_2026-08-28-01-58.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@rushstack/rush-reporter", + "comment": "Add the source-of-truth frozen bootstrap envelope encoder and deterministic generation check for install-run-rush.", + "type": "patch" + } + ], + "packageName": "@rushstack/rush-reporter", + "email": "223556219+Copilot@users.noreply.github.com" +} diff --git a/libraries/reporter/config/heft.json b/libraries/reporter/config/heft.json new file mode 100644 index 00000000000..1c9c3dd515e --- /dev/null +++ b/libraries/reporter/config/heft.json @@ -0,0 +1,24 @@ +/** + * Defines configuration used by core Heft. + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + + "extends": "local-node-rig/profiles/default/config/heft.json", + + "phasesByName": { + "build": { + "tasksByName": { + "check-bootstrap-protocol": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft", + "pluginName": "run-script-plugin", + "options": { + "scriptPath": "./scripts/generateBootstrapProtocol.js" + } + } + } + } + } + } +} diff --git a/libraries/reporter/package.json b/libraries/reporter/package.json index 0dac948f585..899fdc852ee 100644 --- a/libraries/reporter/package.json +++ b/libraries/reporter/package.json @@ -43,6 +43,8 @@ }, "scripts": { "build": "heft build --clean", + "generate-bootstrap-protocol": "node scripts/generateBootstrapProtocol.js --write", + "check-bootstrap-protocol": "node scripts/generateBootstrapProtocol.js --check", "_phase:build": "heft run --only build -- --clean", "_phase:test": "heft run --only test -- --clean" }, diff --git a/libraries/reporter/scripts/generateBootstrapProtocol.js b/libraries/reporter/scripts/generateBootstrapProtocol.js new file mode 100644 index 00000000000..e64a10aad09 --- /dev/null +++ b/libraries/reporter/scripts/generateBootstrapProtocol.js @@ -0,0 +1,104 @@ +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +const SOURCE_START_MARKER = '// BEGIN GENERATED BOOTSTRAP PROTOCOL'; +const SOURCE_END_MARKER = '// END GENERATED BOOTSTRAP PROTOCOL'; +const SOURCE_PATH = path.resolve(__dirname, '../src/bootstrap/BootstrapProtocol.ts'); +const PROTOCOL_SOURCE_PATH = path.resolve(__dirname, '../src/protocol/ReporterProtocol.ts'); +const TARGET_PATH = path.resolve(__dirname, '../../rush-lib/src/scripts/generated/BootstrapProtocol.ts'); + +function renderGeneratedFile() { + const source = fs.readFileSync(SOURCE_PATH, 'utf8').replace(/\r\n/g, '\n'); + const protocolSource = fs.readFileSync(PROTOCOL_SOURCE_PATH, 'utf8').replace(/\r\n/g, '\n'); + const startIndex = source.indexOf(SOURCE_START_MARKER); + const endIndex = source.indexOf(SOURCE_END_MARKER); + if (startIndex < 0 || endIndex < 0 || endIndex <= startIndex) { + throw new Error(`Unable to find the generated bootstrap protocol markers in ${SOURCE_PATH}.`); + } + + const generatedSource = source.slice(startIndex + SOURCE_START_MARKER.length, endIndex).trim(); + if (/^\s*import\b/m.test(generatedSource) || /\brequire\s*\(/.test(generatedSource)) { + throw new Error('The generated bootstrap protocol must not contain imports or require() calls.'); + } + + const bootstrapMajorMatch = generatedSource.match(/export const BOOTSTRAP_PROTOCOL_MAJOR: number = (\d+);/); + const reporterMajorMatch = protocolSource.match(/REPORTER_PROTOCOL_VERSION:[^=]+=\s*\{\s*major:\s*(\d+),/); + if (!bootstrapMajorMatch || !reporterMajorMatch) { + throw new Error('Unable to read the bootstrap and reporter protocol-major constants.'); + } + if (bootstrapMajorMatch[1] !== reporterMajorMatch[1]) { + throw new Error( + `BOOTSTRAP_PROTOCOL_MAJOR (${bootstrapMajorMatch[1]}) must match ` + + `REPORTER_PROTOCOL_VERSION.major (${reporterMajorMatch[1]}).` + ); + } + + return [ + '// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.', + '// See LICENSE in the project root for license information.', + '', + '// THIS FILE IS GENERATED. Run "rushx generate-bootstrap-protocol" in libraries/reporter to update it.', + '// Sources: libraries/reporter/src/bootstrap/BootstrapProtocol.ts', + '// libraries/reporter/src/protocol/ReporterProtocol.ts', + '', + generatedSource, + '' + ].join('\n'); +} + +function writeGeneratedFile() { + fs.mkdirSync(path.dirname(TARGET_PATH), { recursive: true }); + fs.writeFileSync(TARGET_PATH, renderGeneratedFile(), 'utf8'); +} + +function checkGeneratedFile() { + const expected = renderGeneratedFile(); + let actual; + try { + actual = fs.readFileSync(TARGET_PATH, 'utf8'); + } catch (error) { + if (error && error.code === 'ENOENT') { + throw new Error( + `The generated bootstrap protocol is missing at ${TARGET_PATH}. ` + + 'Run "rushx generate-bootstrap-protocol" in libraries/reporter.' + ); + } + throw error; + } + + if (actual !== expected) { + throw new Error( + `The generated bootstrap protocol is stale at ${TARGET_PATH}. ` + + 'Run "rushx generate-bootstrap-protocol" in libraries/reporter.' + ); + } +} + +module.exports = { + runAsync: async ({ + heftTaskSession: { + logger: { terminal } + } + }) => { + checkGeneratedFile(); + terminal.writeVerboseLine('The generated install-run-rush bootstrap protocol is up to date.'); + } +}; + +if (require.main === module) { + try { + const mode = process.argv[2]; + if (mode === '--write') { + writeGeneratedFile(); + } else if (mode === '--check') { + checkGeneratedFile(); + } else { + throw new Error('Specify either --write or --check.'); + } + } catch (error) { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + } +} diff --git a/libraries/reporter/src/bootstrap/BootstrapEventBuffer.ts b/libraries/reporter/src/bootstrap/BootstrapEventBuffer.ts index 0b5846deda1..c3789cf8789 100644 --- a/libraries/reporter/src/bootstrap/BootstrapEventBuffer.ts +++ b/libraries/reporter/src/bootstrap/BootstrapEventBuffer.ts @@ -2,10 +2,10 @@ // See LICENSE in the project root for license information. import { - BOOTSTRAP_PROTOCOL_MAJOR, BOOTSTRAP_BUFFER_MAX_BYTES, BOOTSTRAP_EXTERNAL_CHUNK_MAX_BYTES, - BOOTSTRAP_BUFFER_TRUNCATED_EXTENSION_NAME + BOOTSTRAP_BUFFER_TRUNCATED_EXTENSION_NAME, + encodeBootstrapEnvelope } from './BootstrapProtocol'; import type { ReporterEventType } from '../events/ReporterEventType'; import { chunkUtf8Text } from '../utilities/chunkUtf8Text'; @@ -192,8 +192,7 @@ export class BootstrapEventBuffer { public emit(input: IBootstrapEventInput): string { const eventId: string = `boot_${this._nextEventId++}`; const required: boolean = input.type !== 'activityChanged'; - const envelope: Record = { - protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 }, + const line: string = encodeBootstrapEnvelope({ eventId, sessionId: this._sessionId, sequence: this._nextSequence++, @@ -203,8 +202,7 @@ export class BootstrapEventBuffer { required, type: input.type, payload: input.payload === undefined ? {} : input.payload - }; - const line: string = JSON.stringify(envelope); + }); const bytes: number = Buffer.byteLength(line, 'utf8') + 1; const mustPreserve: boolean = required; const replaceable: boolean = input.type === 'activityChanged'; @@ -257,8 +255,7 @@ export class BootstrapEventBuffer { public serialize(): string { const lines: string[] = this._entries.map((entry: IBufferEntry) => entry.line); if (this._truncated) { - const notice: Record = { - protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 }, + const noticeLine: string = encodeBootstrapEnvelope({ eventId: 'boot_bufferTruncated', sessionId: this._sessionId, sequence: this._nextSequence++, @@ -274,8 +271,7 @@ export class BootstrapEventBuffer { droppedRequired: this._droppedRequired, failed: this._failed } - }; - const noticeLine: string = JSON.stringify(notice); + }); const noticeBytes: number = Buffer.byteLength(noticeLine, 'utf8') + 1; if (noticeBytes > TRUNCATION_NOTICE_RESERVE_BYTES) { throw new Error('The bootstrap truncation notice exceeded its reserved capacity.'); diff --git a/libraries/reporter/src/bootstrap/BootstrapProtocol.ts b/libraries/reporter/src/bootstrap/BootstrapProtocol.ts index 4672d239685..e834f2f49d8 100644 --- a/libraries/reporter/src/bootstrap/BootstrapProtocol.ts +++ b/libraries/reporter/src/bootstrap/BootstrapProtocol.ts @@ -6,18 +6,76 @@ // zero-dependency `install-run-rush` bundle, which must not import // `@rushstack/rush-reporter` at runtime. +// BEGIN GENERATED BOOTSTRAP PROTOCOL + /** * The protocol major version frozen into the bootstrap encoder. * * @remarks - * This constant is generated from `@rushstack/rush-reporter` and must equal - * `REPORTER_PROTOCOL_VERSION.major`. It is duplicated here, rather than - * imported, so the encoder can be embedded without a runtime dependency. + * The `install-run-rush` build embeds a generated copy of this constant and + * the encoder below. The generated module is checked byte-for-byte during the + * reporter build. * * @beta */ export const BOOTSTRAP_PROTOCOL_MAJOR: number = 1; +/** + * The privacy classification accepted by the frozen bootstrap encoder. + * + * @beta + */ +export type BootstrapEnvelopePrivacyClassification = 'public' | 'local-sensitive' | 'secret'; + +/** + * The producer identity stamped onto a bootstrap event. + * + * @beta + */ +export interface IBootstrapEnvelopeSource { + readonly packageName: string; + readonly packageVersion: string; +} + +/** + * The presentation-free fields encoded into a bootstrap event envelope. + * + * @beta + */ +export interface IBootstrapEnvelopeInput { + readonly eventId: string; + readonly sessionId: string; + readonly sequence: number; + readonly timestamp: string; + readonly source: IBootstrapEnvelopeSource; + readonly privacy: BootstrapEnvelopePrivacyClassification; + readonly required: boolean; + readonly type: string; + readonly payload: unknown; +} + +/** + * Encodes one bootstrap event envelope without importing the reporter package. + * + * @beta + */ +export function encodeBootstrapEnvelope(input: IBootstrapEnvelopeInput): string { + return JSON.stringify({ + protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 }, + eventId: input.eventId, + sessionId: input.sessionId, + sequence: input.sequence, + timestamp: input.timestamp, + source: input.source, + privacy: input.privacy, + required: input.required, + type: input.type, + payload: input.payload + }); +} + +// END GENERATED BOOTSTRAP PROTOCOL + /** * The maximum size of the buffered bootstrap event stream, in bytes (1 MiB). * diff --git a/libraries/reporter/src/test/Bootstrap.test.ts b/libraries/reporter/src/test/Bootstrap.test.ts index 30cf44b510f..0a99fbb3787 100644 --- a/libraries/reporter/src/test/Bootstrap.test.ts +++ b/libraries/reporter/src/test/Bootstrap.test.ts @@ -19,6 +19,7 @@ import { type IBootstrapEventBufferOptions, type IEarlyReporterControls } from '../index'; +import { encodeBootstrapEnvelope } from '../bootstrap/BootstrapProtocol'; function decode(ndjson: string): Record[] { return ndjson @@ -74,6 +75,27 @@ describe('BootstrapEventBuffer', () => { expect(BOOTSTRAP_PROTOCOL_MAJOR).toBe(REPORTER_PROTOCOL_VERSION.major); }); + it('encodes the frozen bootstrap envelope deterministically', () => { + expect( + encodeBootstrapEnvelope({ + eventId: 'boot_1', + sessionId: 'sess_boot', + sequence: 1, + timestamp: '2026-01-01T00:00:00.000Z', + source: { packageName: 'install-run-rush', packageVersion: '0.0.0' }, + privacy: 'public', + required: true, + type: 'sessionStarted', + payload: { argv: ['build'] } + }) + ).toBe( + '{"protocolVersion":{"major":1,"minor":0},"eventId":"boot_1","sessionId":"sess_boot",' + + '"sequence":1,"timestamp":"2026-01-01T00:00:00.000Z","source":{"packageName":' + + '"install-run-rush","packageVersion":"0.0.0"},"privacy":"public","required":true,' + + '"type":"sessionStarted","payload":{"argv":["build"]}}' + ); + }); + it('encodes events with assigned ids, sequence, timestamp, and protocol version', () => { const buffer: BootstrapEventBuffer = makeBuffer(); const id: string = buffer.emit({ type: 'sessionStarted', payload: { argv: ['build'] } }); diff --git a/libraries/rush-lib/src/scripts/generated/BootstrapProtocol.ts b/libraries/rush-lib/src/scripts/generated/BootstrapProtocol.ts new file mode 100644 index 00000000000..c30a3a9a38c --- /dev/null +++ b/libraries/rush-lib/src/scripts/generated/BootstrapProtocol.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// THIS FILE IS GENERATED. Run "rushx generate-bootstrap-protocol" in libraries/reporter to update it. +// Sources: libraries/reporter/src/bootstrap/BootstrapProtocol.ts +// libraries/reporter/src/protocol/ReporterProtocol.ts + +/** + * The protocol major version frozen into the bootstrap encoder. + * + * @remarks + * The `install-run-rush` build embeds a generated copy of this constant and + * the encoder below. The generated module is checked byte-for-byte during the + * reporter build. + * + * @beta + */ +export const BOOTSTRAP_PROTOCOL_MAJOR: number = 1; + +/** + * The privacy classification accepted by the frozen bootstrap encoder. + * + * @beta + */ +export type BootstrapEnvelopePrivacyClassification = 'public' | 'local-sensitive' | 'secret'; + +/** + * The producer identity stamped onto a bootstrap event. + * + * @beta + */ +export interface IBootstrapEnvelopeSource { + readonly packageName: string; + readonly packageVersion: string; +} + +/** + * The presentation-free fields encoded into a bootstrap event envelope. + * + * @beta + */ +export interface IBootstrapEnvelopeInput { + readonly eventId: string; + readonly sessionId: string; + readonly sequence: number; + readonly timestamp: string; + readonly source: IBootstrapEnvelopeSource; + readonly privacy: BootstrapEnvelopePrivacyClassification; + readonly required: boolean; + readonly type: string; + readonly payload: unknown; +} + +/** + * Encodes one bootstrap event envelope without importing the reporter package. + * + * @beta + */ +export function encodeBootstrapEnvelope(input: IBootstrapEnvelopeInput): string { + return JSON.stringify({ + protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 }, + eventId: input.eventId, + sessionId: input.sessionId, + sequence: input.sequence, + timestamp: input.timestamp, + source: input.source, + privacy: input.privacy, + required: input.required, + type: input.type, + payload: input.payload + }); +} diff --git a/libraries/rush-lib/src/scripts/install-run-rush.ts b/libraries/rush-lib/src/scripts/install-run-rush.ts index 6fa7e8b21b5..1bb7b29d0c5 100644 --- a/libraries/rush-lib/src/scripts/install-run-rush.ts +++ b/libraries/rush-lib/src/scripts/install-run-rush.ts @@ -6,13 +6,15 @@ import * as path from 'node:path'; import * as fs from 'node:fs'; +import type { ILogger } from '../utilities/npmrcUtilities'; +import { BOOTSTRAP_PROTOCOL_MAJOR, encodeBootstrapEnvelope } from './generated/BootstrapProtocol'; + const { installAndRun, findRushJsonFolder, RUSH_JSON_FILENAME, runWithErrorAndStatusCode }: typeof import('./install-run') = __non_webpack_require__('./install-run'); -import type { ILogger } from '../utilities/npmrcUtilities'; const PACKAGE_NAME: string = '@microsoft/rush'; const RUSH_PREVIEW_VERSION: string = 'RUSH_PREVIEW_VERSION'; @@ -20,6 +22,12 @@ const RUSH_QUIET_MODE: string = 'RUSH_QUIET_MODE'; const INSTALL_RUN_RUSH_LOCKFILE_PATH_VARIABLE: 'INSTALL_RUN_RUSH_LOCKFILE_PATH' = 'INSTALL_RUN_RUSH_LOCKFILE_PATH'; +function _validateBundledBootstrapProtocol(): void { + if (BOOTSTRAP_PROTOCOL_MAJOR < 1 || typeof encodeBootstrapEnvelope !== 'function') { + throw new Error('The bundled Rush reporter bootstrap protocol is invalid.'); + } +} + function _getRushVersion(logger: ILogger): string { const rushPreviewVersion: string | undefined = process.env[RUSH_PREVIEW_VERSION]; if (rushPreviewVersion !== undefined) { @@ -58,6 +66,8 @@ function _getBin(scriptName: string): string { } function _run(): void { + _validateBundledBootstrapProtocol(); + const [ nodePath /* Ex: /bin/node */, scriptPath /* /repo/common/scripts/install-run-rush.js */, From 1b4252ccd83df0e0eb44ac28bbfb6203e8e3a51f Mon Sep 17 00:00:00 2001 From: TheLarkInn Date: Fri, 28 Aug 2026 02:37:09 +0000 Subject: [PATCH 2/2] Normalize generated protocol line endings Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d6318e80-5da9-4858-a147-817e8692f10e --- libraries/reporter/scripts/generateBootstrapProtocol.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/reporter/scripts/generateBootstrapProtocol.js b/libraries/reporter/scripts/generateBootstrapProtocol.js index e64a10aad09..8c68eabfbd0 100644 --- a/libraries/reporter/scripts/generateBootstrapProtocol.js +++ b/libraries/reporter/scripts/generateBootstrapProtocol.js @@ -57,7 +57,7 @@ function checkGeneratedFile() { const expected = renderGeneratedFile(); let actual; try { - actual = fs.readFileSync(TARGET_PATH, 'utf8'); + actual = fs.readFileSync(TARGET_PATH, 'utf8').replace(/\r\n/g, '\n'); } catch (error) { if (error && error.code === 'ENOENT') { throw new Error(