@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.
- package/AGENTS.md +15 -16
- package/CHANGELOG.md +14 -0
- package/README.md +89 -82
- package/coverage/coverage-final.json +4 -4
- package/coverage/index.html +9 -9
- package/coverage/lcov-report/index.html +9 -9
- package/coverage/lcov-report/src/EventBus.ts.html +4 -4
- package/coverage/lcov-report/src/TaskGraphValidationError.ts.html +1 -1
- package/coverage/lcov-report/src/TaskGraphValidator.ts.html +1 -1
- package/coverage/lcov-report/src/TaskRunner.ts.html +1 -1
- package/coverage/lcov-report/src/TaskRunnerBuilder.ts.html +1 -1
- package/coverage/lcov-report/src/TaskRunnerExecutionConfig.ts.html +1 -1
- package/coverage/lcov-report/src/TaskStateManager.ts.html +82 -52
- package/coverage/lcov-report/src/WorkflowExecutor.ts.html +197 -62
- package/coverage/lcov-report/src/contracts/RunnerEvents.ts.html +1 -1
- package/coverage/lcov-report/src/contracts/index.html +1 -1
- package/coverage/lcov-report/src/index.html +12 -12
- package/coverage/lcov-report/src/strategies/DryRunExecutionStrategy.ts.html +1 -1
- package/coverage/lcov-report/src/strategies/RetryingExecutionStrategy.ts.html +1 -1
- package/coverage/lcov-report/src/strategies/StandardExecutionStrategy.ts.html +3 -3
- package/coverage/lcov-report/src/strategies/index.html +1 -1
- package/coverage/lcov.info +202 -163
- package/coverage/src/EventBus.ts.html +4 -4
- package/coverage/src/TaskGraphValidationError.ts.html +1 -1
- package/coverage/src/TaskGraphValidator.ts.html +1 -1
- package/coverage/src/TaskRunner.ts.html +1 -1
- package/coverage/src/TaskRunnerBuilder.ts.html +1 -1
- package/coverage/src/TaskRunnerExecutionConfig.ts.html +1 -1
- package/coverage/src/TaskStateManager.ts.html +82 -52
- package/coverage/src/WorkflowExecutor.ts.html +197 -62
- package/coverage/src/contracts/RunnerEvents.ts.html +1 -1
- package/coverage/src/contracts/index.html +1 -1
- package/coverage/src/index.html +12 -12
- package/coverage/src/strategies/DryRunExecutionStrategy.ts.html +1 -1
- package/coverage/src/strategies/RetryingExecutionStrategy.ts.html +1 -1
- package/coverage/src/strategies/StandardExecutionStrategy.ts.html +3 -3
- package/coverage/src/strategies/index.html +1 -1
- package/dist/TaskStateManager.d.ts +6 -0
- package/dist/TaskStateManager.js +11 -2
- package/dist/TaskStateManager.js.map +1 -1
- package/dist/TaskStep.d.ts +5 -0
- package/dist/WorkflowExecutor.js +49 -8
- package/dist/WorkflowExecutor.js.map +1 -1
- package/openspec/changes/archive/2026-01-18-feat-conditional-execution/proposal.md +35 -0
- package/openspec/changes/archive/2026-01-18-feat-conditional-execution/tasks.md +32 -0
- package/package.json +2 -1
- package/src/TaskStateManager.ts +12 -2
- package/src/TaskStep.ts +6 -0
- package/src/WorkflowExecutor.ts +57 -12
- package/test-report.xml +132 -108
package/dist/WorkflowExecutor.js
CHANGED
|
@@ -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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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":"AAMA;;;GAGG;AACH,MAAM,OAAO,gBAAgB;IAWjB;IACA;IACA;IACA;IACA;IAdF,UAAU,GAAyB,EAAE,CAAC;IAE9C;;;;;;OAMG;IACH,YACU,OAAiB,EACjB,QAA4B,EAC5B,YAAwC,EACxC,QAAsC,EACtC,WAAoB;QAJpB,YAAO,GAAP,OAAO,CAAU;QACjB,aAAQ,GAAR,QAAQ,CAAoB;QAC5B,iBAAY,GAAZ,YAAY,CAA4B;QACxC,aAAQ,GAAR,QAAQ,CAA8B;QACtC,gBAAW,GAAX,WAAW,CAAS;IAC3B,CAAC;IAEJ;;;;;OAKG;IACH,KAAK,CAAC,OAAO,CACX,KAA2B,EAC3B,MAAoB;QAEpB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAEpC,2BAA2B;QAC3B,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAChC,8CAA8C,CAC/C,CAAC;YACF,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,EAAE,CAAC;YAC/C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,aAAa,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;YACtE,OAAO,OAAO,CAAC;QACjB,CAAC;QAED,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAiB,CAAC;QAEnD,MAAM,OAAO,GAAG,GAAG,EAAE;YACnB,sCAAsC;YACtC,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC,qBAAqB,CAAC,CAAC;QAC5D,CAAC,CAAC;QAEF,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC5C,CAAC;QAED,IAAI,CAAC;YACH,eAAe;YACf,IAAI,CAAC,WAAW,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;YAE5C,OACE,IAAI,CAAC,YAAY,CAAC,eAAe,EAAE;gBACnC,IAAI,CAAC,YAAY,CAAC,eAAe,EAAE,
|
|
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.
|
|
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"
|
package/src/TaskStateManager.ts
CHANGED
|
@@ -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.
|
|
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.
|
package/src/WorkflowExecutor.ts
CHANGED
|
@@ -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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
this.
|
|
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
|
}
|