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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Adds an optional safety check flag to the Bridge Cache plugin write action.",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
28 changes: 19 additions & 9 deletions rush-plugins/rush-bridge-cache-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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",
}
```

Expand All @@ -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`
Expand All @@ -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.
50 changes: 48 additions & 2 deletions rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
Expand Down