Skip to content

Commit a457102

Browse files
author
Ben Keen
committed
code review feedback
1 parent 2c1f883 commit a457102

File tree

8 files changed

+164
-99
lines changed

8 files changed

+164
-99
lines changed

common/reviews/api/rush-lib.api.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,72 @@ export class Operation {
946946
weight: number;
947947
}
948948

949+
// Warning: (ae-internal-missing-underscore) The name "OperationExecutionRecord" should be prefixed with an underscore because the declaration is marked as @internal
950+
//
951+
// @internal
952+
export class OperationExecutionRecord implements IOperationRunnerContext, IOperationExecutionResult {
953+
// Warning: (ae-forgotten-export) The symbol "IOperationExecutionRecordContext" needs to be exported by the entry point index.d.ts
954+
constructor(operation: Operation, context: IOperationExecutionRecordContext);
955+
// (undocumented)
956+
readonly associatedPhase: IPhase;
957+
// (undocumented)
958+
readonly associatedProject: RushConfigurationProject;
959+
// (undocumented)
960+
get cobuildRunnerId(): string | undefined;
961+
// (undocumented)
962+
get collatedWriter(): CollatedWriter;
963+
readonly consumers: Set<OperationExecutionRecord>;
964+
criticalPathLength: number | undefined;
965+
// (undocumented)
966+
get debugMode(): boolean;
967+
readonly dependencies: Set<OperationExecutionRecord>;
968+
// (undocumented)
969+
get environment(): IEnvironment | undefined;
970+
error: Error | undefined;
971+
// (undocumented)
972+
executeAsync({ onStart, onResult }: {
973+
onStart: (record: OperationExecutionRecord) => Promise<OperationStatus | undefined>;
974+
onResult: (record: OperationExecutionRecord) => Promise<void>;
975+
}): Promise<void>;
976+
// (undocumented)
977+
getStateHash(): string;
978+
// (undocumented)
979+
getStateHashComponents(): ReadonlyArray<string>;
980+
// (undocumented)
981+
get isTerminal(): boolean;
982+
// (undocumented)
983+
logFilePaths: ILogFilePaths | undefined;
984+
// (undocumented)
985+
get metadataFolderPath(): string | undefined;
986+
// (undocumented)
987+
get name(): string;
988+
// (undocumented)
989+
get nonCachedDurationMs(): number | undefined;
990+
readonly operation: Operation;
991+
// (undocumented)
992+
readonly _operationMetadataManager: _OperationMetadataManager;
993+
// (undocumented)
994+
get quietMode(): boolean;
995+
// (undocumented)
996+
readonly runner: IOperationRunner;
997+
runWithTerminalAsync<T>(callback: (terminal: ITerminal, terminalProvider: ITerminalProvider) => Promise<T>, options: {
998+
createLogFile: boolean;
999+
logFileSuffix: string;
1000+
}): Promise<T>;
1001+
// (undocumented)
1002+
get silent(): boolean;
1003+
get status(): OperationStatus;
1004+
set status(newStatus: OperationStatus);
1005+
// (undocumented)
1006+
readonly stdioSummarizer: StdioSummarizer;
1007+
// Warning: (ae-forgotten-export) The symbol "Stopwatch" needs to be exported by the entry point index.d.ts
1008+
//
1009+
// (undocumented)
1010+
readonly stopwatch: Stopwatch;
1011+
// (undocumented)
1012+
get weight(): number;
1013+
}
1014+
9491015
// @internal
9501016
export class _OperationMetadataManager {
9511017
constructor(options: _IOperationMetadataManagerOptions);
@@ -1134,7 +1200,6 @@ export type PnpmStoreOptions = PnpmStoreLocation;
11341200
export class ProjectBuildCache {
11351201
// (undocumented)
11361202
get cacheId(): string | undefined;
1137-
// Warning: (ae-forgotten-export) The symbol "OperationExecutionRecord" needs to be exported by the entry point index.d.ts
11381203
// Warning: (ae-forgotten-export) The symbol "IOperationBuildCacheOptions" needs to be exported by the entry point index.d.ts
11391204
//
11401205
// (undocumented)

libraries/rush-lib/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ export type {
135135
IExecutionResult,
136136
IOperationExecutionResult
137137
} from './logic/operations/IOperationExecutionResult';
138+
export type { OperationExecutionRecord } from './logic/operations/OperationExecutionRecord';
138139
export { type IOperationOptions, Operation } from './logic/operations/Operation';
139140
export { OperationStatus } from './logic/operations/OperationStatus';
140141
export type { ILogFilePaths } from './logic/operations/ProjectLogWritable';

libraries/rush-lib/src/logic/operations/ShardedPhaseOperationPlugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ function spliceShards(existingOperations: Set<Operation>, context: ICreateOperat
128128
];
129129

130130
const { scripts } = project.packageJson;
131+
132+
//
131133
const commandToRun: string | undefined = phase.shellCommand ?? scripts?.[phase.name];
132134

133135
operation.logFilenameIdentifier = `${baseLogFilenameIdentifier}_collate`;
Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,45 @@
11
# @rushstack/rush-bridge-cache-plugin
22

3-
This plugin allows for interaction with the Rush cache. It exposes some methods to set the cache.
3+
This is a Rush plugin that adds an optional `--set-cache-only` flag to Rush's phased commands, so you can other tools to actual run the scripts and generate the build artifacts on disk, then separately populate the Rush cache for particular actions(s). For integrations with other build orchestrators, this allows the best of both worlds: using a different build tool to orchestrate the work, but still populate the Rush cache for benefiting local use of Rush.
44

5+
## Here be dragons!
56

6-
## Installation
7-
8-
`npm install @rushstack/rush-bridge-cache-plugin`
7+
This is a power-user sort of plugin. It 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!
98

10-
<!--
11-
## Usage
12-
The package contains a binary you can run from the command line. It will auto-detect the location of your rush.json file.
139

14-
`populate-rush-cache --packageName=[package name] --phase=test:unit`
10+
## Installation
1511

16-
- `--packageName` (required) - this should map to the `packageName` entry in your `rush.json` file.
17-
- `--phase` (required): the name of the phased command whose command has already been ran, and you want to cache the result on disk.
18-
-->
12+
(TODO)
1913

20-
--------------------
14+
`npm install @rushstack/rush-bridge-cache-plugin`
2115

22-
So this will:
23-
- tap into the hooks and do all the necessary shit to figure out how to po
24-
- expose a simple API for external users to tap into, e.g. if you want to populate the cache externally you'd import this plugin and use the methods to do so. You could then wrap that in a rush function, or however you want to do it.
2516

17+
## Configuration
2618

19+
First you need to 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:
2720

21+
```json
22+
{
23+
"associatedCommands": ["build", "test", "lint", "a11y", "typecheck"],
24+
"description": "When the flag is added to any associated command, it'll bypass running the command itself, but cache the result of a previous run. Beware! Only run when you know the build artifacts are in a valid state for the command.",
25+
"parameterKind": "flag",
26+
"longName": "--set-cache-only",
27+
"required": false
28+
}
29+
```
2830

2931

30-
--------------------
32+
## Usage
3133

32-
Discussion about the solution for BuildXL here:
33-
https://teams.microsoft.com/l/message/19:d85f52548ec74e8f8a0f107bd4e5ceb6@thread.v2/1740610046597?context=%7B%22contextType%22%3A%22chat%22%7D
34+
Any of the rush command can now just be given a `--set-cache-only` property, e.g.
3435

36+
`rush build --to your-packageX --set-cache-only`
3537

36-
"populate-cache": "..." <--- called after any cacheable unit of work is complete
38+
That will examine `your-packageX` and all of its dependencies, then populate the cache.
3739

38-
OperationExecutionRecord -> the smallest unit of work to be done.
39-
- looks like it's strongly coupled to the runner. We need one-off method calls.
4040

41-
CacheableOperationPlugin
42-
_tryGetProjectBuildCache() -> this returns the project build cache
41+
## Performance
4342

43+
When running within a pipeline, you'll want to populate the cache as fast as possible. So instead of waiting until the full build graph has been processed, you'll wnt to run it after each successful task. For that, just use Rush's `--only` and target whatever task had just completed, for example:
4444

45+
`rush lint --only your-packageY --set-cache-only`

rush-plugins/rush-bridge-cache-plugin/command-line.json

Lines changed: 0 additions & 11 deletions
This file was deleted.

rush-plugins/rush-bridge-cache-plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@rushstack/rush-bridge-cache-plugin",
33
"version": "0.0.1",
44
"private": true,
5-
"description": "A plugin to expose methods to interact with the Rush cache.",
5+
"description": "Rush plugin that provides a --set-cache-only command flag to populates the cache from previous runs.",
66
"license": "MIT",
77
"main": "./lib/index.js",
88
"repository": {

rush-plugins/rush-bridge-cache-plugin/rush-plugin-manifest.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
"plugins": [
44
{
55
"pluginName": "rush-bridge-cache-plugin",
6-
"description": "Rush plugin to allow interactions with the Rush cache.",
7-
"entryPoint": "./lib/index.js",
8-
"commandLineJsonFilePath": "./command-line.json"
6+
"description": "Rush plugin that provides a --set-cache-only command flag to populates the cache from previous runs.",
7+
"entryPoint": "./lib/index.js"
98
}
109
]
1110
}

rush-plugins/rush-bridge-cache-plugin/src/BridgeCachePlugin.ts

Lines changed: 68 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
22
// See LICENSE in the project root for license information.
33

4-
import { BuildCacheConfiguration, ProjectBuildCache } from '@rushstack/rush-sdk';
4+
import { ProjectBuildCache } from '@rushstack/rush-sdk';
55
import type {
6+
BuildCacheConfiguration,
7+
IExecuteOperationsContext,
68
ILogger,
79
IOperationExecutionResult,
810
IPhasedCommand,
911
IRushPlugin,
1012
Operation,
11-
RushConfiguration,
13+
OperationExecutionRecord,
1214
RushSession
1315
} from '@rushstack/rush-sdk';
1416

@@ -17,90 +19,96 @@ const PLUGIN_NAME: 'RushBridgeCachePlugin' = 'RushBridgeCachePlugin';
1719
export class BridgeCachePlugin implements IRushPlugin {
1820
public readonly pluginName: string = PLUGIN_NAME;
1921

20-
public apply(session: RushSession, rushConfiguration: RushConfiguration): void {
21-
// klutzy. Better way?
22-
const actionName: string = process.argv[2];
23-
24-
// const isSetCacheOnly: boolean = process.argv.includes('--set-cache-only'); // has to be allowed for ANY phased command, or be customizable
25-
// if (!isSetCacheOnly) {
26-
// return;
27-
// }
28-
29-
// tracks the projects being targeted by the command (--to, --only etc.)
30-
const targetProjects: string[] = [];
22+
public apply(session: RushSession): void {
23+
const isSetCacheOnly: boolean = process.argv.includes('--set-cache-only');
24+
if (!isSetCacheOnly) {
25+
return;
26+
}
3127

3228
const cancelOperations = (operations: Set<Operation>): Set<Operation> => {
3329
operations.forEach((operation: Operation) => {
34-
if (operation.enabled) {
35-
targetProjects.push(operation.associatedProject.packageName);
36-
}
37-
3830
operation.enabled = false;
3931
});
4032
return operations;
4133
};
4234

43-
session.hooks.runPhasedCommand
44-
.for(actionName)
45-
.tapPromise(PLUGIN_NAME, async (command: IPhasedCommand) => {
46-
// cancel the actual operations. We don't want to actually run the command, just cache the output folders from a previous run
47-
command.hooks.createOperations.tap(
48-
{ name: PLUGIN_NAME, stage: Number.MAX_SAFE_INTEGER },
49-
cancelOperations
50-
);
51-
52-
// now populate the cache for each operation
53-
command.hooks.beforeExecuteOperations.tap(
54-
PLUGIN_NAME,
55-
async (recordByOperation: Map<Operation, IOperationExecutionResult>): Promise<void> => {
56-
await this._setCacheAsync(session, rushConfiguration, recordByOperation, targetProjects);
35+
session.hooks.runAnyPhasedCommand.tapPromise(PLUGIN_NAME, async (command: IPhasedCommand) => {
36+
// tracks the projects being targeted by the command (--to, --only etc.)
37+
const targetProjects: Set<Operation> = new Set<Operation>();
38+
39+
// cancel the actual operations. We don't want to actually run the command, just cache the output folders from a previous run
40+
command.hooks.createOperations.tap(
41+
{ name: PLUGIN_NAME, stage: Number.MAX_SAFE_INTEGER },
42+
(operations: Set<Operation>): Set<Operation> => {
43+
operations.forEach((operation: Operation) => {
44+
if (operation.enabled) {
45+
targetProjects.add(operation);
46+
}
47+
});
48+
return cancelOperations(operations);
49+
}
50+
);
51+
52+
// now populate the cache for each operation
53+
command.hooks.beforeExecuteOperations.tap(
54+
PLUGIN_NAME,
55+
async (
56+
recordByOperation: Map<Operation, IOperationExecutionResult>,
57+
context: IExecuteOperationsContext
58+
): Promise<void> => {
59+
if (!context.buildCacheConfiguration) {
60+
return;
5761
}
58-
);
59-
});
62+
63+
await this._setCacheAsync(
64+
session,
65+
context.buildCacheConfiguration,
66+
recordByOperation,
67+
targetProjects
68+
);
69+
}
70+
);
71+
});
6072
}
6173

6274
private async _setCacheAsync(
6375
session: RushSession,
64-
rushConfiguration: RushConfiguration,
76+
buildCacheConfiguration: BuildCacheConfiguration,
6577
recordByOperation: Map<Operation, IOperationExecutionResult>,
66-
targetProjects: string[]
78+
targetProjects: Set<Operation>
6779
): Promise<void> {
6880
const logger: ILogger = session.getLogger(PLUGIN_NAME);
6981

70-
// const isSetCacheOnly: boolean = process.argv.includes('--set-cache-only');
71-
7282
recordByOperation.forEach(
7383
async (operationExecutionResult: IOperationExecutionResult, operation: Operation) => {
74-
const { associatedProject, associatedPhase, settings } = operation;
84+
const { associatedProject, associatedPhase } = operation;
7585

76-
if (!targetProjects.includes(associatedProject.packageName)) {
86+
// omit operations that aren't targeted, or packages without a command for this phase
87+
const hasCommand: boolean = !!associatedProject.packageJson.scripts?.[associatedPhase.name];
88+
if (!targetProjects.has(operation) || !hasCommand) {
7789
return;
7890
}
7991

80-
const buildCacheConfiguration: BuildCacheConfiguration | undefined =
81-
await BuildCacheConfiguration.tryLoadAsync(logger.terminal, rushConfiguration, session);
82-
83-
if (!buildCacheConfiguration) {
84-
return;
85-
}
86-
87-
const projectBuildCache: ProjectBuildCache = ProjectBuildCache.getProjectBuildCache({
88-
project: associatedProject,
89-
projectOutputFolderNames: settings?.outputFolderNames || [],
90-
buildCacheConfiguration,
91-
terminal: logger.terminal,
92-
operationStateHash: operationExecutionResult.getStateHash(),
93-
phaseName: associatedPhase.name
94-
});
92+
const projectBuildCache: ProjectBuildCache = ProjectBuildCache.forOperation(
93+
operationExecutionResult as OperationExecutionRecord,
94+
{
95+
buildCacheConfiguration,
96+
terminal: logger.terminal
97+
}
98+
);
9599

96100
const success: boolean = await projectBuildCache.trySetCacheEntryAsync(logger.terminal);
97101

98-
// eslint-disable-next-line no-console
99-
console.log('- setting cache for', {
100-
success,
101-
name: associatedPhase.name,
102-
package: associatedProject.packageName
103-
});
102+
if (success) {
103+
logger.terminal.writeLine(
104+
`Cache entry set for ${associatedPhase.name} (${associatedProject.packageName}) from previously generated output folders\n`
105+
);
106+
} else {
107+
logger.terminal.writeErrorLine(
108+
`Error creating a cache entry set for ${associatedPhase.name} (${associatedProject.packageName}) from previously generated output folders\n`
109+
);
110+
process.exit(1);
111+
}
104112
}
105113
);
106114
}

0 commit comments

Comments
 (0)