From 3798b73f9dd3db4c2fe9a8ad7830f66453f50b06 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 11 Jun 2025 12:46:05 -0400 Subject: [PATCH 01/21] feat: add lifecycle events for Heft operations Signed-off-by: Aramis Sennyey --- apps/heft/package.json | 2 +- apps/heft/src/cli/HeftActionRunner.ts | 25 ++++++++- apps/heft/src/index.ts | 6 +- .../heft/src/pluginFramework/HeftLifecycle.ts | 12 +++- .../pluginFramework/HeftLifecycleSession.ts | 34 ++++++++++++ .../.eslintrc.js | 6 +- .../config/heft.json | 24 ++++++++ .../config/rush-project.json | 10 ++++ .../heft-plugin.json | 10 ++++ .../package.json | 23 ++++++++ .../src/index.ts | 55 +++++++++++++++++++ .../tsconfig.json | 25 +++++++++ .../config/heft.json | 6 ++ .../heft-node-everything-test/package.json | 1 + .../rush/browser-approved-packages.json | 4 ++ .../rush/nonbrowser-approved-packages.json | 8 +-- .../config/subspaces/default/pnpm-lock.yaml | 27 +++++++++ common/reviews/api/heft.api.md | 34 ++++++++++++ common/reviews/api/operation-graph.api.md | 12 +++- libraries/operation-graph/src/Operation.ts | 8 +-- .../src/OperationExecutionManager.ts | 53 ++++++++++++------ rush.json | 6 ++ 22 files changed, 354 insertions(+), 37 deletions(-) rename build-tests/{rush-mcp-example-plugin => heft-example-lifecycle-plugin}/.eslintrc.js (70%) create mode 100644 build-tests/heft-example-lifecycle-plugin/config/heft.json create mode 100644 build-tests/heft-example-lifecycle-plugin/config/rush-project.json create mode 100644 build-tests/heft-example-lifecycle-plugin/heft-plugin.json create mode 100644 build-tests/heft-example-lifecycle-plugin/package.json create mode 100644 build-tests/heft-example-lifecycle-plugin/src/index.ts create mode 100644 build-tests/heft-example-lifecycle-plugin/tsconfig.json diff --git a/apps/heft/package.json b/apps/heft/package.json index 73575de3bcd..e8ef1590099 100644 --- a/apps/heft/package.json +++ b/apps/heft/package.json @@ -29,7 +29,7 @@ "license": "MIT", "scripts": { "build": "heft build --clean", - "start": "heft test --clean --watch", + "start": "heft build-watch --clean", "_phase:build": "heft run --only build -- --clean", "_phase:test": "heft run --only test -- --clean" }, diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index e60b78dbac7..bbb4c64d2ae 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -12,6 +12,7 @@ import { type IWatchLoopState, Operation, OperationExecutionManager, + type OperationGroupRecord, OperationStatus, WatchLoop } from '@rushstack/operation-graph'; @@ -350,6 +351,8 @@ export class HeftActionRunner { abortSignal: AbortSignal, requestRun?: (requestor?: string) => void ): Promise { + const { operationStart, operationFinish, operationGroupStart, operationGroupFinish } = + this._internalHeftSession.lifecycle.hooks; // Record this as the start of task execution. this._metricsCollector.setStartTime(); // Execute the action operations @@ -359,7 +362,27 @@ export class HeftActionRunner { terminal: this._terminal, parallelism: this._parallelism, abortSignal, - requestRun + requestRun, + beforeExecuteOperationAsync: async (operation: Operation) => { + if (operationStart.isUsed()) { + await operationStart.promise({ operation }); + } + }, + afterExecuteOperationAsync: async (operation: Operation) => { + if (operationFinish.isUsed()) { + await operationFinish.promise({ operation }); + } + }, + beforeExecuteOperationGroupAsync: async (operationGroup: OperationGroupRecord) => { + if (operationGroupStart.isUsed()) { + await operationGroupStart.promise({ operationGroup }); + } + }, + afterExecuteOperationGroupAsync: async (operationGroup: OperationGroupRecord) => { + if (operationGroupFinish.isUsed()) { + await operationGroupFinish.promise({ operationGroup }); + } + } }; return executionManager.executeAsync(operationExecutionManagerOptions); diff --git a/apps/heft/src/index.ts b/apps/heft/src/index.ts index 68d77c23d1d..59ee8d9d283 100644 --- a/apps/heft/src/index.ts +++ b/apps/heft/src/index.ts @@ -30,7 +30,11 @@ export type { IHeftLifecycleHooks, IHeftLifecycleCleanHookOptions, IHeftLifecycleToolStartHookOptions, - IHeftLifecycleToolFinishHookOptions + IHeftLifecycleToolFinishHookOptions, + IHeftOperationStartHookOptions, + IHeftOperationFinishHookOptions, + IHeftOperationGroupStartHookOptions, + IHeftOperationGroupFinishHookOptions } from './pluginFramework/HeftLifecycleSession'; export type { diff --git a/apps/heft/src/pluginFramework/HeftLifecycle.ts b/apps/heft/src/pluginFramework/HeftLifecycle.ts index 95fc7ee3fb5..6daa44e1a89 100644 --- a/apps/heft/src/pluginFramework/HeftLifecycle.ts +++ b/apps/heft/src/pluginFramework/HeftLifecycle.ts @@ -19,7 +19,11 @@ import { type IHeftLifecycleHooks, type IHeftLifecycleToolStartHookOptions, type IHeftLifecycleToolFinishHookOptions, - type IHeftLifecycleSession + type IHeftLifecycleSession, + type IHeftOperationStartHookOptions, + type IHeftOperationFinishHookOptions, + type IHeftOperationGroupStartHookOptions, + type IHeftOperationGroupFinishHookOptions } from './HeftLifecycleSession'; import type { ScopedLogger } from './logging/ScopedLogger'; @@ -67,7 +71,11 @@ export class HeftLifecycle extends HeftPluginHost { clean: new AsyncParallelHook(), toolStart: new AsyncParallelHook(), toolFinish: new AsyncParallelHook(), - recordMetrics: internalHeftSession.metricsCollector.recordMetricsHook + recordMetrics: internalHeftSession.metricsCollector.recordMetricsHook, + operationStart: new AsyncParallelHook(['operation']), + operationFinish: new AsyncParallelHook(['operation']), + operationGroupStart: new AsyncParallelHook(['operationGroup']), + operationGroupFinish: new AsyncParallelHook(['operationGroup']) }; } diff --git a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts index 1105608446e..1775429f4a0 100644 --- a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts +++ b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts @@ -11,6 +11,7 @@ import type { IHeftParameters } from './HeftParameterManager'; import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin'; import type { HeftPluginDefinitionBase } from '../configuration/HeftPluginDefinition'; import type { HeftPluginHost } from './HeftPluginHost'; +import type { Operation, OperationGroupRecord } from '@rushstack/operation-graph'; /** * The lifecycle session is responsible for providing session-specific information to Heft lifecycle @@ -67,6 +68,34 @@ export interface IHeftLifecycleSession { ): void; } +/** + * @public + */ +export interface IHeftOperationStartHookOptions { + operation: Operation; +} + +/** + * @public + */ +export interface IHeftOperationFinishHookOptions { + operation: Operation; +} + +/** + * @public + */ +export interface IHeftOperationGroupStartHookOptions { + operationGroup: OperationGroupRecord; +} + +/** + * @public + */ +export interface IHeftOperationGroupFinishHookOptions { + operationGroup: OperationGroupRecord; +} + /** * Hooks that are available to the lifecycle plugin. * @@ -111,6 +140,11 @@ export interface IHeftLifecycleHooks { * @public */ recordMetrics: AsyncParallelHook; + + operationStart: AsyncParallelHook; + operationFinish: AsyncParallelHook; + operationGroupStart: AsyncParallelHook; + operationGroupFinish: AsyncParallelHook; } /** diff --git a/build-tests/rush-mcp-example-plugin/.eslintrc.js b/build-tests/heft-example-lifecycle-plugin/.eslintrc.js similarity index 70% rename from build-tests/rush-mcp-example-plugin/.eslintrc.js rename to build-tests/heft-example-lifecycle-plugin/.eslintrc.js index de794c04ae0..066bf07ecc8 100644 --- a/build-tests/rush-mcp-example-plugin/.eslintrc.js +++ b/build-tests/heft-example-lifecycle-plugin/.eslintrc.js @@ -4,10 +4,6 @@ require('local-eslint-config/patch/modern-module-resolution'); require('local-eslint-config/patch/custom-config-package-names'); module.exports = { - extends: [ - 'local-eslint-config/profile/node', - 'local-eslint-config/mixins/friendly-locals', - 'local-eslint-config/mixins/tsdoc' - ], + extends: ['local-eslint-config/profile/node-trusted-tool', 'local-eslint-config/mixins/friendly-locals'], parserOptions: { tsconfigRootDir: __dirname } }; diff --git a/build-tests/heft-example-lifecycle-plugin/config/heft.json b/build-tests/heft-example-lifecycle-plugin/config/heft.json new file mode 100644 index 00000000000..64d969be2eb --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/config/heft.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + + // TODO: Add comments + "phasesByName": { + "build": { + "cleanFiles": [{ "includeGlobs": ["dist", "lib"] }], + + "tasksByName": { + "typescript": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-typescript-plugin" + } + }, + "lint": { + "taskDependencies": ["typescript"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-lint-plugin" + } + } + } + } + } +} diff --git a/build-tests/heft-example-lifecycle-plugin/config/rush-project.json b/build-tests/heft-example-lifecycle-plugin/config/rush-project.json new file mode 100644 index 00000000000..514e557d5eb --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/config/rush-project.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-project.schema.json", + + "operationSettings": [ + { + "operationName": "_phase:build", + "outputFolderNames": ["lib", "dist"] + } + ] +} diff --git a/build-tests/heft-example-lifecycle-plugin/heft-plugin.json b/build-tests/heft-example-lifecycle-plugin/heft-plugin.json new file mode 100644 index 00000000000..d174b5b769b --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/heft-plugin.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-plugin.schema.json", + + "lifecyclePlugins": [ + { + "pluginName": "example-lifecycle-plugin", + "entryPoint": "./lib/index" + } + ] +} diff --git a/build-tests/heft-example-lifecycle-plugin/package.json b/build-tests/heft-example-lifecycle-plugin/package.json new file mode 100644 index 00000000000..a144ef552af --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/package.json @@ -0,0 +1,23 @@ +{ + "name": "heft-example-lifecycle-plugin", + "description": "This is an example heft plugin for testing the lifecycle hooks", + "version": "1.0.0", + "private": true, + "main": "./lib/index.js", + "typings": "./lib/index.d.ts", + "scripts": { + "build": "heft build --clean", + "start": "heft build-watch", + "_phase:build": "heft run --only build -- --clean" + }, + "dependencies": {}, + "devDependencies": { + "local-eslint-config": "workspace:*", + "@rushstack/heft": "workspace:*", + "@rushstack/heft-lint-plugin": "workspace:*", + "@rushstack/heft-typescript-plugin": "workspace:*", + "@types/node": "20.17.19", + "eslint": "~8.57.0", + "typescript": "~5.8.2" + } +} diff --git a/build-tests/heft-example-lifecycle-plugin/src/index.ts b/build-tests/heft-example-lifecycle-plugin/src/index.ts new file mode 100644 index 00000000000..c8dc80b8876 --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/src/index.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { + IHeftLifecyclePlugin, + IHeftLifecycleSession, + IHeftOperationFinishHookOptions, + IHeftOperationGroupStartHookOptions, + IHeftOperationGroupFinishHookOptions, + IHeftOperationStartHookOptions +} from '@rushstack/heft'; + +export const PLUGIN_NAME: 'example-lifecycle-plugin' = 'example-lifecycle-plugin'; + +export default class ExampleLifecyclePlugin implements IHeftLifecyclePlugin { + public apply(session: IHeftLifecycleSession): void { + const { logger } = session; + session.hooks.operationFinish.tapPromise( + PLUGIN_NAME, + async (options: IHeftOperationFinishHookOptions) => { + const { operation } = options; + if (operation.state) { + logger.terminal.writeLine( + `--- ${operation.runner?.name} finished in ${operation.state.stopwatch.duration.toFixed(2)}s ---` + ); + } + } + ); + + session.hooks.operationStart.tapPromise(PLUGIN_NAME, async (options: IHeftOperationStartHookOptions) => { + const { operation } = options; + if (operation.state) { + logger.terminal.writeLine(`--- ${operation.runner?.name} started ---`); + } + }); + + session.hooks.operationGroupStart.tapPromise( + PLUGIN_NAME, + async (options: IHeftOperationGroupStartHookOptions) => { + const { operationGroup } = options; + logger.terminal.writeLine(`--- ${operationGroup.name} started ---`); + } + ); + + session.hooks.operationGroupFinish.tapPromise( + PLUGIN_NAME, + async (options: IHeftOperationGroupFinishHookOptions) => { + const { operationGroup } = options; + logger.terminal.writeLine( + `--- ${operationGroup.name} finished in ${operationGroup.duration.toFixed(2)}s ---` + ); + } + ); + } +} diff --git a/build-tests/heft-example-lifecycle-plugin/tsconfig.json b/build-tests/heft-example-lifecycle-plugin/tsconfig.json new file mode 100644 index 00000000000..2d179c7173f --- /dev/null +++ b/build-tests/heft-example-lifecycle-plugin/tsconfig.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + + "compilerOptions": { + "outDir": "lib", + "rootDir": "src", + + "forceConsistentCasingInFileNames": true, + "jsx": "react", + "declaration": true, + "sourceMap": true, + "declarationMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "types": ["node"], + + "module": "commonjs", + "target": "es2017", + "lib": ["es2017"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "lib"] +} diff --git a/build-tests/heft-node-everything-test/config/heft.json b/build-tests/heft-node-everything-test/config/heft.json index fc24874e5d3..08d9c72b92e 100644 --- a/build-tests/heft-node-everything-test/config/heft.json +++ b/build-tests/heft-node-everything-test/config/heft.json @@ -4,6 +4,12 @@ { "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + "heftPlugins": [ + { + "pluginPackage": "heft-example-lifecycle-plugin" + } + ], + // TODO: Add comments "phasesByName": { "build": { diff --git a/build-tests/heft-node-everything-test/package.json b/build-tests/heft-node-everything-test/package.json index c85e317f85b..90e7090f6f0 100644 --- a/build-tests/heft-node-everything-test/package.json +++ b/build-tests/heft-node-everything-test/package.json @@ -23,6 +23,7 @@ "@types/heft-jest": "1.0.1", "@types/node": "20.17.19", "eslint": "~8.57.0", + "heft-example-lifecycle-plugin": "workspace:*", "heft-example-plugin-01": "workspace:*", "heft-example-plugin-02": "workspace:*", "tslint": "~5.20.1", diff --git a/common/config/rush/browser-approved-packages.json b/common/config/rush/browser-approved-packages.json index 01b44fa2e61..e34c53ccdde 100644 --- a/common/config/rush/browser-approved-packages.json +++ b/common/config/rush/browser-approved-packages.json @@ -58,6 +58,10 @@ "name": "dependency-path", "allowedCategories": [ "libraries" ] }, + { + "name": "heft-example-lifecycle-plugin", + "allowedCategories": [ "tests" ] + }, { "name": "local-web-rig", "allowedCategories": [ "libraries", "vscode-extensions" ] diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index 14eabbce0ae..679a1c8a10a 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -494,6 +494,10 @@ "name": "buttono", "allowedCategories": [ "tests" ] }, + { + "name": "chokidar", + "allowedCategories": [ "libraries" ] + }, { "name": "cli-table", "allowedCategories": [ "libraries" ] @@ -662,10 +666,6 @@ "name": "https-proxy-agent", "allowedCategories": [ "libraries" ] }, - { - "name": "chokidar", - "allowedCategories": [ "libraries" ] - }, { "name": "ignore", "allowedCategories": [ "libraries" ] diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index 2ef7494dd10..76fa8f73e63 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -1294,6 +1294,30 @@ importers: specifier: workspace:* version: link:../../apps/heft + ../../../build-tests/heft-example-lifecycle-plugin: + devDependencies: + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + '@rushstack/heft-lint-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-lint-plugin + '@rushstack/heft-typescript-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-typescript-plugin + '@types/node': + specifier: 20.17.19 + version: 20.17.19 + eslint: + specifier: ~8.57.0 + version: 8.57.0 + local-eslint-config: + specifier: workspace:* + version: link:../../eslint/local-eslint-config + typescript: + specifier: ~5.8.2 + version: 5.8.2 + ../../../build-tests/heft-example-plugin-01: dependencies: tapable: @@ -1562,6 +1586,9 @@ importers: eslint: specifier: ~8.57.0 version: 8.57.0 + heft-example-lifecycle-plugin: + specifier: workspace:* + version: link:../heft-example-lifecycle-plugin heft-example-plugin-01: specifier: workspace:* version: link:../heft-example-plugin-01 diff --git a/common/reviews/api/heft.api.md b/common/reviews/api/heft.api.md index a50dc296624..c6619294075 100644 --- a/common/reviews/api/heft.api.md +++ b/common/reviews/api/heft.api.md @@ -34,6 +34,8 @@ import { IPropertyInheritanceDefaults } from '@rushstack/heft-config-file'; import { IRigConfig } from '@rushstack/rig-package'; import { ITerminal } from '@rushstack/terminal'; import { ITerminalProvider } from '@rushstack/terminal'; +import type { Operation } from '@rushstack/operation-graph'; +import type { OperationGroupRecord } from '@rushstack/operation-graph'; import { PathResolutionMethod } from '@rushstack/heft-config-file'; import { PropertyInheritanceCustomFunction } from '@rushstack/heft-config-file'; @@ -150,6 +152,14 @@ export interface IHeftLifecycleCleanHookOptions { // @public export interface IHeftLifecycleHooks { clean: AsyncParallelHook; + // (undocumented) + operationFinish: AsyncParallelHook; + // (undocumented) + operationGroupFinish: AsyncParallelHook; + // (undocumented) + operationGroupStart: AsyncParallelHook; + // (undocumented) + operationStart: AsyncParallelHook; recordMetrics: AsyncParallelHook; toolFinish: AsyncParallelHook; toolStart: AsyncParallelHook; @@ -176,6 +186,30 @@ export interface IHeftLifecycleToolFinishHookOptions { export interface IHeftLifecycleToolStartHookOptions { } +// @public (undocumented) +export interface IHeftOperationFinishHookOptions { + // (undocumented) + operation: Operation; +} + +// @public (undocumented) +export interface IHeftOperationGroupFinishHookOptions { + // (undocumented) + operationGroup: OperationGroupRecord; +} + +// @public (undocumented) +export interface IHeftOperationGroupStartHookOptions { + // (undocumented) + operationGroup: OperationGroupRecord; +} + +// @public (undocumented) +export interface IHeftOperationStartHookOptions { + // (undocumented) + operation: Operation; +} + // @public export interface IHeftParameters extends IHeftDefaultParameters { getChoiceListParameter(parameterLongName: string): CommandLineChoiceListParameter; diff --git a/common/reviews/api/operation-graph.api.md b/common/reviews/api/operation-graph.api.md index e85f4bad9ed..87c7afc4f40 100644 --- a/common/reviews/api/operation-graph.api.md +++ b/common/reviews/api/operation-graph.api.md @@ -30,8 +30,8 @@ export interface ICancelCommandMessage { // @beta export interface IExecuteOperationContext extends Omit { - afterExecute(operation: Operation, state: IOperationState): void; - beforeExecute(operation: Operation, state: IOperationState): void; + afterExecuteAsync(operation: Operation, state: IOperationState): Promise; + beforeExecuteAsync(operation: Operation, state: IOperationState): Promise; queueWork(workFn: () => Promise, priority: number): Promise; requestRun?: (requestor?: string) => void; terminal: ITerminal; @@ -48,6 +48,14 @@ export interface IOperationExecutionOptions { // (undocumented) abortSignal: AbortSignal; // (undocumented) + afterExecuteOperationAsync?: (operation: Operation) => Promise; + // (undocumented) + afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; + // (undocumented) + beforeExecuteOperationAsync?: (operation: Operation) => Promise; + // (undocumented) + beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; + // (undocumented) parallelism: number; // (undocumented) requestRun?: (requestor?: string) => void; diff --git a/libraries/operation-graph/src/Operation.ts b/libraries/operation-graph/src/Operation.ts index 167c7abcafb..b556bb580c8 100644 --- a/libraries/operation-graph/src/Operation.ts +++ b/libraries/operation-graph/src/Operation.ts @@ -50,12 +50,12 @@ export interface IExecuteOperationContext extends Omit; /** * Function to invoke after execution of an operation, for logging. */ - afterExecute(operation: Operation, state: IOperationState): void; + afterExecuteAsync(operation: Operation, state: IOperationState): Promise; /** * Function used to schedule the concurrency-limited execution of an operation. @@ -300,7 +300,7 @@ export class Operation implements IOperationStates { return innerState.status; } - context.beforeExecute(this, innerState); + await context.beforeExecuteAsync(this, innerState); innerState.stopwatch.start(); innerState.status = OperationStatus.Executing; @@ -337,7 +337,7 @@ export class Operation implements IOperationStates { } state.stopwatch.stop(); - context.afterExecute(this, state); + await context.afterExecuteAsync(this, state); return state.status; }, /* priority */ this.criticalPathLength ?? 0); diff --git a/libraries/operation-graph/src/OperationExecutionManager.ts b/libraries/operation-graph/src/OperationExecutionManager.ts index f780802cd61..4a6dda467dc 100644 --- a/libraries/operation-graph/src/OperationExecutionManager.ts +++ b/libraries/operation-graph/src/OperationExecutionManager.ts @@ -22,6 +22,11 @@ export interface IOperationExecutionOptions { terminal: ITerminal; requestRun?: (requestor?: string) => void; + + beforeExecuteOperationAsync?: (operation: Operation) => Promise; + afterExecuteOperationAsync?: (operation: Operation) => Promise; + beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; + afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; } /** @@ -129,26 +134,35 @@ export class OperationExecutionManager { return workQueue.pushAsync(workFn, priority); }, - beforeExecute: (operation: Operation): void => { + beforeExecuteAsync: async (operation: Operation): Promise => { // Initialize group if uninitialized and log the group name const { groupName } = operation; const groupRecord: OperationGroupRecord | undefined = groupName ? groupRecords.get(groupName) : undefined; - if (groupRecord && !startedGroups.has(groupRecord)) { - startedGroups.add(groupRecord); - groupRecord.startTimer(); - terminal.writeLine(` ---- ${groupRecord.name} started ---- `); + if (groupRecord) { + if (!startedGroups.has(groupRecord)) { + startedGroups.add(groupRecord); + groupRecord.startTimer(); + terminal.writeLine(` ---- ${groupRecord.name} started ---- `); + await executionOptions.beforeExecuteOperationGroupAsync?.(groupRecord); + } else { + await executionOptions.beforeExecuteOperationAsync?.(operation); + } + } else { + await executionOptions.beforeExecuteOperationAsync?.(operation); } }, - afterExecute: (operation: Operation, state: IOperationState): void => { + afterExecuteAsync: async (operation: Operation, state: IOperationState): Promise => { const { groupName } = operation; const groupRecord: OperationGroupRecord | undefined = groupName ? groupRecords.get(groupName) : undefined; if (groupRecord) { groupRecord.setOperationAsComplete(operation, state); + } else { + await executionOptions.afterExecuteOperationAsync?.(operation); } if (state.status === OperationStatus.Failure) { @@ -162,17 +176,22 @@ export class OperationExecutionManager { hasReportedFailures = true; } - // Log out the group name and duration if it is the last operation in the group - if (groupRecord?.finished && !finishedGroups.has(groupRecord)) { - finishedGroups.add(groupRecord); - const finishedLoggingWord: string = groupRecord.hasFailures - ? 'encountered an error' - : groupRecord.hasCancellations - ? 'cancelled' - : 'finished'; - terminal.writeLine( - ` ---- ${groupRecord.name} ${finishedLoggingWord} (${groupRecord.duration.toFixed(3)}s) ---- ` - ); + if (groupRecord) { + // Log out the group name and duration if it is the last operation in the group + if (groupRecord?.finished && !finishedGroups.has(groupRecord)) { + finishedGroups.add(groupRecord); + const finishedLoggingWord: string = groupRecord.hasFailures + ? 'encountered an error' + : groupRecord.hasCancellations + ? 'cancelled' + : 'finished'; + terminal.writeLine( + ` ---- ${groupRecord.name} ${finishedLoggingWord} (${groupRecord.duration.toFixed(3)}s) ---- ` + ); + await executionOptions.afterExecuteOperationGroupAsync?.(groupRecord); + } else { + await executionOptions.afterExecuteOperationAsync?.(operation); + } } } }; diff --git a/rush.json b/rush.json index 0498497f004..77488246618 100644 --- a/rush.json +++ b/rush.json @@ -797,6 +797,12 @@ "reviewCategory": "tests", "shouldPublish": false }, + { + "packageName": "heft-example-lifecycle-plugin", + "projectFolder": "build-tests/heft-example-lifecycle-plugin", + "reviewCategory": "tests", + "shouldPublish": false + }, { "packageName": "heft-example-plugin-01", "projectFolder": "build-tests/heft-example-plugin-01", From c085eaec92a9700528aaa1b76a8f795194012b1a Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 11 Jun 2025 12:53:20 -0400 Subject: [PATCH 02/21] add changeset Signed-off-by: Aramis Sennyey --- .../heft/sennyeya-heft-lifecycle_2025-06-11-16-53.json | 10 ++++++++++ .../sennyeya-heft-lifecycle_2025-06-11-16-53.json | 10 ++++++++++ 2 files changed, 20 insertions(+) create mode 100644 common/changes/@rushstack/heft/sennyeya-heft-lifecycle_2025-06-11-16-53.json create mode 100644 common/changes/@rushstack/operation-graph/sennyeya-heft-lifecycle_2025-06-11-16-53.json diff --git a/common/changes/@rushstack/heft/sennyeya-heft-lifecycle_2025-06-11-16-53.json b/common/changes/@rushstack/heft/sennyeya-heft-lifecycle_2025-06-11-16-53.json new file mode 100644 index 00000000000..657d21c3f30 --- /dev/null +++ b/common/changes/@rushstack/heft/sennyeya-heft-lifecycle_2025-06-11-16-53.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft", + "comment": "Added support for operation lifecycle events, `operationStart`, `operationFinish`, `operationGroupStart`, `operationGroupFinish`.", + "type": "minor" + } + ], + "packageName": "@rushstack/heft" +} \ No newline at end of file diff --git a/common/changes/@rushstack/operation-graph/sennyeya-heft-lifecycle_2025-06-11-16-53.json b/common/changes/@rushstack/operation-graph/sennyeya-heft-lifecycle_2025-06-11-16-53.json new file mode 100644 index 00000000000..a53dd921e99 --- /dev/null +++ b/common/changes/@rushstack/operation-graph/sennyeya-heft-lifecycle_2025-06-11-16-53.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/operation-graph", + "comment": "The OperationExecutionManager `beforeExecute` and `afterExecute` hooks have been made async and renamed to `beforeExecuteAsync` and `afterExecuteAsync`.", + "type": "minor" + } + ], + "packageName": "@rushstack/operation-graph" +} \ No newline at end of file From e25deabe5dd742beb2a2b57b9b2eb6225dcb0162 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 11 Jun 2025 13:57:31 -0400 Subject: [PATCH 03/21] rename to be task or phase-specific Signed-off-by: Aramis Sennyey --- apps/heft/src/cli/HeftActionRunner.ts | 33 ++++++--- apps/heft/src/index.ts | 8 +- .../runners/PhaseOperationRunner.ts | 4 + .../operations/runners/TaskOperationRunner.ts | 4 + .../heft/src/pluginFramework/HeftLifecycle.ts | 16 ++-- .../pluginFramework/HeftLifecycleSession.ts | 26 ++++--- .../src/index.ts | 57 ++++++--------- .../heft-node-everything-test/package.json | 2 +- common/reviews/api/heft.api.md | 73 +++++++++++-------- common/reviews/api/operation-graph.api.md | 4 +- .../src/OperationExecutionManager.ts | 24 +++--- 11 files changed, 138 insertions(+), 113 deletions(-) diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index bbb4c64d2ae..1f1da7656b6 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -351,8 +351,7 @@ export class HeftActionRunner { abortSignal: AbortSignal, requestRun?: (requestor?: string) => void ): Promise { - const { operationStart, operationFinish, operationGroupStart, operationGroupFinish } = - this._internalHeftSession.lifecycle.hooks; + const { taskStart, taskFinish, phaseStart, phaseFinish } = this._internalHeftSession.lifecycle.hooks; // Record this as the start of task execution. this._metricsCollector.setStartTime(); // Execute the action operations @@ -364,23 +363,33 @@ export class HeftActionRunner { abortSignal, requestRun, beforeExecuteOperationAsync: async (operation: Operation) => { - if (operationStart.isUsed()) { - await operationStart.promise({ operation }); + if (operation.runner instanceof TaskOperationRunner && taskStart.isUsed()) { + const runner: TaskOperationRunner = operation.runner as TaskOperationRunner; + await taskStart.promise({ task: runner.task, operation }); } }, afterExecuteOperationAsync: async (operation: Operation) => { - if (operationFinish.isUsed()) { - await operationFinish.promise({ operation }); + if (operation.runner instanceof TaskOperationRunner && taskFinish.isUsed()) { + const runner: TaskOperationRunner = operation.runner as TaskOperationRunner; + await taskFinish.promise({ task: runner.task, operation }); } }, - beforeExecuteOperationGroupAsync: async (operationGroup: OperationGroupRecord) => { - if (operationGroupStart.isUsed()) { - await operationGroupStart.promise({ operationGroup }); + beforeExecuteOperationGroupAsync: async ( + operationGroup: OperationGroupRecord, + operation: Operation + ) => { + if (operation.runner instanceof PhaseOperationRunner && phaseStart.isUsed()) { + const runner: PhaseOperationRunner = operation.runner as PhaseOperationRunner; + await phaseStart.promise({ phase: runner.phase, operation: operationGroup }); } }, - afterExecuteOperationGroupAsync: async (operationGroup: OperationGroupRecord) => { - if (operationGroupFinish.isUsed()) { - await operationGroupFinish.promise({ operationGroup }); + afterExecuteOperationGroupAsync: async ( + operationGroup: OperationGroupRecord, + operation: Operation + ) => { + if (operation.runner instanceof PhaseOperationRunner && phaseFinish.isUsed()) { + const runner: PhaseOperationRunner = operation.runner as PhaseOperationRunner; + await phaseFinish.promise({ phase: runner.phase, operation: operationGroup }); } } }; diff --git a/apps/heft/src/index.ts b/apps/heft/src/index.ts index 59ee8d9d283..5ba77ca9f0c 100644 --- a/apps/heft/src/index.ts +++ b/apps/heft/src/index.ts @@ -31,10 +31,10 @@ export type { IHeftLifecycleCleanHookOptions, IHeftLifecycleToolStartHookOptions, IHeftLifecycleToolFinishHookOptions, - IHeftOperationStartHookOptions, - IHeftOperationFinishHookOptions, - IHeftOperationGroupStartHookOptions, - IHeftOperationGroupFinishHookOptions + IHeftTaskStartHookOptions, + IHeftTaskFinishHookOptions, + IHeftPhaseStartHookOptions, + IHeftPhaseFinishHookOptions } from './pluginFramework/HeftLifecycleSession'; export type { diff --git a/apps/heft/src/operations/runners/PhaseOperationRunner.ts b/apps/heft/src/operations/runners/PhaseOperationRunner.ts index f6f1ba3ed0d..97beade2de0 100644 --- a/apps/heft/src/operations/runners/PhaseOperationRunner.ts +++ b/apps/heft/src/operations/runners/PhaseOperationRunner.ts @@ -27,6 +27,10 @@ export class PhaseOperationRunner implements IOperationRunner { return `Phase ${JSON.stringify(this._options.phase.phaseName)}`; } + public get phase(): HeftPhase { + return this._options.phase; + } + public constructor(options: IPhaseOperationRunnerOptions) { this._options = options; } diff --git a/apps/heft/src/operations/runners/TaskOperationRunner.ts b/apps/heft/src/operations/runners/TaskOperationRunner.ts index 995d10ffbed..ca5726d191f 100644 --- a/apps/heft/src/operations/runners/TaskOperationRunner.ts +++ b/apps/heft/src/operations/runners/TaskOperationRunner.ts @@ -77,6 +77,10 @@ export class TaskOperationRunner implements IOperationRunner { this._options = options; } + public get task(): HeftTask { + return this._options.task; + } + public async executeAsync(context: IOperationRunnerContext): Promise { const { internalHeftSession, task } = this._options; const { parentPhase } = task; diff --git a/apps/heft/src/pluginFramework/HeftLifecycle.ts b/apps/heft/src/pluginFramework/HeftLifecycle.ts index 6daa44e1a89..97a1c15f8eb 100644 --- a/apps/heft/src/pluginFramework/HeftLifecycle.ts +++ b/apps/heft/src/pluginFramework/HeftLifecycle.ts @@ -20,10 +20,10 @@ import { type IHeftLifecycleToolStartHookOptions, type IHeftLifecycleToolFinishHookOptions, type IHeftLifecycleSession, - type IHeftOperationStartHookOptions, - type IHeftOperationFinishHookOptions, - type IHeftOperationGroupStartHookOptions, - type IHeftOperationGroupFinishHookOptions + type IHeftTaskStartHookOptions, + type IHeftTaskFinishHookOptions, + type IHeftPhaseStartHookOptions, + type IHeftPhaseFinishHookOptions } from './HeftLifecycleSession'; import type { ScopedLogger } from './logging/ScopedLogger'; @@ -72,10 +72,10 @@ export class HeftLifecycle extends HeftPluginHost { toolStart: new AsyncParallelHook(), toolFinish: new AsyncParallelHook(), recordMetrics: internalHeftSession.metricsCollector.recordMetricsHook, - operationStart: new AsyncParallelHook(['operation']), - operationFinish: new AsyncParallelHook(['operation']), - operationGroupStart: new AsyncParallelHook(['operationGroup']), - operationGroupFinish: new AsyncParallelHook(['operationGroup']) + taskStart: new AsyncParallelHook(['task']), + taskFinish: new AsyncParallelHook(['task']), + phaseStart: new AsyncParallelHook(['phase']), + phaseFinish: new AsyncParallelHook(['phase']) }; } diff --git a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts index 1775429f4a0..c1d13dacab7 100644 --- a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts +++ b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts @@ -12,6 +12,8 @@ import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin'; import type { HeftPluginDefinitionBase } from '../configuration/HeftPluginDefinition'; import type { HeftPluginHost } from './HeftPluginHost'; import type { Operation, OperationGroupRecord } from '@rushstack/operation-graph'; +import type { HeftTask } from './HeftTask'; +import type { HeftPhase } from './HeftPhase'; /** * The lifecycle session is responsible for providing session-specific information to Heft lifecycle @@ -71,29 +73,33 @@ export interface IHeftLifecycleSession { /** * @public */ -export interface IHeftOperationStartHookOptions { +export interface IHeftTaskStartHookOptions { + task: HeftTask; operation: Operation; } /** * @public */ -export interface IHeftOperationFinishHookOptions { +export interface IHeftTaskFinishHookOptions { + task: HeftTask; operation: Operation; } /** * @public */ -export interface IHeftOperationGroupStartHookOptions { - operationGroup: OperationGroupRecord; +export interface IHeftPhaseStartHookOptions { + phase: HeftPhase; + operation: OperationGroupRecord; } /** * @public */ -export interface IHeftOperationGroupFinishHookOptions { - operationGroup: OperationGroupRecord; +export interface IHeftPhaseFinishHookOptions { + phase: HeftPhase; + operation: OperationGroupRecord; } /** @@ -141,10 +147,10 @@ export interface IHeftLifecycleHooks { */ recordMetrics: AsyncParallelHook; - operationStart: AsyncParallelHook; - operationFinish: AsyncParallelHook; - operationGroupStart: AsyncParallelHook; - operationGroupFinish: AsyncParallelHook; + taskStart: AsyncParallelHook; + taskFinish: AsyncParallelHook; + phaseStart: AsyncParallelHook; + phaseFinish: AsyncParallelHook; } /** diff --git a/build-tests/heft-example-lifecycle-plugin/src/index.ts b/build-tests/heft-example-lifecycle-plugin/src/index.ts index c8dc80b8876..23cd0347943 100644 --- a/build-tests/heft-example-lifecycle-plugin/src/index.ts +++ b/build-tests/heft-example-lifecycle-plugin/src/index.ts @@ -4,10 +4,10 @@ import type { IHeftLifecyclePlugin, IHeftLifecycleSession, - IHeftOperationFinishHookOptions, - IHeftOperationGroupStartHookOptions, - IHeftOperationGroupFinishHookOptions, - IHeftOperationStartHookOptions + IHeftTaskFinishHookOptions, + IHeftTaskStartHookOptions, + IHeftPhaseFinishHookOptions, + IHeftPhaseStartHookOptions } from '@rushstack/heft'; export const PLUGIN_NAME: 'example-lifecycle-plugin' = 'example-lifecycle-plugin'; @@ -15,41 +15,28 @@ export const PLUGIN_NAME: 'example-lifecycle-plugin' = 'example-lifecycle-plugin export default class ExampleLifecyclePlugin implements IHeftLifecyclePlugin { public apply(session: IHeftLifecycleSession): void { const { logger } = session; - session.hooks.operationFinish.tapPromise( - PLUGIN_NAME, - async (options: IHeftOperationFinishHookOptions) => { - const { operation } = options; - if (operation.state) { - logger.terminal.writeLine( - `--- ${operation.runner?.name} finished in ${operation.state.stopwatch.duration.toFixed(2)}s ---` - ); - } - } - ); - - session.hooks.operationStart.tapPromise(PLUGIN_NAME, async (options: IHeftOperationStartHookOptions) => { - const { operation } = options; + session.hooks.taskFinish.tapPromise(PLUGIN_NAME, async (options: IHeftTaskFinishHookOptions) => { + const { operation, task } = options; if (operation.state) { - logger.terminal.writeLine(`--- ${operation.runner?.name} started ---`); + logger.terminal.writeLine( + `--- ${task.taskName} finished in ${operation.state.stopwatch.duration.toFixed(2)}s ---` + ); } }); - session.hooks.operationGroupStart.tapPromise( - PLUGIN_NAME, - async (options: IHeftOperationGroupStartHookOptions) => { - const { operationGroup } = options; - logger.terminal.writeLine(`--- ${operationGroup.name} started ---`); - } - ); + session.hooks.taskStart.tapPromise(PLUGIN_NAME, async (options: IHeftTaskStartHookOptions) => { + const { task } = options; + logger.terminal.writeLine(`--- ${task.taskName} started ---`); + }); - session.hooks.operationGroupFinish.tapPromise( - PLUGIN_NAME, - async (options: IHeftOperationGroupFinishHookOptions) => { - const { operationGroup } = options; - logger.terminal.writeLine( - `--- ${operationGroup.name} finished in ${operationGroup.duration.toFixed(2)}s ---` - ); - } - ); + session.hooks.phaseStart.tapPromise(PLUGIN_NAME, async (options: IHeftPhaseStartHookOptions) => { + const { phase } = options; + logger.terminal.writeLine(`--- ${phase.phaseName} started ---`); + }); + + session.hooks.phaseFinish.tapPromise(PLUGIN_NAME, async (options: IHeftPhaseFinishHookOptions) => { + const { phase, operation } = options; + logger.terminal.writeLine(`--- ${phase.phaseName} finished in ${operation.duration.toFixed(2)}s ---`); + }); } } diff --git a/build-tests/heft-node-everything-test/package.json b/build-tests/heft-node-everything-test/package.json index 90e7090f6f0..74a253a6fcf 100644 --- a/build-tests/heft-node-everything-test/package.json +++ b/build-tests/heft-node-everything-test/package.json @@ -6,7 +6,7 @@ "main": "lib/index.js", "license": "MIT", "scripts": { - "build": "heft build --clean", + "build": "heft --debug build --clean", "_phase:build": "heft run --only build -- --clean", "_phase:build:incremental": "heft run --only build --", "_phase:test": "heft run --only test -- --clean", diff --git a/common/reviews/api/heft.api.md b/common/reviews/api/heft.api.md index c6619294075..8c55851a8a0 100644 --- a/common/reviews/api/heft.api.md +++ b/common/reviews/api/heft.api.md @@ -14,12 +14,15 @@ import { CommandLineFlagParameter } from '@rushstack/ts-command-line'; import { CommandLineIntegerListParameter } from '@rushstack/ts-command-line'; import { CommandLineIntegerParameter } from '@rushstack/ts-command-line'; import { CommandLineParameter } from '@rushstack/ts-command-line'; +import { CommandLineParameterProvider } from '@rushstack/ts-command-line'; import { CommandLineStringListParameter } from '@rushstack/ts-command-line'; import { CommandLineStringParameter } from '@rushstack/ts-command-line'; import { CustomValidationFunction } from '@rushstack/heft-config-file'; +import { FileLocationStyle } from '@rushstack/node-core-library'; import * as fs from 'fs'; import { ICustomJsonPathMetadata } from '@rushstack/heft-config-file'; import { ICustomPropertyInheritance } from '@rushstack/heft-config-file'; +import { IFileErrorFormattingOptions } from '@rushstack/node-core-library'; import { IJsonPathMetadata } from '@rushstack/heft-config-file'; import { IJsonPathMetadataResolverOptions } from '@rushstack/heft-config-file'; import { IJsonPathsMetadata } from '@rushstack/heft-config-file'; @@ -153,14 +156,14 @@ export interface IHeftLifecycleCleanHookOptions { export interface IHeftLifecycleHooks { clean: AsyncParallelHook; // (undocumented) - operationFinish: AsyncParallelHook; + phaseFinish: AsyncParallelHook; // (undocumented) - operationGroupFinish: AsyncParallelHook; + phaseStart: AsyncParallelHook; + recordMetrics: AsyncParallelHook; // (undocumented) - operationGroupStart: AsyncParallelHook; + taskFinish: AsyncParallelHook; // (undocumented) - operationStart: AsyncParallelHook; - recordMetrics: AsyncParallelHook; + taskStart: AsyncParallelHook; toolFinish: AsyncParallelHook; toolStart: AsyncParallelHook; } @@ -186,30 +189,6 @@ export interface IHeftLifecycleToolFinishHookOptions { export interface IHeftLifecycleToolStartHookOptions { } -// @public (undocumented) -export interface IHeftOperationFinishHookOptions { - // (undocumented) - operation: Operation; -} - -// @public (undocumented) -export interface IHeftOperationGroupFinishHookOptions { - // (undocumented) - operationGroup: OperationGroupRecord; -} - -// @public (undocumented) -export interface IHeftOperationGroupStartHookOptions { - // (undocumented) - operationGroup: OperationGroupRecord; -} - -// @public (undocumented) -export interface IHeftOperationStartHookOptions { - // (undocumented) - operation: Operation; -} - // @public export interface IHeftParameters extends IHeftDefaultParameters { getChoiceListParameter(parameterLongName: string): CommandLineChoiceListParameter; @@ -227,6 +206,24 @@ export interface IHeftParsedCommandLine { readonly unaliasedCommandName: string; } +// @public (undocumented) +export interface IHeftPhaseFinishHookOptions { + // (undocumented) + operation: OperationGroupRecord; + // Warning: (ae-forgotten-export) The symbol "HeftPhase" needs to be exported by the entry point index.d.ts + // + // (undocumented) + phase: HeftPhase; +} + +// @public (undocumented) +export interface IHeftPhaseStartHookOptions { + // (undocumented) + operation: OperationGroupRecord; + // (undocumented) + phase: HeftPhase; +} + // @public export interface IHeftPlugin { readonly accessor?: object; @@ -247,6 +244,16 @@ export interface IHeftTaskFileOperations { deleteOperations: Set; } +// @public (undocumented) +export interface IHeftTaskFinishHookOptions { + // (undocumented) + operation: Operation; + // Warning: (ae-forgotten-export) The symbol "HeftTask" needs to be exported by the entry point index.d.ts + // + // (undocumented) + task: HeftTask; +} + // @public export interface IHeftTaskHooks { readonly registerFileOperations: AsyncSeriesWaterfallHook; @@ -283,6 +290,14 @@ export interface IHeftTaskSession { readonly tempFolderPath: string; } +// @public (undocumented) +export interface IHeftTaskStartHookOptions { + // (undocumented) + operation: Operation; + // (undocumented) + task: HeftTask; +} + // @public export interface IIncrementalCopyOperation extends ICopyOperation { onlyIfChanged?: boolean; diff --git a/common/reviews/api/operation-graph.api.md b/common/reviews/api/operation-graph.api.md index 87c7afc4f40..946968ee8d9 100644 --- a/common/reviews/api/operation-graph.api.md +++ b/common/reviews/api/operation-graph.api.md @@ -50,11 +50,11 @@ export interface IOperationExecutionOptions { // (undocumented) afterExecuteOperationAsync?: (operation: Operation) => Promise; // (undocumented) - afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; + afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord, operation: Operation) => Promise; // (undocumented) beforeExecuteOperationAsync?: (operation: Operation) => Promise; // (undocumented) - beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; + beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord, operation: Operation) => Promise; // (undocumented) parallelism: number; // (undocumented) diff --git a/libraries/operation-graph/src/OperationExecutionManager.ts b/libraries/operation-graph/src/OperationExecutionManager.ts index 4a6dda467dc..ccbd0f16c57 100644 --- a/libraries/operation-graph/src/OperationExecutionManager.ts +++ b/libraries/operation-graph/src/OperationExecutionManager.ts @@ -25,8 +25,14 @@ export interface IOperationExecutionOptions { beforeExecuteOperationAsync?: (operation: Operation) => Promise; afterExecuteOperationAsync?: (operation: Operation) => Promise; - beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; - afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; + beforeExecuteOperationGroupAsync?: ( + operationGroup: OperationGroupRecord, + operation: Operation + ) => Promise; + afterExecuteOperationGroupAsync?: ( + operationGroup: OperationGroupRecord, + operation: Operation + ) => Promise; } /** @@ -145,13 +151,10 @@ export class OperationExecutionManager { startedGroups.add(groupRecord); groupRecord.startTimer(); terminal.writeLine(` ---- ${groupRecord.name} started ---- `); - await executionOptions.beforeExecuteOperationGroupAsync?.(groupRecord); - } else { - await executionOptions.beforeExecuteOperationAsync?.(operation); + await executionOptions.beforeExecuteOperationGroupAsync?.(groupRecord, operation); } - } else { - await executionOptions.beforeExecuteOperationAsync?.(operation); } + await executionOptions.beforeExecuteOperationAsync?.(operation); }, afterExecuteAsync: async (operation: Operation, state: IOperationState): Promise => { @@ -161,8 +164,6 @@ export class OperationExecutionManager { : undefined; if (groupRecord) { groupRecord.setOperationAsComplete(operation, state); - } else { - await executionOptions.afterExecuteOperationAsync?.(operation); } if (state.status === OperationStatus.Failure) { @@ -176,6 +177,7 @@ export class OperationExecutionManager { hasReportedFailures = true; } + await executionOptions.afterExecuteOperationAsync?.(operation); if (groupRecord) { // Log out the group name and duration if it is the last operation in the group if (groupRecord?.finished && !finishedGroups.has(groupRecord)) { @@ -188,9 +190,7 @@ export class OperationExecutionManager { terminal.writeLine( ` ---- ${groupRecord.name} ${finishedLoggingWord} (${groupRecord.duration.toFixed(3)}s) ---- ` ); - await executionOptions.afterExecuteOperationGroupAsync?.(groupRecord); - } else { - await executionOptions.afterExecuteOperationAsync?.(operation); + await executionOptions.afterExecuteOperationGroupAsync?.(groupRecord, operation); } } } From 4721472ba3050ebd926337545719d0d5cd3a223f Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 11 Jun 2025 13:57:54 -0400 Subject: [PATCH 04/21] update changeset Signed-off-by: Aramis Sennyey --- .../heft/sennyeya-heft-lifecycle_2025-06-11-16-53.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/changes/@rushstack/heft/sennyeya-heft-lifecycle_2025-06-11-16-53.json b/common/changes/@rushstack/heft/sennyeya-heft-lifecycle_2025-06-11-16-53.json index 657d21c3f30..4c2c4a14783 100644 --- a/common/changes/@rushstack/heft/sennyeya-heft-lifecycle_2025-06-11-16-53.json +++ b/common/changes/@rushstack/heft/sennyeya-heft-lifecycle_2025-06-11-16-53.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@rushstack/heft", - "comment": "Added support for operation lifecycle events, `operationStart`, `operationFinish`, `operationGroupStart`, `operationGroupFinish`.", + "comment": "Added support for task and phase lifecycle events, `taskStart`, `taskFinish`, `phaseStart`, `phaseFinish`.", "type": "minor" } ], From 9a0bebf56444dd7a11527bf640b3eb146113868f Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 11 Jun 2025 14:13:36 -0400 Subject: [PATCH 05/21] fix readme Signed-off-by: Aramis Sennyey --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f8ddf37435b..8722086cd49 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,7 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/build-tests/eslint-bulk-suppressions-test-legacy](./build-tests/eslint-bulk-suppressions-test-legacy/) | Sample code to test eslint bulk suppressions for versions of eslint < 8.57.0 | | [/build-tests/hashed-folder-copy-plugin-webpack5-test](./build-tests/hashed-folder-copy-plugin-webpack5-test/) | Building this project exercises @rushstack/hashed-folder-copy-plugin with Webpack 5. NOTE - THIS TEST IS CURRENTLY EXPECTED TO BE BROKEN | | [/build-tests/heft-copy-files-test](./build-tests/heft-copy-files-test/) | Building this project tests copying files with Heft | +| [/build-tests/heft-example-lifecycle-plugin](./build-tests/heft-example-lifecycle-plugin/) | This is an example heft plugin for testing the lifecycle hooks | | [/build-tests/heft-example-plugin-01](./build-tests/heft-example-plugin-01/) | This is an example heft plugin that exposes hooks for other plugins | | [/build-tests/heft-example-plugin-02](./build-tests/heft-example-plugin-02/) | This is an example heft plugin that taps the hooks exposed from heft-example-plugin-01 | | [/build-tests/heft-fastify-test](./build-tests/heft-fastify-test/) | This project tests Heft support for the Fastify framework for Node.js services | From d4b7dad438b2bd837820ca9e91638ce6e8e1e5ab Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 11 Jun 2025 14:15:25 -0400 Subject: [PATCH 06/21] add comments for hooks Signed-off-by: Aramis Sennyey --- .../pluginFramework/HeftLifecycleSession.ts | 27 +++++++++++++++++++ common/reviews/api/heft.api.md | 4 --- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts index c1d13dacab7..e4ae3123315 100644 --- a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts +++ b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts @@ -147,9 +147,36 @@ export interface IHeftLifecycleHooks { */ recordMetrics: AsyncParallelHook; + /** + * The `taskStart` hook is called at the beginning of a task. It is called before the task has begun + * to execute. To use it, call `taskStart.tapPromise(, )`. + * + * @public + */ taskStart: AsyncParallelHook; + + /** + * The `taskFinish` hook is called at the end of a task. It is called after the task has completed + * execution. To use it, call `taskFinish.tapPromise(, )`. + * + * @public + */ taskFinish: AsyncParallelHook; + + /** + * The `phaseStart` hook is called at the beginning of a phase. It is called before the phase has + * begun to execute. To use it, call `phaseStart.tapPromise(, )`. + * + * @public + */ phaseStart: AsyncParallelHook; + + /** + * The `phaseFinish` hook is called at the end of a phase. It is called after the phase has completed + * execution. To use it, call `phaseFinish.tapPromise(, )`. + * + * @public + */ phaseFinish: AsyncParallelHook; } diff --git a/common/reviews/api/heft.api.md b/common/reviews/api/heft.api.md index 8c55851a8a0..c066ef6a9d8 100644 --- a/common/reviews/api/heft.api.md +++ b/common/reviews/api/heft.api.md @@ -155,14 +155,10 @@ export interface IHeftLifecycleCleanHookOptions { // @public export interface IHeftLifecycleHooks { clean: AsyncParallelHook; - // (undocumented) phaseFinish: AsyncParallelHook; - // (undocumented) phaseStart: AsyncParallelHook; recordMetrics: AsyncParallelHook; - // (undocumented) taskFinish: AsyncParallelHook; - // (undocumented) taskStart: AsyncParallelHook; toolFinish: AsyncParallelHook; toolStart: AsyncParallelHook; From d05ddbabde4235ae986c95c24708cbe716826e14 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 11 Jun 2025 14:18:43 -0400 Subject: [PATCH 07/21] clean up Signed-off-by: Aramis Sennyey --- build-tests/heft-node-everything-test/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build-tests/heft-node-everything-test/package.json b/build-tests/heft-node-everything-test/package.json index 74a253a6fcf..90e7090f6f0 100644 --- a/build-tests/heft-node-everything-test/package.json +++ b/build-tests/heft-node-everything-test/package.json @@ -6,7 +6,7 @@ "main": "lib/index.js", "license": "MIT", "scripts": { - "build": "heft --debug build --clean", + "build": "heft build --clean", "_phase:build": "heft run --only build -- --clean", "_phase:build:incremental": "heft run --only build --", "_phase:test": "heft run --only test -- --clean", From 1e908aecfa73d324fedcbda108341ef5bce1c86a Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 11 Jun 2025 16:34:23 -0400 Subject: [PATCH 08/21] use custom metadata instead of runner Signed-off-by: Aramis Sennyey --- apps/heft/src/cli/HeftActionRunner.ts | 82 ++++++++++++------- .../runners/PhaseOperationRunner.ts | 4 - .../operations/runners/TaskOperationRunner.ts | 4 - .../pluginFramework/HeftLifecycleSession.ts | 15 ++-- common/reviews/api/operation-graph.api.md | 39 +++++---- libraries/operation-graph/src/Operation.ts | 22 +++-- .../src/OperationExecutionManager.ts | 49 ++++++----- .../src/OperationGroupRecord.ts | 6 +- 8 files changed, 130 insertions(+), 91 deletions(-) diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index 1f1da7656b6..280027b7feb 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -46,6 +46,23 @@ export interface IHeftActionRunnerOptions extends IHeftActionOptions { action: IHeftAction; } +/** + * Metadata for an operation that represents a task. + * @public + */ +export interface IHeftTaskOperationMetadata { + task: HeftTask; + phase: HeftPhase; +} + +/** + * Metadata for an operation that represents a phase. + * @public + */ +export interface IHeftPhaseOperationMetadata { + phase: HeftPhase; +} + export function initializeHeft( heftConfiguration: HeftConfiguration, terminal: ITerminal, @@ -292,9 +309,15 @@ export class HeftActionRunner { initializeHeft(this._heftConfiguration, terminal, this.parameterManager.defaultParameters.verbose); - const operations: ReadonlySet = this._generateOperations(); + const operations: ReadonlySet> = this._generateOperations(); - const executionManager: OperationExecutionManager = new OperationExecutionManager(operations); + const executionManager: OperationExecutionManager< + IHeftTaskOperationMetadata, + IHeftPhaseOperationMetadata + > = new OperationExecutionManager( + operations, + (metadata) => ({ phase: metadata.phase }) + ); const cliAbortSignal: AbortSignal = ensureCliAbortSignal(this._terminal); @@ -347,7 +370,7 @@ export class HeftActionRunner { } private async _executeOnceAsync( - executionManager: OperationExecutionManager, + executionManager: OperationExecutionManager, abortSignal: AbortSignal, requestRun?: (requestor?: string) => void ): Promise { @@ -357,39 +380,38 @@ export class HeftActionRunner { // Execute the action operations return await runWithLoggingAsync( () => { - const operationExecutionManagerOptions: IOperationExecutionOptions = { + const operationExecutionManagerOptions: IOperationExecutionOptions< + IHeftTaskOperationMetadata, + IHeftPhaseOperationMetadata + > = { terminal: this._terminal, parallelism: this._parallelism, abortSignal, requestRun, - beforeExecuteOperationAsync: async (operation: Operation) => { - if (operation.runner instanceof TaskOperationRunner && taskStart.isUsed()) { - const runner: TaskOperationRunner = operation.runner as TaskOperationRunner; - await taskStart.promise({ task: runner.task, operation }); + beforeExecuteOperationAsync: async (operation: Operation) => { + if (operation.metadata.task && taskStart.isUsed()) { + await taskStart.promise({ operation }); } }, - afterExecuteOperationAsync: async (operation: Operation) => { - if (operation.runner instanceof TaskOperationRunner && taskFinish.isUsed()) { - const runner: TaskOperationRunner = operation.runner as TaskOperationRunner; - await taskFinish.promise({ task: runner.task, operation }); + afterExecuteOperationAsync: async (operation: Operation) => { + if (operation.metadata.task && taskFinish.isUsed()) { + await taskFinish.promise({ operation }); } }, beforeExecuteOperationGroupAsync: async ( - operationGroup: OperationGroupRecord, - operation: Operation + operationGroup: OperationGroupRecord, + operation: Operation ) => { - if (operation.runner instanceof PhaseOperationRunner && phaseStart.isUsed()) { - const runner: PhaseOperationRunner = operation.runner as PhaseOperationRunner; - await phaseStart.promise({ phase: runner.phase, operation: operationGroup }); + if (operation.metadata.phase && phaseStart.isUsed()) { + await phaseStart.promise({ operation: operationGroup }); } }, afterExecuteOperationGroupAsync: async ( - operationGroup: OperationGroupRecord, - operation: Operation + operationGroup: OperationGroupRecord, + operation: Operation ) => { - if (operation.runner instanceof PhaseOperationRunner && phaseFinish.isUsed()) { - const runner: PhaseOperationRunner = operation.runner as PhaseOperationRunner; - await phaseFinish.promise({ phase: runner.phase, operation: operationGroup }); + if (operation.metadata.phase && phaseFinish.isUsed()) { + await phaseFinish.promise({ operation: operationGroup }); } } }; @@ -405,10 +427,10 @@ export class HeftActionRunner { ); } - private _generateOperations(): Set { + private _generateOperations(): Set> { const { selectedPhases } = this._action; - const operations: Map = new Map(); + const operations: Map> = new Map(); const internalHeftSession: InternalHeftSession = this._internalHeftSession; let hasWarnedAboutSkippedPhases: boolean = false; @@ -431,7 +453,11 @@ export class HeftActionRunner { } // Create operation for the phase start node - const phaseOperation: Operation = _getOrCreatePhaseOperation(internalHeftSession, phase, operations); + const phaseOperation: Operation = _getOrCreatePhaseOperation( + internalHeftSession, + phase, + operations + ); // Create operations for each task for (const task of phase.tasks) { @@ -472,11 +498,11 @@ function _getOrCreatePhaseOperation( this: void, internalHeftSession: InternalHeftSession, phase: HeftPhase, - operations: Map -): Operation { + operations: Map> +): Operation { const key: string = phase.phaseName; - let operation: Operation | undefined = operations.get(key); + let operation: Operation | undefined = operations.get(key); if (!operation) { // Only create the operation. Dependencies are hooked up separately operation = new Operation({ diff --git a/apps/heft/src/operations/runners/PhaseOperationRunner.ts b/apps/heft/src/operations/runners/PhaseOperationRunner.ts index 97beade2de0..f6f1ba3ed0d 100644 --- a/apps/heft/src/operations/runners/PhaseOperationRunner.ts +++ b/apps/heft/src/operations/runners/PhaseOperationRunner.ts @@ -27,10 +27,6 @@ export class PhaseOperationRunner implements IOperationRunner { return `Phase ${JSON.stringify(this._options.phase.phaseName)}`; } - public get phase(): HeftPhase { - return this._options.phase; - } - public constructor(options: IPhaseOperationRunnerOptions) { this._options = options; } diff --git a/apps/heft/src/operations/runners/TaskOperationRunner.ts b/apps/heft/src/operations/runners/TaskOperationRunner.ts index ca5726d191f..995d10ffbed 100644 --- a/apps/heft/src/operations/runners/TaskOperationRunner.ts +++ b/apps/heft/src/operations/runners/TaskOperationRunner.ts @@ -77,10 +77,6 @@ export class TaskOperationRunner implements IOperationRunner { this._options = options; } - public get task(): HeftTask { - return this._options.task; - } - public async executeAsync(context: IOperationRunnerContext): Promise { const { internalHeftSession, task } = this._options; const { parentPhase } = task; diff --git a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts index e4ae3123315..8cadcd7c534 100644 --- a/apps/heft/src/pluginFramework/HeftLifecycleSession.ts +++ b/apps/heft/src/pluginFramework/HeftLifecycleSession.ts @@ -12,8 +12,7 @@ import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin'; import type { HeftPluginDefinitionBase } from '../configuration/HeftPluginDefinition'; import type { HeftPluginHost } from './HeftPluginHost'; import type { Operation, OperationGroupRecord } from '@rushstack/operation-graph'; -import type { HeftTask } from './HeftTask'; -import type { HeftPhase } from './HeftPhase'; +import type { IHeftPhaseOperationMetadata, IHeftTaskOperationMetadata } from '../cli/HeftActionRunner'; /** * The lifecycle session is responsible for providing session-specific information to Heft lifecycle @@ -74,32 +73,28 @@ export interface IHeftLifecycleSession { * @public */ export interface IHeftTaskStartHookOptions { - task: HeftTask; - operation: Operation; + operation: Operation; } /** * @public */ export interface IHeftTaskFinishHookOptions { - task: HeftTask; - operation: Operation; + operation: Operation; } /** * @public */ export interface IHeftPhaseStartHookOptions { - phase: HeftPhase; - operation: OperationGroupRecord; + operation: OperationGroupRecord; } /** * @public */ export interface IHeftPhaseFinishHookOptions { - phase: HeftPhase; - operation: OperationGroupRecord; + operation: OperationGroupRecord; } /** diff --git a/common/reviews/api/operation-graph.api.md b/common/reviews/api/operation-graph.api.md index 946968ee8d9..8230ae22db1 100644 --- a/common/reviews/api/operation-graph.api.md +++ b/common/reviews/api/operation-graph.api.md @@ -44,17 +44,17 @@ export interface IExitCommandMessage { } // @beta -export interface IOperationExecutionOptions { +export interface IOperationExecutionOptions { // (undocumented) abortSignal: AbortSignal; // (undocumented) - afterExecuteOperationAsync?: (operation: Operation) => Promise; + afterExecuteOperationAsync?: (operation: Operation) => Promise; // (undocumented) - afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord, operation: Operation) => Promise; + afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord, operation: Operation) => Promise; // (undocumented) - beforeExecuteOperationAsync?: (operation: Operation) => Promise; + beforeExecuteOperationAsync?: (operation: Operation) => Promise; // (undocumented) - beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord, operation: Operation) => Promise; + beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord, operation: Operation) => Promise; // (undocumented) parallelism: number; // (undocumented) @@ -64,8 +64,9 @@ export interface IOperationExecutionOptions { } // @beta -export interface IOperationOptions { +export interface IOperationOptions { groupName?: string | undefined; + metadata?: TMetadata | undefined; name?: string | undefined; runner?: IOperationRunner | undefined; weight?: number | undefined; @@ -147,19 +148,21 @@ export interface IWatchLoopState { } // @beta -export class Operation implements IOperationStates { - constructor(options?: IOperationOptions); +export class Operation implements IOperationStates { + constructor(options?: IOperationOptions); // (undocumented) - addDependency(dependency: Operation): void; - readonly consumers: Set; + addDependency(dependency: Operation): void; + readonly consumers: Set>; criticalPathLength: number | undefined; // (undocumented) - deleteDependency(dependency: Operation): void; - readonly dependencies: Set; + deleteDependency(dependency: Operation): void; + readonly dependencies: Set>; // @internal (undocumented) _executeAsync(context: IExecuteOperationContext): Promise; readonly groupName: string | undefined; lastState: IOperationState | undefined; + // (undocumented) + readonly metadata: TMetadata; readonly name: string | undefined; // (undocumented) reset(): void; @@ -180,14 +183,14 @@ export class OperationError extends Error { } // @beta -export class OperationExecutionManager { - constructor(operations: ReadonlySet); - executeAsync(executionOptions: IOperationExecutionOptions): Promise; +export class OperationExecutionManager { + constructor(operations: ReadonlySet>, deriveGroupMetadata?: (metadata: TOperationMetadata) => TGroupMetadata); + executeAsync(executionOptions: IOperationExecutionOptions): Promise; } // @beta -export class OperationGroupRecord { - constructor(name: string); +export class OperationGroupRecord { + constructor(name: string, metadata?: TMetadata); // (undocumented) addOperation(operation: Operation): void; // (undocumented) @@ -199,6 +202,8 @@ export class OperationGroupRecord { // (undocumented) get hasFailures(): boolean; // (undocumented) + readonly metadata: TMetadata; + // (undocumented) readonly name: string; // (undocumented) reset(): void; diff --git a/libraries/operation-graph/src/Operation.ts b/libraries/operation-graph/src/Operation.ts index b556bb580c8..fbe547ec0c7 100644 --- a/libraries/operation-graph/src/Operation.ts +++ b/libraries/operation-graph/src/Operation.ts @@ -18,7 +18,7 @@ import { OperationStatus } from './OperationStatus'; * Options for constructing a new Operation. * @beta */ -export interface IOperationOptions { +export interface IOperationOptions { /** * The name of this operation, for logging. */ @@ -39,6 +39,11 @@ export interface IOperationOptions { * The weight used by the scheduler to determine order of execution. */ weight?: number | undefined; + + /** + * The metadata for this operation. + */ + metadata?: TMetadata | undefined; } /** @@ -85,15 +90,15 @@ export interface IExecuteOperationContext extends Omit implements IOperationStates { /** * A set of all dependencies which must be executed before this operation is complete. */ - public readonly dependencies: Set = new Set(); + public readonly dependencies: Set> = new Set>(); /** * A set of all operations that wait for this operation. */ - public readonly consumers: Set = new Set(); + public readonly consumers: Set> = new Set>(); /** * If specified, the name of a grouping to which this Operation belongs, for logging start and end times. */ @@ -174,19 +179,22 @@ export class Operation implements IOperationStates { */ private _runPending: boolean = true; - public constructor(options?: IOperationOptions) { + public readonly metadata: TMetadata; + + public constructor(options?: IOperationOptions) { this.groupName = options?.groupName; this.runner = options?.runner; this.weight = options?.weight || 1; this.name = options?.name; + this.metadata = options?.metadata || ({} as TMetadata); } - public addDependency(dependency: Operation): void { + public addDependency(dependency: Operation): void { this.dependencies.add(dependency); dependency.consumers.add(this); } - public deleteDependency(dependency: Operation): void { + public deleteDependency(dependency: Operation): void { this.dependencies.delete(dependency); dependency.consumers.delete(this); } diff --git a/libraries/operation-graph/src/OperationExecutionManager.ts b/libraries/operation-graph/src/OperationExecutionManager.ts index ccbd0f16c57..348f36fc9c9 100644 --- a/libraries/operation-graph/src/OperationExecutionManager.ts +++ b/libraries/operation-graph/src/OperationExecutionManager.ts @@ -16,22 +16,25 @@ import { WorkQueue } from './WorkQueue'; * * @beta */ -export interface IOperationExecutionOptions { +export interface IOperationExecutionOptions< + TOperationMetadata extends {} = {}, + TGroupMetadata extends {} = {} +> { abortSignal: AbortSignal; parallelism: number; terminal: ITerminal; requestRun?: (requestor?: string) => void; - beforeExecuteOperationAsync?: (operation: Operation) => Promise; - afterExecuteOperationAsync?: (operation: Operation) => Promise; + beforeExecuteOperationAsync?: (operation: Operation) => Promise; + afterExecuteOperationAsync?: (operation: Operation) => Promise; beforeExecuteOperationGroupAsync?: ( - operationGroup: OperationGroupRecord, - operation: Operation + operationGroup: OperationGroupRecord, + operation: Operation ) => Promise; afterExecuteOperationGroupAsync?: ( - operationGroup: OperationGroupRecord, - operation: Operation + operationGroup: OperationGroupRecord, + operation: Operation ) => Promise; } @@ -43,7 +46,7 @@ export interface IOperationExecutionOptions { * * @beta */ -export class OperationExecutionManager { +export class OperationExecutionManager { /** * The set of operations that will be executed */ @@ -52,23 +55,26 @@ export class OperationExecutionManager { * Group records are metadata-only entities used for tracking the start and end of a set of related tasks. * This is the only extent to which the operation graph is aware of Heft phases. */ - private readonly _groupRecordByName: Map; + private readonly _groupRecordByName: Map>; /** * The total number of non-silent operations in the graph. * Silent operations are generally used to simplify the construction of the graph. */ private readonly _trackedOperationCount: number; - public constructor(operations: ReadonlySet) { - const groupRecordByName: Map = new Map(); + public constructor( + operations: ReadonlySet>, + deriveGroupMetadata: (metadata: TOperationMetadata) => TGroupMetadata = () => ({}) as TGroupMetadata + ) { + const groupRecordByName: Map> = new Map(); this._groupRecordByName = groupRecordByName; let trackedOperationCount: number = 0; for (const operation of operations) { const { groupName } = operation; - let group: OperationGroupRecord | undefined = undefined; + let group: OperationGroupRecord | undefined = undefined; if (groupName && !(group = groupRecordByName.get(groupName))) { - group = new OperationGroupRecord(groupName); + group = new OperationGroupRecord(groupName, deriveGroupMetadata(operation.metadata)); groupRecordByName.set(groupName, group); } @@ -100,7 +106,9 @@ export class OperationExecutionManager { * Executes all operations which have been registered, returning a promise which is resolved when all the * operations are completed successfully, or rejects when any operation fails. */ - public async executeAsync(executionOptions: IOperationExecutionOptions): Promise { + public async executeAsync( + executionOptions: IOperationExecutionOptions + ): Promise { let hasReportedFailures: boolean = false; const { abortSignal, parallelism, terminal, requestRun } = executionOptions; @@ -113,7 +121,7 @@ export class OperationExecutionManager { const finishedGroups: Set = new Set(); const maxParallelism: number = Math.min(this._operations.length, parallelism); - const groupRecords: Map = this._groupRecordByName; + const groupRecords: Map> = this._groupRecordByName; for (const groupRecord of groupRecords.values()) { groupRecord.reset(); } @@ -140,10 +148,10 @@ export class OperationExecutionManager { return workQueue.pushAsync(workFn, priority); }, - beforeExecuteAsync: async (operation: Operation): Promise => { + beforeExecuteAsync: async (operation: Operation): Promise => { // Initialize group if uninitialized and log the group name const { groupName } = operation; - const groupRecord: OperationGroupRecord | undefined = groupName + const groupRecord: OperationGroupRecord | undefined = groupName ? groupRecords.get(groupName) : undefined; if (groupRecord) { @@ -157,9 +165,12 @@ export class OperationExecutionManager { await executionOptions.beforeExecuteOperationAsync?.(operation); }, - afterExecuteAsync: async (operation: Operation, state: IOperationState): Promise => { + afterExecuteAsync: async ( + operation: Operation, + state: IOperationState + ): Promise => { const { groupName } = operation; - const groupRecord: OperationGroupRecord | undefined = groupName + const groupRecord: OperationGroupRecord | undefined = groupName ? groupRecords.get(groupName) : undefined; if (groupRecord) { diff --git a/libraries/operation-graph/src/OperationGroupRecord.ts b/libraries/operation-graph/src/OperationGroupRecord.ts index bb99ec38eca..d6d21106253 100644 --- a/libraries/operation-graph/src/OperationGroupRecord.ts +++ b/libraries/operation-graph/src/OperationGroupRecord.ts @@ -13,7 +13,7 @@ import { Stopwatch } from './Stopwatch'; * * @beta */ -export class OperationGroupRecord { +export class OperationGroupRecord { private readonly _operations: Set = new Set(); private _remainingOperations: Set = new Set(); @@ -22,6 +22,7 @@ export class OperationGroupRecord { private _hasFailures: boolean = false; public readonly name: string; + public readonly metadata: TMetadata; public get duration(): number { return this._groupStopwatch ? this._groupStopwatch.duration : 0; @@ -39,8 +40,9 @@ export class OperationGroupRecord { return this._hasFailures; } - public constructor(name: string) { + public constructor(name: string, metadata: TMetadata = {} as TMetadata) { this.name = name; + this.metadata = metadata; } public addOperation(operation: Operation): void { From 2b7d8474573ed616905ea025b7db929cc3fdcc10 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 11 Jun 2025 16:38:38 -0400 Subject: [PATCH 09/21] export props to entry point Signed-off-by: Aramis Sennyey --- apps/heft/src/index.ts | 6 +++ common/reviews/api/heft.api.md | 74 +++++++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 14 deletions(-) diff --git a/apps/heft/src/index.ts b/apps/heft/src/index.ts index 5ba77ca9f0c..872f9cbddbd 100644 --- a/apps/heft/src/index.ts +++ b/apps/heft/src/index.ts @@ -83,3 +83,9 @@ export type { CommandLineStringListParameter, CommandLineStringParameter } from '@rushstack/ts-command-line'; + +export type { IHeftTaskOperationMetadata } from './cli/HeftActionRunner'; +export type { IHeftPhaseOperationMetadata } from './cli/HeftActionRunner'; + +export type { HeftTask } from './pluginFramework/HeftTask'; +export type { HeftPhase } from './pluginFramework/HeftPhase'; diff --git a/common/reviews/api/heft.api.md b/common/reviews/api/heft.api.md index c066ef6a9d8..cd0c01feb76 100644 --- a/common/reviews/api/heft.api.md +++ b/common/reviews/api/heft.api.md @@ -103,6 +103,48 @@ export class HeftConfiguration { tryLoadProjectConfigurationFileAsync(options: IProjectConfigurationFileSpecification, terminal: ITerminal): Promise; } +// @public (undocumented) +export class HeftPhase { + // Warning: (ae-forgotten-export) The symbol "InternalHeftSession" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "IHeftConfigurationJsonPhaseSpecifier" needs to be exported by the entry point index.d.ts + constructor(internalHeftSession: InternalHeftSession, phaseName: string, phaseSpecifier: IHeftConfigurationJsonPhaseSpecifier); + get cleanFiles(): ReadonlySet; + get consumingPhases(): ReadonlySet; + get dependencyPhases(): ReadonlySet; + get phaseDescription(): string | undefined; + get phaseName(): string; + // Warning: (ae-incompatible-release-tags) The symbol "tasks" is marked as @public, but its signature references "HeftTask" which is marked as @internal + get tasks(): ReadonlySet; + // Warning: (ae-incompatible-release-tags) The symbol "tasksByName" is marked as @public, but its signature references "HeftTask" which is marked as @internal + get tasksByName(): ReadonlyMap; +} + +// Warning: (ae-internal-missing-underscore) The name "HeftTask" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export class HeftTask { + // Warning: (ae-forgotten-export) The symbol "IHeftConfigurationJsonTaskSpecifier" needs to be exported by the entry point index.d.ts + constructor(parentPhase: HeftPhase, taskName: string, taskSpecifier: IHeftConfigurationJsonTaskSpecifier); + // (undocumented) + get consumingTasks(): ReadonlySet; + // (undocumented) + get dependencyTasks(): Set; + // (undocumented) + ensureInitializedAsync(): Promise; + // (undocumented) + getPluginAsync(logger: IScopedLogger): Promise>; + // (undocumented) + get parentPhase(): HeftPhase; + // Warning: (ae-forgotten-export) The symbol "HeftTaskPluginDefinition" needs to be exported by the entry point index.d.ts + // + // (undocumented) + get pluginDefinition(): HeftTaskPluginDefinition; + // (undocumented) + get pluginOptions(): object | undefined; + // (undocumented) + get taskName(): string; +} + // @public export interface ICopyOperation extends IFileSelectionSpecifier { destinationFolders: string[]; @@ -205,9 +247,11 @@ export interface IHeftParsedCommandLine { // @public (undocumented) export interface IHeftPhaseFinishHookOptions { // (undocumented) - operation: OperationGroupRecord; - // Warning: (ae-forgotten-export) The symbol "HeftPhase" needs to be exported by the entry point index.d.ts - // + operation: OperationGroupRecord; +} + +// @public +export interface IHeftPhaseOperationMetadata { // (undocumented) phase: HeftPhase; } @@ -215,9 +259,7 @@ export interface IHeftPhaseFinishHookOptions { // @public (undocumented) export interface IHeftPhaseStartHookOptions { // (undocumented) - operation: OperationGroupRecord; - // (undocumented) - phase: HeftPhase; + operation: OperationGroupRecord; } // @public @@ -243,11 +285,7 @@ export interface IHeftTaskFileOperations { // @public (undocumented) export interface IHeftTaskFinishHookOptions { // (undocumented) - operation: Operation; - // Warning: (ae-forgotten-export) The symbol "HeftTask" needs to be exported by the entry point index.d.ts - // - // (undocumented) - task: HeftTask; + operation: Operation; } // @public @@ -257,6 +295,16 @@ export interface IHeftTaskHooks { readonly runIncremental: AsyncParallelHook; } +// @public +export interface IHeftTaskOperationMetadata { + // (undocumented) + phase: HeftPhase; + // Warning: (ae-incompatible-release-tags) The symbol "task" is marked as @public, but its signature references "HeftTask" which is marked as @internal + // + // (undocumented) + task: HeftTask; +} + // @public export interface IHeftTaskPlugin extends IHeftPlugin { } @@ -289,9 +337,7 @@ export interface IHeftTaskSession { // @public (undocumented) export interface IHeftTaskStartHookOptions { // (undocumented) - operation: Operation; - // (undocumented) - task: HeftTask; + operation: Operation; } // @public From 72568499fb9047a36428227d71112ea0acfbaf6d Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 11 Jun 2025 16:50:06 -0400 Subject: [PATCH 10/21] create interfaces for task + phase Signed-off-by: Aramis Sennyey --- apps/heft/src/cli/HeftActionRunner.ts | 10 +-- apps/heft/src/index.ts | 4 +- apps/heft/src/pluginFramework/HeftPhase.ts | 20 +++++- apps/heft/src/pluginFramework/HeftTask.ts | 14 +++- common/reviews/api/heft.api.md | 83 +++++++++------------- 5 files changed, 70 insertions(+), 61 deletions(-) diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index 280027b7feb..c4d716cdbab 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -29,7 +29,7 @@ import type { MetricsCollector } from '../metrics/MetricsCollector'; import { HeftParameterManager } from '../pluginFramework/HeftParameterManager'; import { TaskOperationRunner } from '../operations/runners/TaskOperationRunner'; import { PhaseOperationRunner } from '../operations/runners/PhaseOperationRunner'; -import type { HeftPhase } from '../pluginFramework/HeftPhase'; +import type { IHeftPhase, HeftPhase } from '../pluginFramework/HeftPhase'; import type { IHeftAction, IHeftActionOptions } from './actions/IHeftAction'; import type { IHeftLifecycleCleanHookOptions, @@ -38,7 +38,7 @@ import type { IHeftLifecycleToolStartHookOptions } from '../pluginFramework/HeftLifecycleSession'; import type { HeftLifecycle } from '../pluginFramework/HeftLifecycle'; -import type { HeftTask } from '../pluginFramework/HeftTask'; +import type { IHeftTask, HeftTask } from '../pluginFramework/HeftTask'; import { deleteFilesAsync, type IDeleteOperation } from '../plugins/DeleteFilesPlugin'; import { Constants } from '../utilities/Constants'; @@ -51,8 +51,8 @@ export interface IHeftActionRunnerOptions extends IHeftActionOptions { * @public */ export interface IHeftTaskOperationMetadata { - task: HeftTask; - phase: HeftPhase; + task: IHeftTask; + phase: IHeftPhase; } /** @@ -60,7 +60,7 @@ export interface IHeftTaskOperationMetadata { * @public */ export interface IHeftPhaseOperationMetadata { - phase: HeftPhase; + phase: IHeftPhase; } export function initializeHeft( diff --git a/apps/heft/src/index.ts b/apps/heft/src/index.ts index 872f9cbddbd..0c6006dfc8c 100644 --- a/apps/heft/src/index.ts +++ b/apps/heft/src/index.ts @@ -87,5 +87,5 @@ export type { export type { IHeftTaskOperationMetadata } from './cli/HeftActionRunner'; export type { IHeftPhaseOperationMetadata } from './cli/HeftActionRunner'; -export type { HeftTask } from './pluginFramework/HeftTask'; -export type { HeftPhase } from './pluginFramework/HeftPhase'; +export type { IHeftTask } from './pluginFramework/HeftTask'; +export type { IHeftPhase } from './pluginFramework/HeftPhase'; diff --git a/apps/heft/src/pluginFramework/HeftPhase.ts b/apps/heft/src/pluginFramework/HeftPhase.ts index 1a6de503ef9..e7ac8e65ca6 100644 --- a/apps/heft/src/pluginFramework/HeftPhase.ts +++ b/apps/heft/src/pluginFramework/HeftPhase.ts @@ -1,14 +1,30 @@ // Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. // See LICENSE in the project root for license information. -import { HeftTask } from './HeftTask'; +import { HeftTask, type IHeftTask } from './HeftTask'; import type { InternalHeftSession } from './InternalHeftSession'; import type { IHeftConfigurationJsonPhaseSpecifier } from '../utilities/CoreConfigFiles'; import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin'; const RESERVED_PHASE_NAMES: Set = new Set(['lifecycle']); -export class HeftPhase { +/** + * @public + */ +export interface IHeftPhase { + readonly phaseName: string; + readonly phaseDescription: string | undefined; + cleanFiles: ReadonlySet; + consumingPhases: ReadonlySet; + dependencyPhases: ReadonlySet; + tasks: ReadonlySet; + tasksByName: ReadonlyMap; +} + +/** + * @internal + */ +export class HeftPhase implements IHeftPhase { private _internalHeftSession: InternalHeftSession; private _phaseName: string; private _phaseSpecifier: IHeftConfigurationJsonPhaseSpecifier; diff --git a/apps/heft/src/pluginFramework/HeftTask.ts b/apps/heft/src/pluginFramework/HeftTask.ts index b7b8c539126..d5f098bd1d8 100644 --- a/apps/heft/src/pluginFramework/HeftTask.ts +++ b/apps/heft/src/pluginFramework/HeftTask.ts @@ -8,7 +8,7 @@ import type { HeftTaskPluginDefinition, HeftPluginDefinitionBase } from '../configuration/HeftPluginDefinition'; -import type { HeftPhase } from './HeftPhase'; +import type { HeftPhase, IHeftPhase } from './HeftPhase'; import type { IHeftConfigurationJsonTaskSpecifier, IHeftConfigurationJsonPluginSpecifier @@ -18,10 +18,20 @@ import type { IScopedLogger } from './logging/ScopedLogger'; const RESERVED_TASK_NAMES: Set = new Set(['clean']); +/** + * @public + */ +export interface IHeftTask { + readonly parentPhase: IHeftPhase; + readonly taskName: string; + readonly consumingTasks: ReadonlySet; + readonly dependencyTasks: ReadonlySet; +} + /** * @internal */ -export class HeftTask { +export class HeftTask implements IHeftTask { private _parentPhase: HeftPhase; private _taskName: string; private _taskSpecifier: IHeftConfigurationJsonTaskSpecifier; diff --git a/common/reviews/api/heft.api.md b/common/reviews/api/heft.api.md index cd0c01feb76..caab5d5e14f 100644 --- a/common/reviews/api/heft.api.md +++ b/common/reviews/api/heft.api.md @@ -14,15 +14,12 @@ import { CommandLineFlagParameter } from '@rushstack/ts-command-line'; import { CommandLineIntegerListParameter } from '@rushstack/ts-command-line'; import { CommandLineIntegerParameter } from '@rushstack/ts-command-line'; import { CommandLineParameter } from '@rushstack/ts-command-line'; -import { CommandLineParameterProvider } from '@rushstack/ts-command-line'; import { CommandLineStringListParameter } from '@rushstack/ts-command-line'; import { CommandLineStringParameter } from '@rushstack/ts-command-line'; import { CustomValidationFunction } from '@rushstack/heft-config-file'; -import { FileLocationStyle } from '@rushstack/node-core-library'; import * as fs from 'fs'; import { ICustomJsonPathMetadata } from '@rushstack/heft-config-file'; import { ICustomPropertyInheritance } from '@rushstack/heft-config-file'; -import { IFileErrorFormattingOptions } from '@rushstack/node-core-library'; import { IJsonPathMetadata } from '@rushstack/heft-config-file'; import { IJsonPathMetadataResolverOptions } from '@rushstack/heft-config-file'; import { IJsonPathsMetadata } from '@rushstack/heft-config-file'; @@ -103,48 +100,6 @@ export class HeftConfiguration { tryLoadProjectConfigurationFileAsync(options: IProjectConfigurationFileSpecification, terminal: ITerminal): Promise; } -// @public (undocumented) -export class HeftPhase { - // Warning: (ae-forgotten-export) The symbol "InternalHeftSession" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "IHeftConfigurationJsonPhaseSpecifier" needs to be exported by the entry point index.d.ts - constructor(internalHeftSession: InternalHeftSession, phaseName: string, phaseSpecifier: IHeftConfigurationJsonPhaseSpecifier); - get cleanFiles(): ReadonlySet; - get consumingPhases(): ReadonlySet; - get dependencyPhases(): ReadonlySet; - get phaseDescription(): string | undefined; - get phaseName(): string; - // Warning: (ae-incompatible-release-tags) The symbol "tasks" is marked as @public, but its signature references "HeftTask" which is marked as @internal - get tasks(): ReadonlySet; - // Warning: (ae-incompatible-release-tags) The symbol "tasksByName" is marked as @public, but its signature references "HeftTask" which is marked as @internal - get tasksByName(): ReadonlyMap; -} - -// Warning: (ae-internal-missing-underscore) The name "HeftTask" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) -export class HeftTask { - // Warning: (ae-forgotten-export) The symbol "IHeftConfigurationJsonTaskSpecifier" needs to be exported by the entry point index.d.ts - constructor(parentPhase: HeftPhase, taskName: string, taskSpecifier: IHeftConfigurationJsonTaskSpecifier); - // (undocumented) - get consumingTasks(): ReadonlySet; - // (undocumented) - get dependencyTasks(): Set; - // (undocumented) - ensureInitializedAsync(): Promise; - // (undocumented) - getPluginAsync(logger: IScopedLogger): Promise>; - // (undocumented) - get parentPhase(): HeftPhase; - // Warning: (ae-forgotten-export) The symbol "HeftTaskPluginDefinition" needs to be exported by the entry point index.d.ts - // - // (undocumented) - get pluginDefinition(): HeftTaskPluginDefinition; - // (undocumented) - get pluginOptions(): object | undefined; - // (undocumented) - get taskName(): string; -} - // @public export interface ICopyOperation extends IFileSelectionSpecifier { destinationFolders: string[]; @@ -244,6 +199,24 @@ export interface IHeftParsedCommandLine { readonly unaliasedCommandName: string; } +// @public (undocumented) +export interface IHeftPhase { + // (undocumented) + cleanFiles: ReadonlySet; + // (undocumented) + consumingPhases: ReadonlySet; + // (undocumented) + dependencyPhases: ReadonlySet; + // (undocumented) + readonly phaseDescription: string | undefined; + // (undocumented) + readonly phaseName: string; + // (undocumented) + tasks: ReadonlySet; + // (undocumented) + tasksByName: ReadonlyMap; +} + // @public (undocumented) export interface IHeftPhaseFinishHookOptions { // (undocumented) @@ -253,7 +226,7 @@ export interface IHeftPhaseFinishHookOptions { // @public export interface IHeftPhaseOperationMetadata { // (undocumented) - phase: HeftPhase; + phase: IHeftPhase; } // @public (undocumented) @@ -276,6 +249,18 @@ export interface IHeftRecordMetricsHookOptions { metricName: string; } +// @public (undocumented) +export interface IHeftTask { + // (undocumented) + readonly consumingTasks: ReadonlySet; + // (undocumented) + readonly dependencyTasks: ReadonlySet; + // (undocumented) + readonly parentPhase: IHeftPhase; + // (undocumented) + readonly taskName: string; +} + // @public export interface IHeftTaskFileOperations { copyOperations: Set; @@ -298,11 +283,9 @@ export interface IHeftTaskHooks { // @public export interface IHeftTaskOperationMetadata { // (undocumented) - phase: HeftPhase; - // Warning: (ae-incompatible-release-tags) The symbol "task" is marked as @public, but its signature references "HeftTask" which is marked as @internal - // + phase: IHeftPhase; // (undocumented) - task: HeftTask; + task: IHeftTask; } // @public From 3cc0827189228fd921e5d023e5459444bdf67e67 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Wed, 11 Jun 2025 17:30:02 -0400 Subject: [PATCH 11/21] ensure that metadata is populated Signed-off-by: Aramis Sennyey --- apps/heft/src/cli/HeftActionRunner.ts | 20 ++++++++---- .../src/index.ts | 32 +++++++++++++++---- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index c4d716cdbab..58baa755bdc 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -453,7 +453,7 @@ export class HeftActionRunner { } // Create operation for the phase start node - const phaseOperation: Operation = _getOrCreatePhaseOperation( + const phaseOperation: Operation = _getOrCreatePhaseOperation( internalHeftSession, phase, operations @@ -498,16 +498,19 @@ function _getOrCreatePhaseOperation( this: void, internalHeftSession: InternalHeftSession, phase: HeftPhase, - operations: Map> -): Operation { + operations: Map +): Operation { const key: string = phase.phaseName; - let operation: Operation | undefined = operations.get(key); + let operation: Operation | undefined = operations.get( + key + ) as Operation; if (!operation) { // Only create the operation. Dependencies are hooked up separately operation = new Operation({ groupName: phase.phaseName, - runner: new PhaseOperationRunner({ phase, internalHeftSession }) + runner: new PhaseOperationRunner({ phase, internalHeftSession }), + metadata: { phase } }); operations.set(key, operation); } @@ -522,14 +525,17 @@ function _getOrCreateTaskOperation( ): Operation { const key: string = `${task.parentPhase.phaseName}.${task.taskName}`; - let operation: Operation | undefined = operations.get(key); + let operation: Operation | undefined = operations.get( + key + ) as Operation; if (!operation) { operation = new Operation({ groupName: task.parentPhase.phaseName, runner: new TaskOperationRunner({ internalHeftSession, task - }) + }), + metadata: { task, phase: task.parentPhase } }); operations.set(key, operation); } diff --git a/build-tests/heft-example-lifecycle-plugin/src/index.ts b/build-tests/heft-example-lifecycle-plugin/src/index.ts index 23cd0347943..993db618ad4 100644 --- a/build-tests/heft-example-lifecycle-plugin/src/index.ts +++ b/build-tests/heft-example-lifecycle-plugin/src/index.ts @@ -16,27 +16,45 @@ export default class ExampleLifecyclePlugin implements IHeftLifecyclePlugin { public apply(session: IHeftLifecycleSession): void { const { logger } = session; session.hooks.taskFinish.tapPromise(PLUGIN_NAME, async (options: IHeftTaskFinishHookOptions) => { - const { operation, task } = options; - if (operation.state) { + const { + operation: { + metadata: { task }, + state + } + } = options; + if (state) { logger.terminal.writeLine( - `--- ${task.taskName} finished in ${operation.state.stopwatch.duration.toFixed(2)}s ---` + `--- ${task.taskName} finished in ${state.stopwatch.duration.toFixed(2)}s ---` ); } }); session.hooks.taskStart.tapPromise(PLUGIN_NAME, async (options: IHeftTaskStartHookOptions) => { - const { task } = options; + const { + operation: { + metadata: { task } + } + } = options; logger.terminal.writeLine(`--- ${task.taskName} started ---`); }); session.hooks.phaseStart.tapPromise(PLUGIN_NAME, async (options: IHeftPhaseStartHookOptions) => { - const { phase } = options; + const { + operation: { + metadata: { phase } + } + } = options; logger.terminal.writeLine(`--- ${phase.phaseName} started ---`); }); session.hooks.phaseFinish.tapPromise(PLUGIN_NAME, async (options: IHeftPhaseFinishHookOptions) => { - const { phase, operation } = options; - logger.terminal.writeLine(`--- ${phase.phaseName} finished in ${operation.duration.toFixed(2)}s ---`); + const { + operation: { + metadata: { phase }, + duration + } + } = options; + logger.terminal.writeLine(`--- ${phase.phaseName} finished in ${duration.toFixed(2)}s ---`); }); } } From 61a220f48482d4bb79f82b88d94c66334c08e7f2 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Thu, 12 Jun 2025 09:09:00 -0400 Subject: [PATCH 12/21] remove operation on operationGroup events Signed-off-by: Aramis Sennyey --- apps/heft/src/cli/HeftActionRunner.ts | 10 ++++------ common/reviews/api/operation-graph.api.md | 4 ++-- .../src/OperationExecutionManager.ts | 14 ++++---------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index 58baa755bdc..f7cf74129cf 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -399,18 +399,16 @@ export class HeftActionRunner { } }, beforeExecuteOperationGroupAsync: async ( - operationGroup: OperationGroupRecord, - operation: Operation + operationGroup: OperationGroupRecord ) => { - if (operation.metadata.phase && phaseStart.isUsed()) { + if (operationGroup.metadata.phase && phaseStart.isUsed()) { await phaseStart.promise({ operation: operationGroup }); } }, afterExecuteOperationGroupAsync: async ( - operationGroup: OperationGroupRecord, - operation: Operation + operationGroup: OperationGroupRecord ) => { - if (operation.metadata.phase && phaseFinish.isUsed()) { + if (operationGroup.metadata.phase && phaseFinish.isUsed()) { await phaseFinish.promise({ operation: operationGroup }); } } diff --git a/common/reviews/api/operation-graph.api.md b/common/reviews/api/operation-graph.api.md index 8230ae22db1..41633262529 100644 --- a/common/reviews/api/operation-graph.api.md +++ b/common/reviews/api/operation-graph.api.md @@ -50,11 +50,11 @@ export interface IOperationExecutionOptions) => Promise; // (undocumented) - afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord, operation: Operation) => Promise; + afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; // (undocumented) beforeExecuteOperationAsync?: (operation: Operation) => Promise; // (undocumented) - beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord, operation: Operation) => Promise; + beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; // (undocumented) parallelism: number; // (undocumented) diff --git a/libraries/operation-graph/src/OperationExecutionManager.ts b/libraries/operation-graph/src/OperationExecutionManager.ts index 348f36fc9c9..4eba9a29638 100644 --- a/libraries/operation-graph/src/OperationExecutionManager.ts +++ b/libraries/operation-graph/src/OperationExecutionManager.ts @@ -28,14 +28,8 @@ export interface IOperationExecutionOptions< beforeExecuteOperationAsync?: (operation: Operation) => Promise; afterExecuteOperationAsync?: (operation: Operation) => Promise; - beforeExecuteOperationGroupAsync?: ( - operationGroup: OperationGroupRecord, - operation: Operation - ) => Promise; - afterExecuteOperationGroupAsync?: ( - operationGroup: OperationGroupRecord, - operation: Operation - ) => Promise; + beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; + afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; } /** @@ -159,7 +153,7 @@ export class OperationExecutionManager Date: Thu, 12 Jun 2025 10:43:08 -0400 Subject: [PATCH 13/21] move operation grouping concern to caller Signed-off-by: Aramis Sennyey --- apps/heft/src/cli/HeftActionRunner.ts | 69 +++++++++++----- common/reviews/api/operation-graph.api.md | 24 +++--- libraries/operation-graph/src/Operation.ts | 31 ++++--- .../src/OperationExecutionManager.ts | 80 +++++++------------ 4 files changed, 112 insertions(+), 92 deletions(-) diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index f7cf74129cf..a4125062abd 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -12,7 +12,7 @@ import { type IWatchLoopState, Operation, OperationExecutionManager, - type OperationGroupRecord, + OperationGroupRecord, OperationStatus, WatchLoop } from '@rushstack/operation-graph'; @@ -63,6 +63,11 @@ export interface IHeftPhaseOperationMetadata { phase: IHeftPhase; } +/** + * @internal + */ +export type HeftOperationMetadata = IHeftTaskOperationMetadata | IHeftPhaseOperationMetadata; + export function initializeHeft( heftConfiguration: HeftConfiguration, terminal: ITerminal, @@ -309,15 +314,11 @@ export class HeftActionRunner { initializeHeft(this._heftConfiguration, terminal, this.parameterManager.defaultParameters.verbose); - const operations: ReadonlySet> = this._generateOperations(); + const operations: ReadonlySet> = + this._generateOperations(); - const executionManager: OperationExecutionManager< - IHeftTaskOperationMetadata, - IHeftPhaseOperationMetadata - > = new OperationExecutionManager( - operations, - (metadata) => ({ phase: metadata.phase }) - ); + const executionManager: OperationExecutionManager = + new OperationExecutionManager(operations); const cliAbortSignal: AbortSignal = ensureCliAbortSignal(this._terminal); @@ -425,10 +426,14 @@ export class HeftActionRunner { ); } - private _generateOperations(): Set> { + private _generateOperations(): Set> { const { selectedPhases } = this._action; - const operations: Map> = new Map(); + const operations: Map< + string, + Operation + > = new Map(); + const operationGroups: Map> = new Map(); const internalHeftSession: InternalHeftSession = this._internalHeftSession; let hasWarnedAboutSkippedPhases: boolean = false; @@ -454,19 +459,25 @@ export class HeftActionRunner { const phaseOperation: Operation = _getOrCreatePhaseOperation( internalHeftSession, phase, - operations + operations, + operationGroups ); // Create operations for each task for (const task of phase.tasks) { - const taskOperation: Operation = _getOrCreateTaskOperation(internalHeftSession, task, operations); + const taskOperation: Operation = _getOrCreateTaskOperation( + internalHeftSession, + task, + operations, + operationGroups + ); // Set the phase operation as a dependency of the task operation to ensure the phase operation runs first taskOperation.addDependency(phaseOperation); // Set all dependency tasks as dependencies of the task operation for (const dependencyTask of task.dependencyTasks) { taskOperation.addDependency( - _getOrCreateTaskOperation(internalHeftSession, dependencyTask, operations) + _getOrCreateTaskOperation(internalHeftSession, dependencyTask, operations, operationGroups) ); } @@ -478,7 +489,8 @@ export class HeftActionRunner { const consumingPhaseOperation: Operation = _getOrCreatePhaseOperation( internalHeftSession, consumingPhase, - operations + operations, + operationGroups ); consumingPhaseOperation.addDependency(taskOperation); // This is purely to simplify the reported graph for phase circularities @@ -496,7 +508,8 @@ function _getOrCreatePhaseOperation( this: void, internalHeftSession: InternalHeftSession, phase: HeftPhase, - operations: Map + operations: Map, + operationGroups: Map> ): Operation { const key: string = phase.phaseName; @@ -504,9 +517,17 @@ function _getOrCreatePhaseOperation( key ) as Operation; if (!operation) { + let group: OperationGroupRecord | undefined = operationGroups.get( + phase.phaseName + ); + if (!group) { + group = new OperationGroupRecord(phase.phaseName, { phase }); + operationGroups.set(phase.phaseName, group); + } // Only create the operation. Dependencies are hooked up separately operation = new Operation({ - groupName: phase.phaseName, + group, + name: phase.phaseName, runner: new PhaseOperationRunner({ phase, internalHeftSession }), metadata: { phase } }); @@ -519,7 +540,8 @@ function _getOrCreateTaskOperation( this: void, internalHeftSession: InternalHeftSession, task: HeftTask, - operations: Map + operations: Map, + operationGroups: Map> ): Operation { const key: string = `${task.parentPhase.phaseName}.${task.taskName}`; @@ -527,12 +549,21 @@ function _getOrCreateTaskOperation( key ) as Operation; if (!operation) { + const group: OperationGroupRecord | undefined = operationGroups.get( + task.parentPhase.phaseName + ); + if (!group) { + throw new InternalError( + `Task ${task.taskName} in phase ${task.parentPhase.phaseName} has no group. This should not happen.` + ); + } operation = new Operation({ - groupName: task.parentPhase.phaseName, + group, runner: new TaskOperationRunner({ internalHeftSession, task }), + name: task.taskName, metadata: { task, phase: task.parentPhase } }); operations.set(key, operation); diff --git a/common/reviews/api/operation-graph.api.md b/common/reviews/api/operation-graph.api.md index 41633262529..d8d0790b69f 100644 --- a/common/reviews/api/operation-graph.api.md +++ b/common/reviews/api/operation-graph.api.md @@ -48,11 +48,11 @@ export interface IOperationExecutionOptions) => Promise; + afterExecuteOperationAsync?: (operation: Operation) => Promise; // (undocumented) afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; // (undocumented) - beforeExecuteOperationAsync?: (operation: Operation) => Promise; + beforeExecuteOperationAsync?: (operation: Operation) => Promise; // (undocumented) beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; // (undocumented) @@ -64,8 +64,8 @@ export interface IOperationExecutionOptions { - groupName?: string | undefined; +export interface IOperationOptions { + group?: OperationGroupRecord | undefined; metadata?: TMetadata | undefined; name?: string | undefined; runner?: IOperationRunner | undefined; @@ -148,18 +148,18 @@ export interface IWatchLoopState { } // @beta -export class Operation implements IOperationStates { - constructor(options?: IOperationOptions); +export class Operation implements IOperationStates { + constructor(options?: IOperationOptions); // (undocumented) - addDependency(dependency: Operation): void; - readonly consumers: Set>; + addDependency(dependency: Operation): void; + readonly consumers: Set>; criticalPathLength: number | undefined; // (undocumented) - deleteDependency(dependency: Operation): void; - readonly dependencies: Set>; + deleteDependency(dependency: Operation): void; + readonly dependencies: Set>; // @internal (undocumented) _executeAsync(context: IExecuteOperationContext): Promise; - readonly groupName: string | undefined; + readonly group: OperationGroupRecord | undefined; lastState: IOperationState | undefined; // (undocumented) readonly metadata: TMetadata; @@ -184,7 +184,7 @@ export class OperationError extends Error { // @beta export class OperationExecutionManager { - constructor(operations: ReadonlySet>, deriveGroupMetadata?: (metadata: TOperationMetadata) => TGroupMetadata); + constructor(operations: ReadonlySet>); executeAsync(executionOptions: IOperationExecutionOptions): Promise; } diff --git a/libraries/operation-graph/src/Operation.ts b/libraries/operation-graph/src/Operation.ts index fbe547ec0c7..23dc4455c00 100644 --- a/libraries/operation-graph/src/Operation.ts +++ b/libraries/operation-graph/src/Operation.ts @@ -13,12 +13,13 @@ import type { } from './IOperationRunner'; import type { OperationError } from './OperationError'; import { OperationStatus } from './OperationStatus'; +import { OperationGroupRecord } from './OperationGroupRecord'; /** * Options for constructing a new Operation. * @beta */ -export interface IOperationOptions { +export interface IOperationOptions { /** * The name of this operation, for logging. */ @@ -27,7 +28,7 @@ export interface IOperationOptions { /** * The group that this operation belongs to. Will be used for logging and duration tracking. */ - groupName?: string | undefined; + group?: OperationGroupRecord | undefined; /** * When the scheduler is ready to process this `Operation`, the `runner` implements the actual work of @@ -90,19 +91,25 @@ export interface IExecuteOperationContext extends Omit implements IOperationStates { +export class Operation + implements IOperationStates +{ /** * A set of all dependencies which must be executed before this operation is complete. */ - public readonly dependencies: Set> = new Set>(); + public readonly dependencies: Set> = new Set< + Operation + >(); /** * A set of all operations that wait for this operation. */ - public readonly consumers: Set> = new Set>(); + public readonly consumers: Set> = new Set< + Operation + >(); /** * If specified, the name of a grouping to which this Operation belongs, for logging start and end times. */ - public readonly groupName: string | undefined; + public readonly group: OperationGroupRecord | undefined; /** * The name of this operation, for logging. */ @@ -181,20 +188,24 @@ export class Operation implements IOperationStates { public readonly metadata: TMetadata; - public constructor(options?: IOperationOptions) { - this.groupName = options?.groupName; + public constructor(options?: IOperationOptions) { + this.group = options?.group; this.runner = options?.runner; this.weight = options?.weight || 1; this.name = options?.name; this.metadata = options?.metadata || ({} as TMetadata); + + if (this.group) { + this.group.addOperation(this); + } } - public addDependency(dependency: Operation): void { + public addDependency(dependency: Operation): void { this.dependencies.add(dependency); dependency.consumers.add(this); } - public deleteDependency(dependency: Operation): void { + public deleteDependency(dependency: Operation): void { this.dependencies.delete(dependency); dependency.consumers.delete(this); } diff --git a/libraries/operation-graph/src/OperationExecutionManager.ts b/libraries/operation-graph/src/OperationExecutionManager.ts index 4eba9a29638..74b998b64bd 100644 --- a/libraries/operation-graph/src/OperationExecutionManager.ts +++ b/libraries/operation-graph/src/OperationExecutionManager.ts @@ -26,8 +26,8 @@ export interface IOperationExecutionOptions< requestRun?: (requestor?: string) => void; - beforeExecuteOperationAsync?: (operation: Operation) => Promise; - afterExecuteOperationAsync?: (operation: Operation) => Promise; + beforeExecuteOperationAsync?: (operation: Operation) => Promise; + afterExecuteOperationAsync?: (operation: Operation) => Promise; beforeExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; afterExecuteOperationGroupAsync?: (operationGroup: OperationGroupRecord) => Promise; } @@ -44,36 +44,16 @@ export class OperationExecutionManager>; + private readonly _operations: Operation[]; /** * The total number of non-silent operations in the graph. * Silent operations are generally used to simplify the construction of the graph. */ private readonly _trackedOperationCount: number; - public constructor( - operations: ReadonlySet>, - deriveGroupMetadata: (metadata: TOperationMetadata) => TGroupMetadata = () => ({}) as TGroupMetadata - ) { - const groupRecordByName: Map> = new Map(); - this._groupRecordByName = groupRecordByName; - + public constructor(operations: ReadonlySet>) { let trackedOperationCount: number = 0; for (const operation of operations) { - const { groupName } = operation; - let group: OperationGroupRecord | undefined = undefined; - if (groupName && !(group = groupRecordByName.get(groupName))) { - group = new OperationGroupRecord(groupName, deriveGroupMetadata(operation.metadata)); - groupRecordByName.set(groupName, group); - } - - group?.addOperation(operation); - if (!operation.runner?.silent) { // Only count non-silent operations trackedOperationCount++; @@ -115,8 +95,10 @@ export class OperationExecutionManager = new Set(); const maxParallelism: number = Math.min(this._operations.length, parallelism); - const groupRecords: Map> = this._groupRecordByName; - for (const groupRecord of groupRecords.values()) { + const groupRecords: Set> = new Set( + [...this._operations].map((e) => e.group).filter((e) => e !== undefined) + ); + for (const groupRecord of groupRecords) { groupRecord.reset(); } @@ -142,33 +124,29 @@ export class OperationExecutionManager): Promise => { + beforeExecuteAsync: async ( + operation: Operation + ): Promise => { // Initialize group if uninitialized and log the group name - const { groupName } = operation; - const groupRecord: OperationGroupRecord | undefined = groupName - ? groupRecords.get(groupName) - : undefined; - if (groupRecord) { - if (!startedGroups.has(groupRecord)) { - startedGroups.add(groupRecord); - groupRecord.startTimer(); - terminal.writeLine(` ---- ${groupRecord.name} started ---- `); - await executionOptions.beforeExecuteOperationGroupAsync?.(groupRecord); + const { group } = operation; + if (group) { + if (!startedGroups.has(group)) { + startedGroups.add(group); + group.startTimer(); + terminal.writeLine(` ---- ${group.name} started ---- `); + await executionOptions.beforeExecuteOperationGroupAsync?.(group); } } await executionOptions.beforeExecuteOperationAsync?.(operation); }, afterExecuteAsync: async ( - operation: Operation, + operation: Operation, state: IOperationState ): Promise => { - const { groupName } = operation; - const groupRecord: OperationGroupRecord | undefined = groupName - ? groupRecords.get(groupName) - : undefined; - if (groupRecord) { - groupRecord.setOperationAsComplete(operation, state); + const { group } = operation; + if (group) { + group.setOperationAsComplete(operation, state); } if (state.status === OperationStatus.Failure) { @@ -183,19 +161,19 @@ export class OperationExecutionManager Date: Thu, 12 Jun 2025 10:48:56 -0400 Subject: [PATCH 14/21] fix types Signed-off-by: Aramis Sennyey --- apps/heft/src/cli/HeftActionRunner.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index a4125062abd..bc4fe28a679 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -371,7 +371,7 @@ export class HeftActionRunner { } private async _executeOnceAsync( - executionManager: OperationExecutionManager, + executionManager: OperationExecutionManager, abortSignal: AbortSignal, requestRun?: (requestor?: string) => void ): Promise { @@ -382,21 +382,25 @@ export class HeftActionRunner { return await runWithLoggingAsync( () => { const operationExecutionManagerOptions: IOperationExecutionOptions< - IHeftTaskOperationMetadata, + HeftOperationMetadata, IHeftPhaseOperationMetadata > = { terminal: this._terminal, parallelism: this._parallelism, abortSignal, requestRun, - beforeExecuteOperationAsync: async (operation: Operation) => { - if (operation.metadata.task && taskStart.isUsed()) { - await taskStart.promise({ operation }); + beforeExecuteOperationAsync: async ( + operation: Operation + ) => { + if ('task' in operation.metadata && taskStart.isUsed()) { + await taskStart.promise({ operation: operation as Operation }); } }, - afterExecuteOperationAsync: async (operation: Operation) => { - if (operation.metadata.task && taskFinish.isUsed()) { - await taskFinish.promise({ operation }); + afterExecuteOperationAsync: async ( + operation: Operation + ) => { + if ('task' in operation.metadata && taskFinish.isUsed()) { + await taskFinish.promise({ operation: operation as Operation }); } }, beforeExecuteOperationGroupAsync: async ( From 4c411eb88db9a8b9a9795eb04422498594b93e4a Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Thu, 12 Jun 2025 11:06:08 -0400 Subject: [PATCH 15/21] fix imports Signed-off-by: Aramis Sennyey --- libraries/operation-graph/src/Operation.ts | 2 +- libraries/operation-graph/src/OperationExecutionManager.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/operation-graph/src/Operation.ts b/libraries/operation-graph/src/Operation.ts index 23dc4455c00..c340ef9f434 100644 --- a/libraries/operation-graph/src/Operation.ts +++ b/libraries/operation-graph/src/Operation.ts @@ -13,7 +13,7 @@ import type { } from './IOperationRunner'; import type { OperationError } from './OperationError'; import { OperationStatus } from './OperationStatus'; -import { OperationGroupRecord } from './OperationGroupRecord'; +import type { OperationGroupRecord } from './OperationGroupRecord'; /** * Options for constructing a new Operation. diff --git a/libraries/operation-graph/src/OperationExecutionManager.ts b/libraries/operation-graph/src/OperationExecutionManager.ts index 74b998b64bd..70ad56a7b33 100644 --- a/libraries/operation-graph/src/OperationExecutionManager.ts +++ b/libraries/operation-graph/src/OperationExecutionManager.ts @@ -6,7 +6,7 @@ import type { ITerminal } from '@rushstack/terminal'; import type { IOperationState } from './IOperationRunner'; import type { IExecuteOperationContext, Operation } from './Operation'; -import { OperationGroupRecord } from './OperationGroupRecord'; +import type { OperationGroupRecord } from './OperationGroupRecord'; import { OperationStatus } from './OperationStatus'; import { calculateCriticalPathLengths } from './calculateCriticalPath'; import { WorkQueue } from './WorkQueue'; From a74cbc6b95e145296118a797a515a85c1305c513 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Thu, 12 Jun 2025 18:58:58 -0400 Subject: [PATCH 16/21] don't consider phase nodes in the return type Signed-off-by: Aramis Sennyey --- apps/heft/src/cli/HeftActionRunner.ts | 32 ++++++++----------- ...nyeya-heft-lifecycle_2025-06-11-16-53.json | 2 +- .../src/OperationExecutionManager.ts | 2 +- 3 files changed, 15 insertions(+), 21 deletions(-) diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index bc4fe28a679..ed86344bcca 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -63,11 +63,6 @@ export interface IHeftPhaseOperationMetadata { phase: IHeftPhase; } -/** - * @internal - */ -export type HeftOperationMetadata = IHeftTaskOperationMetadata | IHeftPhaseOperationMetadata; - export function initializeHeft( heftConfiguration: HeftConfiguration, terminal: ITerminal, @@ -314,11 +309,13 @@ export class HeftActionRunner { initializeHeft(this._heftConfiguration, terminal, this.parameterManager.defaultParameters.verbose); - const operations: ReadonlySet> = + const operations: ReadonlySet> = this._generateOperations(); - const executionManager: OperationExecutionManager = - new OperationExecutionManager(operations); + const executionManager: OperationExecutionManager< + IHeftTaskOperationMetadata, + IHeftPhaseOperationMetadata + > = new OperationExecutionManager(operations); const cliAbortSignal: AbortSignal = ensureCliAbortSignal(this._terminal); @@ -371,7 +368,7 @@ export class HeftActionRunner { } private async _executeOnceAsync( - executionManager: OperationExecutionManager, + executionManager: OperationExecutionManager, abortSignal: AbortSignal, requestRun?: (requestor?: string) => void ): Promise { @@ -382,7 +379,7 @@ export class HeftActionRunner { return await runWithLoggingAsync( () => { const operationExecutionManagerOptions: IOperationExecutionOptions< - HeftOperationMetadata, + IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata > = { terminal: this._terminal, @@ -390,14 +387,14 @@ export class HeftActionRunner { abortSignal, requestRun, beforeExecuteOperationAsync: async ( - operation: Operation + operation: Operation ) => { if ('task' in operation.metadata && taskStart.isUsed()) { await taskStart.promise({ operation: operation as Operation }); } }, afterExecuteOperationAsync: async ( - operation: Operation + operation: Operation ) => { if ('task' in operation.metadata && taskFinish.isUsed()) { await taskFinish.promise({ operation: operation as Operation }); @@ -460,7 +457,7 @@ export class HeftActionRunner { } // Create operation for the phase start node - const phaseOperation: Operation = _getOrCreatePhaseOperation( + const phaseOperation: Operation = _getOrCreatePhaseOperation( internalHeftSession, phase, operations, @@ -514,12 +511,10 @@ function _getOrCreatePhaseOperation( phase: HeftPhase, operations: Map, operationGroups: Map> -): Operation { +): Operation { const key: string = phase.phaseName; - let operation: Operation | undefined = operations.get( - key - ) as Operation; + let operation: Operation | undefined = operations.get(key); if (!operation) { let group: OperationGroupRecord | undefined = operationGroups.get( phase.phaseName @@ -532,8 +527,7 @@ function _getOrCreatePhaseOperation( operation = new Operation({ group, name: phase.phaseName, - runner: new PhaseOperationRunner({ phase, internalHeftSession }), - metadata: { phase } + runner: new PhaseOperationRunner({ phase, internalHeftSession }) }); operations.set(key, operation); } diff --git a/common/changes/@rushstack/operation-graph/sennyeya-heft-lifecycle_2025-06-11-16-53.json b/common/changes/@rushstack/operation-graph/sennyeya-heft-lifecycle_2025-06-11-16-53.json index a53dd921e99..7871a59411c 100644 --- a/common/changes/@rushstack/operation-graph/sennyeya-heft-lifecycle_2025-06-11-16-53.json +++ b/common/changes/@rushstack/operation-graph/sennyeya-heft-lifecycle_2025-06-11-16-53.json @@ -2,7 +2,7 @@ "changes": [ { "packageName": "@rushstack/operation-graph", - "comment": "The OperationExecutionManager `beforeExecute` and `afterExecute` hooks have been made async and renamed to `beforeExecuteAsync` and `afterExecuteAsync`.", + "comment": "(BREAKING CHANGE) The OperationExecutionManager `beforeExecute` and `afterExecute` hooks have been made async and renamed to `beforeExecuteAsync` and `afterExecuteAsync`. Operations now have an optional `metadata` field that can be used to store arbitrary data.", "type": "minor" } ], diff --git a/libraries/operation-graph/src/OperationExecutionManager.ts b/libraries/operation-graph/src/OperationExecutionManager.ts index 70ad56a7b33..0cdec8b5fc7 100644 --- a/libraries/operation-graph/src/OperationExecutionManager.ts +++ b/libraries/operation-graph/src/OperationExecutionManager.ts @@ -96,7 +96,7 @@ export class OperationExecutionManager> = new Set( - [...this._operations].map((e) => e.group).filter((e) => e !== undefined) + Array.from(this._operations, (e) => e.group).filter((e) => e !== undefined) ); for (const groupRecord of groupRecords) { groupRecord.reset(); From 21e500daa9b05742ef1f4b9abbae679f8564b7bc Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Thu, 12 Jun 2025 19:10:47 -0400 Subject: [PATCH 17/21] don't report on silent operations Signed-off-by: Aramis Sennyey --- .../src/OperationExecutionManager.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/libraries/operation-graph/src/OperationExecutionManager.ts b/libraries/operation-graph/src/OperationExecutionManager.ts index 0cdec8b5fc7..1f71c596bdb 100644 --- a/libraries/operation-graph/src/OperationExecutionManager.ts +++ b/libraries/operation-graph/src/OperationExecutionManager.ts @@ -128,7 +128,7 @@ export class OperationExecutionManager ): Promise => { // Initialize group if uninitialized and log the group name - const { group } = operation; + const { group, runner } = operation; if (group) { if (!startedGroups.has(group)) { startedGroups.add(group); @@ -137,14 +137,16 @@ export class OperationExecutionManager, state: IOperationState ): Promise => { - const { group } = operation; + const { group, runner } = operation; if (group) { group.setOperationAsComplete(operation, state); } @@ -160,7 +162,10 @@ export class OperationExecutionManager Date: Thu, 12 Jun 2025 22:02:44 -0400 Subject: [PATCH 18/21] Apply suggestions from code review Co-authored-by: David Michon --- apps/heft/src/cli/HeftActionRunner.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index ed86344bcca..d141357f318 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -389,14 +389,14 @@ export class HeftActionRunner { beforeExecuteOperationAsync: async ( operation: Operation ) => { - if ('task' in operation.metadata && taskStart.isUsed()) { + if (taskStart.isUsed()) { await taskStart.promise({ operation: operation as Operation }); } }, afterExecuteOperationAsync: async ( operation: Operation ) => { - if ('task' in operation.metadata && taskFinish.isUsed()) { + if (taskFinish.isUsed()) { await taskFinish.promise({ operation: operation as Operation }); } }, From dd975be51ce071d731557c4d5eef392e66a7e67a Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Thu, 12 Jun 2025 22:05:34 -0400 Subject: [PATCH 19/21] simplify Signed-off-by: Aramis Sennyey --- apps/heft/src/cli/HeftActionRunner.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index d141357f318..ddc141fc8e1 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -390,14 +390,14 @@ export class HeftActionRunner { operation: Operation ) => { if (taskStart.isUsed()) { - await taskStart.promise({ operation: operation as Operation }); + await taskStart.promise({ operation }); } }, afterExecuteOperationAsync: async ( operation: Operation ) => { if (taskFinish.isUsed()) { - await taskFinish.promise({ operation: operation as Operation }); + await taskFinish.promise({ operation }); } }, beforeExecuteOperationGroupAsync: async ( From 7b26e6643eaf4356b9c2c03eb4511178f4a629d1 Mon Sep 17 00:00:00 2001 From: Aramis Sennyey Date: Thu, 12 Jun 2025 22:12:19 -0400 Subject: [PATCH 20/21] only get groups once Signed-off-by: Aramis Sennyey --- .../operation-graph/src/OperationExecutionManager.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/operation-graph/src/OperationExecutionManager.ts b/libraries/operation-graph/src/OperationExecutionManager.ts index 1f71c596bdb..eadd5674978 100644 --- a/libraries/operation-graph/src/OperationExecutionManager.ts +++ b/libraries/operation-graph/src/OperationExecutionManager.ts @@ -51,6 +51,8 @@ export class OperationExecutionManager>; + public constructor(operations: ReadonlySet>) { let trackedOperationCount: number = 0; for (const operation of operations) { @@ -64,6 +66,8 @@ export class OperationExecutionManager e.group).filter((e) => e !== undefined)); + for (const consumer of operations) { for (const dependency of consumer.dependencies) { if (!operations.has(dependency)) { @@ -95,10 +99,8 @@ export class OperationExecutionManager = new Set(); const maxParallelism: number = Math.min(this._operations.length, parallelism); - const groupRecords: Set> = new Set( - Array.from(this._operations, (e) => e.group).filter((e) => e !== undefined) - ); - for (const groupRecord of groupRecords) { + + for (const groupRecord of this._groupRecords) { groupRecord.reset(); } From 57a191bfeba45cb1be96b99bcdc6c3bc6264af0a Mon Sep 17 00:00:00 2001 From: Aramis Sennyey <159921952+aramissennyeydd@users.noreply.github.com> Date: Fri, 13 Jun 2025 14:34:19 -0400 Subject: [PATCH 21/21] Update apps/heft/src/cli/HeftActionRunner.ts Co-authored-by: David Michon --- apps/heft/src/cli/HeftActionRunner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/heft/src/cli/HeftActionRunner.ts b/apps/heft/src/cli/HeftActionRunner.ts index ddc141fc8e1..60d511d5269 100644 --- a/apps/heft/src/cli/HeftActionRunner.ts +++ b/apps/heft/src/cli/HeftActionRunner.ts @@ -315,7 +315,7 @@ export class HeftActionRunner { const executionManager: OperationExecutionManager< IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata - > = new OperationExecutionManager(operations); + > = new OperationExecutionManager(operations); const cliAbortSignal: AbortSignal = ensureCliAbortSignal(this._terminal);