Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
24 changes: 24 additions & 0 deletions libraries/reporter/config/heft.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
}
}
2 changes: 2 additions & 0 deletions libraries/reporter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
104 changes: 104 additions & 0 deletions libraries/reporter/scripts/generateBootstrapProtocol.js
Original file line number Diff line number Diff line change
@@ -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').replace(/\r\n/g, '\n');
} 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;
}
}
16 changes: 6 additions & 10 deletions libraries/reporter/src/bootstrap/BootstrapEventBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<string, unknown> = {
protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 },
const line: string = encodeBootstrapEnvelope({
eventId,
sessionId: this._sessionId,
sequence: this._nextSequence++,
Expand All @@ -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';
Expand Down Expand Up @@ -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<string, unknown> = {
protocolVersion: { major: BOOTSTRAP_PROTOCOL_MAJOR, minor: 0 },
const noticeLine: string = encodeBootstrapEnvelope({
eventId: 'boot_bufferTruncated',
sessionId: this._sessionId,
sequence: this._nextSequence++,
Expand All @@ -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.');
Expand Down
64 changes: 61 additions & 3 deletions libraries/reporter/src/bootstrap/BootstrapProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand Down
22 changes: 22 additions & 0 deletions libraries/reporter/src/test/Bootstrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
type IBootstrapEventBufferOptions,
type IEarlyReporterControls
} from '../index';
import { encodeBootstrapEnvelope } from '../bootstrap/BootstrapProtocol';

function decode(ndjson: string): Record<string, unknown>[] {
return ndjson
Expand Down Expand Up @@ -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'] } });
Expand Down
Loading