@calmo/task-runner 3.4.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AGENTS.md +15 -16
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +89 -82
  4. package/coverage/coverage-final.json +4 -4
  5. package/coverage/index.html +9 -9
  6. package/coverage/lcov-report/index.html +9 -9
  7. package/coverage/lcov-report/src/EventBus.ts.html +4 -4
  8. package/coverage/lcov-report/src/TaskGraphValidationError.ts.html +1 -1
  9. package/coverage/lcov-report/src/TaskGraphValidator.ts.html +1 -1
  10. package/coverage/lcov-report/src/TaskRunner.ts.html +1 -1
  11. package/coverage/lcov-report/src/TaskRunnerBuilder.ts.html +1 -1
  12. package/coverage/lcov-report/src/TaskRunnerExecutionConfig.ts.html +1 -1
  13. package/coverage/lcov-report/src/TaskStateManager.ts.html +82 -52
  14. package/coverage/lcov-report/src/WorkflowExecutor.ts.html +197 -62
  15. package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
  16. package/coverage/lcov-report/src/contracts/index.html +1 -1
  17. package/coverage/lcov-report/src/index.html +12 -12
  18. package/coverage/lcov-report/src/strategies/DryRunExecutionStrategy.ts.html +1 -1
  19. package/coverage/lcov-report/src/strategies/RetryingExecutionStrategy.ts.html +1 -1
  20. package/coverage/lcov-report/src/strategies/StandardExecutionStrategy.ts.html +3 -3
  21. package/coverage/lcov-report/src/strategies/index.html +1 -1
  22. package/coverage/lcov.info +202 -163
  23. package/coverage/src/EventBus.ts.html +4 -4
  24. package/coverage/src/TaskGraphValidationError.ts.html +1 -1
  25. package/coverage/src/TaskGraphValidator.ts.html +1 -1
  26. package/coverage/src/TaskRunner.ts.html +1 -1
  27. package/coverage/src/TaskRunnerBuilder.ts.html +1 -1
  28. package/coverage/src/TaskRunnerExecutionConfig.ts.html +1 -1
  29. package/coverage/src/TaskStateManager.ts.html +82 -52
  30. package/coverage/src/WorkflowExecutor.ts.html +197 -62
  31. package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
  32. package/coverage/src/contracts/index.html +1 -1
  33. package/coverage/src/index.html +12 -12
  34. package/coverage/src/strategies/DryRunExecutionStrategy.ts.html +1 -1
  35. package/coverage/src/strategies/RetryingExecutionStrategy.ts.html +1 -1
  36. package/coverage/src/strategies/StandardExecutionStrategy.ts.html +3 -3
  37. package/coverage/src/strategies/index.html +1 -1
  38. package/dist/TaskStateManager.d.ts +6 -0
  39. package/dist/TaskStateManager.js +11 -2
  40. package/dist/TaskStateManager.js.map +1 -1
  41. package/dist/TaskStep.d.ts +5 -0
  42. package/dist/WorkflowExecutor.js +49 -8
  43. package/dist/WorkflowExecutor.js.map +1 -1
  44. package/openspec/changes/archive/2026-01-18-feat-conditional-execution/proposal.md +35 -0
  45. package/openspec/changes/archive/2026-01-18-feat-conditional-execution/tasks.md +32 -0
  46. package/package.json +2 -1
  47. package/src/TaskStateManager.ts +12 -2
  48. package/src/TaskStep.ts +6 -0
  49. package/src/WorkflowExecutor.ts +57 -12
  50. package/test-report.xml +132 -108
@@ -51,7 +51,8 @@ export class WorkflowExecutor {
51
51
  // Initial pass
52
52
  this.processLoop(executingPromises, signal);
53
53
  while (this.stateManager.hasPendingTasks() ||
54
- this.stateManager.hasRunningTasks()) {
54
+ this.stateManager.hasRunningTasks() ||
55
+ executingPromises.size > 0) {
55
56
  // Safety check: if no tasks are running and we still have pending tasks,
56
57
  // it means we are stuck (e.g. cycle or unhandled dependency).
57
58
  // Since valid graphs shouldn't have this, we break to avoid infinite loop.
@@ -98,13 +99,53 @@ export class WorkflowExecutor {
98
99
  break;
99
100
  }
100
101
  const step = this.readyQueue.shift();
101
- this.stateManager.markRunning(step);
102
- const taskPromise = this.strategy
103
- .execute(step, this.context, signal)
104
- .then((result) => {
105
- this.stateManager.markCompleted(step, result);
106
- })
107
- .finally(() => {
102
+ const taskPromise = (async () => {
103
+ try {
104
+ if (step.condition) {
105
+ const check = step.condition(this.context);
106
+ const shouldRun = check instanceof Promise ? await check : check;
107
+ if (signal?.aborted) {
108
+ this.stateManager.markCompleted(step, {
109
+ status: "cancelled",
110
+ message: "Cancelled during condition evaluation.",
111
+ });
112
+ return;
113
+ }
114
+ if (!shouldRun) {
115
+ const result = {
116
+ status: "skipped",
117
+ message: "Skipped by condition evaluation.",
118
+ };
119
+ this.stateManager.markSkipped(step, result);
120
+ return;
121
+ }
122
+ }
123
+ }
124
+ catch (error) {
125
+ const result = {
126
+ status: "failure",
127
+ message: error instanceof Error
128
+ ? error.message
129
+ : "Condition evaluation failed",
130
+ error: error instanceof Error ? error.message : String(error),
131
+ };
132
+ this.stateManager.markCompleted(step, result);
133
+ return;
134
+ }
135
+ if (signal?.aborted) {
136
+ this.stateManager.markCompleted(step, {
137
+ status: "cancelled",
138
+ message: "Cancelled before execution started.",
139
+ });
140
+ return;
141
+ }
142
+ this.stateManager.markRunning(step);
143
+ await this.strategy
144
+ .execute(step, this.context, signal)
145
+ .then((result) => {
146
+ this.stateManager.markCompleted(step, result);
147
+ });
148
+ })().finally(() => {
108
149
  executingPromises.delete(taskPromise);
109
150
  // When a task finishes, we try to run more
110
151
  this.processLoop(executingPromises, signal);
@@ -1 +1 @@
1
- {"version":3,"file":"WorkflowExecutor.js","sourceRoot":"","sources":["../src/WorkflowExecutor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"WorkflowExecutor.js","sourceRoot":"","sources":["../src/WorkflowExecutor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ # Feature: Conditional Task Execution
2
+
3
+ ## 🎯 User Story
4
+
5
+ "As a developer, I want to define a condition for a task so that it only executes when specific criteria are met (e.g., only on CI, or only if a previous task generated a specific output), avoiding unnecessary execution and manual checks inside the task logic."
6
+
7
+ ## ❓ Why
8
+
9
+ Currently, to skip a task based on context, a developer must implement the check inside the `run` method and return `{ status: 'skipped' }`. This has downsides:
10
+
11
+ 1. **Late Binding**: The task is already "started" (event emitted) before it decides to skip itself.
12
+ 2. **Boilerplate**: Every task needs `if (!shouldRun) return { status: 'skipped' }`.
13
+ 3. **Clarity**: Examining the task definition (e.g. `TaskStep` object) doesn't reveal *when* it runs, only *what* it does. A declarative `condition` property makes the workflow logic more transparent.
14
+ 4. **Dry Run Accuracy**: In a dry run, we might want to know if a task *would* run. If the logic is inside `run`, strictly disjoint from the runner, a dry run (which skips `run`) cannot predict if the task would be skipped.
15
+
16
+ ## 🛠️ What Changes
17
+
18
+ 1. **Interface Update**: Update `TaskStep<T>` to accept an optional `condition` property:
19
+ ```typescript
20
+ condition?: (context: T) => boolean | Promise<boolean>;
21
+ ```
22
+ 2. **State Manager/Workflow Executor Update**:
23
+ - Before marking a task as `running`, evaluate `condition(context)`.
24
+ - If `false`, mark the task as `skipped` immediately without calling `run()`.
25
+ - Emit `taskSkipped` event.
26
+ - Consistency: Ensure that skipping a task triggers the standard skip propagation for dependent tasks (as currently documented in README).
27
+
28
+ ## ✅ Acceptance Criteria
29
+
30
+ - [ ] A task with `condition: () => false` must result in a `skipped` status.
31
+ - [ ] The `run` method of a conditionally skipped task must **not** be called.
32
+ - [ ] A task with `condition: () => true` (or undefined) must execute normally.
33
+ - [ ] The `condition` function receives the current `context`.
34
+ - [ ] Async `condition` functions are awaited.
35
+ - [ ] `taskSkipped` event is emitted when condition fails.
@@ -0,0 +1,32 @@
1
+ # Engineering Tasks
2
+
3
+ - [ ] **Task 1: Update Interface**
4
+ - Modify `src/TaskStep.ts` to add `condition?: (context: T) => boolean | Promise<boolean>;` to the `TaskStep` interface.
5
+ - Add JSDoc to explain that if condition returns false, the task is skipped.
6
+
7
+ - [ ] **Task 2: Implement Conditional Logic in TaskStateManager**
8
+ - Analyze `src/TaskStateManager.ts` and `src/WorkflowExecutor.ts`.
9
+ - Determine the best place to evaluate the condition. Likely in `WorkflowExecutor` before calling `strategy.execute`, OR in `TaskStateManager` when processing dependencies?
10
+ - Actually, `WorkflowExecutor.processLoop` calls `stateManager.markRunning(step)`.
11
+ - It should probably be:
12
+ ```typescript
13
+ const shouldRun = await evaluateCondition(step, context);
14
+ if (!shouldRun) {
15
+ stateManager.markResult(step, { status: 'skipped' });
16
+ // emit skipped event
17
+ return;
18
+ }
19
+ stateManager.markRunning(step);
20
+ strategy.execute(...)
21
+ ```
22
+ - Note: `evaluateCondition` needs to handle both sync and async results.
23
+
24
+ - [ ] **Task 3: Unit Tests**
25
+ - Create `tests/conditional.test.ts` (or add to `tests/TaskRunner.test.ts`).
26
+ - Test: Task with condition `() => false` is skipped.
27
+ - Test: Task with condition `() => true` runs.
28
+ - Test: Task with async condition skipping.
29
+ - Test: Dependent tasks are also skipped (or handle skip propagation correctly).
30
+
31
+ - [ ] **Task 4: Verify Dry Run Behavior**
32
+ - Ensure Dry Run strategy still respects condition if possible, OR clarify in spec that condition is evaluated normally even in dry run (since it's a predicate, not side-effect).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calmo/task-runner",
3
- "version": "3.4.1",
3
+ "version": "3.5.0",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,6 +20,7 @@
20
20
  "test": "tsc --noEmit -p tsconfig.test.json && vitest run --coverage",
21
21
  "lint": "eslint .",
22
22
  "lint:fix": "eslint . --fix",
23
+ "all": "pnpm run build && pnpm run test && pnpm run lint",
23
24
  "format": "prettier --write .",
24
25
  "prepare": "husky",
25
26
  "commit": "git add . &&git-cz"
@@ -55,8 +55,7 @@ export class TaskStateManager<TContext> {
55
55
  status: "skipped",
56
56
  message: `Skipped because dependency '${failedDep}' failed${depError}`,
57
57
  };
58
- this.results.set(step.name, result);
59
- this.eventBus.emit("taskSkipped", { step, result });
58
+ this.markSkipped(step, result);
60
59
  toRemove.push(step);
61
60
  } else if (!blocked) {
62
61
  toRun.push(step);
@@ -139,4 +138,15 @@ export class TaskStateManager<TContext> {
139
138
  hasPendingTasks(): boolean {
140
139
  return this.pendingSteps.size > 0;
141
140
  }
141
+
142
+ /**
143
+ * Marks a task as skipped and emits `taskSkipped`.
144
+ * @param step The task that was skipped.
145
+ * @param result The result object (status: skipped).
146
+ */
147
+ markSkipped(step: TaskStep<TContext>, result: TaskResult): void {
148
+ this.running.delete(step.name);
149
+ this.results.set(step.name, result);
150
+ this.eventBus.emit("taskSkipped", { step, result });
151
+ }
142
152
  }
package/src/TaskStep.ts CHANGED
@@ -12,6 +12,12 @@ export interface TaskStep<TContext> {
12
12
  dependencies?: string[];
13
13
  /** Optional retry configuration for the task. */
14
14
  retry?: TaskRetryConfig;
15
+ /**
16
+ * Optional function to determine if the task should run.
17
+ * If it returns false (synchronously or asynchronously), the task is skipped.
18
+ */
19
+ condition?: (context: TContext) => boolean | Promise<boolean>;
20
+
15
21
  /**
16
22
  * The core logic of the task.
17
23
  * @param context The shared context object, allowing for state to be passed between tasks.
@@ -66,7 +66,8 @@ export class WorkflowExecutor<TContext> {
66
66
 
67
67
  while (
68
68
  this.stateManager.hasPendingTasks() ||
69
- this.stateManager.hasRunningTasks()
69
+ this.stateManager.hasRunningTasks() ||
70
+ executingPromises.size > 0
70
71
  ) {
71
72
  // Safety check: if no tasks are running and we still have pending tasks,
72
73
  // it means we are stuck (e.g. cycle or unhandled dependency).
@@ -124,18 +125,62 @@ export class WorkflowExecutor<TContext> {
124
125
 
125
126
  const step = this.readyQueue.shift()!;
126
127
 
127
- this.stateManager.markRunning(step);
128
-
129
- const taskPromise = this.strategy
130
- .execute(step, this.context, signal)
131
- .then((result) => {
128
+ const taskPromise = (async () => {
129
+ try {
130
+ if (step.condition) {
131
+ const check = step.condition(this.context);
132
+ const shouldRun = check instanceof Promise ? await check : check;
133
+
134
+ if (signal?.aborted) {
135
+ this.stateManager.markCompleted(step, {
136
+ status: "cancelled",
137
+ message: "Cancelled during condition evaluation.",
138
+ });
139
+ return;
140
+ }
141
+
142
+ if (!shouldRun) {
143
+ const result: TaskResult = {
144
+ status: "skipped",
145
+ message: "Skipped by condition evaluation.",
146
+ };
147
+ this.stateManager.markSkipped(step, result);
148
+ return;
149
+ }
150
+ }
151
+ } catch (error) {
152
+ const result: TaskResult = {
153
+ status: "failure",
154
+ message:
155
+ error instanceof Error
156
+ ? error.message
157
+ : "Condition evaluation failed",
158
+ error: error instanceof Error ? error.message : String(error),
159
+ };
132
160
  this.stateManager.markCompleted(step, result);
133
- })
134
- .finally(() => {
135
- executingPromises.delete(taskPromise);
136
- // When a task finishes, we try to run more
137
- this.processLoop(executingPromises, signal);
138
- });
161
+ return;
162
+ }
163
+
164
+ if (signal?.aborted) {
165
+ this.stateManager.markCompleted(step, {
166
+ status: "cancelled",
167
+ message: "Cancelled before execution started.",
168
+ });
169
+ return;
170
+ }
171
+
172
+ this.stateManager.markRunning(step);
173
+
174
+ await this.strategy
175
+ .execute(step, this.context, signal)
176
+ .then((result) => {
177
+ this.stateManager.markCompleted(step, result);
178
+ });
179
+ })().finally(() => {
180
+ executingPromises.delete(taskPromise);
181
+ // When a task finishes, we try to run more
182
+ this.processLoop(executingPromises, signal);
183
+ });
139
184
 
140
185
  executingPromises.add(taskPromise);
141
186
  }