builderman 1.3.0 → 1.4.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/README.md +117 -30
- package/dist/modules/task-executor.js +35 -15
- package/dist/pipeline.js +2 -2
- package/dist/task.js +18 -10
- package/dist/types.d.ts +24 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,28 +10,29 @@ It is designed for monorepos, long-running development processes, and CI/CD pipe
|
|
|
10
10
|
|
|
11
11
|
## Table of Contents
|
|
12
12
|
|
|
13
|
-
- [Key Features](#key-features)
|
|
14
|
-
- [Installation](#installation)
|
|
15
|
-
- [Quick Start](#quick-start)
|
|
16
|
-
- [Core Concepts](#core-concepts)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
- [
|
|
13
|
+
> - [Key Features](#key-features)
|
|
14
|
+
> - [Installation](#installation)
|
|
15
|
+
> - [Quick Start](#quick-start)
|
|
16
|
+
> - [Core Concepts](#core-concepts)
|
|
17
|
+
> - [Tasks](#tasks)
|
|
18
|
+
> - [Commands & Modes](#commands--modes)
|
|
19
|
+
> - [Environment Variables](#environment-variables)
|
|
20
|
+
> - [Dependencies](#dependencies)
|
|
21
|
+
> - [Pipelines](#pipelines)
|
|
22
|
+
> - [Pipeline Composition](#pipeline-composition)
|
|
23
|
+
> - [Error Handling Guarantees](#error-handling-guarantees)
|
|
24
|
+
> - [Cancellation](#cancellation)
|
|
25
|
+
> - [Teardown](#teardown)
|
|
26
|
+
> - [Basic Teardown](#basic-teardown)
|
|
27
|
+
> - [Teardown Callbacks](#teardown-callbacks)
|
|
28
|
+
> - [Teardown Execution Rules](#teardown-execution-rules)
|
|
29
|
+
> - [Skipping Tasks](#skipping-tasks)
|
|
30
|
+
> - [Strict Mode](#strict-mode)
|
|
31
|
+
> - [Task-Level Skip Override](#task-level-skip-override)
|
|
32
|
+
> - [Execution Statistics](#execution-statistics)
|
|
33
|
+
> - [Pipeline Statistics](#pipeline-statistics)
|
|
34
|
+
> - [Task Statistics](#task-statistics)
|
|
35
|
+
> - [When Should I Use builderman?](#when-should-i-use-builderman)
|
|
35
36
|
|
|
36
37
|
## Key Features
|
|
37
38
|
|
|
@@ -62,17 +63,19 @@ import { task, pipeline } from "builderman"
|
|
|
62
63
|
const build = task({
|
|
63
64
|
name: "build",
|
|
64
65
|
commands: { build: "tsc" },
|
|
65
|
-
cwd: "."
|
|
66
|
+
cwd: "packages/my-package", // Optional: defaults to "."
|
|
66
67
|
})
|
|
67
68
|
|
|
68
69
|
const test = task({
|
|
69
70
|
name: "test",
|
|
70
71
|
commands: { build: "npm test" },
|
|
71
|
-
cwd: ".",
|
|
72
72
|
dependencies: [build],
|
|
73
|
+
cwd: "packages/my-package",
|
|
73
74
|
})
|
|
74
75
|
|
|
75
|
-
const result = await pipeline([build, test]).run(
|
|
76
|
+
const result = await pipeline([build, test]).run({
|
|
77
|
+
command: "build",
|
|
78
|
+
})
|
|
76
79
|
|
|
77
80
|
if (!result.ok) {
|
|
78
81
|
console.error("Pipeline failed:", result.error.message)
|
|
@@ -93,6 +96,7 @@ A **task** represents a unit of work. Each task:
|
|
|
93
96
|
- Defines commands for one or more modes
|
|
94
97
|
- May depend on other tasks
|
|
95
98
|
- May register teardown logic
|
|
99
|
+
- Has an optional working directory (`cwd`, defaults to `"."`)
|
|
96
100
|
|
|
97
101
|
```ts
|
|
98
102
|
import { task } from "builderman"
|
|
@@ -130,6 +134,89 @@ Commands may be:
|
|
|
130
134
|
- `run`: the command to execute
|
|
131
135
|
- `readyWhen`: a predicate that marks the task as ready
|
|
132
136
|
- `teardown`: cleanup logic to run after completion
|
|
137
|
+
- `env`: environment variables specific to this command
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### Environment Variables
|
|
142
|
+
|
|
143
|
+
Environment variables can be provided at multiple levels, with more specific levels overriding less specific ones:
|
|
144
|
+
|
|
145
|
+
**Precedence order (highest to lowest):**
|
|
146
|
+
|
|
147
|
+
1. Command-level `env` (in command config)
|
|
148
|
+
2. Task-level `env` (in task config)
|
|
149
|
+
3. Pipeline-level `env` (in `pipeline.run()`)
|
|
150
|
+
4. Process environment variables
|
|
151
|
+
|
|
152
|
+
#### Command-Level Environment Variables
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const apiTask = task({
|
|
156
|
+
name: "api",
|
|
157
|
+
commands: {
|
|
158
|
+
dev: {
|
|
159
|
+
run: "npm run dev",
|
|
160
|
+
env: {
|
|
161
|
+
PORT: "3000",
|
|
162
|
+
NODE_ENV: "development",
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
})
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### Task-Level Environment Variables
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const apiTask = task({
|
|
173
|
+
name: "api",
|
|
174
|
+
commands: {
|
|
175
|
+
dev: "npm run dev",
|
|
176
|
+
build: "npm run build",
|
|
177
|
+
},
|
|
178
|
+
env: {
|
|
179
|
+
API_URL: "http://localhost:3000",
|
|
180
|
+
LOG_LEVEL: "debug",
|
|
181
|
+
},
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### Pipeline-Level Environment Variables
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const result = await pipeline([apiTask]).run({
|
|
189
|
+
env: {
|
|
190
|
+
DATABASE_URL: "postgres://localhost/mydb",
|
|
191
|
+
REDIS_URL: "redis://localhost:6379",
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
#### Nested Pipeline Environment Variables
|
|
197
|
+
|
|
198
|
+
When converting a pipeline to a task, you can provide environment variables that will be merged with the outer pipeline's environment:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const innerPipeline = pipeline([
|
|
202
|
+
/* ... */
|
|
203
|
+
])
|
|
204
|
+
const innerTask = innerPipeline.toTask({
|
|
205
|
+
name: "inner",
|
|
206
|
+
env: {
|
|
207
|
+
INNER_VAR: "inner-value",
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
const outerPipeline = pipeline([innerTask])
|
|
212
|
+
const result = await outerPipeline.run({
|
|
213
|
+
env: {
|
|
214
|
+
OUTER_VAR: "outer-value",
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
In this example, tasks in `innerPipeline` will receive both `INNER_VAR` and `OUTER_VAR`, with `INNER_VAR` taking precedence if there's a conflict.
|
|
133
220
|
|
|
134
221
|
---
|
|
135
222
|
|
|
@@ -187,7 +274,11 @@ const deploy = pipeline([
|
|
|
187
274
|
])
|
|
188
275
|
|
|
189
276
|
const buildTask = build.toTask({ name: "build" })
|
|
190
|
-
const testTask = test.toTask({
|
|
277
|
+
const testTask = test.toTask({
|
|
278
|
+
name: "test",
|
|
279
|
+
dependencies: [buildTask],
|
|
280
|
+
env: { TEST_ENV: "test-value" }, // Optional: env for nested pipeline
|
|
281
|
+
})
|
|
191
282
|
const deployTask = deploy.toTask({ name: "deploy", dependencies: [testTask] })
|
|
192
283
|
|
|
193
284
|
const ci = pipeline([buildTask, testTask, deployTask])
|
|
@@ -278,7 +369,6 @@ const dbTask = task({
|
|
|
278
369
|
},
|
|
279
370
|
build: "echo build",
|
|
280
371
|
},
|
|
281
|
-
cwd: ".",
|
|
282
372
|
})
|
|
283
373
|
```
|
|
284
374
|
|
|
@@ -336,7 +426,6 @@ const dbTask = task({
|
|
|
336
426
|
commands: {
|
|
337
427
|
dev: "docker-compose up",
|
|
338
428
|
},
|
|
339
|
-
cwd: ".",
|
|
340
429
|
})
|
|
341
430
|
|
|
342
431
|
const apiTask = task({
|
|
@@ -345,7 +434,6 @@ const apiTask = task({
|
|
|
345
434
|
dev: "npm run dev",
|
|
346
435
|
build: "npm run build",
|
|
347
436
|
},
|
|
348
|
-
cwd: ".",
|
|
349
437
|
dependencies: [dbTask],
|
|
350
438
|
})
|
|
351
439
|
|
|
@@ -386,7 +474,6 @@ const dbTask = task({
|
|
|
386
474
|
commands: {
|
|
387
475
|
dev: "docker-compose up",
|
|
388
476
|
},
|
|
389
|
-
cwd: ".",
|
|
390
477
|
allowSkip: true,
|
|
391
478
|
})
|
|
392
479
|
|
|
@@ -17,13 +17,13 @@ export function executeTask(task, executorConfig) {
|
|
|
17
17
|
return;
|
|
18
18
|
// Handle pipeline tasks
|
|
19
19
|
if (nestedPipeline) {
|
|
20
|
-
executeNestedPipeline(taskId, taskName, nestedPipeline, executorConfig);
|
|
20
|
+
executeNestedPipeline(task, taskId, taskName, nestedPipeline, executorConfig);
|
|
21
21
|
return;
|
|
22
22
|
}
|
|
23
23
|
// Regular task execution
|
|
24
24
|
executeRegularTask(task, taskId, taskName, executorConfig);
|
|
25
25
|
}
|
|
26
|
-
function executeNestedPipeline(taskId, taskName, nestedPipeline, executorConfig) {
|
|
26
|
+
function executeNestedPipeline(task, taskId, taskName, nestedPipeline, executorConfig) {
|
|
27
27
|
const { config, runningPipelines, pipelineTasksCache, failPipeline, advanceScheduler, updateTaskStatus, } = executorConfig;
|
|
28
28
|
// Track nested pipeline state for skip behavior
|
|
29
29
|
let nestedSkippedCount = 0;
|
|
@@ -33,7 +33,7 @@ function executeNestedPipeline(taskId, taskName, nestedPipeline, executorConfig)
|
|
|
33
33
|
const nestedTotalTasks = nestedTasks
|
|
34
34
|
? createTaskGraph(nestedTasks).nodes.size
|
|
35
35
|
: 0;
|
|
36
|
-
const commandName =
|
|
36
|
+
const commandName = config?.command ?? process.env.NODE_ENV === "production" ? "build" : "dev";
|
|
37
37
|
// Mark as ready immediately (pipeline entry nodes will handle their own ready state)
|
|
38
38
|
const startedAt = Date.now();
|
|
39
39
|
updateTaskStatus(taskId, {
|
|
@@ -51,6 +51,13 @@ function executeNestedPipeline(taskId, taskName, nestedPipeline, executorConfig)
|
|
|
51
51
|
// In a more sophisticated implementation, we could propagate stop signals
|
|
52
52
|
};
|
|
53
53
|
runningPipelines.set(taskId, { stop: stopPipeline });
|
|
54
|
+
// Merge environment variables: pipeline.env -> task.env (from pipeline.toTask config)
|
|
55
|
+
const taskEnv = task[$TASK_INTERNAL].env;
|
|
56
|
+
const pipelineEnv = config?.env ?? {};
|
|
57
|
+
const mergedEnv = {
|
|
58
|
+
...pipelineEnv,
|
|
59
|
+
...taskEnv,
|
|
60
|
+
};
|
|
54
61
|
// Run the nested pipeline with signal propagation
|
|
55
62
|
nestedPipeline
|
|
56
63
|
.run({
|
|
@@ -58,6 +65,7 @@ function executeNestedPipeline(taskId, taskName, nestedPipeline, executorConfig)
|
|
|
58
65
|
command: config?.command,
|
|
59
66
|
strict: config?.strict,
|
|
60
67
|
signal: executorConfig.signal, // Pass signal to nested pipeline
|
|
68
|
+
env: mergedEnv, // Pass merged env to nested pipeline
|
|
61
69
|
onTaskBegin: (nestedTaskName) => {
|
|
62
70
|
if (pipelineStopped)
|
|
63
71
|
return;
|
|
@@ -124,11 +132,11 @@ function executeNestedPipeline(taskId, taskName, nestedPipeline, executorConfig)
|
|
|
124
132
|
}
|
|
125
133
|
function executeRegularTask(task, taskId, taskName, executorConfig) {
|
|
126
134
|
const { spawn: spawnFn, signal, config, runningTasks, teardownManager, failPipeline, advanceScheduler, updateTaskStatus, } = executorConfig;
|
|
127
|
-
const
|
|
128
|
-
const
|
|
135
|
+
const { allowSkip, commands, cwd, env: taskEnv } = task[$TASK_INTERNAL];
|
|
136
|
+
const commandName = config?.command ?? process.env.NODE_ENV === "production" ? "build" : "dev";
|
|
137
|
+
const commandConfig = commands[commandName];
|
|
129
138
|
// Check if command exists
|
|
130
139
|
if (commandConfig === undefined) {
|
|
131
|
-
const allowSkip = task[$TASK_INTERNAL].allowSkip ?? false;
|
|
132
140
|
const strict = config?.strict ?? false;
|
|
133
141
|
// If strict mode and not explicitly allowed to skip, fail
|
|
134
142
|
if (strict && !allowSkip) {
|
|
@@ -159,13 +167,21 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
|
|
|
159
167
|
});
|
|
160
168
|
return;
|
|
161
169
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
let command;
|
|
171
|
+
let readyWhen;
|
|
172
|
+
let readyTimeout = Infinity;
|
|
173
|
+
let teardown;
|
|
174
|
+
let commandEnv = {};
|
|
175
|
+
if (typeof commandConfig === "string") {
|
|
176
|
+
command = commandConfig;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
command = commandConfig.run;
|
|
180
|
+
readyWhen = commandConfig.readyWhen;
|
|
181
|
+
readyTimeout = commandConfig.readyTimeout ?? Infinity;
|
|
182
|
+
teardown = commandConfig.teardown;
|
|
183
|
+
commandEnv = commandConfig.env ?? {};
|
|
184
|
+
}
|
|
169
185
|
const taskCwd = path.isAbsolute(cwd) ? cwd : path.resolve(process.cwd(), cwd);
|
|
170
186
|
if (!fs.existsSync(taskCwd)) {
|
|
171
187
|
const finishedAt = Date.now();
|
|
@@ -187,16 +203,20 @@ function executeRegularTask(task, taskId, taskName, executorConfig) {
|
|
|
187
203
|
]
|
|
188
204
|
.filter(Boolean)
|
|
189
205
|
.join(process.platform === "win32" ? ";" : ":");
|
|
190
|
-
|
|
206
|
+
// Merge environment variables in order: process.env -> pipeline.env -> task.env -> command.env
|
|
207
|
+
const accumulatedEnv = {
|
|
191
208
|
...process.env,
|
|
192
209
|
PATH: accumulatedPath,
|
|
193
210
|
Path: accumulatedPath,
|
|
211
|
+
...config?.env,
|
|
212
|
+
...taskEnv,
|
|
213
|
+
...commandEnv,
|
|
194
214
|
};
|
|
195
215
|
const child = spawnFn(command, {
|
|
196
216
|
cwd: taskCwd,
|
|
197
217
|
stdio: ["inherit", "pipe", "pipe"],
|
|
198
218
|
shell: true,
|
|
199
|
-
env,
|
|
219
|
+
env: accumulatedEnv,
|
|
200
220
|
});
|
|
201
221
|
runningTasks.set(taskId, child);
|
|
202
222
|
const startedAt = Date.now();
|
package/dist/pipeline.js
CHANGED
|
@@ -26,12 +26,12 @@ export function pipeline(tasks) {
|
|
|
26
26
|
graph.validate();
|
|
27
27
|
graph.simplify();
|
|
28
28
|
const pipelineImpl = {
|
|
29
|
-
toTask({ name, dependencies }) {
|
|
29
|
+
toTask({ name, dependencies, env }) {
|
|
30
30
|
const syntheticTask = task({
|
|
31
31
|
name,
|
|
32
32
|
commands: {},
|
|
33
|
-
cwd: ".",
|
|
34
33
|
dependencies,
|
|
34
|
+
env,
|
|
35
35
|
});
|
|
36
36
|
// Mark this task as a pipeline task so it can be detected by the executor
|
|
37
37
|
syntheticTask[$TASK_INTERNAL].pipeline = pipelineImpl;
|
package/dist/task.js
CHANGED
|
@@ -10,18 +10,26 @@ import { validateTasks } from "./util.js";
|
|
|
10
10
|
* await pipeline([build, deploy]).run()
|
|
11
11
|
*/
|
|
12
12
|
export function task(config) {
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
13
|
+
const { name, commands, cwd = ".", dependencies = [], env, allowSkip, } = config;
|
|
14
|
+
const dependenciesClone = [...dependencies];
|
|
15
|
+
validateTasks(dependenciesClone);
|
|
16
|
+
const commandsClone = Object.fromEntries(Object.entries(commands).map(([key, command]) => {
|
|
17
|
+
if (typeof command === "string") {
|
|
18
|
+
return [key, command];
|
|
19
|
+
}
|
|
20
|
+
const { run, readyWhen, readyTimeout, teardown, env } = command;
|
|
21
|
+
return [key, { run, readyWhen, readyTimeout, teardown, env: { ...env } }];
|
|
22
|
+
}));
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
16
25
|
[$TASK_INTERNAL]: {
|
|
17
|
-
|
|
26
|
+
name,
|
|
27
|
+
cwd,
|
|
28
|
+
dependencies: dependenciesClone,
|
|
29
|
+
env: { ...env },
|
|
30
|
+
allowSkip,
|
|
18
31
|
id: crypto.randomUUID(),
|
|
19
|
-
commands:
|
|
20
|
-
key,
|
|
21
|
-
typeof value === "string" ? value : { ...value },
|
|
22
|
-
])),
|
|
23
|
-
dependencies: [...(config.dependencies || [])],
|
|
32
|
+
commands: commandsClone,
|
|
24
33
|
},
|
|
25
34
|
};
|
|
26
|
-
return taskInstance;
|
|
27
35
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export interface CommandConfig {
|
|
|
25
25
|
* Optional command to run during teardown (e.g., to stop a server).
|
|
26
26
|
*/
|
|
27
27
|
teardown?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Optional environment variables to set for the process spawned by this command.
|
|
30
|
+
* Overrides environment variables inherited from the parent process & task config.
|
|
31
|
+
*/
|
|
32
|
+
env?: Record<string, string>;
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
35
|
* A command can be either a simple string or a CommandConfig object.
|
|
@@ -57,8 +62,9 @@ export interface TaskConfig {
|
|
|
57
62
|
/**
|
|
58
63
|
* Working directory for the task's commands.
|
|
59
64
|
* Can be absolute or relative to the current working directory.
|
|
65
|
+
* @default "."
|
|
60
66
|
*/
|
|
61
|
-
cwd
|
|
67
|
+
cwd?: string;
|
|
62
68
|
/**
|
|
63
69
|
* Optional array of tasks that must complete before this task can start.
|
|
64
70
|
* Dependencies are executed in parallel when possible.
|
|
@@ -69,10 +75,17 @@ export interface TaskConfig {
|
|
|
69
75
|
* Use this to explicitly mark tasks that are intentionally mode-specific.
|
|
70
76
|
*/
|
|
71
77
|
allowSkip?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Optional environment variables to set for the process spawned by this task.
|
|
80
|
+
* Overrides environment variables inherited from the parent process.
|
|
81
|
+
*/
|
|
82
|
+
env?: Record<string, string>;
|
|
72
83
|
}
|
|
73
84
|
interface TaskInternal extends TaskConfig {
|
|
74
85
|
id: string;
|
|
86
|
+
cwd: string;
|
|
75
87
|
dependencies: Task[];
|
|
88
|
+
env: Record<string, string>;
|
|
76
89
|
pipeline?: Pipeline;
|
|
77
90
|
}
|
|
78
91
|
/**
|
|
@@ -99,6 +112,11 @@ export interface PipelineRunConfig {
|
|
|
99
112
|
* @default process.env.NODE_ENV === "production" ? "build" : "dev"
|
|
100
113
|
*/
|
|
101
114
|
command?: string;
|
|
115
|
+
/**
|
|
116
|
+
* Optional environment variables to set for processes spawned by this pipeline.
|
|
117
|
+
* Overrides environment variables inherited from the parent process.
|
|
118
|
+
*/
|
|
119
|
+
env?: Record<string, string>;
|
|
102
120
|
/**
|
|
103
121
|
* Provides a custom abort signal for the pipeline.
|
|
104
122
|
* Aborting the signal will cause the pipeline to fail.
|
|
@@ -156,6 +174,11 @@ export interface PipelineTaskConfig {
|
|
|
156
174
|
* Optional array of tasks that must complete before this pipeline task can start.
|
|
157
175
|
*/
|
|
158
176
|
dependencies?: Task[];
|
|
177
|
+
/**
|
|
178
|
+
* Optional environment variables to set for the process spawned by this pipeline task.
|
|
179
|
+
* Overrides environment variables inherited from the parent process.
|
|
180
|
+
*/
|
|
181
|
+
env?: Record<string, string>;
|
|
159
182
|
}
|
|
160
183
|
/**
|
|
161
184
|
* A pipeline manages the execution of tasks with dependency-based coordination.
|