From 83eed957367a6b89450eae9921e6bd1032b71597 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Fri, 30 May 2025 11:39:45 -0600 Subject: [PATCH 01/12] feat(plugin): add bridge-cache plugin --- README.md | 1 + apps/rush/src/start-dev-docs.ts | 2 +- ...-cache-bridge-plugin_2025-05-30-18-02.json | 10 ++ .../rush/browser-approved-packages.json | 4 + .../config/subspaces/default/pnpm-lock.yaml | 18 +++ common/reviews/api/rush-lib.api.md | 91 +++++++++++++- libraries/rush-lib/src/index.ts | 3 + .../src/logic/buildCache/ProjectBuildCache.ts | 3 + .../rush-bridge-cache-plugin/.eslintrc.js | 13 ++ rush-plugins/rush-bridge-cache-plugin/LICENSE | 24 ++++ .../rush-bridge-cache-plugin/README.md | 47 +++++++ .../config/jest.config.json | 3 + .../rush-bridge-cache-plugin/config/rig.json | 6 + .../rush-bridge-cache-plugin/package.json | 24 ++++ .../rush-plugin-manifest.json | 10 ++ .../src/BridgeCachePlugin.ts | 115 ++++++++++++++++++ .../rush-bridge-cache-plugin/src/index.ts | 4 + .../rush-bridge-cache-plugin/tsconfig.json | 3 + rush.json | 6 + 19 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 common/changes/@microsoft/rush/benkeen-cache-bridge-plugin_2025-05-30-18-02.json create mode 100644 rush-plugins/rush-bridge-cache-plugin/.eslintrc.js create mode 100644 rush-plugins/rush-bridge-cache-plugin/LICENSE create mode 100644 rush-plugins/rush-bridge-cache-plugin/README.md create mode 100644 rush-plugins/rush-bridge-cache-plugin/config/jest.config.json create mode 100644 rush-plugins/rush-bridge-cache-plugin/config/rig.json create mode 100644 rush-plugins/rush-bridge-cache-plugin/package.json create mode 100644 rush-plugins/rush-bridge-cache-plugin/rush-plugin-manifest.json create mode 100644 rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts create mode 100644 rush-plugins/rush-bridge-cache-plugin/src/index.ts create mode 100644 rush-plugins/rush-bridge-cache-plugin/tsconfig.json diff --git a/README.md b/README.md index a506c6af0e1..eb57e66ea43 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/rigs/heft-web-rig](./rigs/heft-web-rig/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-web-rig.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-web-rig) | [changelog](./rigs/heft-web-rig/CHANGELOG.md) | [@rushstack/heft-web-rig](https://www.npmjs.com/package/@rushstack/heft-web-rig) | | [/rush-plugins/rush-amazon-s3-build-cache-plugin](./rush-plugins/rush-amazon-s3-build-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-amazon-s3-build-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-amazon-s3-build-cache-plugin) | | [@rushstack/rush-amazon-s3-build-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-amazon-s3-build-cache-plugin) | | [/rush-plugins/rush-azure-storage-build-cache-plugin](./rush-plugins/rush-azure-storage-build-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-azure-storage-build-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-azure-storage-build-cache-plugin) | | [@rushstack/rush-azure-storage-build-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-azure-storage-build-cache-plugin) | +| [/rush-plugins/rush-bridge-cache-plugin](./rush-plugins/rush-bridge-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-bridge-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-bridge-cache-plugin) | | [@rushstack/rush-bridge-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-bridge-cache-plugin) | | [/rush-plugins/rush-buildxl-graph-plugin](./rush-plugins/rush-buildxl-graph-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-buildxl-graph-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-buildxl-graph-plugin) | | [@rushstack/rush-buildxl-graph-plugin](https://www.npmjs.com/package/@rushstack/rush-buildxl-graph-plugin) | | [/rush-plugins/rush-http-build-cache-plugin](./rush-plugins/rush-http-build-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-http-build-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-http-build-cache-plugin) | | [@rushstack/rush-http-build-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-http-build-cache-plugin) | | [/rush-plugins/rush-redis-cobuild-plugin](./rush-plugins/rush-redis-cobuild-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-redis-cobuild-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-redis-cobuild-plugin) | | [@rushstack/rush-redis-cobuild-plugin](https://www.npmjs.com/package/@rushstack/rush-redis-cobuild-plugin) | diff --git a/apps/rush/src/start-dev-docs.ts b/apps/rush/src/start-dev-docs.ts index be5089d5130..4bd9539a8bb 100644 --- a/apps/rush/src/start-dev-docs.ts +++ b/apps/rush/src/start-dev-docs.ts @@ -6,4 +6,4 @@ import { Colorize, ConsoleTerminalProvider, Terminal } from '@rushstack/terminal const terminal: Terminal = new Terminal(new ConsoleTerminalProvider()); terminal.writeLine('For instructions on debugging Rush, please see this documentation:'); -terminal.writeLine(Colorize.bold('https://rushjs.io/pages/contributing/debugging/')); +terminal.writeLine(Colorize.bold('https://rushjs.io/pages/contributing/#debugging-rush')); diff --git a/common/changes/@microsoft/rush/benkeen-cache-bridge-plugin_2025-05-30-18-02.json b/common/changes/@microsoft/rush/benkeen-cache-bridge-plugin_2025-05-30-18-02.json new file mode 100644 index 00000000000..4d549b41c6d --- /dev/null +++ b/common/changes/@microsoft/rush/benkeen-cache-bridge-plugin_2025-05-30-18-02.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "add build cache plugin", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index 01b44fa2e61..f98ee6a7634 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -38,6 +38,10 @@ "name": "@reduxjs/toolkit", "allowedCategories": [ "libraries", "vscode-extensions" ] }, + { + "name": "@rushstack/rush-bridge-cache-plugin", + "allowedCategories": [ "libraries" ] + }, { "name": "@rushstack/rush-themed-ui", "allowedCategories": [ "libraries" ] diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index ecc2d923870..18478480e4c 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -4014,6 +4014,24 @@ importers: specifier: workspace:* version: link:../../rigs/local-node-rig + ../../../rush-plugins/rush-bridge-cache-plugin: + devDependencies: + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + '@rushstack/node-core-library': + specifier: workspace:* + version: link:../../libraries/node-core-library + '@rushstack/rush-sdk': + specifier: workspace:* + version: link:../../libraries/rush-sdk + '@rushstack/terminal': + specifier: workspace:* + version: link:../../libraries/terminal + local-node-rig: + specifier: workspace:* + version: link:../../rigs/local-node-rig + ../../../rush-plugins/rush-buildxl-graph-plugin: dependencies: '@rushstack/node-core-library': diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 76e081722d1..65e4167a3ae 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -10,7 +10,7 @@ import { AsyncParallelHook } from 'tapable'; import { AsyncSeriesBailHook } from 'tapable'; import { AsyncSeriesHook } from 'tapable'; import { AsyncSeriesWaterfallHook } from 'tapable'; -import type { CollatedWriter } from '@rushstack/stream-collator'; +import { CollatedWriter } from '@rushstack/stream-collator'; import type { CommandLineParameter } from '@rushstack/ts-command-line'; import { CommandLineParameterKind } from '@rushstack/ts-command-line'; import { HookMap } from 'tapable'; @@ -23,7 +23,8 @@ import { JsonNull } from '@rushstack/node-core-library'; import { JsonObject } from '@rushstack/node-core-library'; import { LookupByPath } from '@rushstack/lookup-by-path'; import { PackageNameParser } from '@rushstack/node-core-library'; -import type { StdioSummarizer } from '@rushstack/terminal'; +import { StdioSummarizer } from '@rushstack/terminal'; +import { StreamCollator } from '@rushstack/stream-collator'; import { SyncHook } from 'tapable'; import { SyncWaterfallHook } from 'tapable'; import { Terminal } from '@rushstack/terminal'; @@ -947,6 +948,92 @@ export class Operation { weight: number; } +// Warning: (ae-internal-missing-underscore) The name "OperationBuildCache" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export class OperationBuildCache { + // (undocumented) + get cacheId(): string | undefined; + // Warning: (ae-forgotten-export) The symbol "IOperationBuildCacheOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static forOperation(operation: OperationExecutionRecord, options: IOperationBuildCacheOptions): OperationBuildCache; + // Warning: (ae-forgotten-export) The symbol "IProjectBuildCacheOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static getProjectBuildCache(options: IProjectBuildCacheOptions): OperationBuildCache; + // (undocumented) + tryRestoreFromCacheAsync(terminal: ITerminal, specifiedCacheId?: string): Promise; + // (undocumented) + trySetCacheEntryAsync(terminal: ITerminal, specifiedCacheId?: string): Promise; +} + +// Warning: (ae-internal-missing-underscore) The name "OperationExecutionRecord" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal +export class OperationExecutionRecord implements IOperationRunnerContext, IOperationExecutionResult { + // Warning: (ae-forgotten-export) The symbol "IOperationExecutionRecordContext" needs to be exported by the entry point index.d.ts + constructor(operation: Operation, context: IOperationExecutionRecordContext); + // (undocumented) + readonly associatedPhase: IPhase; + // (undocumented) + readonly associatedProject: RushConfigurationProject; + // (undocumented) + get cobuildRunnerId(): string | undefined; + // (undocumented) + get collatedWriter(): CollatedWriter; + readonly consumers: Set; + criticalPathLength: number | undefined; + // (undocumented) + get debugMode(): boolean; + readonly dependencies: Set; + // (undocumented) + get environment(): IEnvironment | undefined; + error: Error | undefined; + // (undocumented) + executeAsync({ onStart, onResult }: { + onStart: (record: OperationExecutionRecord) => Promise; + onResult: (record: OperationExecutionRecord) => Promise; + }): Promise; + // (undocumented) + getStateHash(): string; + // (undocumented) + getStateHashComponents(): ReadonlyArray; + // (undocumented) + get isTerminal(): boolean; + // (undocumented) + logFilePaths: ILogFilePaths | undefined; + // (undocumented) + get metadataFolderPath(): string | undefined; + // (undocumented) + get name(): string; + // (undocumented) + get nonCachedDurationMs(): number | undefined; + readonly operation: Operation; + // (undocumented) + readonly _operationMetadataManager: _OperationMetadataManager; + // (undocumented) + get quietMode(): boolean; + // (undocumented) + readonly runner: IOperationRunner; + runWithTerminalAsync(callback: (terminal: ITerminal, terminalProvider: ITerminalProvider) => Promise, options: { + createLogFile: boolean; + logFileSuffix: string; + }): Promise; + // (undocumented) + get silent(): boolean; + get status(): OperationStatus; + set status(newStatus: OperationStatus); + // (undocumented) + readonly stdioSummarizer: StdioSummarizer; + // Warning: (ae-forgotten-export) The symbol "Stopwatch" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly stopwatch: Stopwatch; + // (undocumented) + get weight(): number; +} + // @internal export class _OperationMetadataManager { constructor(options: _IOperationMetadataManagerOptions); diff --git a/libraries/rush-lib/src/index.ts b/libraries/rush-lib/src/index.ts index d7ef26a364e..9592a1bbecf 100644 --- a/libraries/rush-lib/src/index.ts +++ b/libraries/rush-lib/src/index.ts @@ -135,6 +135,7 @@ export type { IExecutionResult, IOperationExecutionResult } from './logic/operations/IOperationExecutionResult'; +export type { OperationExecutionRecord } from './logic/operations/OperationExecutionRecord'; export { type IOperationOptions, Operation } from './logic/operations/Operation'; export { OperationStatus } from './logic/operations/OperationStatus'; export type { ILogFilePaths } from './logic/operations/ProjectLogWritable'; @@ -197,3 +198,5 @@ export { type IRushCommandLineParameter, type IRushCommandLineAction } from './api/RushCommandLine'; + +export { ProjectBuildCache as OperationBuildCache } from './logic/buildCache/ProjectBuildCache'; diff --git a/libraries/rush-lib/src/logic/buildCache/ProjectBuildCache.ts b/libraries/rush-lib/src/logic/buildCache/ProjectBuildCache.ts index 861d403f029..30199e8c1ac 100644 --- a/libraries/rush-lib/src/logic/buildCache/ProjectBuildCache.ts +++ b/libraries/rush-lib/src/logic/buildCache/ProjectBuildCache.ts @@ -50,6 +50,9 @@ interface IPathsToCache { outputFilePaths: string[]; } +/** + * @internal + */ export class ProjectBuildCache { private static _tarUtilityPromise: Promise | undefined; diff --git a/rush-plugins/rush-bridge-cache-plugin/.eslintrc.js b/rush-plugins/rush-bridge-cache-plugin/.eslintrc.js new file mode 100644 index 00000000000..0b04796d1ee --- /dev/null +++ b/rush-plugins/rush-bridge-cache-plugin/.eslintrc.js @@ -0,0 +1,13 @@ +// This is a workaround for https://github.com/eslint/eslint/issues/3458 +require('local-node-rig/profiles/default/includes/eslint/patch/modern-module-resolution'); +// This is a workaround for https://github.com/microsoft/rushstack/issues/3021 +require('local-node-rig/profiles/default/includes/eslint/patch/custom-config-package-names'); + +module.exports = { + extends: [ + 'local-node-rig/profiles/default/includes/eslint/profile/node', + 'local-node-rig/profiles/default/includes/eslint/mixins/friendly-locals', + 'local-node-rig/profiles/default/includes/eslint/mixins/tsdoc' + ], + parserOptions: { tsconfigRootDir: __dirname } +}; diff --git a/rush-plugins/rush-bridge-cache-plugin/LICENSE b/rush-plugins/rush-bridge-cache-plugin/LICENSE new file mode 100644 index 00000000000..7cfdf630251 --- /dev/null +++ b/rush-plugins/rush-bridge-cache-plugin/LICENSE @@ -0,0 +1,24 @@ +@rushstack/rush-bridge-cache-plugin + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/rush-plugins/rush-bridge-cache-plugin/README.md b/rush-plugins/rush-bridge-cache-plugin/README.md new file mode 100644 index 00000000000..8c009f40552 --- /dev/null +++ b/rush-plugins/rush-bridge-cache-plugin/README.md @@ -0,0 +1,47 @@ +# @rushstack/rush-bridge-cache-plugin + +This is a Rush plugin that lets you to add an optional `--set-cache-only` flag to Rush's phased commands to bypass the actual _action_ of the script (build, test, lint - whatever you have configured), and just populate the cache from the action as though the action had already been performed by Rush. + +This is useful for integrations with other build orchestrators such as BuildXL. You can use those to do the work of actually running the task, then run the equivalent Rush command afterwards with a `--set-cache-only` to populate the Rush cache with whatever had been generated on disk, in addition to whatever cache mechanism is used by the other build orchestrator. + +## Here be dragons! + +This plugin assumes that the work for a particular task has already been completed and the build artifacts have been generated on disk. **If you run this command on a package where the command hasn't already been ran and the build artifacts are missing or incorrect, you will cache invalid content**. Be careful and beware! + + +## Installation + +1. Add the `@rushstack/rush-bridge-cache-plugin` package to your autoinstaller's package.json. +2. Update your `command-line.json` file to add the new flag. Configure it to target whatever specific commands you want to have this feature. Example: + +```json +{ + "associatedCommands": ["build", "test", "lint", "a11y", "typecheck"], + "description": "When the flag is added to any associated command, it'll bypass running the command itself, and cache whatever it finds on disk for the action. Beware! Only run when you know the build artifacts are in a valid state for the command.", + "parameterKind": "flag", + "longName": "--set-cache-only", + "required": false +} +``` + +3. Add a new entry in `common/config/rush/rush-plugins` to register the new plugin: +``` +{ + "packageName": "@rushstack/rush-bridge-cache-plugin", + "pluginName": "rush-bridge-cache-plugin", + "autoinstallerName": "your-auto-installer-name-here" +} +``` +## Usage + +Any of the rush command can now just be given a `--set-cache-only` property, e.g. + +`rush build --to your-packageX --set-cache-only` + +That will examine `your-packageX` and all of its dependencies, then populate the cache. + +## Performance + +When running within a pipeline, you may want to populate the cache as quickly as possible so local Rush users will benefit from the cached entry sooner. So instead of waiting until the full build graph has been processed, running it after each individual task when it's been completed, e.g. + +`rush lint --only your-packageY --set-cache-only` diff --git a/rush-plugins/rush-bridge-cache-plugin/config/jest.config.json b/rush-plugins/rush-bridge-cache-plugin/config/jest.config.json new file mode 100644 index 00000000000..d1749681d90 --- /dev/null +++ b/rush-plugins/rush-bridge-cache-plugin/config/jest.config.json @@ -0,0 +1,3 @@ +{ + "extends": "local-node-rig/profiles/default/config/jest.config.json" +} diff --git a/rush-plugins/rush-bridge-cache-plugin/config/rig.json b/rush-plugins/rush-bridge-cache-plugin/config/rig.json new file mode 100644 index 00000000000..d339847ee0a --- /dev/null +++ b/rush-plugins/rush-bridge-cache-plugin/config/rig.json @@ -0,0 +1,6 @@ +{ + // The "rig.json" file directs tools to look for their config files in an external package. + // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "local-node-rig" +} diff --git a/rush-plugins/rush-bridge-cache-plugin/package.json b/rush-plugins/rush-bridge-cache-plugin/package.json new file mode 100644 index 00000000000..6fdb7a4afca --- /dev/null +++ b/rush-plugins/rush-bridge-cache-plugin/package.json @@ -0,0 +1,24 @@ +{ + "name": "@rushstack/rush-bridge-cache-plugin", + "version": "0.0.1", + "private": true, + "description": "Rush plugin that provides a --set-cache-only command flag to populate the cache from content on disk.", + "license": "MIT", + "main": "./lib/index.js", + "repository": { + "url": "https://github.com/microsoft/rushstack.git", + "type": "git", + "directory": "rush-plugins/rush-bridge-cache-plugin" + }, + "scripts": { + "build": "heft test --clean", + "_phase:build": "heft run --only build -- --clean" + }, + "devDependencies": { + "@rushstack/node-core-library": "workspace:*", + "@rushstack/rush-sdk": "workspace:*", + "@rushstack/terminal": "workspace:*", + "@rushstack/heft": "workspace:*", + "local-node-rig": "workspace:*" + } +} diff --git a/rush-plugins/rush-bridge-cache-plugin/rush-plugin-manifest.json b/rush-plugins/rush-bridge-cache-plugin/rush-plugin-manifest.json new file mode 100644 index 00000000000..8475d026e87 --- /dev/null +++ b/rush-plugins/rush-bridge-cache-plugin/rush-plugin-manifest.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-plugin-manifest.schema.json", + "plugins": [ + { + "pluginName": "rush-bridge-cache-plugin", + "description": "Rush plugin that provides a --set-cache-only command flag to populate the cache from content on disk.", + "entryPoint": "./lib/index.js" + } + ] +} diff --git a/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts b/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts new file mode 100644 index 00000000000..fd781ab848a --- /dev/null +++ b/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { OperationBuildCache } from '@rushstack/rush-sdk'; +import type { + BuildCacheConfiguration, + IExecuteOperationsContext, + ILogger, + IOperationExecutionResult, + IPhasedCommand, + IRushPlugin, + Operation, + OperationExecutionRecord, + RushSession +} from '@rushstack/rush-sdk'; + +const PLUGIN_NAME: 'RushBridgeCachePlugin' = 'RushBridgeCachePlugin'; + +export class BridgeCachePlugin implements IRushPlugin { + public readonly pluginName: string = PLUGIN_NAME; + + public apply(session: RushSession): void { + const isSetCacheOnly: boolean = process.argv.includes('--set-cache-only'); + if (!isSetCacheOnly) { + return; + } + + const cancelOperations = (operations: Set): Set => { + operations.forEach((operation: Operation) => { + operation.enabled = false; + }); + return operations; + }; + + session.hooks.runAnyPhasedCommand.tapPromise(PLUGIN_NAME, async (command: IPhasedCommand) => { + // tracks the projects being targeted by the command (--to, --only etc.) + const targetProjects: Set = new Set(); + + // cancel the actual operations. We don't want to run the command, just cache the output folders on disk + command.hooks.createOperations.tap( + { name: PLUGIN_NAME, stage: Number.MAX_SAFE_INTEGER }, + (operations: Set): Set => { + operations.forEach((operation: Operation) => { + if (operation.enabled) { + targetProjects.add(operation); + } + }); + return cancelOperations(operations); + } + ); + + // populate the cache for each operation + command.hooks.beforeExecuteOperations.tap( + PLUGIN_NAME, + async ( + recordByOperation: Map, + context: IExecuteOperationsContext + ): Promise => { + if (!context.buildCacheConfiguration) { + return; + } + + await this._setCacheAsync( + session, + context.buildCacheConfiguration, + recordByOperation, + targetProjects + ); + } + ); + }); + } + + private async _setCacheAsync( + session: RushSession, + buildCacheConfiguration: BuildCacheConfiguration, + recordByOperation: Map, + targetProjects: Set + ): Promise { + const logger: ILogger = session.getLogger(PLUGIN_NAME); + + recordByOperation.forEach( + async (operationExecutionResult: IOperationExecutionResult, operation: Operation) => { + const { associatedProject, associatedPhase } = operation; + + // omit operations that aren't targeted, or packages without a command for this phase + const hasCommand: boolean = !!associatedProject.packageJson.scripts?.[associatedPhase.name]; + if (!targetProjects.has(operation) || !hasCommand) { + return; + } + + const projectBuildCache: OperationBuildCache = OperationBuildCache.forOperation( + operationExecutionResult as OperationExecutionRecord, + { + buildCacheConfiguration, + terminal: logger.terminal + } + ); + + const success: boolean = await projectBuildCache.trySetCacheEntryAsync(logger.terminal); + + if (success) { + logger.terminal.writeLine( + `Cache entry set for ${associatedPhase.name} (${associatedProject.packageName}) from previously generated output folders\n` + ); + } else { + logger.terminal.writeErrorLine( + `Error creating a cache entry set for ${associatedPhase.name} (${associatedProject.packageName}) from previously generated output folders\n` + ); + process.exit(1); + } + } + ); + } +} diff --git a/rush-plugins/rush-bridge-cache-plugin/src/index.ts b/rush-plugins/rush-bridge-cache-plugin/src/index.ts new file mode 100644 index 00000000000..01e83887250 --- /dev/null +++ b/rush-plugins/rush-bridge-cache-plugin/src/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +export { BridgeCachePlugin as default } from './BridgeCachePlugin'; diff --git a/rush-plugins/rush-bridge-cache-plugin/tsconfig.json b/rush-plugins/rush-bridge-cache-plugin/tsconfig.json new file mode 100644 index 00000000000..dac21d04081 --- /dev/null +++ b/rush-plugins/rush-bridge-cache-plugin/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json" +} diff --git a/rush.json b/rush.json index 3d8f5018a1e..b9fcec2c8bd 100644 --- a/rush.json +++ b/rush.json @@ -1322,6 +1322,12 @@ "reviewCategory": "libraries", "versionPolicyName": "rush" }, + { + "packageName": "@rushstack/rush-bridge-cache-plugin", + "projectFolder": "rush-plugins/rush-bridge-cache-plugin", + "reviewCategory": "libraries", + "versionPolicyName": "rush" + }, // "vscode-extensions" folder (alphabetical order) { From b478224ee6034fddce0b0eafa2a07e5474775f37 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Mon, 2 Jun 2025 14:13:39 -0600 Subject: [PATCH 02/12] Update common/changes/@microsoft/rush/benkeen-cache-bridge-plugin_2025-05-30-18-02.json Co-authored-by: Ian Clanton-Thuon --- .../rush/benkeen-cache-bridge-plugin_2025-05-30-18-02.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/changes/@microsoft/rush/benkeen-cache-bridge-plugin_2025-05-30-18-02.json b/common/changes/@microsoft/rush/benkeen-cache-bridge-plugin_2025-05-30-18-02.json index 4d549b41c6d..75d60bbf09d 100644 --- a/common/changes/@microsoft/rush/benkeen-cache-bridge-plugin_2025-05-30-18-02.json +++ b/common/changes/@microsoft/rush/benkeen-cache-bridge-plugin_2025-05-30-18-02.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@microsoft/rush", - "comment": "add build cache plugin", + "comment": "Introduce a `@rushstack/rush-bridge-cache-plugin` package that adds a `--set-cache-only` flag to phased commands, which sets the cache entry without performing the operation.", "type": "none" } ], From 1bd5cfe14d308dc3483bffc3f8dd97864f708363 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Mon, 2 Jun 2025 14:14:32 -0600 Subject: [PATCH 03/12] Update rush-plugins/rush-bridge-cache-plugin/README.md Co-authored-by: Ian Clanton-Thuon --- rush-plugins/rush-bridge-cache-plugin/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rush-plugins/rush-bridge-cache-plugin/README.md b/rush-plugins/rush-bridge-cache-plugin/README.md index 8c009f40552..745d0cce784 100644 --- a/rush-plugins/rush-bridge-cache-plugin/README.md +++ b/rush-plugins/rush-bridge-cache-plugin/README.md @@ -6,7 +6,7 @@ This is useful for integrations with other build orchestrators such as BuildXL. ## Here be dragons! -This plugin assumes that the work for a particular task has already been completed and the build artifacts have been generated on disk. **If you run this command on a package where the command hasn't already been ran and the build artifacts are missing or incorrect, you will cache invalid content**. Be careful and beware! +This plugin assumes that the work for a particular task has already been completed and the build artifacts have been generated on disk. **If you run this command on a package where the command hasn't already been run and the build artifacts are missing or incorrect, you will cache invalid content**. Be careful and beware! ## Installation From 4110a7be17903d378059bcf4ee7ee7288802b405 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Tue, 3 Jun 2025 12:44:41 -0600 Subject: [PATCH 04/12] Code review feedback --- .../rush/browser-approved-packages.json | 4 - .../rush/nonbrowser-approved-packages.json | 4 + common/reviews/api/rush-lib.api.md | 99 ++++--------------- libraries/rush-lib/src/index.ts | 7 +- ...ctBuildCache.ts => OperationBuildCache.ts} | 34 ++++--- ...he.test.ts => OperationBuildCache.test.ts} | 14 +-- .../rush-lib/src/logic/cobuild/CobuildLock.ts | 12 +-- .../logic/cobuild/test/CobuildLock.test.ts | 6 +- .../operations/CacheableOperationPlugin.ts | 54 +++++----- .../operations/IOperationExecutionResult.ts | 10 ++ .../operations/OperationExecutionRecord.ts | 7 +- .../rush-bridge-cache-plugin/README.md | 20 ++-- .../rush-plugin-manifest.json | 3 +- .../src/BridgeCachePlugin.ts | 42 +++----- .../schemas/bridge-cache-config.schema.json | 17 ++++ 15 files changed, 156 insertions(+), 177 deletions(-) rename libraries/rush-lib/src/logic/buildCache/{ProjectBuildCache.ts => OperationBuildCache.ts} (93%) rename libraries/rush-lib/src/logic/buildCache/test/{ProjectBuildCache.test.ts => OperationBuildCache.test.ts} (79%) create mode 100644 rush-plugins/rush-bridge-cache-plugin/src/schemas/bridge-cache-config.schema.json diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index f98ee6a7634..01b44fa2e61 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -38,10 +38,6 @@ "name": "@reduxjs/toolkit", "allowedCategories": [ "libraries", "vscode-extensions" ] }, - { - "name": "@rushstack/rush-bridge-cache-plugin", - "allowedCategories": [ "libraries" ] - }, { "name": "@rushstack/rush-themed-ui", "allowedCategories": [ "libraries" ] diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index 5127417f3a6..892a1cb77d0 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -262,6 +262,10 @@ "name": "@rushstack/rush-azure-storage-build-cache-plugin", "allowedCategories": [ "libraries" ] }, + { + "name": "@rushstack/rush-bridge-cache-plugin", + "allowedCategories": [ "libraries" ] + }, { "name": "@rushstack/rush-http-build-cache-plugin", "allowedCategories": [ "libraries" ] diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 65e4167a3ae..533d4f2ade4 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -10,7 +10,7 @@ import { AsyncParallelHook } from 'tapable'; import { AsyncSeriesBailHook } from 'tapable'; import { AsyncSeriesHook } from 'tapable'; import { AsyncSeriesWaterfallHook } from 'tapable'; -import { CollatedWriter } from '@rushstack/stream-collator'; +import type { CollatedWriter } from '@rushstack/stream-collator'; import type { CommandLineParameter } from '@rushstack/ts-command-line'; import { CommandLineParameterKind } from '@rushstack/ts-command-line'; import { HookMap } from 'tapable'; @@ -23,8 +23,7 @@ import { JsonNull } from '@rushstack/node-core-library'; import { JsonObject } from '@rushstack/node-core-library'; import { LookupByPath } from '@rushstack/lookup-by-path'; import { PackageNameParser } from '@rushstack/node-core-library'; -import { StdioSummarizer } from '@rushstack/terminal'; -import { StreamCollator } from '@rushstack/stream-collator'; +import type { StdioSummarizer } from '@rushstack/terminal'; import { SyncHook } from 'tapable'; import { SyncWaterfallHook } from 'tapable'; import { Terminal } from '@rushstack/terminal'; @@ -584,8 +583,16 @@ export class IndividualVersionPolicy extends VersionPolicy { export interface _INpmOptionsJson extends IPackageManagerOptionsJsonBase { } +// @internal (undocumented) +export interface _IOperationBuildCacheOptions { + buildCacheConfiguration: BuildCacheConfiguration; + terminal: ITerminal; +} + // @alpha export interface IOperationExecutionResult { + readonly associatedPhase: IPhase; + readonly associatedProject: RushConfigurationProject; readonly error: Error | undefined; getStateHash(): string; getStateHashComponents(): ReadonlyArray; @@ -780,6 +787,14 @@ export interface IPnpmPeerDependencyRules { export { IPrefixMatch } +// @internal (undocumented) +export type _IProjectBuildCacheOptions = _IOperationBuildCacheOptions & { + projectOutputFolderNames: ReadonlyArray; + project: RushConfigurationProject; + operationStateHash: string; + phaseName: string; +}; + // @beta export interface IRushCommand { readonly actionName: string; @@ -948,92 +963,20 @@ export class Operation { weight: number; } -// Warning: (ae-internal-missing-underscore) The name "OperationBuildCache" should be prefixed with an underscore because the declaration is marked as @internal -// // @internal (undocumented) -export class OperationBuildCache { +export class _OperationBuildCache { // (undocumented) get cacheId(): string | undefined; - // Warning: (ae-forgotten-export) The symbol "IOperationBuildCacheOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) - static forOperation(operation: OperationExecutionRecord, options: IOperationBuildCacheOptions): OperationBuildCache; - // Warning: (ae-forgotten-export) The symbol "IProjectBuildCacheOptions" needs to be exported by the entry point index.d.ts - // + static forOperation(operation: IOperationExecutionResult, options: _IOperationBuildCacheOptions): _OperationBuildCache; // (undocumented) - static getProjectBuildCache(options: IProjectBuildCacheOptions): OperationBuildCache; + static getOperationBuildCache(options: _IProjectBuildCacheOptions): _OperationBuildCache; // (undocumented) tryRestoreFromCacheAsync(terminal: ITerminal, specifiedCacheId?: string): Promise; // (undocumented) trySetCacheEntryAsync(terminal: ITerminal, specifiedCacheId?: string): Promise; } -// Warning: (ae-internal-missing-underscore) The name "OperationExecutionRecord" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export class OperationExecutionRecord implements IOperationRunnerContext, IOperationExecutionResult { - // Warning: (ae-forgotten-export) The symbol "IOperationExecutionRecordContext" needs to be exported by the entry point index.d.ts - constructor(operation: Operation, context: IOperationExecutionRecordContext); - // (undocumented) - readonly associatedPhase: IPhase; - // (undocumented) - readonly associatedProject: RushConfigurationProject; - // (undocumented) - get cobuildRunnerId(): string | undefined; - // (undocumented) - get collatedWriter(): CollatedWriter; - readonly consumers: Set; - criticalPathLength: number | undefined; - // (undocumented) - get debugMode(): boolean; - readonly dependencies: Set; - // (undocumented) - get environment(): IEnvironment | undefined; - error: Error | undefined; - // (undocumented) - executeAsync({ onStart, onResult }: { - onStart: (record: OperationExecutionRecord) => Promise; - onResult: (record: OperationExecutionRecord) => Promise; - }): Promise; - // (undocumented) - getStateHash(): string; - // (undocumented) - getStateHashComponents(): ReadonlyArray; - // (undocumented) - get isTerminal(): boolean; - // (undocumented) - logFilePaths: ILogFilePaths | undefined; - // (undocumented) - get metadataFolderPath(): string | undefined; - // (undocumented) - get name(): string; - // (undocumented) - get nonCachedDurationMs(): number | undefined; - readonly operation: Operation; - // (undocumented) - readonly _operationMetadataManager: _OperationMetadataManager; - // (undocumented) - get quietMode(): boolean; - // (undocumented) - readonly runner: IOperationRunner; - runWithTerminalAsync(callback: (terminal: ITerminal, terminalProvider: ITerminalProvider) => Promise, options: { - createLogFile: boolean; - logFileSuffix: string; - }): Promise; - // (undocumented) - get silent(): boolean; - get status(): OperationStatus; - set status(newStatus: OperationStatus); - // (undocumented) - readonly stdioSummarizer: StdioSummarizer; - // Warning: (ae-forgotten-export) The symbol "Stopwatch" needs to be exported by the entry point index.d.ts - // - // (undocumented) - readonly stopwatch: Stopwatch; - // (undocumented) - get weight(): number; -} - // @internal export class _OperationMetadataManager { constructor(options: _IOperationMetadataManagerOptions); diff --git a/libraries/rush-lib/src/index.ts b/libraries/rush-lib/src/index.ts index 9592a1bbecf..685720e0629 100644 --- a/libraries/rush-lib/src/index.ts +++ b/libraries/rush-lib/src/index.ts @@ -135,7 +135,6 @@ export type { IExecutionResult, IOperationExecutionResult } from './logic/operations/IOperationExecutionResult'; -export type { OperationExecutionRecord } from './logic/operations/OperationExecutionRecord'; export { type IOperationOptions, Operation } from './logic/operations/Operation'; export { OperationStatus } from './logic/operations/OperationStatus'; export type { ILogFilePaths } from './logic/operations/ProjectLogWritable'; @@ -199,4 +198,8 @@ export { type IRushCommandLineAction } from './api/RushCommandLine'; -export { ProjectBuildCache as OperationBuildCache } from './logic/buildCache/ProjectBuildCache'; +export { OperationBuildCache as _OperationBuildCache } from './logic/buildCache/OperationBuildCache'; +export type { + IOperationBuildCacheOptions as _IOperationBuildCacheOptions, + IProjectBuildCacheOptions as _IProjectBuildCacheOptions +} from './logic/buildCache/OperationBuildCache'; diff --git a/libraries/rush-lib/src/logic/buildCache/ProjectBuildCache.ts b/libraries/rush-lib/src/logic/buildCache/OperationBuildCache.ts similarity index 93% rename from libraries/rush-lib/src/logic/buildCache/ProjectBuildCache.ts rename to libraries/rush-lib/src/logic/buildCache/OperationBuildCache.ts index 30199e8c1ac..fe74a5312fa 100644 --- a/libraries/rush-lib/src/logic/buildCache/ProjectBuildCache.ts +++ b/libraries/rush-lib/src/logic/buildCache/OperationBuildCache.ts @@ -13,8 +13,11 @@ import type { ICloudBuildCacheProvider } from './ICloudBuildCacheProvider'; import type { FileSystemBuildCacheProvider } from './FileSystemBuildCacheProvider'; import { TarExecutable } from '../../utilities/TarExecutable'; import { EnvironmentVariableNames } from '../../api/EnvironmentConfiguration'; -import type { OperationExecutionRecord } from '../operations/OperationExecutionRecord'; +import type { IOperationExecutionResult } from '../operations/IOperationExecutionResult'; +/** + * @internal + */ export interface IOperationBuildCacheOptions { /** * The repo-wide configuration for the build cache. @@ -26,6 +29,9 @@ export interface IOperationBuildCacheOptions { terminal: ITerminal; } +/** + * @internal + */ export type IProjectBuildCacheOptions = IOperationBuildCacheOptions & { /** * Value from rush-project.json @@ -53,7 +59,7 @@ interface IPathsToCache { /** * @internal */ -export class ProjectBuildCache { +export class OperationBuildCache { private static _tarUtilityPromise: Promise | undefined; private readonly _project: RushConfigurationProject; @@ -85,26 +91,26 @@ export class ProjectBuildCache { } private static _tryGetTarUtility(terminal: ITerminal): Promise { - if (!ProjectBuildCache._tarUtilityPromise) { - ProjectBuildCache._tarUtilityPromise = TarExecutable.tryInitializeAsync(terminal); + if (!OperationBuildCache._tarUtilityPromise) { + OperationBuildCache._tarUtilityPromise = TarExecutable.tryInitializeAsync(terminal); } - return ProjectBuildCache._tarUtilityPromise; + return OperationBuildCache._tarUtilityPromise; } public get cacheId(): string | undefined { return this._cacheId; } - public static getProjectBuildCache(options: IProjectBuildCacheOptions): ProjectBuildCache { - const cacheId: string | undefined = ProjectBuildCache._getCacheId(options); - return new ProjectBuildCache(cacheId, options); + public static getOperationBuildCache(options: IProjectBuildCacheOptions): OperationBuildCache { + const cacheId: string | undefined = OperationBuildCache._getCacheId(options); + return new OperationBuildCache(cacheId, options); } public static forOperation( - operation: OperationExecutionRecord, + operation: IOperationExecutionResult, options: IOperationBuildCacheOptions - ): ProjectBuildCache { + ): OperationBuildCache { const outputFolders: string[] = [...(operation.operation.settings?.outputFolderNames ?? [])]; if (operation.metadataFolderPath) { outputFolders.push(operation.metadataFolderPath); @@ -117,8 +123,8 @@ export class ProjectBuildCache { projectOutputFolderNames: outputFolders, operationStateHash: operation.getStateHash() }; - const cacheId: string | undefined = ProjectBuildCache._getCacheId(buildCacheOptions); - return new ProjectBuildCache(cacheId, buildCacheOptions); + const cacheId: string | undefined = OperationBuildCache._getCacheId(buildCacheOptions); + return new OperationBuildCache(cacheId, buildCacheOptions); } public async tryRestoreFromCacheAsync(terminal: ITerminal, specifiedCacheId?: string): Promise { @@ -178,7 +184,7 @@ export class ProjectBuildCache { ) ); - const tarUtility: TarExecutable | undefined = await ProjectBuildCache._tryGetTarUtility(terminal); + const tarUtility: TarExecutable | undefined = await OperationBuildCache._tryGetTarUtility(terminal); let restoreSuccess: boolean = false; if (tarUtility && localCacheEntryPath) { const logFilePath: string = this._getTarLogFilePath(cacheId, 'untar'); @@ -228,7 +234,7 @@ export class ProjectBuildCache { let localCacheEntryPath: string | undefined; - const tarUtility: TarExecutable | undefined = await ProjectBuildCache._tryGetTarUtility(terminal); + const tarUtility: TarExecutable | undefined = await OperationBuildCache._tryGetTarUtility(terminal); if (tarUtility) { const finalLocalCacheEntryPath: string = this._localBuildCacheProvider.getCacheEntryPath(cacheId); diff --git a/libraries/rush-lib/src/logic/buildCache/test/ProjectBuildCache.test.ts b/libraries/rush-lib/src/logic/buildCache/test/OperationBuildCache.test.ts similarity index 79% rename from libraries/rush-lib/src/logic/buildCache/test/ProjectBuildCache.test.ts rename to libraries/rush-lib/src/logic/buildCache/test/OperationBuildCache.test.ts index 1acc025dee4..82606c11784 100644 --- a/libraries/rush-lib/src/logic/buildCache/test/ProjectBuildCache.test.ts +++ b/libraries/rush-lib/src/logic/buildCache/test/OperationBuildCache.test.ts @@ -8,7 +8,7 @@ import type { RushConfigurationProject } from '../../../api/RushConfigurationPro import type { IGenerateCacheEntryIdOptions } from '../CacheEntryId'; import type { FileSystemBuildCacheProvider } from '../FileSystemBuildCacheProvider'; -import { ProjectBuildCache } from '../ProjectBuildCache'; +import { OperationBuildCache } from '../OperationBuildCache'; interface ITestOptions { enabled: boolean; @@ -16,11 +16,11 @@ interface ITestOptions { trackedProjectFiles: string[] | undefined; } -describe(ProjectBuildCache.name, () => { - function prepareSubject(options: Partial): ProjectBuildCache { +describe(OperationBuildCache.name, () => { + function prepareSubject(options: Partial): OperationBuildCache { const terminal: Terminal = new Terminal(new StringBufferTerminalProvider()); - const subject: ProjectBuildCache = ProjectBuildCache.getProjectBuildCache({ + const subject: OperationBuildCache = OperationBuildCache.getOperationBuildCache({ buildCacheConfiguration: { buildCacheEnabled: options.hasOwnProperty('enabled') ? options.enabled : true, getCacheEntryId: (opts: IGenerateCacheEntryIdOptions) => @@ -46,9 +46,9 @@ describe(ProjectBuildCache.name, () => { return subject; } - describe(ProjectBuildCache.getProjectBuildCache.name, () => { - it('returns a ProjectBuildCache with a calculated cacheId value', () => { - const subject: ProjectBuildCache = prepareSubject({}); + describe(OperationBuildCache.getOperationBuildCache.name, () => { + it('returns an OperationBuildCache with a calculated cacheId value', () => { + const subject: OperationBuildCache = prepareSubject({}); expect(subject['_cacheId']).toMatchInlineSnapshot( `"acme-wizard/1926f30e8ed24cb47be89aea39e7efd70fcda075"` ); diff --git a/libraries/rush-lib/src/logic/cobuild/CobuildLock.ts b/libraries/rush-lib/src/logic/cobuild/CobuildLock.ts index 44a99405aaf..9dbf7114c3d 100644 --- a/libraries/rush-lib/src/logic/cobuild/CobuildLock.ts +++ b/libraries/rush-lib/src/logic/cobuild/CobuildLock.ts @@ -6,7 +6,7 @@ import { InternalError } from '@rushstack/node-core-library'; import type { CobuildConfiguration } from '../../api/CobuildConfiguration'; import type { OperationStatus } from '../operations/OperationStatus'; import type { ICobuildContext } from './ICobuildLockProvider'; -import type { ProjectBuildCache } from '../buildCache/ProjectBuildCache'; +import type { OperationBuildCache } from '../buildCache/OperationBuildCache'; const KEY_SEPARATOR: ':' = ':'; @@ -27,7 +27,7 @@ export interface ICobuildLockOptions { * {@inheritdoc ICobuildContext.phaseName} */ phaseName: string; - projectBuildCache: ProjectBuildCache; + operationBuildCache: OperationBuildCache; /** * The expire time of the lock in seconds. */ @@ -41,23 +41,23 @@ export interface ICobuildCompletedState { export class CobuildLock { public readonly cobuildConfiguration: CobuildConfiguration; - public readonly projectBuildCache: ProjectBuildCache; + public readonly operationBuildCache: OperationBuildCache; private _cobuildContext: ICobuildContext; public constructor(options: ICobuildLockOptions) { const { cobuildConfiguration, - projectBuildCache, + operationBuildCache, cobuildClusterId: clusterId, lockExpireTimeInSeconds, packageName, phaseName } = options; const { cobuildContextId: contextId, cobuildRunnerId: runnerId } = cobuildConfiguration; - const { cacheId } = projectBuildCache; + const { cacheId } = operationBuildCache; this.cobuildConfiguration = cobuildConfiguration; - this.projectBuildCache = projectBuildCache; + this.operationBuildCache = operationBuildCache; if (!cacheId) { // This should never happen diff --git a/libraries/rush-lib/src/logic/cobuild/test/CobuildLock.test.ts b/libraries/rush-lib/src/logic/cobuild/test/CobuildLock.test.ts index b67089460db..bde478c4919 100644 --- a/libraries/rush-lib/src/logic/cobuild/test/CobuildLock.test.ts +++ b/libraries/rush-lib/src/logic/cobuild/test/CobuildLock.test.ts @@ -4,7 +4,7 @@ import { CobuildLock, type ICobuildLockOptions } from '../CobuildLock'; import type { CobuildConfiguration } from '../../../api/CobuildConfiguration'; -import type { ProjectBuildCache } from '../../buildCache/ProjectBuildCache'; +import type { OperationBuildCache } from '../../buildCache/OperationBuildCache'; import type { ICobuildContext } from '../ICobuildLockProvider'; describe(CobuildLock.name, () => { @@ -14,9 +14,9 @@ describe(CobuildLock.name, () => { cobuildContextId: 'context_id', cobuildRunnerId: 'runner_id' } as unknown as CobuildConfiguration, - projectBuildCache: { + operationBuildCache: { cacheId: 'cache_id' - } as unknown as ProjectBuildCache, + } as unknown as OperationBuildCache, cobuildClusterId: 'cluster_id', lockExpireTimeInSeconds: 30, packageName: 'package_name', diff --git a/libraries/rush-lib/src/logic/operations/CacheableOperationPlugin.ts b/libraries/rush-lib/src/logic/operations/CacheableOperationPlugin.ts index 3ccddb4bbeb..e47c454971f 100644 --- a/libraries/rush-lib/src/logic/operations/CacheableOperationPlugin.ts +++ b/libraries/rush-lib/src/logic/operations/CacheableOperationPlugin.ts @@ -10,7 +10,7 @@ import { SplitterTransform, type TerminalWritable, type ITerminal, Terminal } fr import { CollatedTerminalProvider } from '../../utilities/CollatedTerminalProvider'; import { OperationStatus } from './OperationStatus'; import { CobuildLock, type ICobuildCompletedState } from '../cobuild/CobuildLock'; -import { ProjectBuildCache } from '../buildCache/ProjectBuildCache'; +import { OperationBuildCache } from '../buildCache/OperationBuildCache'; import { RushConstants } from '../RushConstants'; import type { RushProjectConfiguration } from '../../api/RushProjectConfiguration'; import { @@ -47,7 +47,7 @@ export interface IOperationBuildCacheContext { isCacheWriteAllowed: boolean; isCacheReadAllowed: boolean; - operationBuildCache: ProjectBuildCache | undefined; + operationBuildCache: OperationBuildCache | undefined; cacheDisabledReason: string | undefined; outputFolderNames: ReadonlyArray; @@ -240,7 +240,7 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { const buildCacheTerminal: ITerminal = buildCacheContext.buildCacheTerminal; - let projectBuildCache: ProjectBuildCache | undefined = this._tryGetProjectBuildCache({ + let operationBuildCache: OperationBuildCache | undefined = this._tryGetOperationBuildCache({ buildCacheContext, buildCacheConfiguration, terminal: buildCacheTerminal, @@ -253,18 +253,18 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { if ( cobuildConfiguration?.cobuildLeafProjectLogOnlyAllowed && operation.consumers.size === 0 && - !projectBuildCache + !operationBuildCache ) { // When the leaf project log only is allowed and the leaf project is build cache "disabled", try to get // a log files only project build cache - projectBuildCache = await this._tryGetLogOnlyProjectBuildCacheAsync({ + operationBuildCache = await this._tryGetLogOnlyOperationBuildCacheAsync({ buildCacheConfiguration, cobuildConfiguration, buildCacheContext, record, terminal: buildCacheTerminal }); - if (projectBuildCache) { + if (operationBuildCache) { buildCacheTerminal.writeVerboseLine( `Log files only build cache is enabled for the project "${project.packageName}" because the cobuild leaf project log only is allowed` ); @@ -277,7 +277,7 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { cobuildLock = await this._tryGetCobuildLockAsync({ buildCacheContext, - projectBuildCache, + operationBuildCache, cobuildConfiguration, packageName: project.packageName, phaseName: phase.name @@ -305,14 +305,14 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { logFilenameIdentifier: operation.logFilenameIdentifier }); const restoreCacheAsync = async ( - // TODO: Investigate if `projectBuildCacheForRestore` is always the same instance as `projectBuildCache` + // TODO: Investigate if `operationBuildCacheForRestore` is always the same instance as `operationBuildCache` // above, and if it is, remove this parameter - projectBuildCacheForRestore: ProjectBuildCache | undefined, + operationBuildCacheForRestore: OperationBuildCache | undefined, specifiedCacheId?: string ): Promise => { buildCacheContext.isCacheReadAttempted = true; const restoreFromCacheSuccess: boolean | undefined = - await projectBuildCacheForRestore?.tryRestoreFromCacheAsync( + await operationBuildCacheForRestore?.tryRestoreFromCacheAsync( buildCacheTerminal, specifiedCacheId ); @@ -350,7 +350,7 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { } const restoreFromCacheSuccess: boolean = await restoreCacheAsync( - cobuildLock.projectBuildCache, + cobuildLock.operationBuildCache, cacheId ); @@ -358,14 +358,14 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { return status; } } else if (!buildCacheContext.isCacheReadAttempted && buildCacheContext.isCacheReadAllowed) { - const restoreFromCacheSuccess: boolean = await restoreCacheAsync(projectBuildCache); + const restoreFromCacheSuccess: boolean = await restoreCacheAsync(operationBuildCache); if (restoreFromCacheSuccess) { return OperationStatus.FromCache; } } } else if (buildCacheContext.isCacheReadAllowed) { - const restoreFromCacheSuccess: boolean = await restoreCacheAsync(projectBuildCache); + const restoreFromCacheSuccess: boolean = await restoreCacheAsync(operationBuildCache); if (restoreFromCacheSuccess) { return OperationStatus.FromCache; @@ -471,7 +471,7 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { }); }; setCacheEntryPromise = () => - cobuildLock.projectBuildCache.trySetCacheEntryAsync(buildCacheTerminal, finalCacheId); + cobuildLock.operationBuildCache.trySetCacheEntryAsync(buildCacheTerminal, finalCacheId); } } } @@ -553,7 +553,7 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { return buildCacheContext; } - private _tryGetProjectBuildCache({ + private _tryGetOperationBuildCache({ buildCacheConfiguration, buildCacheContext, terminal, @@ -563,7 +563,7 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { buildCacheConfiguration: BuildCacheConfiguration | undefined; terminal: ITerminal; record: OperationExecutionRecord; - }): ProjectBuildCache | undefined { + }): OperationBuildCache | undefined { if (!buildCacheContext.operationBuildCache) { const { cacheDisabledReason } = buildCacheContext; if (cacheDisabledReason && !record.operation.settings?.allowCobuildWithoutCache) { @@ -577,7 +577,7 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { } // eslint-disable-next-line require-atomic-updates -- This is guaranteed to not be concurrent - buildCacheContext.operationBuildCache = ProjectBuildCache.forOperation(record, { + buildCacheContext.operationBuildCache = OperationBuildCache.forOperation(record, { buildCacheConfiguration, terminal }); @@ -586,14 +586,14 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { return buildCacheContext.operationBuildCache; } - // Get a ProjectBuildCache only cache/restore log files - private async _tryGetLogOnlyProjectBuildCacheAsync(options: { + // Get an OperationBuildCache only cache/restore log files + private async _tryGetLogOnlyOperationBuildCacheAsync(options: { buildCacheContext: IOperationBuildCacheContext; buildCacheConfiguration: BuildCacheConfiguration | undefined; cobuildConfiguration: CobuildConfiguration; record: IOperationRunnerContext & IOperationExecutionResult; terminal: ITerminal; - }): Promise { + }): Promise { const { buildCacheContext, buildCacheConfiguration, cobuildConfiguration, record, terminal } = options; if (!buildCacheConfiguration?.buildCacheEnabled) { @@ -617,7 +617,7 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { const { associatedPhase, associatedProject } = record.operation; - const projectBuildCache: ProjectBuildCache = ProjectBuildCache.getProjectBuildCache({ + const operationBuildCache: OperationBuildCache = OperationBuildCache.getOperationBuildCache({ project: associatedProject, projectOutputFolderNames: outputFolderNames, buildCacheConfiguration, @@ -627,33 +627,33 @@ export class CacheableOperationPlugin implements IPhasedCommandPlugin { }); // eslint-disable-next-line require-atomic-updates -- This is guaranteed to not be concurrent - buildCacheContext.operationBuildCache = projectBuildCache; + buildCacheContext.operationBuildCache = operationBuildCache; - return projectBuildCache; + return operationBuildCache; } private async _tryGetCobuildLockAsync({ cobuildConfiguration, buildCacheContext, - projectBuildCache, + operationBuildCache, packageName, phaseName }: { cobuildConfiguration: CobuildConfiguration | undefined; buildCacheContext: IOperationBuildCacheContext; - projectBuildCache: ProjectBuildCache | undefined; + operationBuildCache: OperationBuildCache | undefined; packageName: string; phaseName: string; }): Promise { if (!buildCacheContext.cobuildLock) { - if (projectBuildCache && cobuildConfiguration?.cobuildFeatureEnabled) { + if (operationBuildCache && cobuildConfiguration?.cobuildFeatureEnabled) { if (!buildCacheContext.cobuildClusterId) { // This should not happen throw new InternalError('Cobuild cluster id is not defined'); } buildCacheContext.cobuildLock = new CobuildLock({ cobuildConfiguration, - projectBuildCache, + operationBuildCache, cobuildClusterId: buildCacheContext.cobuildClusterId, lockExpireTimeInSeconds: PERIODIC_CALLBACK_INTERVAL_IN_SECONDS * 3, packageName, diff --git a/libraries/rush-lib/src/logic/operations/IOperationExecutionResult.ts b/libraries/rush-lib/src/logic/operations/IOperationExecutionResult.ts index f92f270585a..4db0993d37c 100644 --- a/libraries/rush-lib/src/logic/operations/IOperationExecutionResult.ts +++ b/libraries/rush-lib/src/logic/operations/IOperationExecutionResult.ts @@ -6,6 +6,8 @@ import type { OperationStatus } from './OperationStatus'; import type { Operation } from './Operation'; import type { IStopwatchResult } from '../../utilities/Stopwatch'; import type { ILogFilePaths } from './ProjectLogWritable'; +import type { IPhase } from '../../api/CommandLineConfiguration'; +import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; /** * The `IOperationExecutionResult` interface represents the results of executing an {@link Operation}. @@ -52,6 +54,14 @@ export interface IOperationExecutionResult { * The paths to the log files, if applicable. */ readonly logFilePaths: ILogFilePaths | undefined; + /** + * The Rush phase associated with this Operation. + */ + readonly associatedPhase: IPhase; + /** + * The Rush project associated with this Operation. + */ + readonly associatedProject: RushConfigurationProject; /** * Gets the hash of the state of all registered inputs to this operation. diff --git a/libraries/rush-lib/src/logic/operations/OperationExecutionRecord.ts b/libraries/rush-lib/src/logic/operations/OperationExecutionRecord.ts index f0111b28a15..b10da76af58 100644 --- a/libraries/rush-lib/src/logic/operations/OperationExecutionRecord.ts +++ b/libraries/rush-lib/src/logic/operations/OperationExecutionRecord.ts @@ -34,6 +34,9 @@ import { initializeProjectLogFilesAsync } from './ProjectLogWritable'; +/** + * @internal + */ export interface IOperationExecutionRecordContext { streamCollator: StreamCollator; onOperationStatusChanged?: (record: OperationExecutionRecord) => void; @@ -381,8 +384,8 @@ export class OperationExecutionRecord implements IOperationRunnerContext, IOpera this.status = this.operation.enabled ? await this.runner.executeAsync(this) : this.runner.isNoOp - ? OperationStatus.NoOp - : OperationStatus.Skipped; + ? OperationStatus.NoOp + : OperationStatus.Skipped; } // Make sure that the stopwatch is stopped before reporting the result, otherwise endTime is undefined. this.stopwatch.stop(); diff --git a/rush-plugins/rush-bridge-cache-plugin/README.md b/rush-plugins/rush-bridge-cache-plugin/README.md index 745d0cce784..e7e445de337 100644 --- a/rush-plugins/rush-bridge-cache-plugin/README.md +++ b/rush-plugins/rush-bridge-cache-plugin/README.md @@ -1,6 +1,6 @@ # @rushstack/rush-bridge-cache-plugin -This is a Rush plugin that lets you to add an optional `--set-cache-only` flag to Rush's phased commands to bypass the actual _action_ of the script (build, test, lint - whatever you have configured), and just populate the cache from the action as though the action had already been performed by Rush. +This is a Rush plugin that lets you to add an optional flag to Rush's phased commands to bypass the actual _action_ of the script (build, test, lint - whatever you have configured), and just populate the cache from the action as though the action had already been performed by Rush. The flag name is configurable. This is useful for integrations with other build orchestrators such as BuildXL. You can use those to do the work of actually running the task, then run the equivalent Rush command afterwards with a `--set-cache-only` to populate the Rush cache with whatever had been generated on disk, in addition to whatever cache mechanism is used by the other build orchestrator. @@ -19,26 +19,34 @@ This plugin assumes that the work for a particular task has already been complet "associatedCommands": ["build", "test", "lint", "a11y", "typecheck"], "description": "When the flag is added to any associated command, it'll bypass running the command itself, and cache whatever it finds on disk for the action. Beware! Only run when you know the build artifacts are in a valid state for the command.", "parameterKind": "flag", - "longName": "--set-cache-only", - "required": false + "longName": "--set-cache-only" } ``` 3. Add a new entry in `common/config/rush/rush-plugins` to register the new plugin: -``` +```json { "packageName": "@rushstack/rush-bridge-cache-plugin", "pluginName": "rush-bridge-cache-plugin", "autoinstallerName": "your-auto-installer-name-here" } ``` + +4. Create a configuration file for this plugin at this location: `common/config/rush-plugins/rush-bridge-cache-plugin.json` that defines the flag name you'll use to trigger the plugin: +```json +{ + "flagName": "--set-cache-only" +} +``` + ## Usage -Any of the rush command can now just be given a `--set-cache-only` property, e.g. +You can now add the flag to any Rush phased command, e.g. `rush build --to your-packageX --set-cache-only` -That will examine `your-packageX` and all of its dependencies, then populate the cache. +That will populate the cache for `your-packageX` and all of its dependencies. + ## Performance diff --git a/rush-plugins/rush-bridge-cache-plugin/rush-plugin-manifest.json b/rush-plugins/rush-bridge-cache-plugin/rush-plugin-manifest.json index 8475d026e87..c4e1818152b 100644 --- a/rush-plugins/rush-bridge-cache-plugin/rush-plugin-manifest.json +++ b/rush-plugins/rush-bridge-cache-plugin/rush-plugin-manifest.json @@ -4,7 +4,8 @@ { "pluginName": "rush-bridge-cache-plugin", "description": "Rush plugin that provides a --set-cache-only command flag to populate the cache from content on disk.", - "entryPoint": "./lib/index.js" + "entryPoint": "./lib/index.js", + "optionsSchema": "lib/schemas/bridge-cache-config.schema.json" } ] } diff --git a/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts b/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts index fd781ab848a..5853d008840 100644 --- a/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts +++ b/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { OperationBuildCache } from '@rushstack/rush-sdk'; +import { _OperationBuildCache as OperationBuildCache } from '@rushstack/rush-sdk'; import type { BuildCacheConfiguration, IExecuteOperationsContext, @@ -10,17 +10,24 @@ import type { IPhasedCommand, IRushPlugin, Operation, - OperationExecutionRecord, RushSession } from '@rushstack/rush-sdk'; const PLUGIN_NAME: 'RushBridgeCachePlugin' = 'RushBridgeCachePlugin'; +export interface IBridgeCachePluginOptions { + readonly flagName: string; +} export class BridgeCachePlugin implements IRushPlugin { public readonly pluginName: string = PLUGIN_NAME; + private readonly _flagName: string; + + public constructor(options: IBridgeCachePluginOptions) { + this._flagName = options.flagName; + } public apply(session: RushSession): void { - const isSetCacheOnly: boolean = process.argv.includes('--set-cache-only'); + const isSetCacheOnly: boolean = process.argv.includes(this._flagName); if (!isSetCacheOnly) { return; } @@ -33,20 +40,10 @@ export class BridgeCachePlugin implements IRushPlugin { }; session.hooks.runAnyPhasedCommand.tapPromise(PLUGIN_NAME, async (command: IPhasedCommand) => { - // tracks the projects being targeted by the command (--to, --only etc.) - const targetProjects: Set = new Set(); - // cancel the actual operations. We don't want to run the command, just cache the output folders on disk command.hooks.createOperations.tap( { name: PLUGIN_NAME, stage: Number.MAX_SAFE_INTEGER }, - (operations: Set): Set => { - operations.forEach((operation: Operation) => { - if (operation.enabled) { - targetProjects.add(operation); - } - }); - return cancelOperations(operations); - } + cancelOperations ); // populate the cache for each operation @@ -60,12 +57,7 @@ export class BridgeCachePlugin implements IRushPlugin { return; } - await this._setCacheAsync( - session, - context.buildCacheConfiguration, - recordByOperation, - targetProjects - ); + await this._setCacheAsync(session, context.buildCacheConfiguration, recordByOperation); } ); }); @@ -74,8 +66,7 @@ export class BridgeCachePlugin implements IRushPlugin { private async _setCacheAsync( session: RushSession, buildCacheConfiguration: BuildCacheConfiguration, - recordByOperation: Map, - targetProjects: Set + recordByOperation: Map ): Promise { const logger: ILogger = session.getLogger(PLUGIN_NAME); @@ -83,14 +74,12 @@ export class BridgeCachePlugin implements IRushPlugin { async (operationExecutionResult: IOperationExecutionResult, operation: Operation) => { const { associatedProject, associatedPhase } = operation; - // omit operations that aren't targeted, or packages without a command for this phase - const hasCommand: boolean = !!associatedProject.packageJson.scripts?.[associatedPhase.name]; - if (!targetProjects.has(operation) || !hasCommand) { + if (operation.isNoOp) { return; } const projectBuildCache: OperationBuildCache = OperationBuildCache.forOperation( - operationExecutionResult as OperationExecutionRecord, + operationExecutionResult, { buildCacheConfiguration, terminal: logger.terminal @@ -107,7 +96,6 @@ export class BridgeCachePlugin implements IRushPlugin { logger.terminal.writeErrorLine( `Error creating a cache entry set for ${associatedPhase.name} (${associatedProject.packageName}) from previously generated output folders\n` ); - process.exit(1); } } ); diff --git a/rush-plugins/rush-bridge-cache-plugin/src/schemas/bridge-cache-config.schema.json b/rush-plugins/rush-bridge-cache-plugin/src/schemas/bridge-cache-config.schema.json new file mode 100644 index 00000000000..ca46faa8f89 --- /dev/null +++ b/rush-plugins/rush-bridge-cache-plugin/src/schemas/bridge-cache-config.schema.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Configuration for bridge cache plugin", + "type": "object", + "oneOf": [ + { + "type": "object", + "required": ["flagName"], + "properties": { + "s3Endpoint": { + "type": "string", + "description": "(Required) The name of the flag used to trigger this plugin on your phased commands." + } + } + } + ] +} From dcfd45308c43c65e7d0726b102afa63ca3a2eed2 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Tue, 3 Jun 2025 12:56:14 -0600 Subject: [PATCH 05/12] Move devDeps to deps --- common/config/subspaces/default/pnpm-lock.yaml | 2 +- rush-plugins/rush-bridge-cache-plugin/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 18478480e4c..7dd8ea279c2 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -4015,7 +4015,7 @@ importers: version: link:../../rigs/local-node-rig ../../../rush-plugins/rush-bridge-cache-plugin: - devDependencies: + dependencies: '@rushstack/heft': specifier: workspace:* version: link:../../apps/heft diff --git a/rush-plugins/rush-bridge-cache-plugin/package.json b/rush-plugins/rush-bridge-cache-plugin/package.json index 6fdb7a4afca..731938ae3e9 100644 --- a/rush-plugins/rush-bridge-cache-plugin/package.json +++ b/rush-plugins/rush-bridge-cache-plugin/package.json @@ -14,7 +14,7 @@ "build": "heft test --clean", "_phase:build": "heft run --only build -- --clean" }, - "devDependencies": { + "dependencies": { "@rushstack/node-core-library": "workspace:*", "@rushstack/rush-sdk": "workspace:*", "@rushstack/terminal": "workspace:*", From cecce1577eeca9a30ffe1804c643a25a45619ee7 Mon Sep 17 00:00:00 2001 From: Ben Keen Date: Tue, 3 Jun 2025 14:42:08 -0600 Subject: [PATCH 06/12] Code review feedback --- .../config/subspaces/default/pnpm-lock.yaml | 3 ++ common/reviews/api/rush-lib.api.md | 4 +-- .../logic/buildCache/OperationBuildCache.ts | 14 +++++----- .../operations/IOperationExecutionResult.ts | 10 ------- .../rush-bridge-cache-plugin/package.json | 1 + .../src/BridgeCachePlugin.ts | 28 +++++++++++++++---- 6 files changed, 35 insertions(+), 25 deletions(-) diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 7dd8ea279c2..215ddb8db00 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -4028,6 +4028,9 @@ importers: '@rushstack/terminal': specifier: workspace:* version: link:../../libraries/terminal + '@rushstack/ts-command-line': + specifier: workspace:* + version: link:../../libraries/ts-command-line local-node-rig: specifier: workspace:* version: link:../../rigs/local-node-rig diff --git a/common/reviews/api/rush-lib.api.md b/common/reviews/api/rush-lib.api.md index 533d4f2ade4..1d12b07f21a 100644 --- a/common/reviews/api/rush-lib.api.md +++ b/common/reviews/api/rush-lib.api.md @@ -591,8 +591,6 @@ export interface _IOperationBuildCacheOptions { // @alpha export interface IOperationExecutionResult { - readonly associatedPhase: IPhase; - readonly associatedProject: RushConfigurationProject; readonly error: Error | undefined; getStateHash(): string; getStateHashComponents(): ReadonlyArray; @@ -968,7 +966,7 @@ export class _OperationBuildCache { // (undocumented) get cacheId(): string | undefined; // (undocumented) - static forOperation(operation: IOperationExecutionResult, options: _IOperationBuildCacheOptions): _OperationBuildCache; + static forOperation(executionResult: IOperationExecutionResult, options: _IOperationBuildCacheOptions): _OperationBuildCache; // (undocumented) static getOperationBuildCache(options: _IProjectBuildCacheOptions): _OperationBuildCache; // (undocumented) diff --git a/libraries/rush-lib/src/logic/buildCache/OperationBuildCache.ts b/libraries/rush-lib/src/logic/buildCache/OperationBuildCache.ts index fe74a5312fa..d603c094547 100644 --- a/libraries/rush-lib/src/logic/buildCache/OperationBuildCache.ts +++ b/libraries/rush-lib/src/logic/buildCache/OperationBuildCache.ts @@ -108,20 +108,20 @@ export class OperationBuildCache { } public static forOperation( - operation: IOperationExecutionResult, + executionResult: IOperationExecutionResult, options: IOperationBuildCacheOptions ): OperationBuildCache { - const outputFolders: string[] = [...(operation.operation.settings?.outputFolderNames ?? [])]; - if (operation.metadataFolderPath) { - outputFolders.push(operation.metadataFolderPath); + const outputFolders: string[] = [...(executionResult.operation.settings?.outputFolderNames ?? [])]; + if (executionResult.metadataFolderPath) { + outputFolders.push(executionResult.metadataFolderPath); } const buildCacheOptions: IProjectBuildCacheOptions = { buildCacheConfiguration: options.buildCacheConfiguration, terminal: options.terminal, - project: operation.associatedProject, - phaseName: operation.associatedPhase.name, + project: executionResult.operation.associatedProject, + phaseName: executionResult.operation.associatedPhase.name, projectOutputFolderNames: outputFolders, - operationStateHash: operation.getStateHash() + operationStateHash: executionResult.getStateHash() }; const cacheId: string | undefined = OperationBuildCache._getCacheId(buildCacheOptions); return new OperationBuildCache(cacheId, buildCacheOptions); diff --git a/libraries/rush-lib/src/logic/operations/IOperationExecutionResult.ts b/libraries/rush-lib/src/logic/operations/IOperationExecutionResult.ts index 4db0993d37c..f92f270585a 100644 --- a/libraries/rush-lib/src/logic/operations/IOperationExecutionResult.ts +++ b/libraries/rush-lib/src/logic/operations/IOperationExecutionResult.ts @@ -6,8 +6,6 @@ import type { OperationStatus } from './OperationStatus'; import type { Operation } from './Operation'; import type { IStopwatchResult } from '../../utilities/Stopwatch'; import type { ILogFilePaths } from './ProjectLogWritable'; -import type { IPhase } from '../../api/CommandLineConfiguration'; -import type { RushConfigurationProject } from '../../api/RushConfigurationProject'; /** * The `IOperationExecutionResult` interface represents the results of executing an {@link Operation}. @@ -54,14 +52,6 @@ export interface IOperationExecutionResult { * The paths to the log files, if applicable. */ readonly logFilePaths: ILogFilePaths | undefined; - /** - * The Rush phase associated with this Operation. - */ - readonly associatedPhase: IPhase; - /** - * The Rush project associated with this Operation. - */ - readonly associatedProject: RushConfigurationProject; /** * Gets the hash of the state of all registered inputs to this operation. diff --git a/rush-plugins/rush-bridge-cache-plugin/package.json b/rush-plugins/rush-bridge-cache-plugin/package.json index 731938ae3e9..326366a3ef4 100644 --- a/rush-plugins/rush-bridge-cache-plugin/package.json +++ b/rush-plugins/rush-bridge-cache-plugin/package.json @@ -15,6 +15,7 @@ "_phase:build": "heft run --only build -- --clean" }, "dependencies": { + "@rushstack/ts-command-line": "workspace:*", "@rushstack/node-core-library": "workspace:*", "@rushstack/rush-sdk": "workspace:*", "@rushstack/terminal": "workspace:*", diff --git a/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts b/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts index 5853d008840..ff2e21c173f 100644 --- a/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts +++ b/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts @@ -4,6 +4,7 @@ import { _OperationBuildCache as OperationBuildCache } from '@rushstack/rush-sdk'; import type { BuildCacheConfiguration, + ICreateOperationsContext, IExecuteOperationsContext, ILogger, IOperationExecutionResult, @@ -12,6 +13,8 @@ import type { Operation, RushSession } from '@rushstack/rush-sdk'; +import { CommandLineParameterKind } from '@rushstack/ts-command-line'; +import type { CommandLineParameter } from '@rushstack/ts-command-line'; const PLUGIN_NAME: 'RushBridgeCachePlugin' = 'RushBridgeCachePlugin'; @@ -24,15 +27,23 @@ export class BridgeCachePlugin implements IRushPlugin { public constructor(options: IBridgeCachePluginOptions) { this._flagName = options.flagName; + + if (!this._flagName) { + throw new Error('The "flagName" option must be provided for the BridgeCachePlugin. Please see the plugin README for details.'); + } } public apply(session: RushSession): void { - const isSetCacheOnly: boolean = process.argv.includes(this._flagName); - if (!isSetCacheOnly) { - return; - } + const cancelOperations = ( + operations: Set, + context: ICreateOperationsContext + ): Set => { + + const flagParam: CommandLineParameter | undefined = context.customParameters.get(this._flagName); + if (!flagParam || flagParam.kind !== CommandLineParameterKind.Flag || !flagParam.value) { + return operations; + } - const cancelOperations = (operations: Set): Set => { operations.forEach((operation: Operation) => { operation.enabled = false; }); @@ -40,6 +51,7 @@ export class BridgeCachePlugin implements IRushPlugin { }; session.hooks.runAnyPhasedCommand.tapPromise(PLUGIN_NAME, async (command: IPhasedCommand) => { + // cancel the actual operations. We don't want to run the command, just cache the output folders on disk command.hooks.createOperations.tap( { name: PLUGIN_NAME, stage: Number.MAX_SAFE_INTEGER }, @@ -53,10 +65,16 @@ export class BridgeCachePlugin implements IRushPlugin { recordByOperation: Map, context: IExecuteOperationsContext ): Promise => { + if (!context.buildCacheConfiguration) { return; } + const flagParam: CommandLineParameter | undefined = context.customParameters.get(this._flagName); + if (!flagParam || flagParam.kind !== CommandLineParameterKind.Flag || !flagParam.value) { + return; + } + await this._setCacheAsync(session, context.buildCacheConfiguration, recordByOperation); } ); From b95afb8cc24ba04775ab1f37490b00534c49e91d Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 9 Jun 2025 22:40:04 -0400 Subject: [PATCH 07/12] Fix some dev dependencies. --- rush-plugins/rush-bridge-cache-plugin/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/rush-plugins/rush-bridge-cache-plugin/package.json b/rush-plugins/rush-bridge-cache-plugin/package.json index 326366a3ef4..4cbc2c9bbf4 100644 --- a/rush-plugins/rush-bridge-cache-plugin/package.json +++ b/rush-plugins/rush-bridge-cache-plugin/package.json @@ -19,6 +19,7 @@ "@rushstack/node-core-library": "workspace:*", "@rushstack/rush-sdk": "workspace:*", "@rushstack/terminal": "workspace:*", + "devDependencies": { "@rushstack/heft": "workspace:*", "local-node-rig": "workspace:*" } From c03be3286db9f79427c97452cfc1cc0689bc0ded Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 9 Jun 2025 22:40:26 -0400 Subject: [PATCH 08/12] fixup! Fix some dev dependencies. --- rush-plugins/rush-bridge-cache-plugin/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rush-plugins/rush-bridge-cache-plugin/package.json b/rush-plugins/rush-bridge-cache-plugin/package.json index 4cbc2c9bbf4..ec25c13e8c3 100644 --- a/rush-plugins/rush-bridge-cache-plugin/package.json +++ b/rush-plugins/rush-bridge-cache-plugin/package.json @@ -18,7 +18,8 @@ "@rushstack/ts-command-line": "workspace:*", "@rushstack/node-core-library": "workspace:*", "@rushstack/rush-sdk": "workspace:*", - "@rushstack/terminal": "workspace:*", + "@rushstack/terminal": "workspace:*" + }, "devDependencies": { "@rushstack/heft": "workspace:*", "local-node-rig": "workspace:*" From 0b2abe82e86e24740ae878de5e20ad9ebdab1a95 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 9 Jun 2025 22:53:23 -0400 Subject: [PATCH 09/12] Rush update. --- .../build-tests-subspace/pnpm-lock.yaml | 79 +++++++++++++++---- .../build-tests-subspace/repo-state.json | 2 +- .../config/subspaces/default/pnpm-lock.yaml | 15 +++- .../config/subspaces/default/repo-state.json | 2 +- 4 files changed, 76 insertions(+), 22 deletions(-) diff --git a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml index 52e24abbcc7..b8d783e01eb 100644 --- a/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml +++ b/common/config/subspaces/build-tests-subspace/pnpm-lock.yaml @@ -114,10 +114,10 @@ importers: version: file:../../../apps/heft(@types/node@20.17.19) '@rushstack/heft-lint-plugin': specifier: file:../../heft-plugins/heft-lint-plugin - version: file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@0.73.2)(@types/node@20.17.19) + version: file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@0.73.6)(@types/node@20.17.19) '@rushstack/heft-typescript-plugin': specifier: file:../../heft-plugins/heft-typescript-plugin - version: file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@0.73.2)(@types/node@20.17.19) + version: file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@0.73.6)(@types/node@20.17.19) eslint: specifier: ~8.57.0 version: 8.57.1 @@ -877,10 +877,30 @@ packages: '@pnpm/crypto.polyfill': 1.0.0 rfc4648: 1.5.3 + /@pnpm/crypto.hash@1000.1.1: + resolution: {integrity: sha512-lb5kwXaOXdIW/4bkLLmtM9HEVRvp2eIvp+TrdawcPoaptgA/5f0/sRG0P52BF8dFqeNDj+1tGdqH89WQEqJnxA==} + engines: {node: '>=18.12'} + dependencies: + '@pnpm/crypto.polyfill': 1000.1.0 + '@pnpm/graceful-fs': 1000.0.0 + ssri: 10.0.5 + /@pnpm/crypto.polyfill@1.0.0: resolution: {integrity: sha512-WbmsqqcUXKKaAF77ox1TQbpZiaQcr26myuMUu+WjUtoWYgD3VP6iKYEvSx35SZ6G2L316lu+pv+40A2GbWJc1w==} engines: {node: '>=18.12'} + /@pnpm/crypto.polyfill@1000.1.0: + resolution: {integrity: sha512-tNe7a6U4rCpxLMBaR0SIYTdjxGdL0Vwb3G1zY8++sPtHSvy7qd54u8CIB0Z+Y6t5tc9pNYMYCMwhE/wdSY7ltg==} + engines: {node: '>=18.12'} + + /@pnpm/dependency-path@1000.0.9: + resolution: {integrity: sha512-0AhabApfiq3EEYeed5HKQEU3ftkrfyKTNgkMH9esGdp2yc+62Zu7eWFf8WW6IGyitDQPLWGYjSEWDC9Bvv8nPg==} + engines: {node: '>=18.12'} + dependencies: + '@pnpm/crypto.hash': 1000.1.1 + '@pnpm/types': 1000.6.0 + semver: 7.7.2 + /@pnpm/dependency-path@2.1.8: resolution: {integrity: sha512-ywBaTjy0iSEF7lH3DlF8UXrdL2bw4AQFV2tTOeNeY7wc1W5CE+RHSJhf9MXBYcZPesqGRrPiU7Pimj3l05L9VA==} engines: {node: '>=16.14'} @@ -888,7 +908,7 @@ packages: '@pnpm/crypto.base32-hash': 2.0.0 '@pnpm/types': 9.4.2 encode-registry: 3.0.1 - semver: 7.6.3 + semver: 7.7.2 /@pnpm/dependency-path@5.1.7: resolution: {integrity: sha512-MKCyaTy1r9fhBXAnhDZNBVgo6ThPnicwJEG203FDp7pGhD7NruS/FhBI+uMd7GNsK3D7aIFCDAgbWpNTXn/eWw==} @@ -896,12 +916,18 @@ packages: dependencies: '@pnpm/crypto.base32-hash': 3.0.1 '@pnpm/types': 12.2.0 - semver: 7.6.3 + semver: 7.7.2 /@pnpm/error@1.4.0: resolution: {integrity: sha512-vxkRrkneBPVmP23kyjnYwVOtipwlSl6UfL+h+Xa3TrABJTz5rYBXemlTsU5BzST8U4pD7YDkTb3SQu+MMuIDKA==} engines: {node: '>=10.16'} + /@pnpm/graceful-fs@1000.0.0: + resolution: {integrity: sha512-RvMEliAmcfd/4UoaYQ93DLQcFeqit78jhYmeJJVPxqFGmj0jEcb9Tu0eAOXr7tGP3eJHpgvPbTU4o6pZ1bJhxg==} + engines: {node: '>=18.12'} + dependencies: + graceful-fs: 4.2.11 + /@pnpm/link-bins@5.3.25: resolution: {integrity: sha512-9Xq8lLNRHFDqvYPXPgaiKkZ4rtdsm7izwM/cUsFDc5IMnG0QYIVBXQbgwhz2UvjUotbJrvfKLJaCfA3NGBnLDg==} engines: {node: '>=10.16'} @@ -973,6 +999,10 @@ packages: sort-keys: 4.2.0 strip-bom: 4.0.0 + /@pnpm/types@1000.6.0: + resolution: {integrity: sha512-6PsMNe98VKPGcg6LnXSW/LE3YfJ77nj+bPKiRjYRWAQLZ+xXjEQRaR0dAuyjCmchlv4wR/hpnMVRS21/fCod5w==} + engines: {node: '>=18.12'} + /@pnpm/types@12.2.0: resolution: {integrity: sha512-5RtwWhX39j89/Tmyv2QSlpiNjErA357T/8r1Dkg+2lD3P7RuS7Xi2tChvmOC3VlezEFNcWnEGCOeKoGRkDuqFA==} engines: {node: '>=18.12'} @@ -4321,7 +4351,7 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} dependencies: - semver: 7.6.3 + semver: 7.7.2 dev: true /makeerror@1.0.12: @@ -4453,6 +4483,10 @@ packages: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} + /minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} @@ -5254,6 +5288,11 @@ packages: engines: {node: '>=10'} hasBin: true + /semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + /set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -5350,6 +5389,12 @@ packages: /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + /ssri@10.0.5: + resolution: {integrity: sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dependencies: + minipass: 7.1.2 + /ssri@8.0.1: resolution: {integrity: sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==} engines: {node: '>= 8'} @@ -6238,7 +6283,7 @@ packages: - typescript dev: true - file:../../../heft-plugins/heft-api-extractor-plugin(@rushstack/heft@0.73.2)(@types/node@20.17.19): + file:../../../heft-plugins/heft-api-extractor-plugin(@rushstack/heft@0.73.6)(@types/node@20.17.19): resolution: {directory: ../../../heft-plugins/heft-api-extractor-plugin, type: directory} id: file:../../../heft-plugins/heft-api-extractor-plugin name: '@rushstack/heft-api-extractor-plugin' @@ -6252,7 +6297,7 @@ packages: - '@types/node' dev: true - file:../../../heft-plugins/heft-jest-plugin(@rushstack/heft@0.73.2)(@types/node@20.17.19)(jest-environment-node@29.5.0): + file:../../../heft-plugins/heft-jest-plugin(@rushstack/heft@0.73.6)(@types/node@20.17.19)(jest-environment-node@29.5.0): resolution: {directory: ../../../heft-plugins/heft-jest-plugin, type: directory} id: file:../../../heft-plugins/heft-jest-plugin name: '@rushstack/heft-jest-plugin' @@ -6287,7 +6332,7 @@ packages: - ts-node dev: true - file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@0.73.2)(@types/node@20.17.19): + file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@0.73.6)(@types/node@20.17.19): resolution: {directory: ../../../heft-plugins/heft-lint-plugin, type: directory} id: file:../../../heft-plugins/heft-lint-plugin name: '@rushstack/heft-lint-plugin' @@ -6297,11 +6342,12 @@ packages: '@rushstack/heft': file:../../../apps/heft(@types/node@20.17.19) '@rushstack/node-core-library': file:../../../libraries/node-core-library(@types/node@20.17.19) semver: 7.5.4 + typescript: 5.8.2 transitivePeerDependencies: - '@types/node' dev: true - file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@0.73.2)(@types/node@20.17.19): + file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@0.73.6)(@types/node@20.17.19): resolution: {directory: ../../../heft-plugins/heft-typescript-plugin, type: directory} id: file:../../../heft-plugins/heft-typescript-plugin name: '@rushstack/heft-typescript-plugin' @@ -6430,7 +6476,8 @@ packages: name: '@microsoft/rush-lib' engines: {node: '>=5.6.0'} dependencies: - '@pnpm/dependency-path': 5.1.7 + '@pnpm/dependency-path': 1000.0.9 + '@pnpm/dependency-path-lockfile-pre-v10': /@pnpm/dependency-path@5.1.7 '@pnpm/dependency-path-lockfile-pre-v9': /@pnpm/dependency-path@2.1.8 '@pnpm/link-bins': 5.3.25 '@rushstack/heft-config-file': file:../../../libraries/heft-config-file(@types/node@20.17.19) @@ -6526,7 +6573,7 @@ packages: transitivePeerDependencies: - '@types/node' - file:../../../rigs/heft-node-rig(@rushstack/heft@0.73.2)(@types/node@20.17.19): + file:../../../rigs/heft-node-rig(@rushstack/heft@0.73.6)(@types/node@20.17.19): resolution: {directory: ../../../rigs/heft-node-rig, type: directory} id: file:../../../rigs/heft-node-rig name: '@rushstack/heft-node-rig' @@ -6536,10 +6583,10 @@ packages: '@microsoft/api-extractor': file:../../../apps/api-extractor(@types/node@20.17.19) '@rushstack/eslint-config': file:../../../eslint/eslint-config(eslint@8.57.1)(typescript@5.8.2) '@rushstack/heft': file:../../../apps/heft(@types/node@20.17.19) - '@rushstack/heft-api-extractor-plugin': file:../../../heft-plugins/heft-api-extractor-plugin(@rushstack/heft@0.73.2)(@types/node@20.17.19) - '@rushstack/heft-jest-plugin': file:../../../heft-plugins/heft-jest-plugin(@rushstack/heft@0.73.2)(@types/node@20.17.19)(jest-environment-node@29.5.0) - '@rushstack/heft-lint-plugin': file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@0.73.2)(@types/node@20.17.19) - '@rushstack/heft-typescript-plugin': file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@0.73.2)(@types/node@20.17.19) + '@rushstack/heft-api-extractor-plugin': file:../../../heft-plugins/heft-api-extractor-plugin(@rushstack/heft@0.73.6)(@types/node@20.17.19) + '@rushstack/heft-jest-plugin': file:../../../heft-plugins/heft-jest-plugin(@rushstack/heft@0.73.6)(@types/node@20.17.19)(jest-environment-node@29.5.0) + '@rushstack/heft-lint-plugin': file:../../../heft-plugins/heft-lint-plugin(@rushstack/heft@0.73.6)(@types/node@20.17.19) + '@rushstack/heft-typescript-plugin': file:../../../heft-plugins/heft-typescript-plugin(@rushstack/heft@0.73.6)(@types/node@20.17.19) '@types/heft-jest': 1.0.1 eslint: 8.57.1 jest-environment-node: 29.5.0 @@ -6559,7 +6606,7 @@ packages: dependencies: '@microsoft/api-extractor': file:../../../apps/api-extractor(@types/node@20.17.19) '@rushstack/heft': file:../../../apps/heft(@types/node@20.17.19) - '@rushstack/heft-node-rig': file:../../../rigs/heft-node-rig(@rushstack/heft@0.73.2)(@types/node@20.17.19) + '@rushstack/heft-node-rig': file:../../../rigs/heft-node-rig(@rushstack/heft@0.73.6)(@types/node@20.17.19) '@types/heft-jest': 1.0.1 '@types/node': 20.17.19 eslint: 8.57.1 diff --git a/common/config/subspaces/build-tests-subspace/repo-state.json b/common/config/subspaces/build-tests-subspace/repo-state.json index 72bbe58dbe0..bfa4cc388a9 100644 --- a/common/config/subspaces/build-tests-subspace/repo-state.json +++ b/common/config/subspaces/build-tests-subspace/repo-state.json @@ -1,6 +1,6 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "e47112c7d099f189f37770e10351e406cf1ec451", + "pnpmShrinkwrapHash": "c7ba4d11d03d9e1b14ba33e023a043d385fa3fd8", "preferredVersionsHash": "54149ea3f01558a859c96dee2052b797d4defe68", "packageJsonInjectedDependenciesHash": "8aab06634f5544193a51484804f7d7c4fc2ad986" } diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 215ddb8db00..7368113a2d2 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -4016,9 +4016,6 @@ importers: ../../../rush-plugins/rush-bridge-cache-plugin: dependencies: - '@rushstack/heft': - specifier: workspace:* - version: link:../../apps/heft '@rushstack/node-core-library': specifier: workspace:* version: link:../../libraries/node-core-library @@ -4031,6 +4028,10 @@ importers: '@rushstack/ts-command-line': specifier: workspace:* version: link:../../libraries/ts-command-line + devDependencies: + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft local-node-rig: specifier: workspace:* version: link:../../rigs/local-node-rig @@ -9814,7 +9815,7 @@ packages: dependencies: '@pnpm/crypto.base32-hash': 3.0.1 '@pnpm/types': 12.2.0 - semver: 7.6.3 + semver: 7.7.2 dev: false /@pnpm/error@1.4.0: @@ -26263,6 +26264,12 @@ packages: engines: {node: '>=10'} hasBin: true + /semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + dev: false + /send@0.17.2: resolution: {integrity: sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==} engines: {node: '>= 0.8.0'} diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index bf53d6e721b..86c6e6d4c8d 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "cffd2b8ab4cceebd7d5a02823e015b7eaabe89da", + "pnpmShrinkwrapHash": "a27c324699bd78190141ee2de5781a2fd92c49d2", "preferredVersionsHash": "54149ea3f01558a859c96dee2052b797d4defe68" } From bd123fecc88684bfcc048fe14461915c1f01231e Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 9 Jun 2025 22:54:19 -0400 Subject: [PATCH 10/12] Minor refactors to BridgeCachePlugin. --- .../src/BridgeCachePlugin.ts | 93 +++++++++++-------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts b/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts index ff2e21c173f..158fbb3f77e 100644 --- a/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts +++ b/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. +import { Async } from '@rushstack/node-core-library'; import { _OperationBuildCache as OperationBuildCache } from '@rushstack/rush-sdk'; import type { BuildCacheConfiguration, @@ -21,6 +22,7 @@ const PLUGIN_NAME: 'RushBridgeCachePlugin' = 'RushBridgeCachePlugin'; export interface IBridgeCachePluginOptions { readonly flagName: string; } + export class BridgeCachePlugin implements IRushPlugin { public readonly pluginName: string = PLUGIN_NAME; private readonly _flagName: string; @@ -29,33 +31,29 @@ export class BridgeCachePlugin implements IRushPlugin { this._flagName = options.flagName; if (!this._flagName) { - throw new Error('The "flagName" option must be provided for the BridgeCachePlugin. Please see the plugin README for details.'); + throw new Error( + 'The "flagName" option must be provided for the BridgeCachePlugin. Please see the plugin README for details.' + ); } } public apply(session: RushSession): void { - const cancelOperations = ( - operations: Set, - context: ICreateOperationsContext - ): Set => { - - const flagParam: CommandLineParameter | undefined = context.customParameters.get(this._flagName); - if (!flagParam || flagParam.kind !== CommandLineParameterKind.Flag || !flagParam.value) { - return operations; - } - - operations.forEach((operation: Operation) => { - operation.enabled = false; - }); - return operations; - }; - session.hooks.runAnyPhasedCommand.tapPromise(PLUGIN_NAME, async (command: IPhasedCommand) => { + const logger: ILogger = session.getLogger(PLUGIN_NAME); // cancel the actual operations. We don't want to run the command, just cache the output folders on disk command.hooks.createOperations.tap( { name: PLUGIN_NAME, stage: Number.MAX_SAFE_INTEGER }, - cancelOperations + (operations: Set, context: ICreateOperationsContext): Set => { + const flagValue: boolean = this._getFlagValue(context); + if (flagValue) { + for (const operation of operations) { + operation.enabled = false; + } + } + + return operations; + } ); // populate the cache for each operation @@ -65,34 +63,50 @@ export class BridgeCachePlugin implements IRushPlugin { recordByOperation: Map, context: IExecuteOperationsContext ): Promise => { - if (!context.buildCacheConfiguration) { return; } - const flagParam: CommandLineParameter | undefined = context.customParameters.get(this._flagName); - if (!flagParam || flagParam.kind !== CommandLineParameterKind.Flag || !flagParam.value) { - return; + const flagValue: boolean = this._getFlagValue(context); + if (flagValue) { + await this._setCacheAsync(logger, context.buildCacheConfiguration, recordByOperation); } - - await this._setCacheAsync(session, context.buildCacheConfiguration, recordByOperation); } ); }); } + private _getFlagValue(context: IExecuteOperationsContext): boolean { + const flagParam: CommandLineParameter | undefined = context.customParameters.get(this._flagName); + if (flagParam) { + if (flagParam.kind !== CommandLineParameterKind.Flag) { + throw new Error( + `The parameter "${this._flagName}" must be a flag. Please check the plugin configuration.` + ); + } + + return flagParam.value; + } + + return false; + } + private async _setCacheAsync( - session: RushSession, + { terminal }: ILogger, buildCacheConfiguration: BuildCacheConfiguration, recordByOperation: Map ): Promise { - const logger: ILogger = session.getLogger(PLUGIN_NAME); - - recordByOperation.forEach( - async (operationExecutionResult: IOperationExecutionResult, operation: Operation) => { - const { associatedProject, associatedPhase } = operation; - - if (operation.isNoOp) { + Async.forEachAsync( + recordByOperation, + async ([ + { + associatedProject: { packageName }, + associatedPhase: { name: phaseName }, + isNoOp + }, + operationExecutionResult + ]) => { + if (isNoOp) { return; } @@ -100,22 +114,23 @@ export class BridgeCachePlugin implements IRushPlugin { operationExecutionResult, { buildCacheConfiguration, - terminal: logger.terminal + terminal } ); - const success: boolean = await projectBuildCache.trySetCacheEntryAsync(logger.terminal); + const success: boolean = await projectBuildCache.trySetCacheEntryAsync(terminal); if (success) { - logger.terminal.writeLine( - `Cache entry set for ${associatedPhase.name} (${associatedProject.packageName}) from previously generated output folders\n` + terminal.writeLine( + `Cache entry set for ${phaseName} (${packageName}) from previously generated output folders` ); } else { - logger.terminal.writeErrorLine( - `Error creating a cache entry set for ${associatedPhase.name} (${associatedProject.packageName}) from previously generated output folders\n` + terminal.writeErrorLine( + `Error creating a cache entry set for ${phaseName} (${packageName}) from previously generated output folders` ); } - } + }, + { concurrency: 5 } ); } } From 829335f9c5ce07b9d8a33d0c58bf356132b3fb0c Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 9 Jun 2025 22:59:45 -0400 Subject: [PATCH 11/12] Rush change. --- .../benkeen-cache-bridge-plugin_2025-06-10-02-59.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 common/changes/@rushstack/mcp-server/benkeen-cache-bridge-plugin_2025-06-10-02-59.json diff --git a/common/changes/@rushstack/mcp-server/benkeen-cache-bridge-plugin_2025-06-10-02-59.json b/common/changes/@rushstack/mcp-server/benkeen-cache-bridge-plugin_2025-06-10-02-59.json new file mode 100644 index 00000000000..827fb92cfee --- /dev/null +++ b/common/changes/@rushstack/mcp-server/benkeen-cache-bridge-plugin_2025-06-10-02-59.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/mcp-server", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/mcp-server" +} \ No newline at end of file From f5eae74803ed0b5aff063b5cae68b5782e2ef9a3 Mon Sep 17 00:00:00 2001 From: Ian Clanton-Thuon Date: Mon, 9 Jun 2025 23:26:36 -0400 Subject: [PATCH 12/12] fixup! Minor refactors to BridgeCachePlugin. --- rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts b/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts index 158fbb3f77e..fa93ecf46d0 100644 --- a/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts +++ b/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts @@ -96,7 +96,7 @@ export class BridgeCachePlugin implements IRushPlugin { buildCacheConfiguration: BuildCacheConfiguration, recordByOperation: Map ): Promise { - Async.forEachAsync( + await Async.forEachAsync( recordByOperation, async ([ {