builderman 1.5.0 → 1.5.2
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/README.md +8 -1
- package/dist/internal/task-executor.js +64 -32
- package/dist/internal/teardown-manager.d.ts +4 -3
- package/dist/internal/teardown-manager.js +6 -6
- package/dist/internal/util.d.ts +14 -0
- package/dist/internal/util.js +50 -0
- package/dist/pipeline.js +15 -8
- package/dist/types.d.ts +24 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -538,7 +538,7 @@ console.log(result.stats.summary.running)
|
|
|
538
538
|
Each task provides detailed per-task data:
|
|
539
539
|
|
|
540
540
|
```ts
|
|
541
|
-
for (const task of
|
|
541
|
+
for (const task of result.stats.tasks) {
|
|
542
542
|
console.log(task.name, task.status)
|
|
543
543
|
console.log(task.durationMs)
|
|
544
544
|
|
|
@@ -550,6 +550,13 @@ for (const task of Object.values(result.stats.tasks)) {
|
|
|
550
550
|
if (task.teardown) {
|
|
551
551
|
console.log("Teardown:", task.teardown.status)
|
|
552
552
|
}
|
|
553
|
+
|
|
554
|
+
// when using pipeline.toTask() to convert a pipeline into a task, the task will have subtasks
|
|
555
|
+
if (task.subtasks) {
|
|
556
|
+
for (const subtask of task.subtasks) {
|
|
557
|
+
// ...
|
|
558
|
+
}
|
|
559
|
+
}
|
|
553
560
|
}
|
|
554
561
|
```
|
|
555
562
|
|
|
@@ -2,6 +2,7 @@ import * as path from "node:path";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { $TASK_INTERNAL, $PIPELINE_INTERNAL } from "./constants.js";
|
|
4
4
|
import { PipelineError } from "../errors.js";
|
|
5
|
+
import { parseCommandLine } from "./util.js";
|
|
5
6
|
/**
|
|
6
7
|
* Executes a task (either a regular task or a nested pipeline).
|
|
7
8
|
*/
|
|
@@ -25,6 +26,10 @@ function executeNestedPipeline(task, taskId, taskName, nestedPipeline, context,
|
|
|
25
26
|
// Track nested pipeline state for skip behavior
|
|
26
27
|
let nestedSkippedCount = 0;
|
|
27
28
|
let nestedCompletedCount = 0;
|
|
29
|
+
// Track unique tasks that are ready, complete, or skipped
|
|
30
|
+
// A task can be both ready and complete, so we use Sets to avoid double-counting
|
|
31
|
+
const readyOrCompleteTasks = new Set();
|
|
32
|
+
let didMarkOuterReady = false;
|
|
28
33
|
// Get total tasks from nested pipeline
|
|
29
34
|
const nestedTotalTasks = nestedPipeline[$PIPELINE_INTERNAL].graph.nodes.size;
|
|
30
35
|
const commandName = config?.command ?? (process.env.NODE_ENV === "production" ? "build" : "dev");
|
|
@@ -37,7 +42,7 @@ function executeNestedPipeline(task, taskId, taskName, nestedPipeline, context,
|
|
|
37
42
|
command: commandName,
|
|
38
43
|
startedAt,
|
|
39
44
|
});
|
|
40
|
-
config?.onTaskBegin?.(taskName);
|
|
45
|
+
config?.onTaskBegin?.(taskName, taskId);
|
|
41
46
|
// Create an abort controller to stop the nested pipeline if needed
|
|
42
47
|
let pipelineStopped = false;
|
|
43
48
|
// Merge environment variables: pipeline.env -> task.env (from pipeline.toTask config)
|
|
@@ -47,6 +52,19 @@ function executeNestedPipeline(task, taskId, taskName, nestedPipeline, context,
|
|
|
47
52
|
...pipelineEnv,
|
|
48
53
|
...taskEnv,
|
|
49
54
|
};
|
|
55
|
+
// Helper to check if all inner tasks are ready/complete/skipped
|
|
56
|
+
const checkAllInnerTasksReady = () => {
|
|
57
|
+
if (didMarkOuterReady)
|
|
58
|
+
return;
|
|
59
|
+
// Count unique tasks: ready (via readyWhen), complete, or skipped
|
|
60
|
+
// A task can be both ready and complete, so we use a Set to track unique tasks
|
|
61
|
+
const totalFinished = readyOrCompleteTasks.size + nestedSkippedCount;
|
|
62
|
+
if (totalFinished === nestedTotalTasks && nestedTotalTasks > 0) {
|
|
63
|
+
didMarkOuterReady = true;
|
|
64
|
+
// Mark outer task as ready - this allows dependents to proceed
|
|
65
|
+
callbacks.onTaskReady(taskId);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
50
68
|
// Run the nested pipeline with signal propagation
|
|
51
69
|
// Pass the command from parent pipeline to nested pipeline
|
|
52
70
|
// If undefined, nested pipeline will use its own default (build/dev based on NODE_ENV)
|
|
@@ -57,22 +75,35 @@ function executeNestedPipeline(task, taskId, taskName, nestedPipeline, context,
|
|
|
57
75
|
strict: config?.strict,
|
|
58
76
|
signal: context.signal, // Pass signal to nested pipeline
|
|
59
77
|
env: mergedEnv, // Pass merged env to nested pipeline
|
|
60
|
-
onTaskBegin: (nestedTaskName) => {
|
|
78
|
+
onTaskBegin: (nestedTaskName, nestedTaskId) => {
|
|
61
79
|
if (pipelineStopped)
|
|
62
80
|
return;
|
|
63
|
-
config?.onTaskBegin?.(`${taskName}:${nestedTaskName}
|
|
81
|
+
config?.onTaskBegin?.(`${taskName}:${nestedTaskName}`, nestedTaskId);
|
|
64
82
|
},
|
|
65
|
-
|
|
83
|
+
onTaskReady: (_, nestedTaskId) => {
|
|
84
|
+
if (pipelineStopped)
|
|
85
|
+
return;
|
|
86
|
+
// Track this task as ready (it may later become complete, but we only count it once)
|
|
87
|
+
readyOrCompleteTasks.add(nestedTaskId);
|
|
88
|
+
checkAllInnerTasksReady();
|
|
89
|
+
// Note: We don't call config?.onTaskReady here because the nested pipeline
|
|
90
|
+
// already handles that internally. We only track readiness for the outer task.
|
|
91
|
+
},
|
|
92
|
+
onTaskComplete: (nestedTaskName, nestedTaskId) => {
|
|
66
93
|
if (pipelineStopped)
|
|
67
94
|
return;
|
|
68
95
|
nestedCompletedCount++;
|
|
69
|
-
|
|
96
|
+
// Track this task as complete (if it was already ready, the Set prevents double-counting)
|
|
97
|
+
readyOrCompleteTasks.add(nestedTaskId);
|
|
98
|
+
checkAllInnerTasksReady();
|
|
99
|
+
config?.onTaskComplete?.(`${taskName}:${nestedTaskName}`, nestedTaskId);
|
|
70
100
|
},
|
|
71
|
-
onTaskSkipped: (nestedTaskName, mode) => {
|
|
101
|
+
onTaskSkipped: (nestedTaskName, nestedTaskId, mode) => {
|
|
72
102
|
if (pipelineStopped)
|
|
73
103
|
return;
|
|
74
104
|
nestedSkippedCount++;
|
|
75
|
-
|
|
105
|
+
checkAllInnerTasksReady();
|
|
106
|
+
config?.onTaskSkipped?.(`${taskName}:${nestedTaskName}`, nestedTaskId, mode);
|
|
76
107
|
},
|
|
77
108
|
})
|
|
78
109
|
.then((result) => {
|
|
@@ -81,15 +112,20 @@ function executeNestedPipeline(task, taskId, taskName, nestedPipeline, context,
|
|
|
81
112
|
if (!result.ok) {
|
|
82
113
|
// Nested pipeline failed
|
|
83
114
|
const finishedAt = Date.now();
|
|
115
|
+
// Capture nested pipeline task stats even on failure
|
|
116
|
+
const nestedTaskStats = result.stats.tasks;
|
|
84
117
|
context.updateTaskStatus(taskId, {
|
|
85
118
|
status: "failed",
|
|
86
119
|
finishedAt,
|
|
87
120
|
durationMs: finishedAt - startedAt,
|
|
88
121
|
error: result.error,
|
|
122
|
+
subtasks: nestedTaskStats,
|
|
89
123
|
});
|
|
90
124
|
callbacks.onTaskFailed(taskId, result.error);
|
|
91
125
|
return;
|
|
92
126
|
}
|
|
127
|
+
// Capture nested pipeline task stats for subtasks field
|
|
128
|
+
const nestedTaskStats = result.stats.tasks;
|
|
93
129
|
// Determine nested pipeline result based on skip behavior:
|
|
94
130
|
// - If all inner tasks are skipped → outer task is skipped
|
|
95
131
|
// - If some run, some skip → outer task is completed
|
|
@@ -101,8 +137,9 @@ function executeNestedPipeline(task, taskId, taskName, nestedPipeline, context,
|
|
|
101
137
|
status: "skipped",
|
|
102
138
|
finishedAt,
|
|
103
139
|
durationMs: finishedAt - startedAt,
|
|
140
|
+
subtasks: nestedTaskStats,
|
|
104
141
|
});
|
|
105
|
-
config?.onTaskSkipped?.(taskName, commandName);
|
|
142
|
+
config?.onTaskSkipped?.(taskName, taskId, commandName);
|
|
106
143
|
setImmediate(() => {
|
|
107
144
|
callbacks.onTaskSkipped(taskId);
|
|
108
145
|
});
|
|
@@ -114,8 +151,9 @@ function executeNestedPipeline(task, taskId, taskName, nestedPipeline, context,
|
|
|
114
151
|
status: "completed",
|
|
115
152
|
finishedAt,
|
|
116
153
|
durationMs: finishedAt - startedAt,
|
|
154
|
+
subtasks: nestedTaskStats,
|
|
117
155
|
});
|
|
118
|
-
config?.onTaskComplete?.(taskName);
|
|
156
|
+
config?.onTaskComplete?.(taskName, taskId);
|
|
119
157
|
// Mark as complete - this will update dependent tasks and allow them to start
|
|
120
158
|
callbacks.onTaskComplete(taskId);
|
|
121
159
|
}
|
|
@@ -150,7 +188,7 @@ function executeRegularTask(task, taskId, taskName, context, callbacks) {
|
|
|
150
188
|
finishedAt,
|
|
151
189
|
durationMs: 0,
|
|
152
190
|
});
|
|
153
|
-
config?.onTaskSkipped?.(taskName, commandName);
|
|
191
|
+
config?.onTaskSkipped?.(taskName, taskId, commandName);
|
|
154
192
|
// Mark as skipped - this satisfies dependencies and unblocks dependents
|
|
155
193
|
// Use setImmediate to ensure scheduler is at idle yield before receiving skip
|
|
156
194
|
setImmediate(() => {
|
|
@@ -158,23 +196,25 @@ function executeRegularTask(task, taskId, taskName, context, callbacks) {
|
|
|
158
196
|
});
|
|
159
197
|
return;
|
|
160
198
|
}
|
|
161
|
-
let
|
|
199
|
+
let commandString;
|
|
162
200
|
let readyWhen;
|
|
163
201
|
let readyTimeout = Infinity;
|
|
164
202
|
let completedTimeout = Infinity;
|
|
165
203
|
let teardown;
|
|
166
204
|
let commandEnv = {};
|
|
167
205
|
if (typeof commandConfig === "string") {
|
|
168
|
-
|
|
206
|
+
commandString = commandConfig;
|
|
169
207
|
}
|
|
170
208
|
else {
|
|
171
|
-
|
|
209
|
+
commandString = commandConfig.run;
|
|
172
210
|
readyWhen = commandConfig.readyWhen;
|
|
173
211
|
readyTimeout = commandConfig.readyTimeout ?? Infinity;
|
|
174
212
|
completedTimeout = commandConfig.completedTimeout ?? Infinity;
|
|
175
213
|
teardown = commandConfig.teardown;
|
|
176
214
|
commandEnv = commandConfig.env ?? {};
|
|
177
215
|
}
|
|
216
|
+
// Parse commandSpec into cmd + args
|
|
217
|
+
const { cmd, args } = parseCommandLine(commandString);
|
|
178
218
|
const taskCwd = path.isAbsolute(cwd) ? cwd : path.resolve(process.cwd(), cwd);
|
|
179
219
|
if (!fs.existsSync(taskCwd)) {
|
|
180
220
|
const finishedAt = Date.now();
|
|
@@ -205,10 +245,10 @@ function executeRegularTask(task, taskId, taskName, context, callbacks) {
|
|
|
205
245
|
...taskEnv,
|
|
206
246
|
...commandEnv,
|
|
207
247
|
};
|
|
208
|
-
const child = spawnFn(
|
|
248
|
+
const child = spawnFn(cmd, args, {
|
|
209
249
|
cwd: taskCwd,
|
|
210
250
|
stdio: ["inherit", "pipe", "pipe"],
|
|
211
|
-
shell:
|
|
251
|
+
shell: false,
|
|
212
252
|
env: accumulatedEnv,
|
|
213
253
|
});
|
|
214
254
|
// Update the running task execution with the process
|
|
@@ -224,28 +264,20 @@ function executeRegularTask(task, taskId, taskName, context, callbacks) {
|
|
|
224
264
|
});
|
|
225
265
|
// Store teardown command if provided
|
|
226
266
|
if (teardown) {
|
|
267
|
+
const teardownSpec = parseCommandLine(teardown);
|
|
227
268
|
teardownManager.register(taskId, {
|
|
228
|
-
|
|
269
|
+
cmd: teardownSpec.cmd,
|
|
270
|
+
args: teardownSpec.args,
|
|
229
271
|
cwd: taskCwd,
|
|
230
272
|
taskName,
|
|
231
273
|
});
|
|
232
274
|
}
|
|
233
|
-
config?.onTaskBegin?.(taskName);
|
|
275
|
+
config?.onTaskBegin?.(taskName, taskId);
|
|
234
276
|
let didMarkReady = false;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
setImmediate(() => {
|
|
240
|
-
// Check if task hasn't failed before marking as ready
|
|
241
|
-
const currentStats = context.taskStats.get(taskId);
|
|
242
|
-
if (currentStats?.status !== "failed" && !context.isAborted()) {
|
|
243
|
-
callbacks.onTaskReady(taskId);
|
|
244
|
-
didMarkReady = true;
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
else if (readyTimeout !== Infinity) {
|
|
277
|
+
// For tasks without readyWhen, we wait for the process to exit successfully
|
|
278
|
+
// before allowing dependent tasks to start. This ensures dependencies are
|
|
279
|
+
// fully completed before dependents begin execution.
|
|
280
|
+
if (readyWhen && readyTimeout !== Infinity) {
|
|
249
281
|
// Set up timeout for readyWhen condition using TimeoutManager
|
|
250
282
|
timeoutManager.setReadyTimeout(taskId, readyTimeout, () => {
|
|
251
283
|
if (!didMarkReady) {
|
|
@@ -358,7 +390,7 @@ function executeRegularTask(task, taskId, taskName, context, callbacks) {
|
|
|
358
390
|
durationMs,
|
|
359
391
|
exitCode: code ?? 0,
|
|
360
392
|
});
|
|
361
|
-
config?.onTaskComplete?.(taskName);
|
|
393
|
+
config?.onTaskComplete?.(taskName, taskId);
|
|
362
394
|
// 🔑 Notify scheduler and drain newly runnable tasks
|
|
363
395
|
callbacks.onTaskComplete(taskId);
|
|
364
396
|
});
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import type { TaskGraph } from "../types.js";
|
|
2
2
|
export interface TeardownCommand {
|
|
3
|
-
|
|
3
|
+
cmd: string;
|
|
4
|
+
args: string[];
|
|
4
5
|
cwd: string;
|
|
5
6
|
taskName: string;
|
|
6
7
|
}
|
|
7
8
|
export interface TeardownManagerConfig {
|
|
8
9
|
spawn: typeof import("node:child_process").spawn;
|
|
9
|
-
onTaskTeardown?: (taskName: string) => void;
|
|
10
|
-
onTaskTeardownError?: (taskName: string, error: Error) => void;
|
|
10
|
+
onTaskTeardown?: (taskName: string, taskId: string) => void;
|
|
11
|
+
onTaskTeardownError?: (taskName: string, taskId: string, error: Error) => void;
|
|
11
12
|
updateTaskTeardownStatus: (taskId: string, status: "not-run" | "completed" | "failed", error?: Error) => void;
|
|
12
13
|
}
|
|
13
14
|
export interface TeardownManager {
|
|
@@ -16,13 +16,13 @@ export function createTeardownManager(config) {
|
|
|
16
16
|
}
|
|
17
17
|
// Remove from map so it doesn't run again
|
|
18
18
|
teardownCommands.delete(taskId);
|
|
19
|
-
config.onTaskTeardown?.(teardown.taskName);
|
|
19
|
+
config.onTaskTeardown?.(teardown.taskName, taskId);
|
|
20
20
|
return new Promise((resolve) => {
|
|
21
21
|
try {
|
|
22
|
-
const teardownProcess = config.spawn(teardown.
|
|
22
|
+
const teardownProcess = config.spawn(teardown.cmd, teardown.args, {
|
|
23
23
|
cwd: teardown.cwd,
|
|
24
24
|
stdio: "inherit",
|
|
25
|
-
shell:
|
|
25
|
+
shell: false,
|
|
26
26
|
});
|
|
27
27
|
let resolved = false;
|
|
28
28
|
const resolveOnce = () => {
|
|
@@ -33,14 +33,14 @@ export function createTeardownManager(config) {
|
|
|
33
33
|
};
|
|
34
34
|
teardownProcess.on("error", (error) => {
|
|
35
35
|
const teardownError = new Error(`[${teardown.taskName}] Teardown failed: ${error.message}`);
|
|
36
|
-
config.onTaskTeardownError?.(teardown.taskName, teardownError);
|
|
36
|
+
config.onTaskTeardownError?.(teardown.taskName, taskId, teardownError);
|
|
37
37
|
config.updateTaskTeardownStatus(taskId, "failed", teardownError);
|
|
38
38
|
resolveOnce();
|
|
39
39
|
});
|
|
40
40
|
teardownProcess.on("exit", (code) => {
|
|
41
41
|
if (code !== 0) {
|
|
42
42
|
const teardownError = new Error(`[${teardown.taskName}] Teardown failed with exit code ${code ?? 1}`);
|
|
43
|
-
config.onTaskTeardownError?.(teardown.taskName, teardownError);
|
|
43
|
+
config.onTaskTeardownError?.(teardown.taskName, taskId, teardownError);
|
|
44
44
|
config.updateTaskTeardownStatus(taskId, "failed", teardownError);
|
|
45
45
|
}
|
|
46
46
|
else {
|
|
@@ -51,7 +51,7 @@ export function createTeardownManager(config) {
|
|
|
51
51
|
}
|
|
52
52
|
catch (error) {
|
|
53
53
|
const teardownError = new Error(`[${teardown.taskName}] Teardown failed to start: ${error.message}`);
|
|
54
|
-
config.onTaskTeardownError?.(teardown.taskName, teardownError);
|
|
54
|
+
config.onTaskTeardownError?.(teardown.taskName, taskId, teardownError);
|
|
55
55
|
config.updateTaskTeardownStatus(taskId, "failed", teardownError);
|
|
56
56
|
resolve();
|
|
57
57
|
}
|
package/dist/internal/util.d.ts
CHANGED
|
@@ -1,2 +1,16 @@
|
|
|
1
1
|
import type { Task } from "../types.js";
|
|
2
2
|
export declare function validateTasks(tasks?: Task[]): void;
|
|
3
|
+
/**
|
|
4
|
+
* Very small, cross-platform command-line parser for our default spawn wrapper.
|
|
5
|
+
* - Splits on whitespace outside of single/double quotes
|
|
6
|
+
* - Strips the surrounding quotes
|
|
7
|
+
* - Does NOT implement full shell semantics (no pipes, redirects, etc.)
|
|
8
|
+
*
|
|
9
|
+
* This is sufficient for commands like:
|
|
10
|
+
* - `"node path/to/script.js"`
|
|
11
|
+
* - `"\"C:\\Program Files\\nodejs\\node.exe\" \"C:\\path with spaces\\script.js\""`
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseCommandLine(command: string): {
|
|
14
|
+
cmd: string;
|
|
15
|
+
args: string[];
|
|
16
|
+
};
|
package/dist/internal/util.js
CHANGED
|
@@ -4,3 +4,53 @@ export function validateTasks(tasks) {
|
|
|
4
4
|
throw new Error("Invalid dependency: must be a task or a pipeline converted to a task");
|
|
5
5
|
}
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Very small, cross-platform command-line parser for our default spawn wrapper.
|
|
9
|
+
* - Splits on whitespace outside of single/double quotes
|
|
10
|
+
* - Strips the surrounding quotes
|
|
11
|
+
* - Does NOT implement full shell semantics (no pipes, redirects, etc.)
|
|
12
|
+
*
|
|
13
|
+
* This is sufficient for commands like:
|
|
14
|
+
* - `"node path/to/script.js"`
|
|
15
|
+
* - `"\"C:\\Program Files\\nodejs\\node.exe\" \"C:\\path with spaces\\script.js\""`
|
|
16
|
+
*/
|
|
17
|
+
export function parseCommandLine(command) {
|
|
18
|
+
const tokens = [];
|
|
19
|
+
let current = "";
|
|
20
|
+
let inQuotes = false;
|
|
21
|
+
let quoteChar = null;
|
|
22
|
+
for (let i = 0; i < command.length; i++) {
|
|
23
|
+
const ch = command[i];
|
|
24
|
+
if (ch === '"' || ch === "'") {
|
|
25
|
+
if (!inQuotes) {
|
|
26
|
+
inQuotes = true;
|
|
27
|
+
quoteChar = ch;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (quoteChar === ch) {
|
|
31
|
+
inQuotes = false;
|
|
32
|
+
quoteChar = null;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Different quote type inside current quotes – treat as literal
|
|
36
|
+
current += ch;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!inQuotes && /\s/.test(ch)) {
|
|
40
|
+
if (current.length > 0) {
|
|
41
|
+
tokens.push(current);
|
|
42
|
+
current = "";
|
|
43
|
+
}
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
current += ch;
|
|
47
|
+
}
|
|
48
|
+
if (current.length > 0) {
|
|
49
|
+
tokens.push(current);
|
|
50
|
+
}
|
|
51
|
+
if (tokens.length === 0) {
|
|
52
|
+
throw new Error(`Invalid command: "${command}"`);
|
|
53
|
+
}
|
|
54
|
+
const [cmd, ...args] = tokens;
|
|
55
|
+
return { cmd, args };
|
|
56
|
+
}
|
package/dist/pipeline.js
CHANGED
|
@@ -144,6 +144,10 @@ export function pipeline(tasks) {
|
|
|
144
144
|
// Task is ready (via readyWhen) - update dependent tasks immediately
|
|
145
145
|
// Only process ready tasks if pipeline hasn't failed
|
|
146
146
|
if (!isPipelineFailed()) {
|
|
147
|
+
const taskName = taskStats.get(taskId)?.name;
|
|
148
|
+
if (taskName) {
|
|
149
|
+
config?.onTaskReady?.(taskName, taskId);
|
|
150
|
+
}
|
|
147
151
|
queueManager.markRunningTaskReady(taskId);
|
|
148
152
|
// Process newly ready tasks immediately
|
|
149
153
|
processReadyTasks();
|
|
@@ -171,10 +175,16 @@ export function pipeline(tasks) {
|
|
|
171
175
|
const isPipelineFailed = () => {
|
|
172
176
|
return failed || queueManager.hasFailed();
|
|
173
177
|
};
|
|
174
|
-
// Helper to safely kill a process (ignores errors if process is already dead)
|
|
178
|
+
// Helper to safely kill a process (ignores errors if process is already dead).
|
|
179
|
+
// With shell:false and direct child processes, a single SIGTERM is usually enough;
|
|
180
|
+
// we avoid force-killing from the outside so the child has a chance to run its
|
|
181
|
+
// own signal handlers and exit cleanly.
|
|
175
182
|
const safeKillProcess = (process) => {
|
|
183
|
+
if (!process)
|
|
184
|
+
return;
|
|
176
185
|
try {
|
|
177
|
-
|
|
186
|
+
// Try a graceful SIGTERM and let the child handle it.
|
|
187
|
+
process.kill("SIGTERM");
|
|
178
188
|
}
|
|
179
189
|
catch {
|
|
180
190
|
// Process might already be dead, ignore
|
|
@@ -236,7 +246,7 @@ export function pipeline(tasks) {
|
|
|
236
246
|
const signalHandler = createSignalHandler({
|
|
237
247
|
abortSignal: signal,
|
|
238
248
|
onAborted: () => failPipeline(new PipelineError("Aborted", PipelineError.Aborted)),
|
|
239
|
-
onProcessTerminated: (message) => new PipelineError(message, PipelineError.ProcessTerminated),
|
|
249
|
+
onProcessTerminated: (message) => failPipeline(new PipelineError(message, PipelineError.ProcessTerminated)),
|
|
240
250
|
});
|
|
241
251
|
// 🚀 Start processing ready tasks
|
|
242
252
|
processReadyTasks();
|
|
@@ -268,11 +278,8 @@ export function pipeline(tasks) {
|
|
|
268
278
|
function buildResult(error, startedAt, command, taskStats, status) {
|
|
269
279
|
const finishedAt = Date.now();
|
|
270
280
|
const durationMs = finishedAt - startedAt;
|
|
271
|
-
// Convert task stats map to
|
|
272
|
-
const tasks =
|
|
273
|
-
for (const [taskId, stats] of taskStats) {
|
|
274
|
-
tasks[taskId] = stats;
|
|
275
|
-
}
|
|
281
|
+
// Convert task stats map to array
|
|
282
|
+
const tasks = Array.from(taskStats.values());
|
|
276
283
|
// Calculate summary
|
|
277
284
|
let completed = 0;
|
|
278
285
|
let failed = 0;
|
package/dist/types.d.ts
CHANGED
|
@@ -145,31 +145,42 @@ export interface PipelineRunConfig {
|
|
|
145
145
|
/**
|
|
146
146
|
* Callback invoked when a task begins execution.
|
|
147
147
|
* @param taskName The name of the task that started.
|
|
148
|
+
* @param taskId The id of the task that started.
|
|
148
149
|
*/
|
|
149
|
-
onTaskBegin?: (taskName: string) => void;
|
|
150
|
+
onTaskBegin?: (taskName: string, taskId: string) => void;
|
|
151
|
+
/**
|
|
152
|
+
* Callback invoked when a task becomes ready (via readyWhen condition).
|
|
153
|
+
* This is called when a task with a `readyWhen` function satisfies its condition.
|
|
154
|
+
* @param taskName The name of the task that became ready.
|
|
155
|
+
* @param taskId The id of the task that became ready.
|
|
156
|
+
*/
|
|
157
|
+
onTaskReady?: (taskName: string, taskId: string) => void;
|
|
150
158
|
/**
|
|
151
159
|
* Callback invoked when a task completes successfully.
|
|
152
160
|
* @param taskName The name of the task that completed.
|
|
161
|
+
* @param taskId The id of the task that completed.
|
|
153
162
|
*/
|
|
154
|
-
onTaskComplete?: (taskName: string) => void;
|
|
163
|
+
onTaskComplete?: (taskName: string, taskId: string) => void;
|
|
155
164
|
/**
|
|
156
165
|
* Callback invoked when a task is skipped (e.g., when a command doesn't exist for the current mode).
|
|
157
166
|
* @param taskName The name of the task that was skipped.
|
|
167
|
+
* @param taskId The id of the task that was skipped.
|
|
158
168
|
* @param mode The command mode that was requested (e.g., "dev", "build").
|
|
159
169
|
*/
|
|
160
|
-
onTaskSkipped?: (taskName: string, mode: string) => void;
|
|
170
|
+
onTaskSkipped?: (taskName: string, taskId: string, mode: string) => void;
|
|
161
171
|
/**
|
|
162
172
|
* Callback invoked when a task's teardown command begins execution.
|
|
163
173
|
* @param taskName The name of the task whose teardown is running.
|
|
174
|
+
* @param taskId The id of the task whose teardown is running.
|
|
164
175
|
*/
|
|
165
|
-
onTaskTeardown?: (taskName: string) => void;
|
|
176
|
+
onTaskTeardown?: (taskName: string, taskId: string) => void;
|
|
166
177
|
/**
|
|
167
178
|
* Callback invoked when a task's teardown command fails.
|
|
168
179
|
* Note: Teardown failures do not cause the pipeline to fail.
|
|
169
180
|
* @param taskName The name of the task whose teardown failed.
|
|
170
181
|
* @param error The error that occurred during teardown.
|
|
171
182
|
*/
|
|
172
|
-
onTaskTeardownError?: (taskName: string, error: Error) => void;
|
|
183
|
+
onTaskTeardownError?: (taskName: string, taskId: string, error: Error) => void;
|
|
173
184
|
}
|
|
174
185
|
/**
|
|
175
186
|
* Configuration for converting a pipeline into a task.
|
|
@@ -329,6 +340,12 @@ export interface TaskStats {
|
|
|
329
340
|
* These tasks cannot start until this task completes.
|
|
330
341
|
*/
|
|
331
342
|
dependents: string[];
|
|
343
|
+
/**
|
|
344
|
+
* Statistics for nested tasks when this task represents a pipeline
|
|
345
|
+
* (created via pipeline.toTask()). Only present for pipeline-tasks.
|
|
346
|
+
* Contains statistics for all tasks within the nested pipeline.
|
|
347
|
+
*/
|
|
348
|
+
subtasks?: TaskStats[];
|
|
332
349
|
}
|
|
333
350
|
/**
|
|
334
351
|
* Statistics for the entire pipeline execution.
|
|
@@ -358,10 +375,10 @@ export interface PipelineStats {
|
|
|
358
375
|
*/
|
|
359
376
|
status: "success" | "failed" | "aborted";
|
|
360
377
|
/**
|
|
361
|
-
*
|
|
378
|
+
* Array of task statistics.
|
|
362
379
|
* Contains statistics for all tasks in the pipeline, regardless of their status.
|
|
363
380
|
*/
|
|
364
|
-
tasks:
|
|
381
|
+
tasks: TaskStats[];
|
|
365
382
|
/**
|
|
366
383
|
* Summary of task execution counts.
|
|
367
384
|
*/
|