Skip to content

Commit df5971f

Browse files
[heft] feat: add lifecycle events for Heft tasks + phases (#5250)
* feat: add lifecycle events for Heft operations Signed-off-by: Aramis Sennyey <[email protected]> * add changeset Signed-off-by: Aramis Sennyey <[email protected]> * rename to be task or phase-specific Signed-off-by: Aramis Sennyey <[email protected]> * update changeset Signed-off-by: Aramis Sennyey <[email protected]> * fix readme Signed-off-by: Aramis Sennyey <[email protected]> * add comments for hooks Signed-off-by: Aramis Sennyey <[email protected]> * clean up Signed-off-by: Aramis Sennyey <[email protected]> * use custom metadata instead of runner Signed-off-by: Aramis Sennyey <[email protected]> * export props to entry point Signed-off-by: Aramis Sennyey <[email protected]> * create interfaces for task + phase Signed-off-by: Aramis Sennyey <[email protected]> * ensure that metadata is populated Signed-off-by: Aramis Sennyey <[email protected]> * remove operation on operationGroup events Signed-off-by: Aramis Sennyey <[email protected]> * move operation grouping concern to caller Signed-off-by: Aramis Sennyey <[email protected]> * fix types Signed-off-by: Aramis Sennyey <[email protected]> * fix imports Signed-off-by: Aramis Sennyey <[email protected]> * don't consider phase nodes in the return type Signed-off-by: Aramis Sennyey <[email protected]> * don't report on silent operations Signed-off-by: Aramis Sennyey <[email protected]> * Apply suggestions from code review Co-authored-by: David Michon <[email protected]> * simplify Signed-off-by: Aramis Sennyey <[email protected]> * only get groups once Signed-off-by: Aramis Sennyey <[email protected]> * Update apps/heft/src/cli/HeftActionRunner.ts Co-authored-by: David Michon <[email protected]> --------- Signed-off-by: Aramis Sennyey <[email protected]> Co-authored-by: Aramis Sennyey <[email protected]> Co-authored-by: David Michon <[email protected]>
1 parent 8c312b2 commit df5971f

File tree

27 files changed

+643
-116
lines changed

27 files changed

+643
-116
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ These GitHub repositories provide supplementary resources for Rush Stack:
167167
| [/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 |
168168
| [/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 |
169169
| [/build-tests/heft-copy-files-test](./build-tests/heft-copy-files-test/) | Building this project tests copying files with Heft |
170+
| [/build-tests/heft-example-lifecycle-plugin](./build-tests/heft-example-lifecycle-plugin/) | This is an example heft plugin for testing the lifecycle hooks |
170171
| [/build-tests/heft-example-plugin-01](./build-tests/heft-example-plugin-01/) | This is an example heft plugin that exposes hooks for other plugins |
171172
| [/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 |
172173
| [/build-tests/heft-fastify-test](./build-tests/heft-fastify-test/) | This project tests Heft support for the Fastify framework for Node.js services |

apps/heft/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"license": "MIT",
3030
"scripts": {
3131
"build": "heft build --clean",
32-
"start": "heft test --clean --watch",
32+
"start": "heft build-watch --clean",
3333
"_phase:build": "heft run --only build -- --clean",
3434
"_phase:test": "heft run --only test -- --clean"
3535
},

apps/heft/src/cli/HeftActionRunner.ts

Lines changed: 110 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type IWatchLoopState,
1313
Operation,
1414
OperationExecutionManager,
15+
OperationGroupRecord,
1516
OperationStatus,
1617
WatchLoop
1718
} from '@rushstack/operation-graph';
@@ -28,7 +29,7 @@ import type { MetricsCollector } from '../metrics/MetricsCollector';
2829
import { HeftParameterManager } from '../pluginFramework/HeftParameterManager';
2930
import { TaskOperationRunner } from '../operations/runners/TaskOperationRunner';
3031
import { PhaseOperationRunner } from '../operations/runners/PhaseOperationRunner';
31-
import type { HeftPhase } from '../pluginFramework/HeftPhase';
32+
import type { IHeftPhase, HeftPhase } from '../pluginFramework/HeftPhase';
3233
import type { IHeftAction, IHeftActionOptions } from './actions/IHeftAction';
3334
import type {
3435
IHeftLifecycleCleanHookOptions,
@@ -37,14 +38,31 @@ import type {
3738
IHeftLifecycleToolStartHookOptions
3839
} from '../pluginFramework/HeftLifecycleSession';
3940
import type { HeftLifecycle } from '../pluginFramework/HeftLifecycle';
40-
import type { HeftTask } from '../pluginFramework/HeftTask';
41+
import type { IHeftTask, HeftTask } from '../pluginFramework/HeftTask';
4142
import { deleteFilesAsync, type IDeleteOperation } from '../plugins/DeleteFilesPlugin';
4243
import { Constants } from '../utilities/Constants';
4344

4445
export interface IHeftActionRunnerOptions extends IHeftActionOptions {
4546
action: IHeftAction;
4647
}
4748

49+
/**
50+
* Metadata for an operation that represents a task.
51+
* @public
52+
*/
53+
export interface IHeftTaskOperationMetadata {
54+
task: IHeftTask;
55+
phase: IHeftPhase;
56+
}
57+
58+
/**
59+
* Metadata for an operation that represents a phase.
60+
* @public
61+
*/
62+
export interface IHeftPhaseOperationMetadata {
63+
phase: IHeftPhase;
64+
}
65+
4866
export function initializeHeft(
4967
heftConfiguration: HeftConfiguration,
5068
terminal: ITerminal,
@@ -291,9 +309,13 @@ export class HeftActionRunner {
291309

292310
initializeHeft(this._heftConfiguration, terminal, this.parameterManager.defaultParameters.verbose);
293311

294-
const operations: ReadonlySet<Operation> = this._generateOperations();
312+
const operations: ReadonlySet<Operation<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>> =
313+
this._generateOperations();
295314

296-
const executionManager: OperationExecutionManager = new OperationExecutionManager(operations);
315+
const executionManager: OperationExecutionManager<
316+
IHeftTaskOperationMetadata,
317+
IHeftPhaseOperationMetadata
318+
> = new OperationExecutionManager(operations);
297319

298320
const cliAbortSignal: AbortSignal = ensureCliAbortSignal(this._terminal);
299321

@@ -346,20 +368,52 @@ export class HeftActionRunner {
346368
}
347369

348370
private async _executeOnceAsync(
349-
executionManager: OperationExecutionManager,
371+
executionManager: OperationExecutionManager<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>,
350372
abortSignal: AbortSignal,
351373
requestRun?: (requestor?: string) => void
352374
): Promise<OperationStatus> {
375+
const { taskStart, taskFinish, phaseStart, phaseFinish } = this._internalHeftSession.lifecycle.hooks;
353376
// Record this as the start of task execution.
354377
this._metricsCollector.setStartTime();
355378
// Execute the action operations
356379
return await runWithLoggingAsync(
357380
() => {
358-
const operationExecutionManagerOptions: IOperationExecutionOptions = {
381+
const operationExecutionManagerOptions: IOperationExecutionOptions<
382+
IHeftTaskOperationMetadata,
383+
IHeftPhaseOperationMetadata
384+
> = {
359385
terminal: this._terminal,
360386
parallelism: this._parallelism,
361387
abortSignal,
362-
requestRun
388+
requestRun,
389+
beforeExecuteOperationAsync: async (
390+
operation: Operation<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>
391+
) => {
392+
if (taskStart.isUsed()) {
393+
await taskStart.promise({ operation });
394+
}
395+
},
396+
afterExecuteOperationAsync: async (
397+
operation: Operation<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>
398+
) => {
399+
if (taskFinish.isUsed()) {
400+
await taskFinish.promise({ operation });
401+
}
402+
},
403+
beforeExecuteOperationGroupAsync: async (
404+
operationGroup: OperationGroupRecord<IHeftPhaseOperationMetadata>
405+
) => {
406+
if (operationGroup.metadata.phase && phaseStart.isUsed()) {
407+
await phaseStart.promise({ operation: operationGroup });
408+
}
409+
},
410+
afterExecuteOperationGroupAsync: async (
411+
operationGroup: OperationGroupRecord<IHeftPhaseOperationMetadata>
412+
) => {
413+
if (operationGroup.metadata.phase && phaseFinish.isUsed()) {
414+
await phaseFinish.promise({ operation: operationGroup });
415+
}
416+
}
363417
};
364418

365419
return executionManager.executeAsync(operationExecutionManagerOptions);
@@ -373,10 +427,14 @@ export class HeftActionRunner {
373427
);
374428
}
375429

376-
private _generateOperations(): Set<Operation> {
430+
private _generateOperations(): Set<Operation<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>> {
377431
const { selectedPhases } = this._action;
378432

379-
const operations: Map<string, Operation> = new Map();
433+
const operations: Map<
434+
string,
435+
Operation<IHeftTaskOperationMetadata, IHeftPhaseOperationMetadata>
436+
> = new Map();
437+
const operationGroups: Map<string, OperationGroupRecord<IHeftPhaseOperationMetadata>> = new Map();
380438
const internalHeftSession: InternalHeftSession = this._internalHeftSession;
381439

382440
let hasWarnedAboutSkippedPhases: boolean = false;
@@ -399,18 +457,28 @@ export class HeftActionRunner {
399457
}
400458

401459
// Create operation for the phase start node
402-
const phaseOperation: Operation = _getOrCreatePhaseOperation(internalHeftSession, phase, operations);
460+
const phaseOperation: Operation = _getOrCreatePhaseOperation(
461+
internalHeftSession,
462+
phase,
463+
operations,
464+
operationGroups
465+
);
403466

404467
// Create operations for each task
405468
for (const task of phase.tasks) {
406-
const taskOperation: Operation = _getOrCreateTaskOperation(internalHeftSession, task, operations);
469+
const taskOperation: Operation = _getOrCreateTaskOperation(
470+
internalHeftSession,
471+
task,
472+
operations,
473+
operationGroups
474+
);
407475
// Set the phase operation as a dependency of the task operation to ensure the phase operation runs first
408476
taskOperation.addDependency(phaseOperation);
409477

410478
// Set all dependency tasks as dependencies of the task operation
411479
for (const dependencyTask of task.dependencyTasks) {
412480
taskOperation.addDependency(
413-
_getOrCreateTaskOperation(internalHeftSession, dependencyTask, operations)
481+
_getOrCreateTaskOperation(internalHeftSession, dependencyTask, operations, operationGroups)
414482
);
415483
}
416484

@@ -422,7 +490,8 @@ export class HeftActionRunner {
422490
const consumingPhaseOperation: Operation = _getOrCreatePhaseOperation(
423491
internalHeftSession,
424492
consumingPhase,
425-
operations
493+
operations,
494+
operationGroups
426495
);
427496
consumingPhaseOperation.addDependency(taskOperation);
428497
// This is purely to simplify the reported graph for phase circularities
@@ -440,15 +509,24 @@ function _getOrCreatePhaseOperation(
440509
this: void,
441510
internalHeftSession: InternalHeftSession,
442511
phase: HeftPhase,
443-
operations: Map<string, Operation>
512+
operations: Map<string, Operation>,
513+
operationGroups: Map<string, OperationGroupRecord<IHeftPhaseOperationMetadata>>
444514
): Operation {
445515
const key: string = phase.phaseName;
446516

447517
let operation: Operation | undefined = operations.get(key);
448518
if (!operation) {
519+
let group: OperationGroupRecord<IHeftPhaseOperationMetadata> | undefined = operationGroups.get(
520+
phase.phaseName
521+
);
522+
if (!group) {
523+
group = new OperationGroupRecord(phase.phaseName, { phase });
524+
operationGroups.set(phase.phaseName, group);
525+
}
449526
// Only create the operation. Dependencies are hooked up separately
450527
operation = new Operation({
451-
groupName: phase.phaseName,
528+
group,
529+
name: phase.phaseName,
452530
runner: new PhaseOperationRunner({ phase, internalHeftSession })
453531
});
454532
operations.set(key, operation);
@@ -460,18 +538,31 @@ function _getOrCreateTaskOperation(
460538
this: void,
461539
internalHeftSession: InternalHeftSession,
462540
task: HeftTask,
463-
operations: Map<string, Operation>
541+
operations: Map<string, Operation>,
542+
operationGroups: Map<string, OperationGroupRecord<IHeftPhaseOperationMetadata>>
464543
): Operation {
465544
const key: string = `${task.parentPhase.phaseName}.${task.taskName}`;
466545

467-
let operation: Operation | undefined = operations.get(key);
546+
let operation: Operation<IHeftTaskOperationMetadata> | undefined = operations.get(
547+
key
548+
) as Operation<IHeftTaskOperationMetadata>;
468549
if (!operation) {
550+
const group: OperationGroupRecord<IHeftPhaseOperationMetadata> | undefined = operationGroups.get(
551+
task.parentPhase.phaseName
552+
);
553+
if (!group) {
554+
throw new InternalError(
555+
`Task ${task.taskName} in phase ${task.parentPhase.phaseName} has no group. This should not happen.`
556+
);
557+
}
469558
operation = new Operation({
470-
groupName: task.parentPhase.phaseName,
559+
group,
471560
runner: new TaskOperationRunner({
472561
internalHeftSession,
473562
task
474-
})
563+
}),
564+
name: task.taskName,
565+
metadata: { task, phase: task.parentPhase }
475566
});
476567
operations.set(key, operation);
477568
}

apps/heft/src/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,11 @@ export type {
3030
IHeftLifecycleHooks,
3131
IHeftLifecycleCleanHookOptions,
3232
IHeftLifecycleToolStartHookOptions,
33-
IHeftLifecycleToolFinishHookOptions
33+
IHeftLifecycleToolFinishHookOptions,
34+
IHeftTaskStartHookOptions,
35+
IHeftTaskFinishHookOptions,
36+
IHeftPhaseStartHookOptions,
37+
IHeftPhaseFinishHookOptions
3438
} from './pluginFramework/HeftLifecycleSession';
3539

3640
export type {
@@ -79,3 +83,9 @@ export type {
7983
CommandLineStringListParameter,
8084
CommandLineStringParameter
8185
} from '@rushstack/ts-command-line';
86+
87+
export type { IHeftTaskOperationMetadata } from './cli/HeftActionRunner';
88+
export type { IHeftPhaseOperationMetadata } from './cli/HeftActionRunner';
89+
90+
export type { IHeftTask } from './pluginFramework/HeftTask';
91+
export type { IHeftPhase } from './pluginFramework/HeftPhase';

apps/heft/src/pluginFramework/HeftLifecycle.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ import {
1919
type IHeftLifecycleHooks,
2020
type IHeftLifecycleToolStartHookOptions,
2121
type IHeftLifecycleToolFinishHookOptions,
22-
type IHeftLifecycleSession
22+
type IHeftLifecycleSession,
23+
type IHeftTaskStartHookOptions,
24+
type IHeftTaskFinishHookOptions,
25+
type IHeftPhaseStartHookOptions,
26+
type IHeftPhaseFinishHookOptions
2327
} from './HeftLifecycleSession';
2428
import type { ScopedLogger } from './logging/ScopedLogger';
2529

@@ -67,7 +71,11 @@ export class HeftLifecycle extends HeftPluginHost {
6771
clean: new AsyncParallelHook<IHeftLifecycleCleanHookOptions>(),
6872
toolStart: new AsyncParallelHook<IHeftLifecycleToolStartHookOptions>(),
6973
toolFinish: new AsyncParallelHook<IHeftLifecycleToolFinishHookOptions>(),
70-
recordMetrics: internalHeftSession.metricsCollector.recordMetricsHook
74+
recordMetrics: internalHeftSession.metricsCollector.recordMetricsHook,
75+
taskStart: new AsyncParallelHook<IHeftTaskStartHookOptions>(['task']),
76+
taskFinish: new AsyncParallelHook<IHeftTaskFinishHookOptions>(['task']),
77+
phaseStart: new AsyncParallelHook<IHeftPhaseStartHookOptions>(['phase']),
78+
phaseFinish: new AsyncParallelHook<IHeftPhaseFinishHookOptions>(['phase'])
7179
};
7280
}
7381

apps/heft/src/pluginFramework/HeftLifecycleSession.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type { IHeftParameters } from './HeftParameterManager';
1111
import type { IDeleteOperation } from '../plugins/DeleteFilesPlugin';
1212
import type { HeftPluginDefinitionBase } from '../configuration/HeftPluginDefinition';
1313
import type { HeftPluginHost } from './HeftPluginHost';
14+
import type { Operation, OperationGroupRecord } from '@rushstack/operation-graph';
15+
import type { IHeftPhaseOperationMetadata, IHeftTaskOperationMetadata } from '../cli/HeftActionRunner';
1416

1517
/**
1618
* The lifecycle session is responsible for providing session-specific information to Heft lifecycle
@@ -67,6 +69,34 @@ export interface IHeftLifecycleSession {
6769
): void;
6870
}
6971

72+
/**
73+
* @public
74+
*/
75+
export interface IHeftTaskStartHookOptions {
76+
operation: Operation<IHeftTaskOperationMetadata>;
77+
}
78+
79+
/**
80+
* @public
81+
*/
82+
export interface IHeftTaskFinishHookOptions {
83+
operation: Operation<IHeftTaskOperationMetadata>;
84+
}
85+
86+
/**
87+
* @public
88+
*/
89+
export interface IHeftPhaseStartHookOptions {
90+
operation: OperationGroupRecord<IHeftPhaseOperationMetadata>;
91+
}
92+
93+
/**
94+
* @public
95+
*/
96+
export interface IHeftPhaseFinishHookOptions {
97+
operation: OperationGroupRecord<IHeftPhaseOperationMetadata>;
98+
}
99+
70100
/**
71101
* Hooks that are available to the lifecycle plugin.
72102
*
@@ -111,6 +141,38 @@ export interface IHeftLifecycleHooks {
111141
* @public
112142
*/
113143
recordMetrics: AsyncParallelHook<IHeftRecordMetricsHookOptions>;
144+
145+
/**
146+
* The `taskStart` hook is called at the beginning of a task. It is called before the task has begun
147+
* to execute. To use it, call `taskStart.tapPromise(<pluginName>, <callback>)`.
148+
*
149+
* @public
150+
*/
151+
taskStart: AsyncParallelHook<IHeftTaskStartHookOptions>;
152+
153+
/**
154+
* The `taskFinish` hook is called at the end of a task. It is called after the task has completed
155+
* execution. To use it, call `taskFinish.tapPromise(<pluginName>, <callback>)`.
156+
*
157+
* @public
158+
*/
159+
taskFinish: AsyncParallelHook<IHeftTaskFinishHookOptions>;
160+
161+
/**
162+
* The `phaseStart` hook is called at the beginning of a phase. It is called before the phase has
163+
* begun to execute. To use it, call `phaseStart.tapPromise(<pluginName>, <callback>)`.
164+
*
165+
* @public
166+
*/
167+
phaseStart: AsyncParallelHook<IHeftPhaseStartHookOptions>;
168+
169+
/**
170+
* The `phaseFinish` hook is called at the end of a phase. It is called after the phase has completed
171+
* execution. To use it, call `phaseFinish.tapPromise(<pluginName>, <callback>)`.
172+
*
173+
* @public
174+
*/
175+
phaseFinish: AsyncParallelHook<IHeftPhaseFinishHookOptions>;
114176
}
115177

116178
/**

0 commit comments

Comments
 (0)