diff --git a/common/changes/@microsoft/rush/benkeen-bridge-cache-safety-check_2025-07-16-16-53.json b/common/changes/@microsoft/rush/benkeen-bridge-cache-safety-check_2025-07-16-16-53.json new file mode 100644 index 00000000000..08baea98f88 --- /dev/null +++ b/common/changes/@microsoft/rush/benkeen-bridge-cache-safety-check_2025-07-16-16-53.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft/rush", + "comment": "Adds an optional safety check flag to the Bridge Cache plugin write action.", + "type": "none" + } + ], + "packageName": "@microsoft/rush" +} \ No newline at end of file diff --git a/rush-plugins/rush-bridge-cache-plugin/README.md b/rush-plugins/rush-bridge-cache-plugin/README.md index bc2ba2f975f..8643f2db143 100644 --- a/rush-plugins/rush-bridge-cache-plugin/README.md +++ b/rush-plugins/rush-bridge-cache-plugin/README.md @@ -8,7 +8,7 @@ Alternatively, the `--bridge-cache-action=read` parameter is useful for tasks su ## Here be dragons! -The `write` action for 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! +The `write` action for 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! See the optional `requireOutputFoldersParameterName` setting below to include a safety check to require all expected output folders for a command to actually be on disk. The `read` action for this plugin makes no guarantee that the requested operations will have their outputs restored and is purely a best-effort. @@ -34,6 +34,14 @@ The `read` action for this plugin makes no guarantee that the requested operatio "description": "When specified for any associated command, bypass running the command itself, and cache whatever outputs exist in the output folders as-is. Beware! Only run when you know the build artifacts are in a valid state for the command." } ] +}, + +// optional +{ + "associatedCommands": ["build", "test", "lint", "a11y", "typecheck"], + "description": "Optional flag that can be used in combination with --bridge-cache-action=write. When used, this will only populate a cache entry when all defined output folders for a command are present on disk.", + "parameterKind": "flag", + "longName": "--require-output-folders", } ``` @@ -47,15 +55,20 @@ The `read` action for this plugin makes no guarantee that the requested operatio ``` 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 { - "actionParameterName": "--bridge-cache-action" + "actionParameterName": "--bridge-cache-action", + + // optional + "requireOutputFoldersParameterName": "--require-output-folders" } ``` + ## Usage -You can now use the parameter to have any Rush phased command either *only* restore from the cache (without any local building), or *only* write the cache, assuming all current output files are correct. +You can now use this plugin to have any Rush phased command either *only* restore from the cache (without any local building), or *only* write the cache, assuming all current output files are correct. **Replay the cache entries for this command as best-effort, but don't execute any build processes** `rush build --to your-packageX --bridge-cache-action=read` @@ -65,9 +78,6 @@ That will populate the cache for `your-packageX` and all of its dependencies. `rush build --to your-packageX --bridge-cache-action=write` That will populate the cache for `your-packageX` and all of its dependencies. - -## 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` +**Write whatever outputs are on disk for this command to the cache, but only if all output folders are present** +`rush build --to your-packageX --bridge-cache-action=write --require-output-folders` +That will populate the cache for `your-packageX` and all of its dependencies, skipping any that don't have all output folders present. diff --git a/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts b/rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts index 3b2ce0bf559..0f22d3d85b0 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 { Async } from '@rushstack/node-core-library'; +import { Async, FileSystem } from '@rushstack/node-core-library'; import { _OperationBuildCache as OperationBuildCache } from '@rushstack/rush-sdk'; import type { ICreateOperationsContext, @@ -25,14 +25,17 @@ type CacheAction = typeof CACHE_ACTION_READ | typeof CACHE_ACTION_WRITE; export interface IBridgeCachePluginOptions { readonly actionParameterName: string; + readonly requireOutputFoldersParameterName: string | undefined; } export class BridgeCachePlugin implements IRushPlugin { public readonly pluginName: string = PLUGIN_NAME; private readonly _actionParameterName: string; + private readonly _requireOutputFoldersParameterName: string | undefined; public constructor(options: IBridgeCachePluginOptions) { this._actionParameterName = options.actionParameterName; + this._requireOutputFoldersParameterName = options.requireOutputFoldersParameterName; if (!this._actionParameterName) { throw new Error( @@ -46,6 +49,7 @@ export class BridgeCachePlugin implements IRushPlugin { const logger: ILogger = session.getLogger(PLUGIN_NAME); let cacheAction: CacheAction | undefined; + let requireOutputFolders: boolean = false; // cancel the actual operations. We don't want to run the command, just cache the output folders on disk command.hooks.createOperations.tap( @@ -63,12 +67,13 @@ export class BridgeCachePlugin implements IRushPlugin { for (const operation of operations) { operation.enabled = false; } + + requireOutputFolders = this._isRequireOutputFoldersFlagSet(context); } return operations; } ); - // populate the cache for each operation command.hooks.beforeExecuteOperations.tap( PLUGIN_NAME, @@ -123,6 +128,27 @@ export class BridgeCachePlugin implements IRushPlugin { ); } } else if (cacheAction === CACHE_ACTION_WRITE) { + // if the require output folders flag has been passed, skip populating the cache if any of the expected output folders does not exist + if ( + requireOutputFolders && + operation.settings?.outputFolderNames && + operation.settings?.outputFolderNames?.length > 0 + ) { + const projectFolder: string = operation.associatedProject?.projectFolder; + const missingFolders: string[] = []; + operation.settings.outputFolderNames.forEach((outputFolderName: string) => { + if (!FileSystem.exists(`${projectFolder}/${outputFolderName}`)) { + missingFolders.push(outputFolderName); + } + }); + if (missingFolders.length > 0) { + terminal.writeWarningLine( + `Operation "${operation.name}": The following output folders do not exist: "${missingFolders.join('", "')}". Skipping cache population.` + ); + return; + } + } + const success: boolean = await projectBuildCache.trySetCacheEntryAsync(terminal); if (success) { ++successCount; @@ -186,4 +212,24 @@ export class BridgeCachePlugin implements IRushPlugin { return undefined; } + + private _isRequireOutputFoldersFlagSet(context: IExecuteOperationsContext): boolean { + if (!this._requireOutputFoldersParameterName) { + return false; + } + + const requireOutputFoldersParam: CommandLineParameter | undefined = context.customParameters.get( + this._requireOutputFoldersParameterName + ); + + if (!requireOutputFoldersParam) { + return false; + } + + if (requireOutputFoldersParam.kind !== CommandLineParameterKind.Flag) { + throw new Error(`The parameter "${this._requireOutputFoldersParameterName}" must be a flag.`); + } + + return requireOutputFoldersParam.value; + } } 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 index 695adc8de13..c0361a879d2 100644 --- 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 @@ -10,6 +10,10 @@ "actionParameterName": { "type": "string", "description": "(Required) The name of the choice parameter used to trigger this plugin on your phased commands. It should accept two values, 'read' and 'write'." + }, + "requireOutputFoldersParameterName": { + "type": "string", + "description": "(Optional) The name of the parameter used to specify whether the output folders must exist for the action in order to populate the cache." } } }