@livingdata/pipex 0.0.4 → 0.0.6
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 +50 -13
- package/dist/cli/pipeline-loader.js +8 -5
- package/dist/cli/reporter.js +23 -2
- package/dist/kits/builtin/shell.js +31 -0
- package/dist/kits/index.js +3 -3
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -14,14 +14,14 @@ Runs containers with explicit volume mounts and manages artifacts through a stag
|
|
|
14
14
|
Run directly without installing:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx @livingdata/pipex run pipeline.yaml
|
|
17
|
+
npx @livingdata/pipex run pipeline.yaml
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
Or install globally:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npm install -g @livingdata/pipex
|
|
24
|
-
pipex run pipeline.yaml
|
|
24
|
+
pipex run pipeline.yaml
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
## Usage
|
|
@@ -30,9 +30,6 @@ pipex run pipeline.yaml --workspace my-build
|
|
|
30
30
|
# Interactive mode (default)
|
|
31
31
|
pipex run pipeline.yaml
|
|
32
32
|
|
|
33
|
-
# With workspace name (enables caching)
|
|
34
|
-
pipex run pipeline.yaml --workspace my-build
|
|
35
|
-
|
|
36
33
|
# JSON mode (for CI/CD)
|
|
37
34
|
pipex run pipeline.yaml --json
|
|
38
35
|
|
|
@@ -126,6 +123,10 @@ steps:
|
|
|
126
123
|
- id: analyze
|
|
127
124
|
uses: python
|
|
128
125
|
with: { script: analyze.py, src: scripts }
|
|
126
|
+
- id: extract
|
|
127
|
+
uses: shell
|
|
128
|
+
with: { packages: [unzip], run: "unzip /input/build/archive.zip -d /output/" }
|
|
129
|
+
inputs: [{ step: build }]
|
|
129
130
|
```
|
|
130
131
|
|
|
131
132
|
`uses` and `image`/`cmd` are mutually exclusive. All other step fields (`env`, `inputs`, `mounts`, `caches`, `timeoutSec`, `allowFailure`, `allowNetwork`) remain available and merge with kit defaults (user values take priority). The `src` parameter in `with` generates a read-only mount at `/app` in the container.
|
|
@@ -154,13 +155,32 @@ steps:
|
|
|
154
155
|
| `install` | `true` | Run dependency install before script |
|
|
155
156
|
| `variant` | `"slim"` | Image variant |
|
|
156
157
|
|
|
157
|
-
**`
|
|
158
|
+
**`shell`** -- Run a shell command in a container, with optional apt package installation.
|
|
158
159
|
|
|
159
160
|
| Parameter | Default | Description |
|
|
160
161
|
|-----------|---------|-------------|
|
|
161
162
|
| `run` | *(required)* | Shell command to execute |
|
|
163
|
+
| `packages` | -- | Apt packages to install before running |
|
|
162
164
|
| `src` | -- | Host directory to mount at `/app` |
|
|
163
|
-
| `image` | `"alpine:3.20"` | Docker image |
|
|
165
|
+
| `image` | `"alpine:3.20"` | Docker image (defaults to `"debian:bookworm-slim"` when `packages` is set) |
|
|
166
|
+
|
|
167
|
+
When `packages` is provided, the kit automatically switches to a Debian image, enables network access, and provides an `apt-cache` cache. Without packages, it runs on a minimal Alpine image with no network.
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
# Simple command (alpine, no network)
|
|
171
|
+
- id: list-files
|
|
172
|
+
uses: shell
|
|
173
|
+
with:
|
|
174
|
+
run: ls -lhR /input/data/
|
|
175
|
+
|
|
176
|
+
# With system packages (debian, network + apt cache)
|
|
177
|
+
- id: extract
|
|
178
|
+
uses: shell
|
|
179
|
+
with:
|
|
180
|
+
packages: [unzip, jq]
|
|
181
|
+
run: unzip /input/download/data.zip -d /output/
|
|
182
|
+
inputs: [{ step: download }]
|
|
183
|
+
```
|
|
164
184
|
|
|
165
185
|
### Raw Steps
|
|
166
186
|
|
|
@@ -253,12 +273,29 @@ Common use cases:
|
|
|
253
273
|
|
|
254
274
|
**Note**: Caches are workspace-scoped (not global). Different workspaces have isolated caches.
|
|
255
275
|
|
|
256
|
-
##
|
|
276
|
+
## Examples
|
|
277
|
+
|
|
278
|
+
### Geodata Processing
|
|
279
|
+
|
|
280
|
+
The `examples/geodata/` pipeline downloads a shapefile archive, extracts it, and produces a CSV inventory — using the `debian` and `bash` kits:
|
|
281
|
+
|
|
282
|
+
```
|
|
283
|
+
examples/geodata/
|
|
284
|
+
└── pipeline.yaml
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Steps: `download` → `extract` → `list-files` / `build-csv`
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
pipex run examples/geodata/pipeline.yaml
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### Multi-Language
|
|
257
294
|
|
|
258
|
-
The `
|
|
295
|
+
The `examples/multi-language/` pipeline chains Node.js and Python steps using kits:
|
|
259
296
|
|
|
260
297
|
```
|
|
261
|
-
|
|
298
|
+
examples/multi-language/
|
|
262
299
|
├── pipeline.yaml
|
|
263
300
|
└── scripts/
|
|
264
301
|
├── nodejs/ # lodash-based data analysis
|
|
@@ -272,10 +309,10 @@ example/
|
|
|
272
309
|
└── transform.py
|
|
273
310
|
```
|
|
274
311
|
|
|
275
|
-
|
|
312
|
+
Steps: `node-analyze` → `node-transform` → `python-analyze` → `python-transform`
|
|
276
313
|
|
|
277
314
|
```bash
|
|
278
|
-
pipex run
|
|
315
|
+
pipex run examples/multi-language/pipeline.yaml
|
|
279
316
|
```
|
|
280
317
|
|
|
281
318
|
## Caching & Workspaces
|
|
@@ -334,7 +371,7 @@ cp .env.example .env
|
|
|
334
371
|
Run the CLI without building (via tsx):
|
|
335
372
|
|
|
336
373
|
```bash
|
|
337
|
-
npm run cli -- run pipeline.yaml
|
|
374
|
+
npm run cli -- run pipeline.yaml
|
|
338
375
|
npm run cli -- list
|
|
339
376
|
```
|
|
340
377
|
|
|
@@ -7,6 +7,9 @@ import { isKitStep } from '../types.js';
|
|
|
7
7
|
export class PipelineLoader {
|
|
8
8
|
async load(filePath) {
|
|
9
9
|
const content = await readFile(filePath, 'utf8');
|
|
10
|
+
return this.parse(content, filePath);
|
|
11
|
+
}
|
|
12
|
+
parse(content, filePath) {
|
|
10
13
|
const input = parsePipelineFile(content, filePath);
|
|
11
14
|
if (!input.id && !input.name) {
|
|
12
15
|
throw new Error('Invalid pipeline: at least one of "id" or "name" must be defined');
|
|
@@ -132,7 +135,7 @@ export class PipelineLoader {
|
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
/** Convert a free-form name into a valid identifier. */
|
|
135
|
-
function slugify(name) {
|
|
138
|
+
export function slugify(name) {
|
|
136
139
|
return deburr(name)
|
|
137
140
|
.toLowerCase()
|
|
138
141
|
.replaceAll(/[^\w-]/g, '-')
|
|
@@ -140,20 +143,20 @@ function slugify(name) {
|
|
|
140
143
|
.replace(/^-/, '')
|
|
141
144
|
.replace(/-$/, '');
|
|
142
145
|
}
|
|
143
|
-
function parsePipelineFile(content, filePath) {
|
|
146
|
+
export function parsePipelineFile(content, filePath) {
|
|
144
147
|
const ext = extname(filePath).toLowerCase();
|
|
145
148
|
if (ext === '.yaml' || ext === '.yml') {
|
|
146
149
|
return parseYaml(content);
|
|
147
150
|
}
|
|
148
151
|
return JSON.parse(content);
|
|
149
152
|
}
|
|
150
|
-
function mergeEnv(kitEnv, userEnv) {
|
|
153
|
+
export function mergeEnv(kitEnv, userEnv) {
|
|
151
154
|
if (!kitEnv && !userEnv) {
|
|
152
155
|
return undefined;
|
|
153
156
|
}
|
|
154
157
|
return { ...kitEnv, ...userEnv };
|
|
155
158
|
}
|
|
156
|
-
function mergeCaches(kitCaches, userCaches) {
|
|
159
|
+
export function mergeCaches(kitCaches, userCaches) {
|
|
157
160
|
if (!kitCaches && !userCaches) {
|
|
158
161
|
return undefined;
|
|
159
162
|
}
|
|
@@ -166,7 +169,7 @@ function mergeCaches(kitCaches, userCaches) {
|
|
|
166
169
|
}
|
|
167
170
|
return [...map.values()];
|
|
168
171
|
}
|
|
169
|
-
function mergeMounts(kitMounts, userMounts) {
|
|
172
|
+
export function mergeMounts(kitMounts, userMounts) {
|
|
170
173
|
if (!kitMounts && !userMounts) {
|
|
171
174
|
return undefined;
|
|
172
175
|
}
|
package/dist/cli/reporter.js
CHANGED
|
@@ -23,8 +23,10 @@ export class ConsoleReporter {
|
|
|
23
23
|
* Suitable for local development and manual execution.
|
|
24
24
|
*/
|
|
25
25
|
export class InteractiveReporter {
|
|
26
|
+
static maxStderrLines = 20;
|
|
26
27
|
spinner;
|
|
27
28
|
stepSpinners = new Map();
|
|
29
|
+
stderrBuffers = new Map();
|
|
28
30
|
state(workspaceId, event, step, meta) {
|
|
29
31
|
if (event === 'PIPELINE_START') {
|
|
30
32
|
const displayName = meta?.pipelineName ?? workspaceId;
|
|
@@ -50,6 +52,7 @@ export class InteractiveReporter {
|
|
|
50
52
|
spinner.stopAndPersist({ symbol: chalk.green('✓'), text: chalk.green(step.displayName) });
|
|
51
53
|
this.stepSpinners.delete(step.id);
|
|
52
54
|
}
|
|
55
|
+
this.stderrBuffers.delete(step.id);
|
|
53
56
|
}
|
|
54
57
|
if (event === 'STEP_FAILED' && step) {
|
|
55
58
|
const spinner = this.stepSpinners.get(step.id);
|
|
@@ -62,6 +65,14 @@ export class InteractiveReporter {
|
|
|
62
65
|
});
|
|
63
66
|
this.stepSpinners.delete(step.id);
|
|
64
67
|
}
|
|
68
|
+
const stderr = this.stderrBuffers.get(step.id);
|
|
69
|
+
if (stderr && stderr.length > 0) {
|
|
70
|
+
console.log(chalk.red(' ── stderr ──'));
|
|
71
|
+
for (const line of stderr) {
|
|
72
|
+
console.log(chalk.red(` ${line}`));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.stderrBuffers.delete(step.id);
|
|
65
76
|
}
|
|
66
77
|
if (event === 'PIPELINE_FINISHED') {
|
|
67
78
|
console.log(chalk.bold.green('\n✓ Pipeline completed\n'));
|
|
@@ -70,8 +81,18 @@ export class InteractiveReporter {
|
|
|
70
81
|
console.log(chalk.bold.red('\n✗ Pipeline failed\n'));
|
|
71
82
|
}
|
|
72
83
|
}
|
|
73
|
-
log(_workspaceId,
|
|
74
|
-
|
|
84
|
+
log(_workspaceId, step, stream, line) {
|
|
85
|
+
if (stream === 'stderr') {
|
|
86
|
+
let buffer = this.stderrBuffers.get(step.id);
|
|
87
|
+
if (!buffer) {
|
|
88
|
+
buffer = [];
|
|
89
|
+
this.stderrBuffers.set(step.id, buffer);
|
|
90
|
+
}
|
|
91
|
+
buffer.push(line);
|
|
92
|
+
if (buffer.length > InteractiveReporter.maxStderrLines) {
|
|
93
|
+
buffer.shift();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
75
96
|
}
|
|
76
97
|
result(_workspaceId, _step, _result) {
|
|
77
98
|
// Results shown via state updates
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const shellKit = {
|
|
2
|
+
name: 'shell',
|
|
3
|
+
resolve(params) {
|
|
4
|
+
const run = params.run;
|
|
5
|
+
if (!run || typeof run !== 'string') {
|
|
6
|
+
throw new Error('Kit "shell": "run" parameter is required');
|
|
7
|
+
}
|
|
8
|
+
const packages = params.packages;
|
|
9
|
+
const hasPackages = packages && packages.length > 0;
|
|
10
|
+
const src = params.src;
|
|
11
|
+
const defaultImage = hasPackages ? 'debian:bookworm-slim' : 'alpine:3.20';
|
|
12
|
+
const image = params.image ?? defaultImage;
|
|
13
|
+
const parts = [];
|
|
14
|
+
if (hasPackages) {
|
|
15
|
+
parts.push(`apt-get update && apt-get install -y --no-install-recommends ${packages.join(' ')} && rm -rf /var/lib/apt/lists/*`);
|
|
16
|
+
}
|
|
17
|
+
parts.push(run);
|
|
18
|
+
const output = {
|
|
19
|
+
image,
|
|
20
|
+
cmd: ['sh', '-c', parts.join(' && ')]
|
|
21
|
+
};
|
|
22
|
+
if (hasPackages) {
|
|
23
|
+
output.caches = [{ name: 'apt-cache', path: '/var/cache/apt' }];
|
|
24
|
+
output.allowNetwork = true;
|
|
25
|
+
}
|
|
26
|
+
if (src) {
|
|
27
|
+
output.mounts = [{ host: src, container: '/app' }];
|
|
28
|
+
}
|
|
29
|
+
return output;
|
|
30
|
+
}
|
|
31
|
+
};
|
package/dist/kits/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { bashKit } from './builtin/bash.js';
|
|
2
1
|
import { nodeKit } from './builtin/node.js';
|
|
3
2
|
import { pythonKit } from './builtin/python.js';
|
|
3
|
+
import { shellKit } from './builtin/shell.js';
|
|
4
4
|
const kits = new Map([
|
|
5
|
-
[bashKit.name, bashKit],
|
|
6
5
|
[nodeKit.name, nodeKit],
|
|
7
|
-
[pythonKit.name, pythonKit]
|
|
6
|
+
[pythonKit.name, pythonKit],
|
|
7
|
+
[shellKit.name, shellKit]
|
|
8
8
|
]);
|
|
9
9
|
export function getKit(name) {
|
|
10
10
|
const kit = kits.get(name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livingdata/pipex",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Execution engine for containerized pipeline steps",
|
|
5
5
|
"author": "Jérôme Desboeufs <jerome@livingdata.co>",
|
|
6
6
|
"type": "module",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"cli": "tsx src/cli/index.ts",
|
|
24
|
+
"test": "ava",
|
|
24
25
|
"lint": "xo",
|
|
25
26
|
"lint:fix": "xo --fix",
|
|
26
27
|
"build": "tsc"
|
|
@@ -36,8 +37,10 @@
|
|
|
36
37
|
"yaml": "^2.8.2"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
40
|
+
"@ava/typescript": "^6.0.0",
|
|
39
41
|
"@types/lodash-es": "^4.17.12",
|
|
40
42
|
"@types/node": "^25.2.0",
|
|
43
|
+
"ava": "^6.4.1",
|
|
41
44
|
"tsx": "^4.21.0",
|
|
42
45
|
"xo": "^1.2.3"
|
|
43
46
|
}
|